-
Notifications
You must be signed in to change notification settings - Fork 6
ENG-1553: Embed canvas in block #988
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
eae1fde
e2ee387
08161b0
56a67d6
3af3e2c
e918a8a
01fc0cf
383136d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import React from "react"; | ||
| import ExtensionApiContextProvider from "roamjs-components/components/ExtensionApiContext"; | ||
| import { OnloadArgs } from "roamjs-components/types"; | ||
| import renderWithUnmount from "roamjs-components/util/renderWithUnmount"; | ||
| import getBlockUidFromTarget from "roamjs-components/dom/getBlockUidFromTarget"; | ||
| import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; | ||
| import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; | ||
| import { TldrawCanvas } from "./Tldraw"; | ||
|
|
||
| const BLOCK_TEXT_REGEX = /\{\{dg-canvas:\s*\[\[(.+?)\]\]\s*\}\}/i; | ||
|
|
||
| const extractCanvasTitle = (button: HTMLElement): string | null => { | ||
| const blockUid = getBlockUidFromTarget(button); | ||
| if (!blockUid) return null; | ||
| const blockText = getTextByBlockUid(blockUid); | ||
| if (!blockText) return null; | ||
| const match = blockText.match(BLOCK_TEXT_REGEX); | ||
| if (!match) return null; | ||
| return match[1].trim(); | ||
| }; | ||
|
|
||
| const CanvasEmbedPlaceholder = ({ message }: { message: string }) => ( | ||
| <div className="dg-canvas-embed-placeholder">{message}</div> | ||
| ); | ||
|
|
||
| export const renderCanvasEmbed = ( | ||
| button: HTMLElement, | ||
| onloadArgs: OnloadArgs, | ||
| ) => { | ||
| button.classList.add("dg-canvas-embed-source-hidden"); | ||
|
|
||
| if (!button.parentElement) return; | ||
|
|
||
| const title = extractCanvasTitle(button); | ||
| if (!title) return; | ||
|
|
||
| const pageUid = getPageUidByPageTitle(title); | ||
| if (!pageUid) { | ||
| const wrapper = document.createElement("div"); | ||
| button.parentElement.appendChild(wrapper); | ||
| renderWithUnmount( | ||
| <CanvasEmbedPlaceholder message={`Canvas not found: ${title}`} />, | ||
| wrapper, | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| const wrapper = document.createElement("div"); | ||
| wrapper.className = "dg-canvas-embed"; | ||
| wrapper.onmousedown = (e: MouseEvent) => e.stopPropagation(); | ||
| button.parentElement.appendChild(wrapper); | ||
|
|
||
| renderWithUnmount( | ||
| <ExtensionApiContextProvider {...onloadArgs}> | ||
| <TldrawCanvas title={title} /> | ||
| </ExtensionApiContextProvider>, | ||
| wrapper, | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import React, { useCallback, useEffect, useState } from "react"; | ||
| import { Dialog } from "@blueprintjs/core"; | ||
| import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; | ||
| import renderOverlay, { | ||
| RoamOverlayProps, | ||
| } from "roamjs-components/util/renderOverlay"; | ||
| import { getCanvasPageTitles } from "~/utils/isCanvasPage"; | ||
|
|
||
| type CanvasEmbedDialogProps = { | ||
| onSelect: (title: string) => void; | ||
| }; | ||
|
|
||
| const CanvasEmbedDialog = ({ | ||
| isOpen, | ||
| onClose, | ||
| onSelect, | ||
| }: RoamOverlayProps<CanvasEmbedDialogProps>) => { | ||
| const [canvasPages, setCanvasPages] = useState<string[] | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| void getCanvasPageTitles().then(setCanvasPages); | ||
| }, []); | ||
|
|
||
| const handleSetValue = useCallback( | ||
| (title: string) => { | ||
| if (canvasPages?.includes(title)) { | ||
| onSelect(title); | ||
| onClose(); | ||
| } | ||
| }, | ||
| [canvasPages, onSelect, onClose], | ||
| ); | ||
|
|
||
| const renderContent = () => { | ||
| if (canvasPages === null) | ||
| return ( | ||
| <div className="dg-canvas-embed-dialog-message"> | ||
| Loading canvas pages... | ||
| </div> | ||
| ); | ||
| if (canvasPages.length === 0) | ||
| return ( | ||
| <div className="dg-canvas-embed-dialog-message"> | ||
| No canvas pages found | ||
| </div> | ||
| ); | ||
| return ( | ||
| <AutocompleteInput | ||
| setValue={handleSetValue} | ||
| options={canvasPages} | ||
| placeholder="Search canvas pages..." | ||
| autoFocus | ||
| autoSelectFirstOption={false} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| return ( | ||
| <Dialog | ||
| isOpen={isOpen} | ||
| onClose={onClose} | ||
| title="Embed Canvas" | ||
| className="dg-canvas-embed-dialog" | ||
| > | ||
| <div className="dg-canvas-embed-dialog-body">{renderContent()}</div> | ||
| </Dialog> | ||
| ); | ||
| }; | ||
|
|
||
| export const renderCanvasEmbedDialog = (props: CanvasEmbedDialogProps) => | ||
| renderOverlay({ | ||
| // eslint-disable-next-line @typescript-eslint/naming-convention | ||
| Overlay: CanvasEmbedDialog, | ||
| props, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -191,3 +191,51 @@ | |
| #dg-left-sidebar-root .bp3-dark .bp3-button:not([class*="bp3-intent-"]):hover { | ||
| color: #f5f8fa; | ||
| } | ||
|
|
||
| .dg-canvas-embed-source-hidden { | ||
| display: none; | ||
| } | ||
|
|
||
| .dg-canvas-embed { | ||
| height: 400px; | ||
| width: 100%; | ||
| overflow: hidden; | ||
| border-radius: 6px; | ||
| margin: 8px 0; | ||
| } | ||
|
|
||
| .dg-canvas-embed > .roamjs-tldraw-canvas-container { | ||
| height: 100%; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .dg-canvas-embed-placeholder { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| height: 100px; | ||
| color: #8a9ba8; | ||
| font-size: 14px; | ||
| border: 1px dashed #d1d5db; | ||
| border-radius: 6px; | ||
| } | ||
|
|
||
| .dg-canvas-embed-dialog.bp3-dialog { | ||
| width: 400px; | ||
| padding-bottom: 0; | ||
| } | ||
|
|
||
| .dg-canvas-embed-dialog-body { | ||
| padding: 16px; | ||
| } | ||
|
|
||
| .dg-canvas-embed-dialog-message { | ||
| color: #5c7080; | ||
| font-size: 14px; | ||
| } | ||
|
|
||
| .dg-canvas-embed-dialog .roamjs-autocomplete-input-target, | ||
| .dg-canvas-embed-dialog .roamjs-autocomplete-input-target .bp3-input-group { | ||
| display: block; | ||
| width: 100%; | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How many of these could we move to inline tailwind classes?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can move many of these, we need
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the new implementation, if we reuse the existing
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { updateBlock } from "roamjs-components/writes"; | ||
| import { renderCanvasEmbedDialog } from "~/components/canvas/CanvasEmbedDialog"; | ||
|
|
||
| type SlashCommandContext = { | ||
| // eslint-disable-next-line @typescript-eslint/naming-convention | ||
| "block-uid": string; | ||
| }; | ||
|
|
||
| type SlashCommandApi = { | ||
| addCommand: (cmd: { | ||
| label: string; | ||
| callback: (context: SlashCommandContext) => void; | ||
| }) => void; | ||
| removeCommand: (cmd: { label: string }) => void; | ||
| }; | ||
|
|
||
| const getSlashCommandApi = (): SlashCommandApi => | ||
| (window.roamAlphaAPI.ui as unknown as { slashCommand: SlashCommandApi }) | ||
| .slashCommand; | ||
|
|
||
| const SLASH_COMMANDS: { | ||
| label: string; | ||
| callback: (context: SlashCommandContext) => void; | ||
| }[] = [ | ||
| { | ||
| label: "DG: Embed canvas", | ||
| callback: (context) => { | ||
| const uid = context["block-uid"]; | ||
| if (!uid) return; | ||
| renderCanvasEmbedDialog({ | ||
| onSelect: (title: string) => { | ||
| void updateBlock({ | ||
| uid, | ||
| text: `{{dg-canvas: [[${title}]]}}`, | ||
|
sid597 marked this conversation as resolved.
|
||
| }); | ||
| }, | ||
| }); | ||
| }, | ||
| }, | ||
| ]; | ||
|
|
||
| export const registerSlashCommands = (): (() => void) => { | ||
| const api = getSlashCommandApi(); | ||
| for (const cmd of SLASH_COMMANDS) api.addCommand(cmd); | ||
| return () => { | ||
| for (const { label } of SLASH_COMMANDS) api.removeCommand({ label }); | ||
| }; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.