feat: You can now drag-n-drop notes onto tags to move them from one tag to another (#2334)

This commit is contained in:
Aman Harwara
2023-06-03 01:46:29 +05:30
committed by GitHub
parent 153ec272d2
commit 08c38fc7cf
3 changed files with 83 additions and 17 deletions

View File

@@ -13,6 +13,8 @@ import { ListItemTitle } from './ListItemTitle'
import { log, LoggingDomain } from '@/Logging' import { log, LoggingDomain } from '@/Logging'
import { classNames } from '@standardnotes/utils' import { classNames } from '@standardnotes/utils'
import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintForNoteType' import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintForNoteType'
import { NoteDragDataFormat } from '../Tags/DragNDrop'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
application, application,
@@ -70,6 +72,34 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
const hasOffsetBorder = !isNextItemTiled const hasOffsetBorder = !isNextItemTiled
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const dragPreview = useRef<HTMLDivElement>()
const createDragPreview = () => {
if (!listItemRef.current) {
throw new Error('List item ref is not set')
}
const element = listItemRef.current.cloneNode(true)
// Only keep icon & title in drag preview
Array.from(element.childNodes[1].childNodes).forEach((node, key) => {
if (key !== 0) {
node.remove()
}
})
element.childNodes[2].remove()
if (element instanceof HTMLDivElement) {
element.style.width = `${listItemRef.current.clientWidth}px`
element.style.position = 'absolute'
element.style.top = '0'
element.style.left = '0'
element.style.zIndex = '-100000'
document.body.appendChild(element)
dragPreview.current = element
}
return element as HTMLDivElement
}
return ( return (
<div <div
ref={listItemRef} ref={listItemRef}
@@ -81,6 +111,23 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
)} )}
id={item.uuid} id={item.uuid}
onClick={onClick} onClick={onClick}
draggable={!isMobileScreen}
onDragStart={(event) => {
if (!listItemRef.current) {
return
}
const { dataTransfer } = event
const element = createDragPreview()
dataTransfer.setDragImage(element, 0, 0)
dataTransfer.setData(NoteDragDataFormat, item.uuid)
}}
onDragLeave={() => {
if (dragPreview.current) {
dragPreview.current.remove()
}
}}
> >
{!hideIcon ? ( {!hideIcon ? (
<div className="mr-0 flex flex-col items-center justify-between gap-2 p-4 pr-4"> <div className="mr-0 flex flex-col items-center justify-between gap-2 p-4 pr-4">

View File

@@ -1 +1,2 @@
export const TagDragDataFormat = 'application/x-sn-drag-tag' export const TagDragDataFormat = 'application/x-sn-drag-tag'
export const NoteDragDataFormat = 'application/x-sn-drag-note'

View File

@@ -3,7 +3,7 @@ import { FOCUSABLE_BUT_NOT_TABBABLE, TAG_FOLDERS_FEATURE_NAME } from '@/Constant
import { KeyboardKey } from '@standardnotes/ui-services' import { KeyboardKey } from '@standardnotes/ui-services'
import { FeaturesController } from '@/Controllers/FeaturesController' import { FeaturesController } from '@/Controllers/FeaturesController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController' import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { IconType, SNTag } from '@standardnotes/snjs' import { IconType, SNNote, SNTag } from '@standardnotes/snjs'
import { computed } from 'mobx' import { computed } from 'mobx'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { import {
@@ -23,8 +23,9 @@ import { useFileDragNDrop } from '../FileDragNDropProvider'
import { LinkingController } from '@/Controllers/LinkingController' import { LinkingController } from '@/Controllers/LinkingController'
import { TagListSectionType } from './TagListSection' import { TagListSectionType } from './TagListSection'
import { log, LoggingDomain } from '@/Logging' import { log, LoggingDomain } from '@/Logging'
import { TagDragDataFormat } from './DragNDrop' import { NoteDragDataFormat, TagDragDataFormat } from './DragNDrop'
import { usePremiumModal } from '@/Hooks/usePremiumModal' import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { useApplication } from '../ApplicationProvider'
type Props = { type Props = {
tag: SNTag tag: SNTag
@@ -40,7 +41,9 @@ const PADDING_BASE_PX = 14
const PADDING_PER_LEVEL_PX = 21 const PADDING_PER_LEVEL_PX = 21
export const TagsListItem: FunctionComponent<Props> = observer( export const TagsListItem: FunctionComponent<Props> = observer(
({ tag, type, features, navigationController: navigationController, level, onContextMenu, linkingController }) => { ({ tag, type, features, navigationController, level, onContextMenu, linkingController }) => {
const application = useApplication()
const [title, setTitle] = useState(tag.title || '') const [title, setTitle] = useState(tag.title || '')
const [subtagTitle, setSubtagTitle] = useState('') const [subtagTitle, setSubtagTitle] = useState('')
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
@@ -200,7 +203,10 @@ export const TagsListItem: FunctionComponent<Props> = observer(
) )
const onDragEnter: DragEventHandler<HTMLDivElement> = useCallback((event): void => { const onDragEnter: DragEventHandler<HTMLDivElement> = useCallback((event): void => {
if (event.dataTransfer.types.includes(TagDragDataFormat)) { if (
event.dataTransfer.types.includes(TagDragDataFormat) ||
event.dataTransfer.types.includes(NoteDragDataFormat)
) {
event.preventDefault() event.preventDefault()
setIsBeingDraggedOver(true) setIsBeingDraggedOver(true)
} }
@@ -211,30 +217,42 @@ export const TagsListItem: FunctionComponent<Props> = observer(
}, []) }, [])
const onDragOver: DragEventHandler<HTMLDivElement> = useCallback((event): void => { const onDragOver: DragEventHandler<HTMLDivElement> = useCallback((event): void => {
if (event.dataTransfer.types.includes(TagDragDataFormat)) { if (
event.dataTransfer.types.includes(TagDragDataFormat) ||
event.dataTransfer.types.includes(NoteDragDataFormat)
) {
event.preventDefault() event.preventDefault()
} }
}, []) }, [])
const onDrop: DragEventHandler<HTMLDivElement> = useCallback( const onDrop: DragEventHandler<HTMLDivElement> = useCallback(
(event): void => { async (event) => {
setIsBeingDraggedOver(false) setIsBeingDraggedOver(false)
const draggedTagUuid = event.dataTransfer.getData(TagDragDataFormat) const draggedTagUuid = event.dataTransfer.getData(TagDragDataFormat)
if (!draggedTagUuid) { const draggedNoteUuid = event.dataTransfer.getData(NoteDragDataFormat)
return
}
if (!navigationController.isValidTagParent(tag, { uuid: draggedTagUuid } as SNTag)) {
return
}
if (!hasFolders) {
premiumModal.activate(TAG_FOLDERS_FEATURE_NAME)
return
}
if (draggedTagUuid) { if (draggedTagUuid) {
if (!navigationController.isValidTagParent(tag, { uuid: draggedTagUuid } as SNTag)) {
return
}
if (!hasFolders) {
premiumModal.activate(TAG_FOLDERS_FEATURE_NAME)
return
}
void navigationController.assignParent(draggedTagUuid, tag.uuid) void navigationController.assignParent(draggedTagUuid, tag.uuid)
return
} else if (draggedNoteUuid) {
const currentTag = navigationController.selected
const shouldSwapTags = currentTag instanceof SNTag && currentTag.uuid !== tag.uuid
const note = application.items.findSureItem<SNNote>(draggedNoteUuid)
await linkingController.linkItems(note, tag)
if (shouldSwapTags) {
await linkingController.unlinkItems(note, currentTag)
}
return
} }
}, },
[hasFolders, navigationController, premiumModal, tag], [application.items, hasFolders, linkingController, navigationController, premiumModal, tag],
) )
return ( return (