Browse Source

fix: quick add by block or page

Peng Xiao 3 years ago
parent
commit
7ba324bfd2

+ 1 - 1
package.json

@@ -84,7 +84,7 @@
         "@logseq/react-tweet-embed": "1.3.1-1",
         "@sentry/react": "^6.18.2",
         "@sentry/tracing": "^6.18.2",
-        "@tabler/icons": "1.54.0",
+        "@tabler/icons": "1.78.0",
         "@tippyjs/react": "4.2.5",
         "bignumber.js": "^9.0.2",
         "capacitor-voice-recorder": "2.1.0",

+ 7 - 2
src/main/frontend/extensions/tldraw.cljs

@@ -2,12 +2,13 @@
   (:require ["/tldraw-logseq" :as TldrawLogseq]
             [frontend.components.block :as block]
             [frontend.components.page :as page]
+            [frontend.handler.search :as search]
             [frontend.handler.whiteboard :as whiteboard-handler]
             [frontend.rum :as r]
-            [frontend.search :as search]
             [frontend.state :as state]
             [frontend.util :as util]
             [goog.object :as gobj]
+            [promesa.core :as p]
             [rum.core :as rum]))
 
 (def tldraw (r/adapt-class (gobj/get TldrawLogseq "App")))
@@ -37,6 +38,10 @@
           client-y (gobj/get e "clientY")]
       (whiteboard-handler/add-new-block-shape! uuid client-x client-y))))
 
