diff --git a/.github/workflows/manual-release.yml b/.github/workflows/manual-release.yml new file mode 100644 index 0000000..411c48a --- /dev/null +++ b/.github/workflows/manual-release.yml @@ -0,0 +1,48 @@ +# This is a basic workflow that is manually triggered + +name: Manual workflow + +# Controls when the action will run. Workflow runs when manually triggered using the UI +# or API. +on: + workflow_dispatch: + # Inputs the workflow accepts. + inputs: + tag: + # Friendly description to be shown in the UI instead of 'name' + description: 'Existing tag to create release from.' + # Default value if no value is explicitly provided + default: '0.0.0' + # Input has to be provided for the workflow to run + required: true + # The data type of the input + type: string + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: "18.x" + + - name: Build plugin + run: | + npm install + npm run dist + + - name: Create release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + tag="${{inputs.tag}}" + + gh release create "$tag" \ + --title="$tag" \ + --draft \ + main.js manifest.json styles.css diff --git a/manifest.json b/manifest.json index d930fd2..66715a0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "tldraw", "name": "Tldraw", - "version": "1.2.0", + "version": "1.3.0", "minAppVersion": "0.15.0", "description": "Integrates Tldraw into Obsidian, allowing users to draw and edit content on a virtual whiteboard.", "author": "Sam Alhaqab", diff --git a/package-lock.json b/package-lock.json index 0462876..58a836a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "esbuild": "0.17.3", "obsidian": "latest", "tslib": "2.6.3", - "typescript": "5.4.5" + "typescript": "^5.5.4" } }, "node_modules/@babel/runtime": { @@ -3988,9 +3988,9 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index f43626f..a0dc42a 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "esbuild": "0.17.3", "obsidian": "latest", "tslib": "2.6.3", - "typescript": "5.4.5" + "typescript": "^5.5.4" }, "dependencies": { "@tldraw/tldraw": "2.4.4", diff --git a/src/components/TldrawApp.tsx b/src/components/TldrawApp.tsx index 54f48d4..467479d 100644 --- a/src/components/TldrawApp.tsx +++ b/src/components/TldrawApp.tsx @@ -4,6 +4,7 @@ import { createRoot } from "react-dom/client"; import { DefaultMainMenu, DefaultMainMenuContent, + Editor, TLComponents, TLStore, Tldraw, @@ -26,6 +27,12 @@ import { createRawTldrawFile } from "src/utils/tldraw-file"; type TldrawAppOptions = { isReadonly?: boolean, autoFocus?: boolean, + inputFocus?: boolean, + hideUi?: boolean, + /** + * Whether to call `.selectNone` on the Tldraw editor instance when it is mounted. + */ + selectNone?: boolean, /** * Whether or not to initially zoom to the bounds of the document when the component is mounted. */ @@ -68,7 +75,10 @@ function LocalFileMenu() { const TldrawApp = ({ settings, initialData, setFileData, options: { autoFocus = true, + hideUi = false, + inputFocus = false, isReadonly = false, + selectNone = false, zoomToBounds = false, defaultFontOverrides } }: TldrawAppProps) => { @@ -114,26 +124,38 @@ const TldrawApp = ({ settings, initialData, setFileData, options: { }; }, [store]); + const editorRef = React.useRef(null); return (
e.stopPropagation()} + onBlur={!inputFocus ? undefined : () => { + editorRef.current?.selectNone() + editorRef.current?.blur() + }} + onFocus={!inputFocus ? undefined : () => editorRef.current?.focus()} > { + editorRef.current = editor; + if(selectNone) { + editor.selectNone(); + } + const { themeMode, gridMode, diff --git a/src/main.ts b/src/main.ts index ac5f606..363ac1e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -458,7 +458,7 @@ export default class TldrawPlugin extends Plugin { return await this.createTldrFile(res.filename, res.folder); }; - public openTldrFile = async (file: TFile, location: PaneTarget) => { + public openTldrFile = async (file: TFile, location: PaneTarget, viewType: ViewType = VIEW_TYPE_TLDRAW) => { let leaf: WorkspaceLeaf; if (location === "current-tab") @@ -472,7 +472,7 @@ export default class TldrawPlugin extends Plugin { else leaf = this.app.workspace.getLeaf(false); await leaf.openFile(file); - await this.updateViewMode(VIEW_TYPE_TLDRAW, leaf); + await this.updateViewMode(viewType, leaf); }; public createAndOpenUntitledTldrFile = async (location: PaneTarget) => { diff --git a/src/obsidian/plugin/markdown-post-processor.ts b/src/obsidian/plugin/markdown-post-processor.ts index cf60e4b..cdd3158 100644 --- a/src/obsidian/plugin/markdown-post-processor.ts +++ b/src/obsidian/plugin/markdown-post-processor.ts @@ -1,6 +1,7 @@ -import { MarkdownPostProcessorContext, TFile } from "obsidian"; +import { ButtonComponent, MarkdownPostProcessorContext, MarkdownView, TFile } from "obsidian"; import { createRootAndRenderTldrawApp } from "src/components/TldrawApp"; import TldrawPlugin from "src/main"; +import { MARKDOWN_ICON_NAME, TLDRAW_ICON_NAME } from "src/utils/constants"; import { CustomMutationObserver } from "src/utils/debug-mutation-observer"; import { ConsoleLogParams, LOGGING_ENABLED, logFn } from "src/utils/logging"; import { parseTLDataDocument } from "src/utils/parse"; @@ -90,32 +91,7 @@ export async function markdownPostProcessor(plugin: TldrawPlugin, element: HTMLE internalEmbedDiv.empty() - internalEmbedDiv.addClass('tldraw-markdown-view'); - if (markdownEmbed) { - const tldrawViewHeader = internalEmbedDiv.createDiv({ - cls: ['tldraw-view-header'], - }); - - tldrawViewHeader.style.display = 'flex'; - tldrawViewHeader.style.justifyContent = 'space-between'; - tldrawViewHeader.style.alignItems = 'baseline'; - - const tldrawTitle = tldrawViewHeader.createDiv({ - cls: ['embed-title', 'markdown-embed-title'] - }) - - tldrawTitle.innerText = file.name; - - tldrawViewHeader.createEl('button', { - cls: ['clickable'], - text: 'Edit', - }, (el) => { - el.addEventListener('click', async (ev) => { - plugin.openTldrFile(file, 'new-tab') - }) - }); - internalEmbedDiv.removeClass("markdown-embed"); internalEmbedDiv.removeClass("inline-embed"); // TODO: Uncomment later when added prerendered tldraw view support. @@ -123,17 +99,13 @@ export async function markdownPostProcessor(plugin: TldrawPlugin, element: HTMLE // internalEmbedDiv.addClass("image-embed"); } - const tldrawViewContent = internalEmbedDiv.createDiv({ - cls: ['tldraw-view-content'], - }, (el) => { - el.style.height = '300px'; - // Prevent the Obsidian editor from selecting the embed link with the editing cursor when a user interacts with the view. - el.addEventListener('click', (ev) => ev.stopPropagation()); - el.addEventListener('focus', function tldrawFocusListener() { - log(`${tldrawFocusListener.name}`) - }); + const tldrawEmbedView = createTldrawEmbedView(internalEmbedDiv, { + file, plugin }); + const tldrawEmbedViewContent = tldrawEmbedView.createDiv({ + cls: 'ptl-view-content' + }) const parent = internalEmbedDiv.parentElement; @@ -141,10 +113,8 @@ export async function markdownPostProcessor(plugin: TldrawPlugin, element: HTMLE const fileData = await plugin.app.vault.read(file); const parsedData = parseTLDataDocument(plugin.manifest.version, fileData); - log('tldrawViewContent', tldrawViewContent); - log('parsedData', parsedData); - const reactRoot = createRootAndRenderTldrawApp(tldrawViewContent, + const reactRoot = createRootAndRenderTldrawApp(tldrawEmbedViewContent, parsedData, (_) => { console.log('Ignore saving file due to read only mode.'); @@ -153,8 +123,11 @@ export async function markdownPostProcessor(plugin: TldrawPlugin, element: HTMLE { isReadonly: true, autoFocus: false, + inputFocus: true, + selectNone: true, + hideUi: true, zoomToBounds: true, - defaultFontOverrides: plugin.getFontOverrides(), + defaultFontOverrides: plugin.getFontOverrides() } ); @@ -202,3 +175,92 @@ export async function markdownPostProcessor(plugin: TldrawPlugin, element: HTMLE } throw new Error(`${markdownPostProcessor.name}: Unexpected`); } + + +function createTldrawViewHeader(embedViewContent: HTMLElement, { + file, plugin, selectEmbedText +}: { + file: TFile, + plugin: TldrawPlugin, + selectEmbedText: (ev: MouseEvent) => void +}) { + const tldrawViewHeader = embedViewContent.createDiv({ + cls: ['ptl-embed-context-bar'], + }); + + const tldrawTitle = tldrawViewHeader.createDiv({ + cls: ['ptl-embed-title-bar'] + }, (el) => { + el.onClickEvent((ev) => { + selectEmbedText(ev); + ev.stopPropagation(); + }) + }) + + tldrawTitle.innerText = file.name; + + const actionBar = tldrawViewHeader.createDiv({ cls: 'ptl-embed-action-bar' }) + + new ButtonComponent(actionBar) + .setClass('clickable-icon') + .setIcon(MARKDOWN_ICON_NAME) + .setTooltip('Open as markdown').onClick(() => { + plugin.openTldrFile(file, 'new-tab', 'markdown') + }); + + new ButtonComponent(actionBar) + .setClass('clickable-icon') + .setIcon(TLDRAW_ICON_NAME) + .setTooltip('Edit').onClick(() => { + plugin.openTldrFile(file, 'new-tab') + }); + + new ButtonComponent(actionBar) + .setClass('clickable-icon') + .setIcon('view') + .setTooltip('Read-only view').onClick((ev) => { + plugin.openTldrFile(file, 'new-tab', 'tldraw-read-only') + }); + + return tldrawViewHeader; +} + +function createTldrawEmbedView(internalEmbedDiv: HTMLElement, { + file, plugin +}: { + file: TFile, + plugin: TldrawPlugin, +}) { + return internalEmbedDiv.createDiv({ + cls: 'ptl-markdown-embed' + }, (el) => { + const viewHeader = createTldrawViewHeader(el, { + file, plugin, + selectEmbedText: (ev) => { + internalEmbedDiv.dispatchEvent(new MouseEvent('click', { + bubbles: ev.bubbles, + cancelable: ev.cancelable, + clientX: ev.clientX, + clientY: ev.clientY + })) + } + }) + + // Prevent the Obsidian editor from selecting the embed link with the editing cursor when a user interacts with the view. + el.addEventListener('click', (ev) => ev.stopPropagation()); + + viewHeader.hide(); + + internalEmbedDiv.addEventListener('focusin', () => { + plugin.app.workspace.getActiveViewOfType(MarkdownView)?.editor.setCursor(0, 0) + viewHeader.show(); + }) + + internalEmbedDiv.addEventListener('focusout', (event) => { + if (event.relatedTarget instanceof Node && internalEmbedDiv.contains(event.relatedTarget)) { + return; + } + viewHeader.hide(); + }) + }) +} \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index 7b002b7..6e8d683 100644 --- a/src/styles.css +++ b/src/styles.css @@ -17,7 +17,7 @@ should be (classes prefixed with 'tl' or 'tlui' then find a way to increase the specificity over button:not(.clickable-icon) using selectors and paste in those styles. Look for the comments that start with 'OVERRIDING'. */ -#tldraw-view-root { +.tldraw-view-root { touch-action: none; position: relative; width: 100%; @@ -28,18 +28,18 @@ } /* OVERRIDING button:not(.clickable-icon) back to tldraw's original styles: */ -#tldraw-view-root .tlui-button { +.tldraw-view-root .tlui-button { background-color: transparent; padding: 0px 13px; box-shadow: none; } /* OVERRIDING button:not(.clickable-icon) */ -#tldraw-view-root .tlui-button.tlui-help-menu__button { +.tldraw-view-root .tlui-button.tlui-help-menu__button { background-color: var(--color-low); } -#tldraw-view-root kbd { +.tldraw-view-root kbd { background: inherit; border-radius: inherit; padding: inherit; @@ -120,3 +120,36 @@ div[data-type="tldraw-read-only"] .view-content.tldraw-view-content { .status-bar-item.plugin-tldraw { background-color: var(--background-modifier-border); } + +.ptl-markdown-embed { + position: relative; +} + +.ptl-embed-context-bar { + padding: 2px; + display: flex; + background: var(--background-primary-alt); +} + +.ptl-embed-title-bar { + font-size: var(--font-smallest); + margin-left: 8px; + align-self: center; +} + +/* Render the context bar for embeds in the top right corner. */ +.ptl-markdown-embed .ptl-embed-context-bar { + display: flex; + justify-content: space-between; + position: absolute; + width: 100%; + z-index: 1; +} + +.ptl-embed-action-bar { + display: flex; +} + +.ptl-markdown-embed .ptl-view-content { + height: 300px; +}