@@ -90,6 +90,7 @@ export class CalendarComponent extends Component {
9090 public containerEl : HTMLElement ;
9191 private tasks : Task [ ] = [ ] ;
9292 private events : CalendarEvent [ ] = [ ] ;
93+ private externalTextEvents : AdapterCalendarEvent [ ] = [ ] ;
9394 private currentViewMode : CalendarViewMode = "month" ;
9495 private currentDate : moment . Moment = moment ( ) ;
9596
@@ -194,6 +195,7 @@ export class CalendarComponent extends Component {
194195 super . onload ( ) ;
195196 this . processTasks ( ) ;
196197 this . render ( ) ;
198+ this . registerExternalTextDropHandlers ( ) ;
197199
198200 // Listen for calendar views configuration changes
199201 this . registerEvent (
@@ -968,7 +970,14 @@ export class CalendarComponent extends Component {
968970 // ============================================
969971
970972 private handleTGEventClick ( event : AdapterCalendarEvent ) {
971- const task = getTaskFromEvent ( event as any ) ;
973+ let task = getTaskFromEvent ( event as any ) ;
974+
975+ // Fallback: try to find task by ID if metadata is missing
976+ // This fixes issues where the event object is reconstructed/cloned by the library
977+ if ( ! task && event . id ) {
978+ task = this . tasks . find ( ( t ) => t . id === event . id ) || null ;
979+ }
980+
972981 if ( task ) {
973982 this . params ?. onTaskSelected ?.( task ) ;
974983 }
@@ -980,17 +989,28 @@ export class CalendarComponent extends Component {
980989 * Based on CalendarEventComponent implementation in event-renderer.ts
981990 */
982991 private handleTGRenderEvent ( ctx : EventRenderContext ) {
983- console . log (
984- "[Calendar] handleTGRenderEvent called" ,
985- ctx . event . id ,
986- ctx . viewType ,
987- ) ;
988-
989992 // First render the default content
990993 ctx . defaultRender ( ) ;
991994
992995 const { event, el } = ctx ;
993996
997+ // Backup click handler to ensure selection always works
998+ // This guards against cases where the library's click handler fails
999+ if ( ! el . hasAttribute ( "data-click-handler" ) ) {
1000+ el . setAttribute ( "data-click-handler" , "true" ) ;
1001+ this . registerDomEvent ( el , "click" , ( ev ) => {
1002+ // Ignore clicks on the checkbox (it handles its own logic)
1003+ if (
1004+ ( ev . target as HTMLElement ) . closest (
1005+ ".task-list-item-checkbox" ,
1006+ )
1007+ ) {
1008+ return ;
1009+ }
1010+ this . handleTGEventClick ( event ) ;
1011+ } ) ;
1012+ }
1013+
9941014 // Skip if checkbox already added
9951015 if ( el . querySelector ( ".task-list-item-checkbox" ) ) {
9961016 return ;
@@ -1081,15 +1101,22 @@ export class CalendarComponent extends Component {
10811101 newStart : Date | string ,
10821102 newEnd : Date | string ,
10831103 ) {
1104+ if ( event . metadata ?. externalDrop === true ) {
1105+ this . updateExternalTextEventTime ( event , newStart , newEnd ) ;
1106+ return ;
1107+ }
1108+
10841109 const task = getTaskFromEvent ( event as any ) ;
10851110 if ( ! task ) {
10861111 new Notice ( t ( "Failed to update task: Task not found" ) ) ;
10871112 return ;
10881113 }
10891114
1090- // Don't allow dragging ICS tasks (external calendar events)
1115+ // Don't allow dragging read-only ICS tasks (URL-based external calendar events)
1116+ // OAuth providers (Google, Outlook, Apple) support two-way sync and can be dragged
10911117 const isIcsTask = ( task as any ) . source ?. type === "ics" ;
1092- if ( isIcsTask ) {
1118+ const isReadOnly = ( task as any ) . readonly === true ;
1119+ if ( isIcsTask && isReadOnly ) {
10931120 new Notice (
10941121 t (
10951122 "Cannot move external calendar events. Please update them in the original calendar." ,
@@ -1309,16 +1336,26 @@ export class CalendarComponent extends Component {
13091336 newStart : Date | string ,
13101337 newEnd : Date | string ,
13111338 ) {
1339+ if ( event . metadata ?. externalDrop === true ) {
1340+ this . updateExternalTextEventTime ( event , newStart , newEnd ) ;
1341+ return ;
1342+ }
1343+
13121344 const task = getTaskFromEvent ( event as any ) ;
13131345 if ( ! task ) {
13141346 new Notice ( t ( "Failed to update task: Task not found" ) ) ;
13151347 return ;
13161348 }
13171349
1350+ // Don't allow resizing read-only ICS tasks (URL-based external calendar events)
1351+ // OAuth providers (Google, Outlook, Apple) support two-way sync and can be resized
13181352 const isIcsTask = ( task as any ) . source ?. type === "ics" ;
1319- if ( isIcsTask ) {
1353+ const isReadOnly = ( task as any ) . readonly === true ;
1354+ if ( isIcsTask && isReadOnly ) {
13201355 new Notice (
1321- "In current version, cannot resize external calendar events" ,
1356+ t (
1357+ "Cannot resize external calendar events. Please update them in the original calendar." ,
1358+ ) ,
13221359 ) ;
13231360 setTimeout ( ( ) => {
13241361 this . processTasks ( ) ;
@@ -1559,6 +1596,205 @@ export class CalendarComponent extends Component {
15591596 }
15601597 }
15611598
1599+ private registerExternalTextDropHandlers ( ) : void {
1600+ const supportsTextDrop = (
1601+ dt : DataTransfer | null ,
1602+ ) : dt is DataTransfer => {
1603+ if ( ! dt ) return false ;
1604+ const types = Array . from ( dt . types ?? [ ] ) . map ( ( t ) =>
1605+ String ( t ) . toLowerCase ( ) ,
1606+ ) ;
1607+ const hasStringItem = Array . from ( dt . items ?? [ ] ) . some (
1608+ ( item ) => item . kind === "string" ,
1609+ ) ;
1610+ return (
1611+ hasStringItem ||
1612+ types . includes ( "text/plain" ) ||
1613+ types . includes ( "text" ) ||
1614+ types . includes ( "text/unicode" ) ||
1615+ types . includes ( "text/html" ) ||
1616+ types . includes ( "text/uri-list" )
1617+ ) ;
1618+ } ;
1619+
1620+ const readDroppedText = ( dt : DataTransfer ) : string => {
1621+ const raw =
1622+ dt . getData ( "text/plain" ) ||
1623+ dt . getData ( "text" ) ||
1624+ dt . getData ( "Text" ) ||
1625+ dt . getData ( "text/unicode" ) ||
1626+ dt . getData ( "text/uri-list" ) ||
1627+ "" ;
1628+ return raw . trim ( ) ;
1629+ } ;
1630+
1631+ this . registerDomEvent (
1632+ this . viewContainerEl ,
1633+ "dragover" ,
1634+ ( e : DragEvent ) => {
1635+ if ( ! supportsTextDrop ( e . dataTransfer ) ) return ;
1636+ e . preventDefault ( ) ;
1637+ e . dataTransfer . dropEffect = "copy" ;
1638+ } ,
1639+ ) ;
1640+
1641+ this . registerDomEvent ( this . viewContainerEl , "drop" , ( e : DragEvent ) => {
1642+ if ( ! supportsTextDrop ( e . dataTransfer ) ) return ;
1643+
1644+ const text = readDroppedText ( e . dataTransfer ) ;
1645+ if ( ! text ) return ;
1646+
1647+ const target = this . resolveExternalDropTarget ( e ) ;
1648+ if ( ! target ) return ;
1649+
1650+ e . preventDefault ( ) ;
1651+ e . stopPropagation ( ) ;
1652+
1653+ this . addExternalTextEvent ( text , target ) ;
1654+ } ) ;
1655+ }
1656+
1657+ private resolveExternalDropTarget (
1658+ e : DragEvent ,
1659+ ) : { start : Date ; end : Date ; allDay : boolean } | null {
1660+ const targetEl = e . target instanceof HTMLElement ? e . target : null ;
1661+ if ( ! targetEl ) return null ;
1662+
1663+ // Week/Day view: drop into a time column -> snap start time to the slot
1664+ const dayColumn = targetEl . closest < HTMLElement > (
1665+ ".tg-day-column[data-date]" ,
1666+ ) ;
1667+ if ( dayColumn ?. dataset . date ) {
1668+ const rect = dayColumn . getBoundingClientRect ( ) ;
1669+ const relY = e . clientY - rect . top ;
1670+
1671+ // Keep in sync with our TGCalendar config (theme.cellHeight / draggable.snapMinutes)
1672+ const cellHeightPx = 60 ;
1673+ const snapMinutes = 15 ;
1674+
1675+ const rawMinutes = ( relY / cellHeightPx ) * 60 ;
1676+ const snappedMinutes = Math . max (
1677+ 0 ,
1678+ Math . min (
1679+ 1440 ,
1680+ Math . round ( rawMinutes / snapMinutes ) * snapMinutes ,
1681+ ) ,
1682+ ) ;
1683+
1684+ const baseDate = dateFns . parseISO ( dayColumn . dataset . date ) ;
1685+ const start = dateFns . setMinutes (
1686+ dateFns . setHours ( baseDate , Math . floor ( snappedMinutes / 60 ) ) ,
1687+ snappedMinutes % 60 ,
1688+ ) ;
1689+ const end = dateFns . addMinutes ( start , 30 ) ;
1690+
1691+ return { start, end, allDay : false } ;
1692+ }
1693+
1694+ // Month view: drop into a day cell -> place at that day (all-day)
1695+ const monthCell = targetEl . closest < HTMLElement > ( ".tg-month-cell" ) ;
1696+ const monthRow = monthCell ?. closest < HTMLElement > (
1697+ ".tg-month-row[data-date]" ,
1698+ ) ;
1699+ if ( monthCell && monthRow ?. dataset . date ) {
1700+ const cells = Array . from (
1701+ monthRow . querySelectorAll < HTMLElement > ( ".tg-month-cell" ) ,
1702+ ) ;
1703+ const index = cells . indexOf ( monthCell ) ;
1704+ if ( index < 0 ) return null ;
1705+
1706+ const rowStart = dateFns . parseISO ( monthRow . dataset . date ) ;
1707+ const date = addDays ( rowStart , index ) ;
1708+ const start = startOfDay ( date ) ;
1709+
1710+ return { start, end : start , allDay : true } ;
1711+ }
1712+
1713+ return null ;
1714+ }
1715+
1716+ private addExternalTextEvent (
1717+ text : string ,
1718+ target : { start : Date ; end : Date ; allDay : boolean } ,
1719+ ) : void {
1720+ const title =
1721+ text
1722+ . split ( / \r ? \n / )
1723+ . map ( ( l ) => l . trim ( ) )
1724+ . find ( ( l ) => l . length > 0 ) ?? t ( "New event" ) ;
1725+
1726+ const id = `external-drop:${ Date . now ( ) } :${ Math . random ( ) . toString ( 16 ) . slice ( 2 ) } ` ;
1727+ const toDateTime = ( d : Date ) => dateFns . format ( d , "yyyy-MM-dd HH:mm" ) ;
1728+
1729+ const normalizedStart = target . allDay
1730+ ? startOfDay ( target . start )
1731+ : target . start ;
1732+ const normalizedEnd = target . allDay
1733+ ? startOfDay ( target . end )
1734+ : target . end ;
1735+
1736+ const event : AdapterCalendarEvent = {
1737+ id,
1738+ title,
1739+ start : toDateTime ( normalizedStart ) ,
1740+ end : toDateTime ( normalizedEnd ) ,
1741+ color : "var(--interactive-accent)" ,
1742+ allDay : target . allDay ,
1743+ metadata : {
1744+ externalDrop : true ,
1745+ rawText : text ,
1746+ createdAt : Date . now ( ) ,
1747+ } ,
1748+ } ;
1749+
1750+ this . externalTextEvents . push ( event ) ;
1751+ this . tgCalendar ?. addEvent ( event ) ;
1752+ }
1753+
1754+ /**
1755+ * Update the time of an external text event when dragged
1756+ */
1757+ private updateExternalTextEventTime (
1758+ event : AdapterCalendarEvent ,
1759+ newStart : Date | string ,
1760+ newEnd : Date | string ,
1761+ ) : void {
1762+ const index = this . externalTextEvents . findIndex (
1763+ ( e ) => e . id === event . id ,
1764+ ) ;
1765+ if ( index < 0 ) return ;
1766+
1767+ const startDate =
1768+ newStart instanceof Date ? newStart : new Date ( newStart ) ;
1769+ const endInput = newEnd ?? newStart ;
1770+ const endDate =
1771+ endInput instanceof Date ? endInput : new Date ( endInput ) ;
1772+
1773+ if (
1774+ Number . isNaN ( startDate . getTime ( ) ) ||
1775+ Number . isNaN ( endDate . getTime ( ) )
1776+ ) {
1777+ return ;
1778+ }
1779+
1780+ const isAllDayEvent = event . allDay === true ;
1781+ const normalizedStart = isAllDayEvent
1782+ ? startOfDay ( startDate )
1783+ : startDate ;
1784+ const normalizedEnd = isAllDayEvent ? startOfDay ( endDate ) : endDate ;
1785+ const toDateTime = ( d : Date ) => dateFns . format ( d , "yyyy-MM-dd HH:mm" ) ;
1786+
1787+ const current = this . externalTextEvents [ index ] ;
1788+ if ( ! current ) return ;
1789+
1790+ this . externalTextEvents [ index ] = {
1791+ ...current ,
1792+ start : toDateTime ( normalizedStart ) ,
1793+ end : toDateTime ( normalizedEnd ) ,
1794+ allDay : isAllDayEvent ,
1795+ } ;
1796+ }
1797+
15621798 // ============================================
15631799 // TGCalendar Interaction Handlers (v0.6.0+)
15641800 // ============================================
@@ -1775,7 +2011,10 @@ export class CalendarComponent extends Component {
17752011 const tasksWithDates = this . tasks . filter ( ( task ) =>
17762012 hasDateInformation ( task ) ,
17772013 ) ;
1778- return tasksToCalendarEvents ( tasksWithDates ) ;
2014+ return [
2015+ ...tasksToCalendarEvents ( tasksWithDates ) ,
2016+ ...this . externalTextEvents ,
2017+ ] ;
17792018 }
17802019
17812020 private getTasksForDate ( date : Date ) : Task [ ] {
0 commit comments