+(defn search-handler [q]
+  (p/let [results (search/search q)]
+    (clj->js results)))
+
 (rum/defc tldraw-app
   [name block-id]
   (let [data (whiteboard-handler/page-name->tldr! name block-id)
@@ -62,7 +67,7 @@
                             :Block block-cp
                             :Breadcrumb breadcrumb
                             :PageNameLink page-name-link}
-                :handlers (clj->js {:search (comp clj->js vec search/page-search)
+                :handlers (clj->js {:search search-handler
                                     :addNewBlock (fn [content]
                                                    (str (whiteboard-handler/add-new-block! name content)))})
                 :onMount (fn [app] (set-tln ^js app))

+ 2 - 1
src/main/frontend/handler/search.cljs

@@ -48,7 +48,8 @@
                          {:pages (search/page-search q)
                           :files (search/file-search q)}))
                search-key (if more? :search/more-result :search/result)]
-           (swap! state/state assoc search-key result)))))))
+           (swap! state/state assoc search-key result)
+           result))))))
 
 (defn clear-search!
   ([]

+ 3 - 10
tldraw/apps/tldraw-logseq/src/app.tsx

@@ -14,7 +14,7 @@ import { ContextBar } from '~components/ContextBar/ContextBar'
 import { useFileDrop } from '~hooks/useFileDrop'
 import { usePaste } from '~hooks/usePaste'
 import { useQuickAdd } from '~hooks/useQuickAdd'
-import { LogseqContext } from '~lib/logseq-context'
+import { LogseqContext, LogseqContextValue } from '~lib/logseq-context'
 import { Shape, shapes } from '~lib/shapes'
 import {
   HighlighterTool,
@@ -47,15 +47,8 @@ const tools: TLReactToolConstructor<Shape>[] = [
 ]
 
 interface LogseqTldrawProps {
-  renderers: {
-    Page: React.FC
-    Breadcrumb: React.FC
-    PageNameLink: React.FC
-  }
-  handlers: {
-    search: (query: string) => string[]
-    addNewBlock: (content: string) => string
-  }
+  renderers: LogseqContextValue['renderers']
+  handlers: LogseqContextValue['handlers']
   model?: TLDocumentModel<Shape>
   onMount?: TLReactCallbacks<Shape>['onMount']
   onPersist?: TLReactCallbacks<Shape>['onPersist']

+ 14 - 7
tldraw/apps/tldraw-logseq/src/components/PrimaryTools/PrimaryTools.tsx

@@ -3,7 +3,7 @@ import { useApp } from '@tldraw/react'
 import { observer } from 'mobx-react-lite'
 import * as React from 'react'
 import { Button } from '~components/Button'
-import { ArrowIcon, EraserIcon, LogseqIcon } from '~components/icons'
+import { ArrowIcon, EraserIcon, LogseqIcon, TablerIcon } from '~components/icons'
 
 export const PrimaryTools = observer(function PrimaryTools() {
   const app = useApp()
@@ -31,50 +31,57 @@ export const PrimaryTools = observer(function PrimaryTools() {
     <div className="tl-primary-tools">
       <div className="tl-tools-floating-panel" data-tool-locked={app.settings.isToolLocked}>
         <Button
+          title="Select tool"
           data-tool="select"
           data-selected={selectedToolId === 'select'}
           onClick={handleToolClick}
         >
-          <CursorArrowIcon />
+          <TablerIcon name="click" />
         </Button>
         <Button
+          title="Draw tool"
           data-tool="pencil"
           data-selected={selectedToolId === 'pencil'}
           onClick={handleToolClick}
         >
-          <Pencil1Icon />
+          <TablerIcon name="ballpen" />
         </Button>
         <Button
+          title="Highlight tool"
           data-tool="highlighter"
           data-selected={selectedToolId === 'highlighter'}
           onClick={handleToolClick}
         >
-          <ShadowIcon />
+          <TablerIcon name="highlight" />
         </Button>
         <Button
+          title="Eraser tool"
           data-tool="erase"
           data-selected={selectedToolId === 'erase'}
           onClick={handleToolClick}
         >
-          <EraserIcon />
+          <TablerIcon name="eraser" />
         </Button>
         <Button
+          title="Line tool"
           data-tool="line"
           data-selected={selectedToolId === 'line'}
           onClick={handleToolClick}
           onDoubleClick={handleToolDoubleClick}
         >
-          <ArrowIcon />
+          <TablerIcon name="line" />
         </Button>
         <Button
+          title="Text tool"
           data-tool="text"
           data-selected={selectedToolId === 'text'}
           onClick={handleToolClick}
           onDoubleClick={handleToolDoubleClick}
         >
-          <TextIcon />
+          <TablerIcon name="text-resize" />
         </Button>
         <Button
+          title="Logseq Portal tool"
           data-tool="logseq-portal"
           data-selected={selectedToolId === 'logseq-portal'}
           onClick={handleToolClick}

+ 1 - 1
tldraw/apps/tldraw-logseq/src/components/icons/LogseqIcon.tsx

@@ -6,7 +6,7 @@ const iconBase64 =
 export function LogseqIcon() {
   return (
     <img
-      style={{ borderRadius: '4px', width: '20px', height: '20px' }}
+      style={{ borderRadius: '4px', width: '22px', height: '22px' }}
       src={'data:image/png;base64,' + iconBase64}
       alt="logseq"
     />

+ 21 - 0
tldraw/apps/tldraw-logseq/src/components/icons/TablerIcon.tsx

@@ -0,0 +1,21 @@
+import React from 'react'
+
+const extendedIcons = [
+  'block',
+  'page',
+  'references-hide',
+  'references-show',
+  'whiteboard',
+  'whiteboard-element',
+]
+
+const cx = (...args: (string | undefined)[]) => args.join(' ')
+
+export const TablerIcon = ({
+  name,
+  className,
+  ...props
+}: { name: string } & React.HTMLAttributes<HTMLElement>) => {
+  const classNamePrefix = extendedIcons.includes(name) ? `ti tie-` : `ti ti-`
+  return <i className={cx(classNamePrefix + name, className)} {...props} />
+}

+ 1 - 0
tldraw/apps/tldraw-logseq/src/components/icons/index.ts

@@ -16,3 +16,4 @@ export * from './MultiplayerIcon'
 export * from './DiscordIcon'
 export * from './LineIcon'
 export * from './LogseqIcon'
+export * from './TablerIcon'

+ 8 - 1
tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts

@@ -1,5 +1,12 @@
 import React from 'react'
 
+export interface SearchResult {
+  pages: string[]
+  blocks: { content: string; page: number; uuid: string }[]
+  'has-more?': boolean
+  files?: string[]
+}
+
 export interface LogseqContextValue {
   renderers: {
     Page: React.FC<{
@@ -16,7 +23,7 @@ export interface LogseqContextValue {
     }>
   }
   handlers: {
-    search: (query: string) => string[]
+    search: (query: string) => Promise<SearchResult>
     addNewBlock: (content: string) => string // returns the new block uuid
   }
 }

+ 85 - 15
tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx

@@ -6,10 +6,11 @@ import Vec from '@tldraw/vec'
 import { makeObservable, runInAction } from 'mobx'
 import { observer } from 'mobx-react-lite'
 import * as React from 'react'
+import { TablerIcon } from '~components/icons'
 import { SwitchInput } from '~components/inputs/SwitchInput'
 import { useCameraMovingRef } from '~hooks/useCameraMoving'
 import type { Shape } from '~lib'
-import { LogseqContext } from '~lib/logseq-context'
+import { LogseqContext, SearchResult } from '~lib/logseq-context'
 import { CustomStyleProps, withClampedStyles } from './style-props'
 
 const HEADER_HEIGHT = 40
@@ -27,9 +28,9 @@ interface LogseqQuickSearchProps {
   onChange: (id: string) => void
 }
 
-const LogseqTypeTag = ({ type }: { type: 'B' | 'P' }) => {
+const LogseqTypeTag = ({ type, active }: { type: 'B' | 'P'; active?: boolean }) => {
   return (
-    <span className="tl-type-tag">
+    <span className="tl-type-tag" data-active={active}>
       <i className={`tie tie-${type === 'B' ? 'block' : 'page'}`} />
     </span>
   )
@@ -46,6 +47,48 @@ const LogseqPortalShapeHeader = observer(
   }
 )
 
+const highlightedJSX = (input: string, keyword: string) => {
+  return (
+    <span>
+      {input
+        .split(new RegExp(`(${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) => {
+  const { handlers } = React.useContext(LogseqContext)
+  const [results, setResults] = React.useState<SearchResult | null>(null)
+
+  React.useEffect(() => {
+    let canceled = false
+    const searchHandler = handlers?.search
+    if (q.length > 0 && searchHandler) {
+      handlers.search(q).then(_results => {
+        if (!canceled) {
+          setResults(_results)
+        }
+      })
+    } else {
+      setResults(null)
+    }
+    return () => {
+      canceled = true
+    }
+  }, [q, handlers?.search])
+
+  return results
+}
+
 export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
   static id = 'logseq-portal'
 
@@ -227,7 +270,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
   LogseqQuickSearch = observer(({ onChange }: LogseqQuickSearchProps) => {
     const [q, setQ] = React.useState('')
     const rInput = React.useRef<HTMLInputElement>(null)
-    const { handlers } = React.useContext(LogseqContext)
+    const { handlers, renderers } = React.useContext(LogseqContext)
     const app = useApp<Shape>()
 
     const finishCreating = React.useCallback((id: string) => {
@@ -251,9 +294,8 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
       }
     }, [])
 
-    const options = React.useMemo(() => {
-      return handlers?.search?.(q)
-    }, [handlers?.search, q])
+    const searchResult = useSearch(q)
+    const Breadcrumb = renderers?.Breadcrumb
 
     React.useEffect(() => {
       // autofocus seems not to be working
@@ -262,10 +304,14 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
       })
     }, [])
 
+    if (!Breadcrumb) {
+      return null
+    }
+
     return (
       <div className="tl-quick-search">
         <div className="tl-quick-search-input-container">
-          <MagnifyingGlassIcon className="tl-quick-search-icon" width={24} height={24} />
+          <TablerIcon name="search" className="tl-quick-search-icon" />
           <div className="tl-quick-search-input-sizer" data-value={q}>
             <input
               ref={rInput}
@@ -284,20 +330,44 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
         </div>
         <div className="tl-quick-search-options">
           <div className="tl-quick-search-option" onClick={() => onAddBlock(q)}>
-            <LogseqTypeTag type="B" />
-            New block{q.length > 0 ? `: ${q}` : ''}
+            <div className="tl-quick-search-option-row">
+              <LogseqTypeTag active type="B" />
+              New block{q.length > 0 ? `: ${q}` : ''}
+            </div>
           </div>
-          {options?.length === 0 && q && (
+          {searchResult?.pages?.length === 0 && q && (
             <div className="tl-quick-search-option" onClick={() => finishCreating(q)}>
-              <LogseqTypeTag type="P" />
-              New page: {q}
+              <div className="tl-quick-search-option-row">
+                <LogseqTypeTag active type="P" />
+                New page: {q}
+              </div>
             </div>
           )}
-          {options?.map(name => (
+          {searchResult?.pages?.map(name => (
             <div key={name} className="tl-quick-search-option" onClick={() => finishCreating(name)}>
-              {name}
+              <div className="tl-quick-search-option-row">
+                <LogseqTypeTag type="P" />
+                {highlightedJSX(name, q)}
+              </div>
             </div>
           ))}
+          {searchResult?.blocks
+            ?.filter(block => block.content && block.uuid)
+            .map(({ content, uuid }) => (
+              <div
+                key={uuid}
+                className="tl-quick-search-option"
+                onClick={() => finishCreating(uuid)}
+              >
+                <div className="tl-quick-search-option-row">
+                  <LogseqTypeTag type="B" />
+                  <Breadcrumb blockId={uuid} />
+                </div>
+                <div className="tl-quick-search-option-row">
+                  {highlightedJSX(content, q)}
+                </div>
+              </div>
+            ))}
         </div>
       </div>
     )

+ 28 - 5
tldraw/apps/tldraw-logseq/src/styles.css

@@ -241,7 +241,7 @@
   flex-flow: column;
   border-radius: 8px;
   overflow: hidden;
-  padding: 8px;
+  padding: 4px;
   gap: 8px;
   background-color: var(--color-panel);
   box-shadow: var(--shadow-medium);
@@ -256,12 +256,12 @@
   display: flex;
   align-items: center;
   justify-content: center;
-  font-size: 13px;
+  font-size: 24px;
   font-family: var(--ls-font-family);
   background: none;
   border: none;
   cursor: pointer;
-  color: var(--ls-secondary-text-color);
+  color: var(--ls-primary-text-color);
 }
 
 .tl-primary-tools .tl-button:hover {
@@ -506,9 +506,10 @@
 }
 
 .tl-quick-search-option {
-  padding: 8px 16px;
-  cursor: pointer;
   display: flex;
+  flex-flow: column;
+  padding: 12px 16px;
+  cursor: pointer;
   font-size: 0.875rem;
   line-height: 1.25rem;
   gap: 0.5em;
@@ -518,6 +519,20 @@
   background-color: var(--ls-menu-hover-color, #f4f5f7);
 }
 
+.tl-quick-search-option-row {
+  display: flex;
+  gap: 0.5em;
+}
+
+.tl-quick-search-option-row .breadcrumb {
+  margin: 0;
+}
+
+.tl-quick-search-option-placeholder {
+  width: 20px;
+  flex-shrink: 0;
+}
+
 .tl-logseq-portal-container {
   width: calc(100% - 2px);
   height: calc(100% - 2px);
@@ -562,6 +577,10 @@
   flex-grow: 0;
 }
 
+.tl-type-tag[data-active='true'] {
+  background: var(--color-selectedFill);
+}
+
 .tl-logseq-portal-header .page-ref {
   color: var(--ls-title-text-color);
 }
@@ -588,3 +607,7 @@ html[data-theme='dark'] .tl-logseq-portal-header {
   flex: 1 1 0%;
   cursor: default;
 }
+
+.tl-highlighted {
+  padding: 0;
+}

+ 5 - 0
tldraw/demo/index.html

@@ -2,6 +2,11 @@
 <html lang="en">
   <head>
     <meta charset="UTF-8" />
+    <link
+      rel="stylesheet"
+      href="https://cdn.jsdelivr.net/npm/@tabler/icons@latest/iconfont/tabler-icons.min.css"
+    />
+
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>Vite App</title>
   </head>

+ 13 - 11
tldraw/demo/src/App.jsx

@@ -44,8 +44,6 @@ const documentModel = onLoad() ?? {
   ],
 }
 
-const list = ['foo', 'bar']
-
 const Page = props => {
   const [value, setValue] = React.useState(JSON.stringify(props, null, 2))
   return (
@@ -72,14 +70,7 @@ const Block = props => {
 }
 
 const Breadcrumb = props => {
-  const [value, setValue] = React.useState(JSON.stringify(props))
-  return (
-    <input
-      className="whitespace-pre w-full h-full font-mono"
-      value={value}
-      onChange={e => setValue(e.target.value)}
-    />
-  )
+  return <div className="font-mono">{props.blockId}</div>
 }
 
 const PageNameLink = props => {
@@ -134,6 +125,17 @@ const ThemeSwitcher = ({ theme, setTheme }) => {
   )
 }
 
+const searchHandler = q => {
+  return Promise.resolve({
+    pages: ['foo', 'bar', 'asdf'].filter(p => p.includes(q)),
+    blocks: [
+      { content: 'foo content 1', uuid: 'uuid 1', page: 0 },
+      { content: 'bar content 2', uuid: 'uuid 2', page: 1 },
+      { content: 'asdf content 3', uuid: 'uuid 3', page: 2 },
+    ],
+  })
+}
+
 export default function App() {
   const [theme, setTheme] = React.useState('light')
 
@@ -148,7 +150,7 @@ export default function App() {
           PageNameLink,
         }}
         handlers={{
-          search: q => (q ? list.filter(item => item.includes(q)) : []),
+          search: searchHandler,
           addNewBlock: () => uniqueId(),
         }}
         model={documentModel}

+ 4 - 4
yarn.lock

@@ -786,10 +786,10 @@
   dependencies:
     defer-to-connect "^1.0.1"
 
-"@tabler/icons@1.54.0":
-  version "1.54.0"
-  resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.54.0.tgz#c8cf4e777e61b19004d0e21443c9a7fae31d70c4"
-  integrity sha512-X0SjUMWlu6IWsWIZP6gtMZhi9Q7pO2+BQ9vex28rOu+gtym7fZjnDXWG0okzVhtt4mlOwJ2BHQllRky29lsn7Q==
+"@tabler/icons@1.78.0":
+  version "1.78.0"
+  resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.78.0.tgz#6687acf21d8663858cb266ed0d79913058f20376"
+  integrity sha512-Aq4mHpQcvJSRb0+m1azJPDs/0ZvyoR1da8JmLMzVfObS1tRcJYsvuxBJfYCreFtBT33w4PtBTD+7J+2hk8JTQw==
 
 "@tailwindcss/custom-forms@^0.2.1":
   version "0.2.1"