Browse Source

feat: youtube link input

Peng Xiao 3 years ago
parent
commit
21c7f53993

+ 18 - 2
tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx

@@ -4,8 +4,9 @@ import { observer } from 'mobx-react-lite'
 import React from 'react'
 import { TablerIcon } from '~components/icons'
 import { SelectInput, SelectOption } from '~components/inputs/SelectInput'
+import { TextInput } from '~components/inputs/TextInput'
 import { ToggleGroupInput, ToggleGroupInputOption } from '~components/inputs/ToggleGroupInput'
-import { LogseqPortalShape, Shape } from '~lib'
+import { LogseqPortalShape, Shape, YouTubeShape } from '~lib'
 import { LogseqContext } from '~lib/logseq-context'
 
 export const contextBarActionTypes = [
@@ -15,12 +16,13 @@ export const contextBarActionTypes = [
   'StrokeColor',
   'NoStroke',
   'ScaleLevel',
+  'YoutubeLink',
   'LogseqPortalViewMode',
   'OpenPage',
 ] as const
 
 type ContextBarActionType = typeof contextBarActionTypes[number]
-const singleShapeActions: ContextBarActionType[] = ['OpenPage']
+const singleShapeActions: ContextBarActionType[] = ['YoutubeLink', 'OpenPage']
 
 const contextBarActionMapping = new Map<ContextBarActionType, React.FC>()
 
@@ -131,14 +133,28 @@ const OpenPageAction = observer(() => {
   )
 })
 
+const YoutubeLinkAction = observer(() => {
+  const app = useApp<Shape>()
+  const shape = app.selectedShapesArray.find(
+    s => s.props.type === YouTubeShape.defaultProps.type
+  ) as YouTubeShape
+  const handleChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
+    shape.onYoutubeLinkChange(e.target.value)
+  }, [])
+
+  return <TextInput className="tl-youtube-link" value={`${shape.props.url}`} onChange={handleChange} />
+})
+
 contextBarActionMapping.set('LogseqPortalViewMode', LogseqPortalViewModeAction)
 contextBarActionMapping.set('ScaleLevel', ScaleLevelAction)
 contextBarActionMapping.set('OpenPage', OpenPageAction)
+contextBarActionMapping.set('YoutubeLink', YoutubeLinkAction)
 
 type ShapeType = Shape['props']['type']
 
 const shapeMapping: Partial<Record<ShapeType, ContextBarActionType[]>> = {
   'logseq-portal': ['LogseqPortalViewMode', 'ScaleLevel', 'OpenPage'],
+  youtube: ['YoutubeLink'],
 }
 
 const getContextBarActionTypes = (type: ShapeType) => {

+ 7 - 5
tldraw/apps/tldraw-logseq/src/components/inputs/TextInput.tsx

@@ -1,15 +1,17 @@
 import * as React from 'react'
 
 interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
-  label: string
+  autoResize?: boolean
 }
 
 export const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
-  ({ label, ...rest }, ref) => {
+  ({ autoResize = true, value, className, ...rest }, ref) => {
     return (
-      <div className="tl-input">
-        <label htmlFor={`text-${label}`}>{label}</label>
-        <input ref={ref} className="tl-text-input" name={`text-${label}`} type="text" {...rest} />
+      <div className={'tl-input' + (className ? ' ' + className : '')}>
+        <div className="tl-input-sizer">
+          <div className="tl-input-hidden">{value}</div>
+          <input ref={ref} value={value} className="tl-text-input" type="text" {...rest} />
+        </div>
       </div>
     )
   }

+ 16 - 8
tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts

@@ -23,6 +23,14 @@ const isValidURL = (url: string) => {
   }
 }
 
