mirror of
https://github.com/haiwen/seahub.git
synced 2025-04-28 03:10:45 +00:00
optimize tag editor ui (#7647)
* optimize tag editor ui * optimize selection by up/down key * optimize popover position * fix searched tree nodes folding --------- Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
parent
0abb343b4b
commit
005ddb4dca
@ -185,3 +185,16 @@ export const getTreeChildNodes = (parentNode, tree) => {
|
||||
}
|
||||
return childNodes;
|
||||
};
|
||||
|
||||
export const getNodesWithAncestors = (node, tree) => {
|
||||
const nodeKey = getTreeNodeKey(node);
|
||||
|
||||
let nodesWithAncestors = [];
|
||||
tree.forEach((node, i) => {
|
||||
if (!nodeKey.includes(getTreeNodeKey(node))) {
|
||||
return;
|
||||
}
|
||||
nodesWithAncestors.push({ ...node, node_index: i });
|
||||
});
|
||||
return nodesWithAncestors;
|
||||
};
|
||||
|
@ -16,11 +16,21 @@
|
||||
padding: 10px 10px 0;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor.tags-tree-container .sf-metadata-search-tags-container {
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-search-tags-container .sf-metadata-search-tags {
|
||||
font-size: 14px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-search-tags-container .search-control {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 26px;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-container {
|
||||
max-height: 200px;
|
||||
min-height: 100px;
|
||||
@ -28,20 +38,27 @@
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor.tags-tree-container .sf-metadata-tags-editor-container {
|
||||
max-height: 280px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor.tags-tree-container .sf-metadata-tags-editor-container .sf-metadata-tags-editor-tag-item {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-container .none-search-result {
|
||||
font-size: 14px;
|
||||
opacity: 0.5;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container {
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
color: #212529;
|
||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-container .sf-metadata-tags-editor-title {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container-highlight {
|
||||
@ -54,23 +71,10 @@
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.sf-metadata-tag-color-and-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sf-metadata-tag-color-and-name .sf-metadata-tag-color {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sf-metadata-tag-color-and-name .sf-metadata-tag-name {
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-divider {
|
||||
border-top: 1px solid rgba(0, 40, 100, .12);
|
||||
height: 0;
|
||||
margin: 0.5rem 0;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import CommonAddTool from '../../../../components/common-add-tool';
|
||||
import SearchInput from '../../../../components/search-input';
|
||||
import Icon from '../../../../components/icon';
|
||||
import DeleteTags from './delete-tags';
|
||||
import { Utils } from '../../../../utils/utils';
|
||||
import { KeyCodes } from '../../../../constants';
|
||||
@ -14,9 +13,13 @@ import { getRecordIdFromRecord } from '../../../utils/cell';
|
||||
import { getRowById } from '../../../../components/sf-table/utils/table';
|
||||
import { SELECT_OPTION_COLORS } from '../../../constants';
|
||||
import { PRIVATE_COLUMN_KEY as TAG_PRIVATE_COLUMN_KEY } from '../../../../tag/constants';
|
||||
import { checkIsTreeNodeShown, checkTreeNodeHasChildNodes, getNodesWithAncestors, getTreeNodeDepth, getTreeNodeId, getTreeNodeKey } from '../../../../components/sf-table/utils/tree';
|
||||
import TagItem from './tag-item';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const RECENTLY_USED_TAG_IDS = 'recently_used_tag_ids';
|
||||
|
||||
const TagsEditor = forwardRef(({
|
||||
height,
|
||||
column,
|
||||
@ -25,6 +28,7 @@ const TagsEditor = forwardRef(({
|
||||
editorPosition = { left: 0, top: 0 },
|
||||
onPressTab,
|
||||
updateFileTags,
|
||||
showTagsAsTree,
|
||||
}, ref) => {
|
||||
const { tagsData, addTag, context } = useTags();
|
||||
|
||||
@ -33,12 +37,17 @@ const TagsEditor = forwardRef(({
|
||||
const [value, setValue] = useState((oldValue || []).map(item => item.row_id).filter(item => getRowById(tagsData, item)));
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [highlightIndex, setHighlightIndex] = useState(-1);
|
||||
const [highlightNodeIndex, setHighlightNodeIndex] = useState(-1);
|
||||
const [maxItemNum, setMaxItemNum] = useState(0);
|
||||
const [nodes, setNodes] = useState([]);
|
||||
const [keyNodeFoldedMap, setKeyNodeFoldedMap] = useState({});
|
||||
const [recentlyUsed, setRecentlyUsed] = useState([]);
|
||||
const itemHeight = 30;
|
||||
const editorContainerRef = useRef(null);
|
||||
const editorRef = useRef(null);
|
||||
const selectItemRef = useRef(null);
|
||||
|
||||
const canEditData = window.sfMetadataContext.canModifyColumnData(column);
|
||||
const localStorage = window.sfMetadataContext.localStorage;
|
||||
|
||||
const tags = useMemo(() => {
|
||||
if (!tagsData) return [];
|
||||
@ -46,6 +55,7 @@ const TagsEditor = forwardRef(({
|
||||
}, [tagsData]);
|
||||
|
||||
const displayTags = useMemo(() => getTagsByNameOrColor(tags, searchValue), [searchValue, tags]);
|
||||
const recentlyUsedTags = useMemo(() => recentlyUsed, [recentlyUsed]);
|
||||
|
||||
const isShowCreateBtn = useMemo(() => {
|
||||
if (!canAddTag) return false;
|
||||
@ -73,7 +83,16 @@ const TagsEditor = forwardRef(({
|
||||
setValue(newValue);
|
||||
const recordId = getRecordIdFromRecord(record);
|
||||
updateFileTags([{ record_id: recordId, tags: newValue, old_tags: value }]);
|
||||
}, [value, record, updateFileTags]);
|
||||
|
||||
const ids = recentlyUsed.map(item => getTagId(item));
|
||||
if (ids.indexOf(tagId) > -1) return;
|
||||
const tag = getRowById(tagsData, tagId);
|
||||
const updated = [tag, ...recentlyUsed.filter(item => getTagId(item) !== tagId)].slice(0, 2);
|
||||
setRecentlyUsed(updated);
|
||||
|
||||
const newIds = updated.map(tag => getTagId(tag));
|
||||
localStorage.setItem(RECENTLY_USED_TAG_IDS, JSON.stringify(newIds));
|
||||
}, [value, record, tagsData, updateFileTags, recentlyUsed, localStorage]);
|
||||
|
||||
const onDeleteTag = useCallback((tagId) => {
|
||||
const newValue = value.slice(0);
|
||||
@ -86,14 +105,22 @@ const TagsEditor = forwardRef(({
|
||||
updateFileTags([{ record_id: recordId, tags: newValue, old_tags: value }]);
|
||||
}, [value, record, updateFileTags]);
|
||||
|
||||
const onMenuMouseEnter = useCallback((highlightIndex) => {
|
||||
setHighlightIndex(highlightIndex);
|
||||
const onMenuMouseEnter = useCallback((i, id) => {
|
||||
setHighlightIndex(i);
|
||||
}, []);
|
||||
|
||||
const onMenuMouseLeave = useCallback((index) => {
|
||||
const onMenuMouseLeave = useCallback(() => {
|
||||
setHighlightIndex(-1);
|
||||
}, []);
|
||||
|
||||
const onTreeMenuMouseEnter = useCallback((i) => {
|
||||
setHighlightNodeIndex(i);
|
||||
}, []);
|
||||
|
||||
const onTreeMenuMouseLeave = useCallback(() => {
|
||||
setHighlightNodeIndex(-1);
|
||||
}, []);
|
||||
|
||||
const createTag = useCallback((event) => {
|
||||
event && event.stopPropagation();
|
||||
event && event.nativeEvent.stopImmediatePropagation();
|
||||
@ -115,18 +142,24 @@ const TagsEditor = forwardRef(({
|
||||
|
||||
const getMaxItemNum = useCallback(() => {
|
||||
let selectContainerStyle = getComputedStyle(editorContainerRef.current, null);
|
||||
let selectItemStyle = getComputedStyle(selectItemRef.current, null);
|
||||
let maxSelectItemNum = Math.floor(parseInt(selectContainerStyle.maxHeight) / parseInt(selectItemStyle.height));
|
||||
let maxSelectItemNum = Math.floor(parseInt(selectContainerStyle.maxHeight) / parseInt(itemHeight));
|
||||
return maxSelectItemNum - 1;
|
||||
}, [editorContainerRef, selectItemRef]);
|
||||
}, [editorContainerRef]);
|
||||
|
||||
const onEnter = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
let tag;
|
||||
if (displayTags.length === 1) {
|
||||
tag = displayTags[0];
|
||||
} else if (highlightIndex > -1) {
|
||||
tag = displayTags[highlightIndex];
|
||||
if (showTagsAsTree) {
|
||||
if (highlightNodeIndex > -1 && nodes[highlightNodeIndex]) {
|
||||
const tagId = getTreeNodeId(nodes[highlightNodeIndex]);
|
||||
tag = getRowById(tagsData, tagId);
|
||||
}
|
||||
} else {
|
||||
if (displayTags.length === 1) {
|
||||
tag = displayTags[0];
|
||||
} else if (highlightIndex > -1) {
|
||||
tag = displayTags[highlightIndex];
|
||||
}
|
||||
}
|
||||
if (tag) {
|
||||
const newTagId = getTagId(tag);
|
||||
@ -136,27 +169,54 @@ const TagsEditor = forwardRef(({
|
||||
if (isShowCreateBtn) {
|
||||
createTag();
|
||||
}
|
||||
}, [displayTags, highlightIndex, isShowCreateBtn, onSelectTag, createTag]);
|
||||
}, [displayTags, highlightIndex, isShowCreateBtn, onSelectTag, createTag, showTagsAsTree, tagsData, highlightNodeIndex, nodes]);
|
||||
|
||||
const updateScroll = useCallback((index) => {
|
||||
const visibleStart = Math.floor(editorContainerRef.current.scrollTop / itemHeight);
|
||||
const visibleEnd = visibleStart + maxItemNum;
|
||||
|
||||
if (index < visibleStart) {
|
||||
editorContainerRef.current.scrollTop -= itemHeight;
|
||||
} else if (index >= visibleEnd) {
|
||||
editorContainerRef.current.scrollTop += itemHeight;
|
||||
}
|
||||
}, [maxItemNum]);
|
||||
|
||||
const onUpArrow = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (highlightIndex === 0) return;
|
||||
setHighlightIndex(highlightIndex - 1);
|
||||
if (highlightIndex > displayTags.length - maxItemNum) {
|
||||
editorContainerRef.current.scrollTop -= itemHeight;
|
||||
|
||||
if (showTagsAsTree) {
|
||||
const newIndex = highlightNodeIndex - 1;
|
||||
if (newIndex < 0) return;
|
||||
const pos = recentlyUsedTags.length > 0 ? newIndex + recentlyUsedTags.length + 2 : newIndex;
|
||||
updateScroll(pos);
|
||||
setHighlightNodeIndex(newIndex);
|
||||
} else {
|
||||
const newIndex = highlightIndex - 1;
|
||||
if (newIndex < 0) return;
|
||||
updateScroll(highlightIndex, displayTags.length, setHighlightIndex);
|
||||
setHighlightIndex(newIndex);
|
||||
}
|
||||
}, [editorContainerRef, highlightIndex, maxItemNum, displayTags, itemHeight]);
|
||||
}, [highlightIndex, displayTags, showTagsAsTree, recentlyUsedTags, highlightNodeIndex, updateScroll]);
|
||||
|
||||
const onDownArrow = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (highlightIndex === displayTags.length - 1) return;
|
||||
setHighlightIndex(highlightIndex + 1);
|
||||
if (highlightIndex >= maxItemNum) {
|
||||
editorContainerRef.current.scrollTop += itemHeight;
|
||||
|
||||
if (showTagsAsTree) {
|
||||
const newIndex = highlightNodeIndex + 1;
|
||||
if (newIndex >= nodes.length) return;
|
||||
const pos = recentlyUsedTags.length > 0 ? newIndex + recentlyUsedTags.length + 2 : newIndex;
|
||||
updateScroll(pos);
|
||||
setHighlightNodeIndex(newIndex);
|
||||
} else {
|
||||
const newIndex = highlightIndex + 1;
|
||||
if (newIndex >= displayTags.length) return;
|
||||
updateScroll(newIndex);
|
||||
setHighlightIndex(newIndex);
|
||||
}
|
||||
}, [editorContainerRef, highlightIndex, maxItemNum, displayTags, itemHeight]);
|
||||
}, [highlightIndex, displayTags, showTagsAsTree, nodes, recentlyUsedTags, highlightNodeIndex, updateScroll]);
|
||||
|
||||
const onHotKey = useCallback((event) => {
|
||||
if (event.keyCode === KeyCodes.Enter) {
|
||||
@ -183,6 +243,50 @@ const TagsEditor = forwardRef(({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getShownNodes = useCallback((tree, keyNodeFoldedMap) => {
|
||||
if (!Array.isArray(tree)) {
|
||||
return [];
|
||||
}
|
||||
let shownNodes = [];
|
||||
|
||||
tree.forEach((node, index) => {
|
||||
const nodeId = getTreeNodeId(node);
|
||||
const row = getRowById(tagsData, nodeId);
|
||||
if (!row) return;
|
||||
if (searchValue) {
|
||||
const value = searchValue.toLowerCase();
|
||||
const tagName = getTagName(row).toLowerCase();
|
||||
const tagColor = getTagColor(row).toLowerCase();
|
||||
if (!tagName.includes(value) && !tagColor.includes(value)) return;
|
||||
if (showTagsAsTree) {
|
||||
const nodesWithAncestors = getNodesWithAncestors(node, tree).filter(node => checkIsTreeNodeShown(getTreeNodeKey(node), keyNodeFoldedMap));
|
||||
shownNodes = [...shownNodes, ...nodesWithAncestors];
|
||||
return;
|
||||
}
|
||||
}
|
||||
const nodeKey = getTreeNodeKey(node);
|
||||
if (row && checkIsTreeNodeShown(nodeKey, keyNodeFoldedMap)) {
|
||||
shownNodes.push({
|
||||
...node,
|
||||
node_index: index,
|
||||
});
|
||||
}
|
||||
});
|
||||
return shownNodes;
|
||||
}, [tagsData, searchValue, showTagsAsTree]);
|
||||
|
||||
const toggleExpandTreeNode = useCallback((nodeKey) => {
|
||||
const updatedKeyNodeFoldedMap = { ...keyNodeFoldedMap };
|
||||
if (updatedKeyNodeFoldedMap[nodeKey]) {
|
||||
delete updatedKeyNodeFoldedMap[nodeKey];
|
||||
} else {
|
||||
updatedKeyNodeFoldedMap[nodeKey] = true;
|
||||
}
|
||||
const updatedNodes = getShownNodes(tagsData.rows_tree, updatedKeyNodeFoldedMap);
|
||||
setNodes(updatedNodes);
|
||||
setKeyNodeFoldedMap(updatedKeyNodeFoldedMap);
|
||||
}, [tagsData, keyNodeFoldedMap, getShownNodes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
const { bottom } = editorRef.current.getBoundingClientRect();
|
||||
@ -191,7 +295,7 @@ const TagsEditor = forwardRef(({
|
||||
editorRef.current.style.bottom = editorPosition.top + height - window.innerHeight + 'px';
|
||||
}
|
||||
}
|
||||
if (editorContainerRef.current && selectItemRef.current) {
|
||||
if (editorContainerRef.current) {
|
||||
setMaxItemNum(getMaxItemNum());
|
||||
}
|
||||
document.addEventListener('keydown', onHotKey, true);
|
||||
@ -202,9 +306,18 @@ const TagsEditor = forwardRef(({
|
||||
}, [onHotKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const highlightIndex = displayTags.length === 0 ? -1 : 0;
|
||||
setHighlightIndex(highlightIndex);
|
||||
}, [displayTags]);
|
||||
const saved = localStorage.getItem(RECENTLY_USED_TAG_IDS);
|
||||
const ids = saved ? JSON.parse(saved) : [];
|
||||
const tags = ids.map(id => getRowById(tagsData, id)).filter(Boolean);
|
||||
setRecentlyUsed(tags);
|
||||
}, [tagsData, localStorage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tagsData?.rows_tree) {
|
||||
const shownNodes = getShownNodes(tagsData.rows_tree, keyNodeFoldedMap);
|
||||
setNodes(shownNodes);
|
||||
}
|
||||
}, [tagsData, keyNodeFoldedMap, getShownNodes]);
|
||||
|
||||
const renderOptions = useCallback(() => {
|
||||
if (displayTags.length === 0) {
|
||||
@ -214,33 +327,84 @@ const TagsEditor = forwardRef(({
|
||||
|
||||
return displayTags.map((tag, i) => {
|
||||
const tagId = getTagId(tag);
|
||||
const tagName = getTagName(tag);
|
||||
const tagColor = getTagColor(tag);
|
||||
const isSelected = Array.isArray(value) ? value.includes(tagId) : false;
|
||||
return (
|
||||
<div key={tagId} className="sf-metadata-tags-editor-tag-item" ref={selectItemRef}>
|
||||
<div
|
||||
className={classnames('sf-metadata-tags-editor-tag-container pl-2', { 'sf-metadata-tags-editor-tag-container-highlight': i === highlightIndex })}
|
||||
onMouseDown={() => onSelectTag(tagId)}
|
||||
onMouseEnter={() => onMenuMouseEnter(i)}
|
||||
onMouseLeave={() => onMenuMouseLeave(i)}
|
||||
>
|
||||
<div className="sf-metadata-tag-color-and-name">
|
||||
<div className="sf-metadata-tag-color" style={{ backgroundColor: tagColor }}></div>
|
||||
<div className="sf-metadata-tag-name">{tagName}</div>
|
||||
</div>
|
||||
<div className="sf-metadata-tags-editor-tag-check-icon">
|
||||
{isSelected && (<Icon className="sf-metadata-icon" symbol="check-mark" />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TagItem
|
||||
key={tagId}
|
||||
tag={tag}
|
||||
isSelected={value.includes(tagId)}
|
||||
highlight={highlightIndex === i}
|
||||
onSelect={onSelectTag}
|
||||
onMouseEnter={() => onMenuMouseEnter(i, tagId)}
|
||||
onMouseLeave={onMenuMouseLeave}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
}, [displayTags, searchValue, value, highlightIndex, onMenuMouseEnter, onMenuMouseLeave, onSelectTag]);
|
||||
}, [displayTags, searchValue, value, highlightIndex, onSelectTag, onMenuMouseEnter, onMenuMouseLeave]);
|
||||
|
||||
const renderRecentlyUsed = useCallback(() => {
|
||||
return recentlyUsedTags.length > 0 && recentlyUsedTags.map((tag, i) => {
|
||||
const tagId = getTagId(tag);
|
||||
return (
|
||||
<TagItem
|
||||
key={tagId}
|
||||
tag={tag}
|
||||
isSelected={value.includes(tagId)}
|
||||
highlight={highlightIndex === i}
|
||||
onSelect={onSelectTag}
|
||||
onMouseEnter={() => onMenuMouseEnter(i, tagId)}
|
||||
onMouseLeave={onMenuMouseLeave}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
}, [recentlyUsedTags, value, highlightIndex, onSelectTag, onMenuMouseEnter, onMenuMouseLeave]);
|
||||
|
||||
const renderOptionsAsTree = useCallback(() => {
|
||||
if (nodes.length === 0) {
|
||||
const noOptionsTip = searchValue ? gettext('No tags available') : gettext('No tag');
|
||||
return (<span className="none-search-result px-4">{noOptionsTip}</span>);
|
||||
}
|
||||
const showRecentlyUsed = recentlyUsedTags.length > 0 && !searchValue;
|
||||
return (
|
||||
<>
|
||||
{showRecentlyUsed && (
|
||||
<>
|
||||
<div className="sf-metadata-tags-editor-title">{gettext('Recently used tags')}</div>
|
||||
{renderRecentlyUsed()}
|
||||
<div className="sf-metadata-tags-editor-divider"></div>
|
||||
</>
|
||||
)}
|
||||
{!searchValue && <div className="sf-metadata-tags-editor-title">{gettext('All tags')}</div>}
|
||||
{nodes.map((node, i) => {
|
||||
const nodeKey = getTreeNodeKey(node);
|
||||
const tagId = getTreeNodeId(node);
|
||||
const tag = getRowById(tagsData, tagId);
|
||||
if (!tag) return null;
|
||||
|
||||
return (
|
||||
<TagItem
|
||||
node={node}
|
||||
key={`${nodeKey}_${i}`}
|
||||
tag={tag}
|
||||
isSelected={value.includes(tagId)}
|
||||
highlight={highlightNodeIndex === i}
|
||||
onSelect={onSelectTag}
|
||||
onMouseEnter={() => onTreeMenuMouseEnter(i)}
|
||||
onMouseLeave={onTreeMenuMouseLeave}
|
||||
depth={getTreeNodeDepth(node)}
|
||||
hasChildren={checkTreeNodeHasChildNodes(node)}
|
||||
isFolded={keyNodeFoldedMap[nodeKey]}
|
||||
onToggleExpand={() => toggleExpandTreeNode(nodeKey)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}, [nodes, tagsData, value, highlightNodeIndex, searchValue, recentlyUsedTags, renderRecentlyUsed, toggleExpandTreeNode, keyNodeFoldedMap, onSelectTag, onTreeMenuMouseEnter, onTreeMenuMouseLeave]);
|
||||
|
||||
return (
|
||||
<div className="sf-metadata-tags-editor" style={style} ref={editorRef}>
|
||||
<div className={classnames('sf-metadata-tags-editor', { 'tags-tree-container': showTagsAsTree })} style={style} ref={editorRef}>
|
||||
<DeleteTags value={value} tags={tagsData} onDelete={onDeleteTag} />
|
||||
<div className="sf-metadata-search-tags-container">
|
||||
<SearchInput
|
||||
@ -249,10 +413,21 @@ const TagsEditor = forwardRef(({
|
||||
onChange={onChangeSearch}
|
||||
autoFocus={true}
|
||||
className="sf-metadata-search-tags"
|
||||
isClearable={showTagsAsTree}
|
||||
components={{
|
||||
ClearIndicator: ({ clearValue }) => (
|
||||
<i
|
||||
className="search-control attr-action-icon sf3-font sf3-font-x-01"
|
||||
aria-label={gettext('Clear')}
|
||||
onClick={clearValue}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
clearValue={() => setSearchValue('')}
|
||||
/>
|
||||
</div>
|
||||
<div className="sf-metadata-tags-editor-container" ref={editorContainerRef}>
|
||||
{renderOptions()}
|
||||
{showTagsAsTree ? renderOptionsAsTree() : renderOptions()}
|
||||
</div>
|
||||
{isShowCreateBtn && (
|
||||
<CommonAddTool
|
||||
@ -272,6 +447,7 @@ TagsEditor.propTypes = {
|
||||
editorPosition: PropTypes.object,
|
||||
onPressTab: PropTypes.func,
|
||||
updateFileTags: PropTypes.func,
|
||||
showTagsAsTree: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default TagsEditor;
|
||||
|
@ -0,0 +1,42 @@
|
||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container {
|
||||
position: relative;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
color: #212529;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container .sf-metadata-tags-editor-tree-expand-icon {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #666;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sf-metadata-tag-color-and-name {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sf-metadata-tag-color-and-name .sf-metadata-tag-color {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sf-metadata-tag-color-and-name .sf-metadata-tag-name {
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { getTagColor, getTagId, getTagName } from '../../../../../tag/utils/cell';
|
||||
import { NODE_CONTENT_LEFT_INDENT, NODE_ICON_LEFT_INDENT } from '../../../../../components/sf-table/constants/tree';
|
||||
import Icon from '../../../../../components/icon';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const TagItem = ({
|
||||
node,
|
||||
tag,
|
||||
isSelected,
|
||||
highlight,
|
||||
onSelect,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
depth = 0,
|
||||
hasChildren = false,
|
||||
isFolded = false,
|
||||
onToggleExpand
|
||||
}) => {
|
||||
const tagId = getTagId(tag);
|
||||
const tagName = getTagName(tag);
|
||||
const tagColor = getTagColor(tag);
|
||||
const paddingLeft = node ? NODE_CONTENT_LEFT_INDENT + NODE_ICON_LEFT_INDENT * depth : 8;
|
||||
|
||||
return (
|
||||
<div className="sf-metadata-tags-editor-tag-item">
|
||||
<div
|
||||
className={classNames('sf-metadata-tags-editor-tag-container', {
|
||||
'sf-metadata-tags-editor-tag-container-highlight': highlight,
|
||||
})}
|
||||
style={{ paddingLeft }}
|
||||
onMouseDown={!node ? () => onSelect(tagId) : () => {}}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{hasChildren && (
|
||||
<span
|
||||
className="sf-metadata-tags-editor-tree-expand-icon"
|
||||
style={{ left: depth * NODE_ICON_LEFT_INDENT }}
|
||||
onClick={onToggleExpand}
|
||||
>
|
||||
<i className={classNames('sf3-font sf3-font-down', { 'rotate-270': isFolded })}></i>
|
||||
</span>
|
||||
)}
|
||||
<div className="sf-metadata-tag-color-and-name" onClick={node ? () => onSelect(tagId) : () => {}}>
|
||||
<div className="sf-metadata-tag-color" style={{ backgroundColor: tagColor }} />
|
||||
<div className="sf-metadata-tag-name">{tagName}</div>
|
||||
</div>
|
||||
<div className="sf-metadata-tags-editor-tag-check-icon">
|
||||
{isSelected && <Icon className="sf-metadata-icon" symbol="check-mark" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TagItem.propTypes = {
|
||||
node: PropTypes.object,
|
||||
tag: PropTypes.object,
|
||||
isSelected: PropTypes.bool,
|
||||
highlight: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
onMouseEnter: PropTypes.func,
|
||||
onMouseLeave: PropTypes.func,
|
||||
depth: PropTypes.number,
|
||||
hasChildren: PropTypes.bool,
|
||||
isFolded: PropTypes.bool,
|
||||
onToggleExpand: PropTypes.func,
|
||||
};
|
||||
|
||||
export default TagItem;
|
@ -71,23 +71,36 @@ const TagsEditor = ({ record, value, field, updateFileTags }) => {
|
||||
|
||||
const renderEditor = useCallback(() => {
|
||||
if (!showEditor) return null;
|
||||
const { width } = ref.current.getBoundingClientRect();
|
||||
const { width, bottom } = ref.current.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
const shouldPlaceBottom = (viewportHeight - bottom) > 300;
|
||||
|
||||
return (
|
||||
<Popover
|
||||
target={ref}
|
||||
isOpen={true}
|
||||
placement="bottom-end"
|
||||
placement={shouldPlaceBottom ? 'bottom-end' : 'top-end'}
|
||||
hideArrow={true}
|
||||
fade={false}
|
||||
className="sf-metadata-property-editor-popover sf-metadata-tags-property-editor-popover"
|
||||
boundariesElement={document.body}
|
||||
boundariesElement="viewport"
|
||||
popperModifiers={{
|
||||
preventOverflow: {
|
||||
enabled: true,
|
||||
padding: 8
|
||||
},
|
||||
offset: {
|
||||
offset: '0, 8',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Editor
|
||||
saveImmediately={true}
|
||||
value={value}
|
||||
column={{ ...field, width: Math.max(width - 2, 200) }}
|
||||
column={{ ...field, width: Math.max(width - 2, 400) }}
|
||||
record={record}
|
||||
updateFileTags={updateFileTags}
|
||||
showTagsAsTree={true}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user