222 lines
7.1 KiB
TypeScript
222 lines
7.1 KiB
TypeScript
import { StateNode } from "tldraw"
|
|
import { ObsNoteShape } from "@/shapes/ObsNoteShapeUtil"
|
|
import { findNonOverlappingPosition } from "@/utils/shapeCollisionUtils"
|
|
|
|
export class ObsNoteTool extends StateNode {
|
|
static override id = "obs_note"
|
|
static override initial = "idle"
|
|
static override children = () => [ObsNoteIdle]
|
|
|
|
onSelect() {
|
|
// Check if there are existing ObsNote shapes on the canvas
|
|
const allShapes = this.editor.getCurrentPageShapes()
|
|
const obsNoteShapes = allShapes.filter(shape => shape.type === 'ObsNote')
|
|
|
|
if (obsNoteShapes.length > 0) {
|
|
// If ObsNote shapes exist, select them and center the view
|
|
this.editor.setSelectedShapes(obsNoteShapes.map(shape => shape.id))
|
|
this.editor.zoomToFit()
|
|
|
|
// Add refresh all functionality
|
|
this.addRefreshAllListener()
|
|
}
|
|
}
|
|
|
|
private addRefreshAllListener() {
|
|
// Listen for refresh-all-obsnotes event
|
|
const handleRefreshAll = async () => {
|
|
const shapeUtil = new ObsNoteShape(this.editor)
|
|
shapeUtil.editor = this.editor
|
|
|
|
const result = await shapeUtil.refreshAllFromVault()
|
|
if (result.success > 0) {
|
|
alert(`✅ Refreshed ${result.success} notes from vault!${result.failed > 0 ? ` (${result.failed} failed)` : ''}`)
|
|
} else {
|
|
alert('❌ Failed to refresh any notes. Check console for details.')
|
|
}
|
|
}
|
|
|
|
window.addEventListener('refresh-all-obsnotes', handleRefreshAll)
|
|
|
|
// Clean up listener when tool is deselected
|
|
const cleanup = () => {
|
|
window.removeEventListener('refresh-all-obsnotes', handleRefreshAll)
|
|
}
|
|
|
|
// Store cleanup function for later use
|
|
;(this as any).cleanup = cleanup
|
|
}
|
|
|
|
onExit() {
|
|
// Clean up event listeners
|
|
if ((this as any).cleanup) {
|
|
;(this as any).cleanup()
|
|
}
|
|
}
|
|
}
|
|
|
|
export class ObsNoteIdle extends StateNode {
|
|
static override id = "idle"
|
|
|
|
tooltipElement?: HTMLDivElement
|
|
mouseMoveHandler?: (e: MouseEvent) => void
|
|
|
|
override onEnter = () => {
|
|
// Set cursor to cross (looks like +)
|
|
this.editor.setCursor({ type: "cross", rotation: 0 })
|
|
|
|
// Create tooltip element
|
|
this.tooltipElement = document.createElement('div')
|
|
this.tooltipElement.style.cssText = `
|
|
position: fixed;
|
|
background: rgba(0, 0, 0, 0.85);
|
|
color: white;
|
|
padding: 8px 12px;
|
|
border-radius: 6px;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
font-size: 13px;
|
|
white-space: nowrap;
|
|
z-index: 10000;
|
|
pointer-events: none;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
`
|
|
this.tooltipElement.textContent = 'Click anywhere to place tool'
|
|
|
|
// Add tooltip to DOM
|
|
document.body.appendChild(this.tooltipElement)
|
|
|
|
// Function to update tooltip position
|
|
this.mouseMoveHandler = (e: MouseEvent) => {
|
|
if (this.tooltipElement) {
|
|
const x = e.clientX + 15
|
|
const y = e.clientY - 35
|
|
|
|
// Keep tooltip within viewport bounds
|
|
const rect = this.tooltipElement.getBoundingClientRect()
|
|
const viewportWidth = window.innerWidth
|
|
const viewportHeight = window.innerHeight
|
|
|
|
let finalX = x
|
|
let finalY = y
|
|
|
|
// Adjust if tooltip would go off the right edge
|
|
if (x + rect.width > viewportWidth) {
|
|
finalX = e.clientX - rect.width - 15
|
|
}
|
|
|
|
// Adjust if tooltip would go off the bottom edge
|
|
if (y + rect.height > viewportHeight) {
|
|
finalY = e.clientY - rect.height - 15
|
|
}
|
|
|
|
// Ensure tooltip doesn't go off the top or left
|
|
finalX = Math.max(10, finalX)
|
|
finalY = Math.max(10, finalY)
|
|
|
|
this.tooltipElement.style.left = `${finalX}px`
|
|
this.tooltipElement.style.top = `${finalY}px`
|
|
}
|
|
}
|
|
|
|
// Add mouse move listener
|
|
document.addEventListener('mousemove', this.mouseMoveHandler)
|
|
}
|
|
|
|
override onPointerDown = () => {
|
|
// Get the click position in page coordinates
|
|
const { currentPagePoint } = this.editor.inputs
|
|
|
|
// Create an ObsidianBrowser shape on the canvas at the click location
|
|
this.createObsidianBrowserShape(currentPagePoint.x, currentPagePoint.y)
|
|
}
|
|
|
|
override onExit = () => {
|
|
this.cleanupTooltip()
|
|
// Clean up event listeners
|
|
if ((this as any).cleanup) {
|
|
;(this as any).cleanup()
|
|
}
|
|
}
|
|
|
|
private cleanupTooltip = () => {
|
|
// Remove mouse move listener
|
|
if (this.mouseMoveHandler) {
|
|
document.removeEventListener('mousemove', this.mouseMoveHandler)
|
|
this.mouseMoveHandler = undefined
|
|
}
|
|
|
|
// Remove tooltip element
|
|
if (this.tooltipElement) {
|
|
document.body.removeChild(this.tooltipElement)
|
|
this.tooltipElement = undefined
|
|
}
|
|
}
|
|
|
|
private createObsidianBrowserShape(clickX?: number, clickY?: number) {
|
|
try {
|
|
// Check if ObsidianBrowser already exists
|
|
const allShapes = this.editor.getCurrentPageShapes()
|
|
const existingBrowserShapes = allShapes.filter(shape => shape.type === 'ObsidianBrowser')
|
|
|
|
if (existingBrowserShapes.length > 0) {
|
|
// If a browser already exists, just select it
|
|
console.log('✅ ObsidianBrowser already exists, selecting it')
|
|
this.editor.setSelectedShapes([existingBrowserShapes[0].id])
|
|
this.editor.setCurrentTool('select')
|
|
return
|
|
}
|
|
|
|
// No existing browser, create a new one
|
|
// Standardized size: 800x600
|
|
const shapeWidth = 800
|
|
const shapeHeight = 600
|
|
|
|
let finalX: number
|
|
let finalY: number
|
|
|
|
if (clickX !== undefined && clickY !== undefined) {
|
|
// User clicked - ALWAYS use that exact position (centered on click), no collision detection
|
|
// This ensures the shape appears exactly where the user clicked, regardless of overlaps
|
|
finalX = clickX - shapeWidth / 2 // Center the shape on click
|
|
finalY = clickY - shapeHeight / 2 // Center the shape on click
|
|
} else {
|
|
// Fallback to viewport center if no click coordinates, with collision detection
|
|
const viewport = this.editor.getViewportPageBounds()
|
|
const centerX = viewport.x + viewport.w / 2
|
|
const centerY = viewport.y + viewport.h / 2
|
|
const baseX = centerX - shapeWidth / 2
|
|
const baseY = centerY - shapeHeight / 2
|
|
|
|
// Use collision detection for fallback case
|
|
const position = findNonOverlappingPosition(
|
|
this.editor,
|
|
baseX,
|
|
baseY,
|
|
shapeWidth,
|
|
shapeHeight
|
|
)
|
|
finalX = position.x
|
|
finalY = position.y
|
|
}
|
|
|
|
const browserShape = this.editor.createShape({
|
|
type: 'ObsidianBrowser',
|
|
x: finalX,
|
|
y: finalY,
|
|
props: {
|
|
w: shapeWidth,
|
|
h: shapeHeight,
|
|
}
|
|
})
|
|
|
|
// Select the new shape and switch to select tool
|
|
this.editor.setSelectedShapes([`shape:${browserShape.id}`] as any)
|
|
this.editor.setCurrentTool('select')
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error creating ObsidianBrowser shape:', error)
|
|
}
|
|
}
|
|
}
|