+const safeParseJson = (json: string) => {
+  try {
+    return JSON.parse(json)
+  } catch {
+    return null
+  }
+}
+
 export function usePaste(context: LogseqContextValue) {
   const { handlers } = context
 
@@ -123,9 +131,9 @@ export function usePaste(context: LogseqContextValue) {
       }
 
       function handleTldrawShapes(rawText: string) {
+        const data = safeParseJson(rawText)
         try {
-          const data = JSON.parse(rawText)
-          if (data.type === 'logseq/whiteboard-shapes') {
+          if (data?.type === 'logseq/whiteboard-shapes') {
             const shapes = data.shapes as TLShapeModel[]
             assetsToClone = data.assets as TLAsset[]
             const commonBounds = BoundsUtils.getCommonBounds(
@@ -191,15 +199,15 @@ export function usePaste(context: LogseqContextValue) {
 
       function handleURL(rawText: string) {
         if (isValidURL(rawText)) {
-          const getYoutubeId = (url: string) => {
-            const match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#&?]*).*/)
-            return match && match[2].length === 11 ? match[2] : null
+          const isYoutubeUrl = (url: string) => {
+            const youtubeRegex = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/
+            return youtubeRegex.test(url)
           }
-          const youtubeId = getYoutubeId(rawText)
-          if (youtubeId) {
+          console.log(rawText)
+          if (isYoutubeUrl(rawText)) {
             shapesToCreate.push({
               ...YouTubeShape.defaultProps,
-              embedId: youtubeId,
+              url: rawText,
               point: [point[0], point[1]],
             })
             return true

+ 14 - 16
tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx

@@ -6,6 +6,7 @@ import { action, computed, makeObservable } from 'mobx'
 import { observer } from 'mobx-react-lite'
 import * as React from 'react'
 import { TablerIcon } from '~components/icons'
+import { TextInput } from '~components/inputs/TextInput'
 import { useCameraMovingRef } from '~hooks/useCameraMoving'
 import type { Shape } from '~lib'
 import { LogseqContext, SearchResult } from '~lib/logseq-context'
@@ -538,22 +539,19 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
               </div>
             </div>
           )}
-          <div className="tl-quick-search-input-sizer" data-value={q}>
-            <div className="tl-quick-search-input-hidden">{q}</div>
-            <input
-              ref={rInput}
-              type="text"
-              value={q}
-              placeholder="Create or search your graph..."
-              onChange={q => setQ(q.target.value)}
-              onKeyDown={e => {
-                if (e.key === 'Enter') {
-                  finishCreating(q)
-                }
-              }}
-              className="tl-quick-search-input"
-            />
-          </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}>
           {options.map(({ actionIcon, onChosen, element }, index) => {

+ 17 - 29
tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx

@@ -5,10 +5,11 @@ import { HTMLContainer, TLComponentProps, useApp } from '@tldraw/react'
 import { observer } from 'mobx-react-lite'
 import { CustomStyleProps, withClampedStyles } from './style-props'
 import { TextInput } from '~components/inputs/TextInput'
+import { action, computed } from 'mobx'
 
 export interface YouTubeShapeProps extends TLBoxShapeProps, CustomStyleProps {
   type: 'youtube'
-  embedId: string
+  url: string
 }
 
 export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
@@ -24,7 +25,7 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
     fill: '#ffffff',
     strokeWidth: 2,
     opacity: 1,
-    embedId: '',
+    url: '',
   }
 
   aspectRatio = 480 / 853
@@ -35,35 +36,22 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
 
   canEdit = true
 
-  ReactContextBar = observer(() => {
-    const { embedId } = this.props
-    const rInput = React.useRef<HTMLInputElement>(null)
-    const app = useApp()
-    const handleChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
-      const url = e.currentTarget.value
-      const match = url.match(
-        /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/
-      )
-      const embedId = match?.[1] ?? url ?? ''
-      this.update({ embedId, size: YouTubeShape.defaultProps.size })
-      app.persist()
-    }, [])
-    return (
-      <>
-        <TextInput
-          ref={rInput}
-          label="Youtube Video ID"
-          type="text"
-          value={embedId}
-          onChange={handleChange}
-        />
-      </>
+  @computed get embedId() {
+    const url = this.props.url
+    const match = url.match(
+      /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/
     )
-  })
+    const embedId = match?.[1] ?? url ?? ''
+    return embedId
+  }
+
+  @action onYoutubeLinkChange = (url: string) => {
+    this.update({ url, size: YouTubeShape.defaultProps.size })
+  }
 
   ReactComponent = observer(({ events, isErasing, isEditing }: TLComponentProps) => {
     const {
-      props: { opacity, embedId },
+      props: { opacity },
     } = this
     return (
       <HTMLContainer
@@ -83,7 +71,7 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
             position: 'relative',
           }}
         >
-          {embedId ? (
+          {this.embedId ? (
             <div
               style={{
                 overflow: 'hidden',
@@ -103,7 +91,7 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
                 }}
                 width="853"
                 height="480"
-                src={`https://www.youtube.com/embed/${embedId}`}
+                src={`https://www.youtube.com/embed/${this.embedId}`}
                 frameBorder="0"
                 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
                 allowFullScreen

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

@@ -512,21 +512,29 @@ button.tl-select-input-trigger {
   }
 }
 
-.tl-quick-search-input-sizer {
+.tl-input {
   position: relative;
 }
 
-.tl-quick-search-input {
+.tl-input-sizer {
+  position: relative;
+}
+
+.tl-text-input {
   @apply absolute inset-0;
 
   outline: none;
 }
 
-.tl-quick-search-input-hidden {
+.tl-input-hidden {
   white-space: pre;
   opacity: 0;
-  min-width: 240px;
   min-height: 24px;
+  min-width: 60px;
+}
+
+.tl-quick-search-input {
+  min-width: 240px;
 }
 
 .tl-quick-search-options {
@@ -737,4 +745,11 @@ html[data-theme='dark'] {
 .tl-contextbar-separator {
   background-color: var(--ls-border-color);
   width: 1px;
-}
+}
+
+.tl-youtube-link {
+  border-radius: 8px;
+  color: var(--ls-primary-text-color);
+  box-shadow: 0 0 0 1px var(--ls-secondary-border-color);
+  padding: 4px 14px;
+}

+ 7 - 2
tldraw/packages/react/src/hooks/useKeyboardEvents.ts

@@ -24,7 +24,11 @@ export function useKeyboardEvents(ref: React.RefObject<HTMLDivElement>) {
     }
 
     const onPaste = (e: ClipboardEvent) => {
-      if (!app.editingShape && ref.current?.contains(document.activeElement)) {
+      if (
+        !app.editingShape &&
+        ref.current?.contains(document.activeElement) &&
+        !['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName ?? '')
+      ) {
         e.preventDefault()
         app.paste(e, shiftKeyDownRef.current)
       }
@@ -34,7 +38,8 @@ export function useKeyboardEvents(ref: React.RefObject<HTMLDivElement>) {
       if (
         !app.editingShape &&
         app.selectedShapes.size > 0 &&
-        ref.current?.contains(document.activeElement)
+        ref.current?.contains(document.activeElement) &&
+        !['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName ?? '')
       ) {
         e.preventDefault()
         app.copy()