fix: more styles chanes

feat/wb-linkings
Peng Xiao 2022-11-20 17:25:43 +08:00
parent 77f77422e6
commit 4f96e42b01
9 changed files with 546 additions and 477 deletions

View File

@ -2,6 +2,6 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
children: React.ReactNode
}
export function Button(props: ButtonProps) {
return <button className="tl-button" {...props} />
export function Button({ className, ...rest }: ButtonProps) {
return <button className={'tl-button ' + (className ?? '')} {...rest} />
}

View File

@ -0,0 +1,41 @@
import React from 'react'
import { TablerIcon } from '../icons'
export const CircleButton = ({
active,
style,
icon,
otherIcon,
onClick,
}: {
active?: boolean
style?: React.CSSProperties
icon: string
otherIcon?: string
onClick: () => void
}) => {
const [recentlyChanged, setRecentlyChanged] = React.useState(false)
React.useEffect(() => {
setRecentlyChanged(true)
const timer = setTimeout(() => {
setRecentlyChanged(false)
}, 500)
return () => clearTimeout(timer)
}, [active])
return (
<button
data-active={active}
data-recently-changed={recentlyChanged}
style={style}
className="tl-circle-button"
onMouseDown={onClick}
>
<div className="tl-circle-button-icons-wrapper" data-icons-count={otherIcon ? 2 : 1}>
{otherIcon && <TablerIcon name={otherIcon} />}
<TablerIcon name={icon} />
</div>
</button>
)
}

View File

@ -1 +1,2 @@
export * from './Button'
export * from './CircleButton'

View File

@ -56,7 +56,11 @@ export const PopoverButton = observer(
return (
<Popover.Root onOpenChange={o => setIsOpen(o)}>
<Popover.Trigger {...rest} data-border={border} className="tl-popover-trigger-button">
<Popover.Trigger
{...rest}
data-border={border}
className="tl-button tl-popover-trigger-button"
>
{label}
</Popover.Trigger>

View File

@ -0,0 +1,437 @@
import { useApp, useDebouncedValue } from '@tldraw/react'
import { observer } from 'mobx-react-lite'
import React from 'react'
import { Virtuoso } from 'react-virtuoso'
import { LogseqPortalShape, type Shape } from '../../lib'
import { LogseqContext, SearchResult } from '../../lib/logseq-context'
import { CircleButton } from '../Button'
import { TablerIcon } from '../icons'
import { TextInput } from '../inputs/TextInput'
interface LogseqQuickSearchProps {
onChange: (id: string) => void
className?: string
create?: boolean
placeholder?: string
style?: React.CSSProperties
onBlur?: () => void
shape?: LogseqPortalShape
}
const LogseqTypeTag = ({
type,
active,
}: {
type: 'B' | 'P' | 'BA' | 'PA' | 'WA' | 'WP' | 'BS' | 'PS'
active?: boolean
}) => {
const nameMapping = {
B: 'block',
P: 'page',
WP: 'whiteboard',
BA: 'new-block',
PA: 'new-page',
WA: 'new-whiteboard',
BS: 'block-search',
PS: 'page-search',
}
return (
<span className="tl-type-tag" data-active={active}>
<i className={`tie tie-${nameMapping[type]}`} />
</span>
)
}
function escapeRegExp(text: string) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
}
const highlightedJSX = (input: string, keyword: string) => {
return (
<span>
{input
.split(new RegExp(`(${escapeRegExp(keyword)})`, 'gi'))
.map((part, index) => {
if (index % 2 === 1) {
return <mark className="tl-highlighted">{part}</mark>
}
return part
})
.map((frag, idx) => (
<React.Fragment key={idx}>{frag}</React.Fragment>
))}
</span>
)
}
const useSearch = (q: string, searchFilter: 'B' | 'P' | null) => {
const { handlers } = React.useContext(LogseqContext)
const [results, setResults] = React.useState<SearchResult | null>(null)
const dq = useDebouncedValue(q, 200)
React.useEffect(() => {
let canceled = false
if (dq.length > 0) {
const filter = { 'pages?': true, 'blocks?': true, 'files?': false }
if (searchFilter === 'B') {
filter['pages?'] = false
} else if (searchFilter === 'P') {
filter['blocks?'] = false
}
handlers.search(dq, filter).then(_results => {
if (!canceled) {
setResults(_results)
}
})
} else {
setResults(null)
}
return () => {
canceled = true
}
}, [dq, handlers?.search])
return results
}
export const LogseqQuickSearch = observer(
({ className, style, placeholder, create, onChange, onBlur, shape }: LogseqQuickSearchProps) => {
const [q, setQ] = React.useState(LogseqPortalShape.defaultSearchQuery)
const [searchFilter, setSearchFilter] = React.useState<'B' | 'P' | null>(
LogseqPortalShape.defaultSearchFilter
)
const rInput = React.useRef<HTMLInputElement>(null)
const { handlers, renderers } = React.useContext(LogseqContext)
const app = useApp<Shape>()
const finishSearching = React.useCallback((id: string) => {
onChange(id)
rInput.current?.blur()
if (id) {
LogseqPortalShape.defaultSearchQuery = ''
LogseqPortalShape.defaultSearchFilter = null
}
}, [])
// TODO: should move this implementation to LogseqPortalShape
const onAddBlock = React.useCallback(
(content: string) => {
const uuid = handlers?.addNewBlock(content)
if (uuid) {
finishSearching(uuid)
// wait until the editor is mounted
setTimeout(() => {
app.api.editShape(shape)
window.logseq?.api?.edit_block?.(uuid)
})
}
return uuid
},
[shape]
)
const optionsWrapperRef = React.useRef<HTMLDivElement>(null)
const [focusedOptionIdx, setFocusedOptionIdx] = React.useState<number>(0)
const searchResult = useSearch(q, searchFilter)
const [prefixIcon, setPrefixIcon] = React.useState<string>('circle-plus')
React.useEffect(() => {
// autofocus seems not to be working
setTimeout(() => {
rInput.current?.focus()
})
}, [searchFilter])
React.useEffect(() => {
LogseqPortalShape.defaultSearchQuery = q
LogseqPortalShape.defaultSearchFilter = searchFilter
}, [q, searchFilter])
type Option = {
actionIcon: 'search' | 'circle-plus'
onChosen: () => boolean // return true if the action was handled
element: React.ReactNode
}
const options: Option[] = React.useMemo(() => {
const options: Option[] = []
const Breadcrumb = renderers?.Breadcrumb
if (!Breadcrumb || !handlers) {
return []
}
if (create) {
// New block option
options.push({
actionIcon: 'circle-plus',
onChosen: () => {
return !!onAddBlock(q)
},
element: (
<div className="tl-quick-search-option-row">
<LogseqTypeTag active type="BA" />
{q.length > 0 ? (
<>
<strong>New block:</strong>
{q}
</>
) : (
<strong>New block</strong>
)}
</div>
),
})
}
// New page or whiteboard option when no exact match
if (!searchResult?.pages?.some(p => p.toLowerCase() === q.toLowerCase()) && q && create) {
options.push(
{
actionIcon: 'circle-plus',
onChosen: () => {
finishSearching(q)
return true
},
element: (
<div className="tl-quick-search-option-row">
<LogseqTypeTag active type="PA" />
<strong>New page:</strong>
{q}
</div>
),
},
{
actionIcon: 'circle-plus',
onChosen: () => {
handlers?.addNewWhiteboard(q)
finishSearching(q)
return true
},
element: (
<div className="tl-quick-search-option-row">
<LogseqTypeTag active type="WA" />
<strong>New whiteboard:</strong>
{q}
</div>
),
}
)
}
// search filters
if (q.length === 0 && searchFilter === null) {
options.push(
{
actionIcon: 'search',
onChosen: () => {
setSearchFilter('B')
return true
},
element: (
<div className="tl-quick-search-option-row">
<LogseqTypeTag type="BS" />
Search only blocks
</div>
),
},
{
actionIcon: 'search',
onChosen: () => {
setSearchFilter('P')
return true
},
element: (
<div className="tl-quick-search-option-row">
<LogseqTypeTag type="PS" />
Search only pages
</div>
),
}
)
}
// Page results
if ((!searchFilter || searchFilter === 'P') && searchResult && searchResult.pages) {
options.push(
...searchResult.pages.map(page => {
return {
actionIcon: 'search' as 'search',
onChosen: () => {
finishSearching(page)
return true
},
element: (
<div className="tl-quick-search-option-row">
<LogseqTypeTag type={handlers.isWhiteboardPage(page) ? 'WP' : 'P'} />
{highlightedJSX(page, q)}
</div>
),
}
})
)
}
// Block results
if ((!searchFilter || searchFilter === 'B') && searchResult && searchResult.blocks) {
options.push(
...searchResult.blocks
.filter(block => block.content && block.uuid)
.map(({ content, uuid }) => {
const block = handlers.queryBlockByUUID(uuid)
return {
actionIcon: 'search' as 'search',
onChosen: () => {
if (block) {
finishSearching(uuid)
window.logseq?.api?.set_blocks_id?.([uuid])
return true
}
return false
},
element: block ? (
<>
<div className="tl-quick-search-option-row">
<LogseqTypeTag type="B" />
<div className="tl-quick-search-option-breadcrumb">
<Breadcrumb blockId={uuid} />
</div>
</div>
<div className="tl-quick-search-option-row">
<div className="tl-quick-search-option-placeholder" />
{highlightedJSX(content, q)}
</div>
</>
) : (
<div className="tl-quick-search-option-row">
Cache is outdated. Please click the 'Re-index' button in the graph's dropdown
menu.
</div>
),
}
})
)
}
return options
}, [q, searchFilter, searchResult, renderers?.Breadcrumb, handlers])
React.useEffect(() => {
const keydownListener = (e: KeyboardEvent) => {
let newIndex = focusedOptionIdx
if (e.key === 'ArrowDown') {
newIndex = Math.min(options.length - 1, focusedOptionIdx + 1)
} else if (e.key === 'ArrowUp') {
newIndex = Math.max(0, focusedOptionIdx - 1)
} else if (e.key === 'Enter') {
options[focusedOptionIdx]?.onChosen()
e.stopPropagation()
e.preventDefault()
} else if (e.key === 'Backspace' && q.length === 0) {
setSearchFilter(null)
} else if (e.key === 'Escape') {
finishSearching('')
}
if (newIndex !== focusedOptionIdx) {
const option = options[newIndex]
setFocusedOptionIdx(newIndex)
setPrefixIcon(option.actionIcon)
e.stopPropagation()
e.preventDefault()
const optionElement = optionsWrapperRef.current?.querySelector(
'.tl-quick-search-option:nth-child(' + (newIndex + 1) + ')'
)
if (optionElement) {
// @ts-expect-error we are using scrollIntoViewIfNeeded, which is not in standards
optionElement?.scrollIntoViewIfNeeded(false)
}
}
}
document.addEventListener('keydown', keydownListener, true)
return () => {
document.removeEventListener('keydown', keydownListener, true)
}
}, [options, focusedOptionIdx, q])
return (
<div className={'tl-quick-search ' + (className ?? '')} style={style}>
<CircleButton
icon={prefixIcon}
onClick={() => {
options[focusedOptionIdx]?.onChosen()
}}
/>
<div className="tl-quick-search-input-container">
{searchFilter && (
<div className="tl-quick-search-input-filter">
<LogseqTypeTag type={searchFilter} />
{searchFilter === 'B' ? 'Search blocks' : 'Search pages'}
<div
className="tl-quick-search-input-filter-remove"
onClick={() => setSearchFilter(null)}
>
<TablerIcon name="x" />
</div>
</div>
)}
<TextInput
ref={rInput}
type="text"
value={q}
className="tl-quick-search-input"
placeholder={placeholder ?? 'Create or search your graph...'}
onChange={q => setQ(q.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') {
finishSearching(q)
}
}}
onBlur={onBlur}
/>
</div>
{/* TODO: refactor to radix-ui popover */}
{options.length > 0 && (
<div
onWheelCapture={e => e.stopPropagation()}
className="tl-quick-search-options"
ref={optionsWrapperRef}
>
<Virtuoso
style={{ height: Math.min(Math.max(1, options.length), 12) * 40 }}
totalCount={options.length}
itemContent={index => {
const { actionIcon, onChosen, element } = options[index]
return (
<div
key={index}
data-focused={index === focusedOptionIdx}
className="tl-quick-search-option"
tabIndex={0}
onMouseEnter={() => {
setPrefixIcon(actionIcon)
setFocusedOptionIdx(index)
}}
// we have to use mousedown && stop propagation EARLY, otherwise some
// default behavior of clicking the rendered elements will happen
onMouseDownCapture={e => {
if (onChosen()) {
e.stopPropagation()
e.preventDefault()
}
}}
>
{element}
</div>
)
}}
/>
</div>
)}
</div>
)
}
)

View File

@ -0,0 +1 @@
export * from './QuickSearch'

View File

@ -5,7 +5,7 @@ import { LogseqContext } from '../../lib/logseq-context'
import { Button } from '../Button'
import { TablerIcon } from '../icons'
import { PopoverButton } from '../PopoverButton'
import { TextInput } from './TextInput'
import { LogseqQuickSearch } from '../QuickSearch'
interface ShapeLinksInputProps extends React.HTMLAttributes<HTMLButtonElement> {
shapeType: string
@ -33,7 +33,9 @@ function ShapeLinkItem({
return (
<div className="tl-shape-links-panel-item color-level">
<TablerIcon name={type === 'P' ? 'page' : 'block'} />
{type === 'P' ? <PageNameLink pageName={id} /> : <Breadcrumb levelLimit={2} blockId={id} />}
<div className="whitespace-pre break-all overflow-hidden text-ellipsis">
{type === 'P' ? <PageNameLink pageName={id} /> : <Breadcrumb levelLimit={1} blockId={id} />}
</div>
<div className="flex-1" />
<Button title="Open Page" type="button" onClick={() => handlers?.redirectToPage(id)}>
<TablerIcon name="external-link" />
@ -64,7 +66,7 @@ export function ShapeLinksInput({
...rest
}: ShapeLinksInputProps) {
const noOfLinks = refs.length + (pageId ? 1 : 0)
const [value, setValue] = React.useState('')
const [showQuickSearch, setShowQuickSearch] = React.useState(false)
return (
<PopoverButton
@ -98,20 +100,36 @@ export function ShapeLinksInput({
This <strong>{shapeType}</strong> can be linked to any other block, page or whiteboard
element you have stored in Logseq.
</div>
<TextInput
value={value}
onChange={e => {
setValue(e.target.value)
}}
onKeyDown={e => {
if (e.key === 'Enter') {
if (value && !refs.includes(value)) {
onRefsChange([...refs, value])
<div className="h-2" />
{showQuickSearch ? (
<LogseqQuickSearch
style={{
width: 'calc(100% - 46px)',
marginLeft: '46px',
}}
onBlur={() => setShowQuickSearch(false)}
placeholder="Start typing to search..."
onChange={newValue => {
if (newValue && !refs.includes(newValue)) {
onRefsChange([...refs, newValue])
setShowQuickSearch(false)
}
}
e.stopPropagation()
}}
/>
}}
/>
) : (
<div>
<Button
className="tl-shape-links-panel-add-button"
onClick={() => setShowQuickSearch(true)}
>
<TablerIcon name="plus" />
Add a new link
</Button>
</div>
)}
<div className="h-2" />
<div className="flex flex-col items-stretch gap-2">
{refs.map((ref, i) => {
return (

View File

@ -1,25 +1,23 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
delay,
getComputedColor,
TLBoxShape,
TLBoxShapeProps,
TLResetBoundsInfo,
TLResizeInfo,
validUUID,
getComputedColor,
} from '@tldraw/core'
import { Virtuoso } from 'react-virtuoso'
import { HTMLContainer, TLComponentProps, useApp } from '@tldraw/react'
import { useDebouncedValue } from '@tldraw/react'
import Vec from '@tldraw/vec'
import { action, computed, makeObservable } from 'mobx'
import { observer } from 'mobx-react-lite'
import * as React from 'react'
import type { SizeLevel, Shape } from '.'
import { TablerIcon } from '../../components/icons'
import { TextInput } from '../../components/inputs/TextInput'
import type { Shape, SizeLevel } from '.'
import { CircleButton } from '../../components/Button'
import { LogseqQuickSearch } from '../../components/QuickSearch'
import { useCameraMovingRef } from '../../hooks/useCameraMoving'
import { LogseqContext, type SearchResult } from '../logseq-context'
import { LogseqContext } from '../logseq-context'
import { BindingIndicator } from './BindingIndicator'
import { CustomStyleProps, withClampedStyles } from './style-props'
@ -37,10 +35,6 @@ export interface LogseqPortalShapeProps extends TLBoxShapeProps, CustomStyleProp
scaleLevel?: SizeLevel
}
interface LogseqQuickSearchProps {
onChange: (id: string) => void
}
const levelToScale = {
xs: 0.5,
sm: 0.8,
@ -50,30 +44,6 @@ const levelToScale = {
xxl: 3,
}
const LogseqTypeTag = ({
type,
active,
}: {
type: 'B' | 'P' | 'BA' | 'PA' | 'WA' | 'WP' | 'BS' | 'PS'
active?: boolean
}) => {
const nameMapping = {
B: 'block',
P: 'page',
WP: 'whiteboard',
BA: 'new-block',
PA: 'new-page',
WA: 'new-whiteboard',
BS: 'block-search',
PS: 'page-search',
}
return (
<span className="tl-type-tag" data-active={active}>
<i className={`tie tie-${nameMapping[type]}`} />
</span>
)
}
const LogseqPortalShapeHeader = observer(
({
type,
@ -110,97 +80,6 @@ const LogseqPortalShapeHeader = observer(
}
)
function escapeRegExp(text: string) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
}
const highlightedJSX = (input: string, keyword: string) => {
return (
<span>
{input
.split(new RegExp(`(${escapeRegExp(keyword)})`, 'gi'))
.map((part, index) => {
if (index % 2 === 1) {
return <mark className="tl-highlighted">{part}</mark>
}
return part
})
.map((frag, idx) => (
<React.Fragment key={idx}>{frag}</React.Fragment>
))}
</span>
)
}
const useSearch = (q: string, searchFilter: 'B' | 'P' | null) => {
const { handlers } = React.useContext(LogseqContext)
const [results, setResults] = React.useState<SearchResult | null>(null)
const dq = useDebouncedValue(q, 200)
React.useEffect(() => {
let canceled = false
if (dq.length > 0) {
const filter = { 'pages?': true, 'blocks?': true, 'files?': false }
if (searchFilter === 'B') {
filter['pages?'] = false
} else if (searchFilter === 'P') {
filter['blocks?'] = false
}
handlers.search(dq, filter).then(_results => {
if (!canceled) {
setResults(_results)
}
})
} else {
setResults(null)
}
return () => {
canceled = true
}
}, [dq, handlers?.search])
return results
}
const CircleButton = ({
active,
style,
icon,
otherIcon,
onClick,
}: {
active?: boolean
style?: React.CSSProperties
icon: string
otherIcon?: string
onClick: () => void
}) => {
const [recentlyChanged, setRecentlyChanged] = React.useState(false)
React.useEffect(() => {
setRecentlyChanged(true)
const timer = setTimeout(() => {
setRecentlyChanged(false)
}, 500)
return () => clearTimeout(timer)
}, [active])
return (
<button
data-active={active}
data-recently-changed={recentlyChanged}
style={style}
className="tl-circle-button"
onMouseDown={onClick}
>
<div className="tl-circle-button-icons-wrapper" data-icons-count={otherIcon ? 2 : 1}>
{otherIcon && <TablerIcon name={otherIcon} />}
<TablerIcon name={icon} />
</div>
</button>
)
}
export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
static id = 'logseq-portal'
static defaultSearchQuery = ''
@ -384,332 +263,6 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
})
}
LogseqQuickSearch = observer(({ onChange }: LogseqQuickSearchProps) => {
const [q, setQ] = React.useState(LogseqPortalShape.defaultSearchQuery)
const [searchFilter, setSearchFilter] = React.useState<'B' | 'P' | null>(
LogseqPortalShape.defaultSearchFilter
)
const rInput = React.useRef<HTMLInputElement>(null)
const { handlers, renderers } = React.useContext(LogseqContext)
const app = useApp<Shape>()
const finishCreating = React.useCallback((id: string) => {
onChange(id)
rInput.current?.blur()
if (id) {
LogseqPortalShape.defaultSearchQuery = ''
LogseqPortalShape.defaultSearchFilter = null
}
}, [])
const onAddBlock = React.useCallback((content: string) => {
const uuid = handlers?.addNewBlock(content)
if (uuid) {
finishCreating(uuid)
// wait until the editor is mounted
setTimeout(() => {
app.api.editShape(this)
window.logseq?.api?.edit_block?.(uuid)
})
}
return uuid
}, [])
const optionsWrapperRef = React.useRef<HTMLDivElement>(null)
const [focusedOptionIdx, setFocusedOptionIdx] = React.useState<number>(0)
const searchResult = useSearch(q, searchFilter)
const [prefixIcon, setPrefixIcon] = React.useState<string>('circle-plus')
React.useEffect(() => {
// autofocus seems not to be working
setTimeout(() => {
rInput.current?.focus()
})
}, [searchFilter])
React.useEffect(() => {
LogseqPortalShape.defaultSearchQuery = q
LogseqPortalShape.defaultSearchFilter = searchFilter
}, [q, searchFilter])
type Option = {
actionIcon: 'search' | 'circle-plus'
onChosen: () => boolean // return true if the action was handled
element: React.ReactNode
}
const options: Option[] = React.useMemo(() => {
const options: Option[] = []
const Breadcrumb = renderers?.Breadcrumb
if (!Breadcrumb || !handlers) {
return []
}
// New block option
options.push({
actionIcon: 'circle-plus',
onChosen: () => {
return !!onAddBlock(q)
},
element: (
<div className="tl-quick-search-option-row">
<LogseqTypeTag active type="BA" />
{q.length > 0 ? (
<>
<strong>New block:</strong>
{q}
</>
) : (
<strong>New block</strong>
)}
</div>
),
})
// New page or whiteboard option when no exact match
if (!searchResult?.pages?.some(p => p.toLowerCase() === q.toLowerCase()) && q) {
options.push(
{
actionIcon: 'circle-plus',
onChosen: () => {
finishCreating(q)
return true
},
element: (
<div className="tl-quick-search-option-row">
<LogseqTypeTag active type="PA" />
<strong>New page:</strong>
{q}
</div>
),
},
{
actionIcon: 'circle-plus',
onChosen: () => {
handlers?.addNewWhiteboard(q)
finishCreating(q)
return true
},
element: (
<div className="tl-quick-search-option-row">
<LogseqTypeTag active type="WA" />
<strong>New whiteboard:</strong>
{q}
</div>
),
}
)
}
// search filters
if (q.length === 0 && searchFilter === null) {
options.push(
{
actionIcon: 'search',
onChosen: () => {
setSearchFilter('B')
return true
},
element: (
<div className="tl-quick-search-option-row">
<LogseqTypeTag type="BS" />
Search only blocks
</div>
),
},
{
actionIcon: 'search',
onChosen: () => {
setSearchFilter('P')
return true
},
element: (
<div className="tl-quick-search-option-row">
<LogseqTypeTag type="PS" />
Search only pages
</div>
),
}
)
}
// Page results
if ((!searchFilter || searchFilter === 'P') && searchResult && searchResult.pages) {
options.push(
...searchResult.pages.map(page => {
return {
actionIcon: 'search' as 'search',
onChosen: () => {
finishCreating(page)
return true
},
element: (
<div className="tl-quick-search-option-row">
<LogseqTypeTag type={handlers.isWhiteboardPage(page) ? 'WP' : 'P'} />
{highlightedJSX(page, q)}
</div>
),
}
})
)
}
// Block results
if ((!searchFilter || searchFilter === 'B') && searchResult && searchResult.blocks) {
options.push(
...searchResult.blocks
.filter(block => block.content && block.uuid)
.map(({ content, uuid }) => {
const block = handlers.queryBlockByUUID(uuid)
return {
actionIcon: 'search' as 'search',
onChosen: () => {
if (block) {
finishCreating(uuid)
window.logseq?.api?.set_blocks_id?.([uuid])
return true
}
return false
},
element: block ? (
<>
<div className="tl-quick-search-option-row">
<LogseqTypeTag type="B" />
<div className="tl-quick-search-option-breadcrumb">
<Breadcrumb blockId={uuid} />
</div>
</div>
<div className="tl-quick-search-option-row">
<div className="tl-quick-search-option-placeholder" />
{highlightedJSX(content, q)}
</div>
</>
) : (
<div className="tl-quick-search-option-row">
Cache is outdated. Please click the 'Re-index' button in the graph's dropdown
menu.
</div>
),
}
})
)
}
return options
}, [q, searchFilter, searchResult, renderers?.Breadcrumb, handlers])
React.useEffect(() => {
const keydownListener = (e: KeyboardEvent) => {
let newIndex = focusedOptionIdx
if (e.key === 'ArrowDown') {
newIndex = Math.min(options.length - 1, focusedOptionIdx + 1)
} else if (e.key === 'ArrowUp') {
newIndex = Math.max(0, focusedOptionIdx - 1)
} else if (e.key === 'Enter') {
options[focusedOptionIdx]?.onChosen()
e.stopPropagation()
e.preventDefault()
} else if (e.key === 'Backspace' && q.length === 0) {
setSearchFilter(null)
} else if (e.key === 'Escape') {
finishCreating('')
}
if (newIndex !== focusedOptionIdx) {
const option = options[newIndex]
setFocusedOptionIdx(newIndex)
setPrefixIcon(option.actionIcon)
e.stopPropagation()
e.preventDefault()
const optionElement = optionsWrapperRef.current?.querySelector(
'.tl-quick-search-option:nth-child(' + (newIndex + 1) + ')'
)
if (optionElement) {
// @ts-expect-error we are using scrollIntoViewIfNeeded, which is not in standards
optionElement?.scrollIntoViewIfNeeded(false)
}
}
}
document.addEventListener('keydown', keydownListener, true)
return () => {
document.removeEventListener('keydown', keydownListener, true)
}
}, [options, focusedOptionIdx, q])
return (
<div className="tl-quick-search">
<CircleButton
icon={prefixIcon}
onClick={() => {
options[focusedOptionIdx]?.onChosen()
}}
/>
<div className="tl-quick-search-input-container">
{searchFilter && (
<div className="tl-quick-search-input-filter">
<LogseqTypeTag type={searchFilter} />
{searchFilter === 'B' ? 'Search blocks' : 'Search pages'}
<div
className="tl-quick-search-input-filter-remove"
onClick={() => setSearchFilter(null)}
>
<TablerIcon name="x" />
</div>
</div>
)}
<TextInput
ref={rInput}
type="text"
value={q}
className="tl-quick-search-input"
placeholder="Create or search your graph..."
onChange={q => setQ(q.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') {
finishCreating(q)
}
}}
/>
</div>
<div className="tl-quick-search-options" ref={optionsWrapperRef}>
<Virtuoso
style={{ height: Math.min(Math.max(1, options.length), 12) * 40 }}
totalCount={options.length}
itemContent={index => {
const { actionIcon, onChosen, element } = options[index]
return (
<div
key={index}
data-focused={index === focusedOptionIdx}
className="tl-quick-search-option"
tabIndex={0}
onMouseEnter={() => {
setPrefixIcon(actionIcon)
setFocusedOptionIdx(index)
}}
// we have to use mousedown && stop propagation EARLY, otherwise some
// default behavior of clicking the rendered elements will happen
onMouseDownCapture={e => {
if (onChosen()) {
e.stopPropagation()
e.preventDefault()
}
}}
>
{element}
</div>
)
}}
/>
</div>
</div>
)
})
PortalComponent = observer(({}: TLComponentProps) => {
const {
props: { pageId, fill, opacity },
@ -852,7 +405,6 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
}, [])
const PortalComponent = this.PortalComponent
const LogseqQuickSearch = this.LogseqQuickSearch
const blockContent = React.useMemo(() => {
if (pageId && this.props.blockType === 'B') {
@ -889,7 +441,12 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
}}
>
{isCreating ? (
<LogseqQuickSearch onChange={onPageNameChanged} />
<LogseqQuickSearch
onChange={onPageNameChanged}
create
shape={this}
placeholder="Create or search your graph..."
/>
) : (
<>
<div

View File

@ -572,8 +572,9 @@ button.tl-select-input-trigger {
}
.tl-quick-search-input-container {
@apply flex items-center rounded-lg text-base;
@apply flex items-center rounded-lg text-base max-w-full;
min-height: 40px;
background-color: var(--ls-secondary-background-color);
padding: 6px 16px;
box-shadow: var(--shadow-small);
@ -618,7 +619,11 @@ button.tl-select-input-trigger {
.tl-text-input {
@apply absolute inset-0;
outline: none;
outline: none !important;
}
.tl-quick-search .tl-text-input {
border: none;
}
.tl-input-hidden {
@ -633,7 +638,7 @@ button.tl-select-input-trigger {
}
.tl-quick-search-options {
@apply absolute left-0 w-full flex;
@apply absolute left-0 w-full flex z-10;
top: calc(100% + 12px);
background-color: var(--ls-primary-background-color);
@ -1025,7 +1030,7 @@ html[data-theme='dark'] {
.tl-shape-links-panel,
.tl-shape-links-reference-panel {
@apply p-3;
width: 320px;
width: 340px;
color: var(--ls-primary-text-color);
}
@ -1046,6 +1051,11 @@ html[data-theme='dark'] {
}
}
.tl-shape-links-panel-add-button {
@apply w-full font-medium text-base h-[40px];
background-color: var(--ls-secondary-background-color);
}
.tl-popover-trigger-button {
@apply rounded text-sm;