From 5c7c99ba7edc5df3006dec0d51cbd833eec4c5a4 Mon Sep 17 00:00:00 2001 From: bhuvaneshdhakshinmaoorthy Date: Wed, 7 Oct 2026 18:46:13 +0530 Subject: [PATCH 1/2] 1055046: Made corrections and added comments for code snippets --- client/vue/package.json | 2 + client/vue/src/App.vue | 304 +++++++++++++---- client/vue/src/SpreadsheetEditorAdapter.ts | 147 ++++---- client/vue/src/style.css | 149 ++++++++- .../CollaborativeEditingController.cs | 314 +++++++++--------- 5 files changed, 611 insertions(+), 305 deletions(-) diff --git a/client/vue/package.json b/client/vue/package.json index 282e3ef..77f0037 100644 --- a/client/vue/package.json +++ b/client/vue/package.json @@ -12,6 +12,8 @@ "@microsoft/signalr": "^9.0.0", "@syncfusion/ej2-collaborator": "35.1.37", "@syncfusion/ej2-spreadsheet": "35.1.37", + "@syncfusion/ej2-buttons": "35.1.37", + "@syncfusion/ej2-popups": "35.1.37", "@syncfusion/ej2-tailwind3-theme": "35.1.37", "@syncfusion/ej2-vue-spreadsheet": "35.1.37", "vue": "^3.5.22" diff --git a/client/vue/src/App.vue b/client/vue/src/App.vue index bb6b0fc..eb6293d 100644 --- a/client/vue/src/App.vue +++ b/client/vue/src/App.vue @@ -1,102 +1,262 @@ diff --git a/client/vue/src/SpreadsheetEditorAdapter.ts b/client/vue/src/SpreadsheetEditorAdapter.ts index 16b703a..88dce89 100644 --- a/client/vue/src/SpreadsheetEditorAdapter.ts +++ b/client/vue/src/SpreadsheetEditorAdapter.ts @@ -1,83 +1,100 @@ import type { - ICollaborationActionData, - ICollaborationProvider + ICollaborationActionData, + ICollaborationProvider } from '@syncfusion/ej2-collaborator'; import type { Spreadsheet } from '@syncfusion/ej2-spreadsheet'; interface ImportFileResponse { - sfdt: string; - version: number; + sfdt: string; + version: number; } +/** Connects the Spreadsheet with the Collaboration Client and Server. */ export class SpreadsheetEditorAdapter implements ICollaborationProvider { - public currentRoomName: string = ''; + public currentRoomName: string = ''; - public constructor( - private spreadsheet: Spreadsheet, - private serviceUrl: string, - private currentUser: string - ) { - this.serviceUrl = serviceUrl.endsWith('/') - ? serviceUrl - : serviceUrl + '/'; - } - - public async loadFromServer( - fileName: string, - roomName: string - ): Promise { - const response: Response = await fetch( - this.serviceUrl + 'api/CollaborativeEditing/ImportFile', - { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ fileName, roomName }) - } - ); - - if (!response.ok) { - throw new Error('Failed to load the workbook.'); + /** + * Initializes a new instance of the Spreadsheet collaboration adapter. + * + * @param spreadsheet - The Spreadsheet instance used for collaborative editing. + * @param serviceUrl - The Collaboration Server URL. + * @param currentUser - The display name of the current participant. + */ + public constructor( + private spreadsheet: Spreadsheet, + private serviceUrl: string, + private currentUser: string + ) { + this.serviceUrl = serviceUrl.endsWith('/') + ? serviceUrl + : serviceUrl + '/'; } - const data: ImportFileResponse = JSON.parse( - await response.text() - ); + /** + * Loads the latest synchronized workbook and initializes the collaboration room. + * + * @param fileName - The name of the workbook to load. + * @param roomName - The unique collaboration room name. + */ + public async loadFromServer(fileName: string, roomName: string): Promise { + const response: Response = await fetch( + this.serviceUrl + 'api/CollaborativeEditing/ImportFile', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + fileName, + roomName + }) + } + ); - this.currentRoomName = roomName; - this.spreadsheet.collaborativeEditingModule.updateRoomInfo( - roomName, - data.version, - this.serviceUrl + 'api/CollaborativeEditing/' - ); - this.spreadsheet.collaborativeEditingModule.setLocalUser( - this.currentUser - ); - this.spreadsheet.openFromJson({ file: data.sfdt }); - } + if (!response.ok) { + throw new Error('Failed to load the workbook.'); + } - public sendActionToServer(action: unknown): void { - if (!action) { - return; - } + const data: ImportFileResponse = JSON.parse(await response.text()); - this.spreadsheet.collaborativeEditingModule.sendActionToServer( - action - ); - } + this.currentRoomName = roomName; + this.spreadsheet.collaborativeEditingModule.updateRoomInfo( + roomName, + data.version, + this.serviceUrl + 'api/CollaborativeEditing/' + ); + this.spreadsheet.collaborativeEditingModule.setLocalUser(this.currentUser); + this.spreadsheet.openFromJson({ + file: data.sfdt + }); + } - public applyRemoteAction( - action: string, - data: ICollaborationActionData - ): void { - if (!data) { - return; + /** + * Sends a completed local Spreadsheet action to the Collaboration Server. + * + * @param action - The completed Spreadsheet action. + */ + public sendActionToServer(action: unknown): void { + if (action) { + this.spreadsheet.collaborativeEditingModule.sendActionToServer(action); + } } - this.spreadsheet.collaborativeEditingModule.applyRemoteAction( - action, - data.payload - ); - } + /** + * Applies an action received from another participant to the Spreadsheet. + * + * @param action - The collaborative action name. + * @param data - The collaborative action data received from the server. + */ + public applyRemoteAction( + action: string, + data: ICollaborationActionData + ): void { + if (data) { + this.spreadsheet.collaborativeEditingModule.applyRemoteAction( + action, + data.payload + ); + } + } } diff --git a/client/vue/src/style.css b/client/vue/src/style.css index 997bf1a..3808579 100644 --- a/client/vue/src/style.css +++ b/client/vue/src/style.css @@ -3,6 +3,151 @@ html, body, #app { - height: 100%; - margin: 0; + height: 100%; + margin: 0; +} + +.collaborative-sample, +.collaborative-sample * { + box-sizing: border-box; +} + +.collaborative-sample { + width: 100%; + margin: 0; + padding: 0; + color: #242424; + font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, + "Helvetica Neue", Arial, sans-serif; +} + +.collaboration-info-bar { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: 50px; + min-height: 50px; + max-height: 50px; + margin: 0; + padding: 6px 12px; + border: 1px solid #d6d6d6; + border-bottom: 0; + background-color: #f8f9fa; +} + +.collaboration-info-text { + min-width: 0; + margin: 0; + padding-right: 16px; + overflow: hidden; + color: #424242; + font-family: inherit; + font-size: 14px; + font-weight: 400; + line-height: 20px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.share-tooltip-host { + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + height: 36px; +} + +.share-button.e-btn, +.copy-url-button.e-btn { + display: flex !important; + align-items: center !important; + justify-content: center !important; + height: 36px; + font-family: inherit; + line-height: normal !important; +} + +.share-button.e-btn { + min-width: 88px; + margin: 0; + padding: 0 16px !important; + font-size: 14px; + font-weight: 600; +} + +.spreadsheet-container { + width: 100%; + margin: 0; + padding: 0; + overflow: hidden; +} + +.collaboration-share-tooltip.e-tooltip-wrap { + max-width: none; + font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, + "Helvetica Neue", Arial, sans-serif; +} + +.collaboration-share-tooltip .e-tip-content { + padding: 12px; +} + +.share-tooltip-content { + width: 420px; + max-width: calc(100vw - 48px); +} + +.share-url-label { + display: block; + margin-bottom: 8px; + font-size: 14px; + font-weight: 500; + line-height: 20px; +} + +.share-url-row { + display: flex; + align-items: center; + gap: 8px; + width: 100%; +} + +.share-url-input { + flex: 1 1 auto; + min-width: 0; + height: 36px; + font-family: inherit; + font-size: 13px; +} + +.copy-url-button.e-btn { + flex: 0 0 auto; + min-width: 106px; + padding: 0 12px !important; + white-space: nowrap; +} + +@media (max-width: 600px) { + .collaboration-info-bar { + padding-right: 8px; + padding-left: 8px; + } + + .collaboration-info-text { + font-size: 13px; + } + + .share-tooltip-content { + width: calc(100vw - 64px); + } + + .share-url-row { + align-items: stretch; + flex-direction: column; + } + + .copy-url-button.e-btn { + width: 100%; + } } diff --git a/server/aspnet-core/Controllers/CollaborativeEditingController.cs b/server/aspnet-core/Controllers/CollaborativeEditingController.cs index ed826f9..ab93d23 100644 --- a/server/aspnet-core/Controllers/CollaborativeEditingController.cs +++ b/server/aspnet-core/Controllers/CollaborativeEditingController.cs @@ -19,6 +19,9 @@ namespace EJ2SpreadsheetServer.Controllers { + /// + /// Provides APIs for Spreadsheet collaborative editing actions and participant selections. + /// [Route("api/[controller]")] [ApiController] public class CollaborativeEditingController : ControllerBase @@ -26,20 +29,17 @@ public class CollaborativeEditingController : ControllerBase private static readonly ConcurrentDictionary< string, ConcurrentDictionary> - RoomSelections = - new ConcurrentDictionary< - string, - ConcurrentDictionary< - string, - SpreadsheetSelectionInfo>>(); + RoomSelections = new ConcurrentDictionary< + string, + ConcurrentDictionary>(); private readonly IWebHostEnvironment hostingEnvironment; private readonly IActionService actionService; private readonly ICollaborationAdapter adapter; private readonly IActiveTransport transport; - private static readonly JsonSerializerSettings - ControllerJsonSettings = new JsonSerializerSettings + private static readonly JsonSerializerSettings ControllerJsonSettings = + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new DefaultContractResolver @@ -48,6 +48,13 @@ private static readonly JsonSerializerSettings } }; + /// + /// Initializes a new instance of the class. + /// + /// Provides access to the server hosting environment. + /// Manages collaborative editing actions and versions. + /// Maps Spreadsheet actions to and from collaboration actions. + /// Broadcasts collaboration updates to connected participants. public CollaborativeEditingController( IWebHostEnvironment hostingEnvironment, IActionService actionService, @@ -60,14 +67,17 @@ public CollaborativeEditingController( this.transport = transport; } + /// + /// Loads the source workbook, applies pending room actions, and returns the synchronized workbook. + /// + /// Contains the workbook name and collaboration room name. + /// The serialized workbook content and current collaboration version. [HttpPost] [Route("ImportFile")] [EnableCors("AllowAllOrigins")] - public async Task ImportFile( - [FromBody] FileInfo param) + public async Task ImportFile([FromBody] FileInfo param) { - if (param == null || - string.IsNullOrWhiteSpace(param.roomName)) + if (param == null || string.IsNullOrWhiteSpace(param.roomName)) { return null; } @@ -91,61 +101,42 @@ await actionService.GetPendingOperationsAsync( 0, -1 ); + List spreadsheetActions = collaborationActions == null + ? new List() + : collaborationActions + .Select(action => + adapter.MapGenericToControlAction(action) as ActionInfo + ) + .Where(action => action != null) + .OrderBy(action => action.Version) + .ToList(); - List spreadsheetActions = - collaborationActions == null - ? new List() - : collaborationActions - .Select(action => - adapter.MapGenericToControlAction( - action - ) as ActionInfo - ) - .Where(action => action != null) - .OrderBy(action => action.Version) - .ToList(); - - using (ExcelEngine temporaryExcelEngine = - new ExcelEngine()) + using (ExcelEngine temporaryExcelEngine = new ExcelEngine()) { - IApplication application = - temporaryExcelEngine.Excel; - - IWorkbook temporaryWorkbook = - application.Workbooks.Open(filePath); + IApplication application = temporaryExcelEngine.Excel; + IWorkbook temporaryWorkbook = application.Workbooks.Open(filePath); try { if (spreadsheetActions.Count > 0) { CollaborativeEditingHandler handler = - new CollaborativeEditingHandler( - temporaryWorkbook - ); + new CollaborativeEditingHandler(temporaryWorkbook); - foreach (ActionInfo action in - spreadsheetActions) + foreach (ActionInfo action in spreadsheetActions) { handler.UpdateAction(action); } } - using (MemoryStream workbookStream = - new MemoryStream()) + using (MemoryStream workbookStream = new MemoryStream()) { - temporaryWorkbook.SaveAs( - workbookStream - ); - + temporaryWorkbook.SaveAs(workbookStream); workbookStream.Position = 0; - string clientFileName = - string.IsNullOrWhiteSpace( - param.fileName - ) - ? "Sample" - : param.fileName; - + string clientFileName = string.IsNullOrWhiteSpace(param.fileName) + ? "Sample" + : param.fileName; IFormFile formFile = new FormFile( workbookStream, 0, @@ -153,33 +144,21 @@ await actionService.GetPendingOperationsAsync( clientFileName, "Sample.xlsx" ); + OpenRequest openRequest = new OpenRequest + { + File = formFile + }; + string workbookJson = Workbook.Open(openRequest); + int currentVersion = spreadsheetActions.Count > 0 + ? spreadsheetActions.Max(action => action.Version) + : 0; + DocumentContent content = new DocumentContent + { + sfdt = workbookJson, + version = currentVersion + }; - OpenRequest openRequest = - new OpenRequest - { - File = formFile - }; - - string workbookJson = - Workbook.Open(openRequest); - - int currentVersion = - spreadsheetActions.Count > 0 - ? spreadsheetActions.Max( - action => action.Version - ) - : 0; - - DocumentContent content = - new DocumentContent - { - sfdt = workbookJson, - version = currentVersion - }; - - return JsonConvert.SerializeObject( - content - ); + return JsonConvert.SerializeObject(content); } } finally @@ -190,42 +169,33 @@ await actionService.GetPendingOperationsAsync( } catch (Exception exception) { - Console.WriteLine( - "Spreadsheet import failed: " + - exception - ); - + Console.WriteLine("Spreadsheet import failed: " + exception); return null; } } + /// + /// Stores a Spreadsheet action and broadcasts the processed action to the collaboration room. + /// + /// Contains the Spreadsheet action and collaboration room information. + /// The serialized action after collaboration processing. [HttpPost] [Route("UpdateAction")] [EnableCors("AllowAllOrigins")] - public async Task UpdateAction( - [FromBody] ActionInfo param) + public async Task UpdateAction([FromBody] ActionInfo param) { - if (param == null || - string.IsNullOrWhiteSpace(param.RoomName)) + if (param == null || string.IsNullOrWhiteSpace(param.RoomName)) { return null; } CollaborationAction collaborationAction = adapter.MapControlToGenericAction(param); - CollaborationAction modifiedAction = - await actionService.AddOperationAsync( - collaborationAction, - adapter - ); - - ActionInfo updatedAction = - modifiedAction == null - ? null - : adapter.MapGenericToControlAction( - modifiedAction - ) as ActionInfo; + await actionService.AddOperationAsync(collaborationAction, adapter); + ActionInfo updatedAction = modifiedAction == null + ? null + : adapter.MapGenericToControlAction(modifiedAction) as ActionInfo; if (updatedAction == null) { @@ -236,7 +206,6 @@ await actionService.AddOperationAsync( updatedAction, ControllerJsonSettings ); - await transport.SendToGroupAsync( param.RoomName, "action", @@ -246,12 +215,16 @@ await transport.SendToGroupAsync( return payload; } + /// + /// Updates a participant selection and broadcasts it to other users in the room. + /// + /// Contains the participant selection and connection details. + /// The updated participant selection. [HttpPost] [Route("UpdateSelection")] [EnableCors("AllowAllOrigins")] - public async Task - UpdateSelection( - [FromBody] SpreadsheetSelectionInfo param) + public async Task UpdateSelection( + [FromBody] SpreadsheetSelectionInfo param) { if (param == null || string.IsNullOrWhiteSpace(param.RoomName) || @@ -260,22 +233,16 @@ public async Task return param; } - ConcurrentDictionary< - string, - SpreadsheetSelectionInfo> selections = - RoomSelections.GetOrAdd( - param.RoomName, - _ => new ConcurrentDictionary< - string, - SpreadsheetSelectionInfo>() - ); - + ConcurrentDictionary selections = + RoomSelections.GetOrAdd( + param.RoomName, + _ => new ConcurrentDictionary() + ); selections.AddOrUpdate( param.ConnectionId, param, (_, _) => param ); - await transport.SendToGroupExceptAsync( param.RoomName, param.ConnectionId, @@ -286,28 +253,34 @@ await transport.SendToGroupExceptAsync( return param; } + /// + /// Returns the active participant selections for a collaboration room. + /// + /// The collaboration room name. + /// The active participant selections in the room. [HttpGet] [Route("GetRoomSelections/{roomName}")] [EnableCors("AllowAllOrigins")] - public ActionResult> - GetRoomSelections(string roomName) + public ActionResult> GetRoomSelections( + string roomName) { if (string.IsNullOrWhiteSpace(roomName) || !RoomSelections.TryGetValue( roomName, - out ConcurrentDictionary< - string, - SpreadsheetSelectionInfo> selections + out ConcurrentDictionary selections )) { - return Ok( - new List() - ); + return Ok(new List()); } return Ok(selections.Values.ToList()); } + /// + /// Removes a disconnected participant selection from the collaboration room. + /// + /// Contains the collaboration room and connection identifiers. + /// An HTTP success result. [HttpPost] [Route("RemoveUserSelection")] [EnableCors("AllowAllOrigins")] @@ -315,79 +288,61 @@ public ActionResult RemoveUserSelection( [FromBody] RemoveSelectionRequest request) { if (request == null || - string.IsNullOrWhiteSpace( - request.RoomName - ) || - string.IsNullOrWhiteSpace( - request.ConnectionId - )) + string.IsNullOrWhiteSpace(request.RoomName) || + string.IsNullOrWhiteSpace(request.ConnectionId)) { return Ok(); } if (RoomSelections.TryGetValue( request.RoomName, - out ConcurrentDictionary< - string, - SpreadsheetSelectionInfo> selections + out ConcurrentDictionary selections )) { - selections.TryRemove( - request.ConnectionId, - out _ - ); + selections.TryRemove(request.ConnectionId, out _); if (selections.IsEmpty) { - RoomSelections.TryRemove( - request.RoomName, - out _ - ); + RoomSelections.TryRemove(request.RoomName, out _); } } return Ok(); } + /// + /// Returns collaboration actions newer than the client's last synchronized version. + /// + /// Contains the room name and last synchronized version. + /// The serialized list of pending Spreadsheet actions. [HttpPost] [Route("GetActionsFromServer")] [EnableCors("AllowAllOrigins")] - public async Task>> - GetActionsFromServer( - [FromBody] ActionInfo param) + public async Task>> GetActionsFromServer( + [FromBody] ActionInfo param) { - if (param == null || - string.IsNullOrWhiteSpace(param.RoomName)) + if (param == null || string.IsNullOrWhiteSpace(param.RoomName)) { return Ok(new List()); } int lastSyncedVersion = param.Version; - List collaborationActions = - await actionService - .GetEffectivePendingVersionAsync( - param.RoomName, - lastSyncedVersion - ); - - List actions = - collaborationActions == null - ? new List() - : collaborationActions - .Select(action => - adapter.MapGenericToControlAction( - action - ) as ActionInfo - ) - .Where(action => - action != null && - action.Version > - lastSyncedVersion - ) - .OrderBy(action => action.Version) - .ToList(); - + await actionService.GetEffectivePendingVersionAsync( + param.RoomName, + lastSyncedVersion + ); + List actions = collaborationActions == null + ? new List() + : collaborationActions + .Select(action => + adapter.MapGenericToControlAction(action) as ActionInfo + ) + .Where(action => + action != null && action.Version > lastSyncedVersion + ) + .OrderBy(action => action.Version) + .ToList(); string payload = JsonConvert.SerializeObject( actions, ControllerJsonSettings @@ -396,24 +351,51 @@ await actionService return Ok(payload); } + /// + /// Represents a request to remove a participant selection. + /// public class RemoveSelectionRequest { + /// + /// Gets or sets the collaboration room name. + /// public string RoomName { get; set; } + /// + /// Gets or sets the participant connection identifier. + /// public string ConnectionId { get; set; } } + /// + /// Represents the synchronized workbook content and collaboration version. + /// public class DocumentContent { + /// + /// Gets or sets the current collaboration version. + /// public int version { get; set; } + /// + /// Gets or sets the serialized workbook content. + /// public string sfdt { get; set; } } + /// + /// Represents a workbook import request. + /// public class FileInfo { + /// + /// Gets or sets the workbook file name. + /// public string fileName { get; set; } + /// + /// Gets or sets the collaboration room name. + /// public string roomName { get; set; } } } From 667d7557d1f0d298adae2e8ac1796d25f38a0ec6 Mon Sep 17 00:00:00 2001 From: bhuvaneshdhakshinmaoorthy Date: Wed, 7 Oct 2026 19:09:43 +0530 Subject: [PATCH 2/2] 1055046: Made corrections and added comments for code snippets --- client/vue/src/App.vue | 80 ++++++++++++++++++++++++++++------------ client/vue/src/style.css | 10 +++++ 2 files changed, 67 insertions(+), 23 deletions(-) diff --git a/client/vue/src/App.vue b/client/vue/src/App.vue index eb6293d..bb0a34b 100644 --- a/client/vue/src/App.vue +++ b/client/vue/src/App.vue @@ -25,18 +25,22 @@ const userNames: string[] = [ 'Charles Edwards', 'Lucy Walker' ]; +// Injects the collaborative editing service into the Spreadsheet. provide('spreadsheet', [CollaborativeEditingHandler]); const spreadsheetRef = ref | null>(null); -const tooltipHostRef = ref(null); const shareButtonRef = ref(null); -const currentUser: string = userNames[Math.floor(Math.random() * userNames.length)] ?? 'John Sullivan'; +const currentUser: string = + userNames[Math.floor(Math.random() * userNames.length)] ?? 'John Sullivan'; let adapter: SpreadsheetEditorAdapter | null = null; let collaborationClient: CollaborationClient | null = null; let shareButton: Button | null = null; let copyButton: Button | null = null; let shareTooltip: Tooltip | null = null; +let tooltipContent: HTMLElement | null = null; +let collaborationUrlInput: HTMLInputElement | null = null; +let copyButtonElement: HTMLButtonElement | null = null; let copyResetTimer: number | null = null; let initialized: boolean = false; @@ -77,6 +81,21 @@ function closeShareTooltip(): void { } } +/** Opens the collaboration Share tooltip. */ +function openShareTooltip(): void { + if (!shareButtonRef.value || !shareTooltip) { + return; + } + + resetCopyButton(); + + if (collaborationUrlInput) { + collaborationUrlInput.value = window.location.href; + } + + shareTooltip.open(shareButtonRef.value); +} + /** Copies the collaboration URL and displays the copied state temporarily. */ async function copyUrl(): Promise { try { @@ -97,7 +116,7 @@ async function copyUrl(): Promise { } } -/** Creates the share tooltip content and initializes the Copy URL button. */ +/** Creates the content displayed in the Share tooltip. */ function createShareTooltipContent(): HTMLElement { const content: HTMLDivElement = document.createElement('div'); const label: HTMLLabelElement = document.createElement('label'); @@ -116,6 +135,7 @@ function createShareTooltipContent(): HTMLElement { input.value = window.location.href; input.readOnly = true; input.setAttribute('aria-label', 'Collaboration URL'); + collaborationUrlInput = input; buttonElement.type = 'button'; row.append(input, buttonElement); @@ -127,11 +147,12 @@ function createShareTooltipContent(): HTMLElement { }); copyButton.appendTo(buttonElement); buttonElement.addEventListener('click', copyUrl); + copyButtonElement = buttonElement; return content; } -/** Closes the share tooltip when a pointer action occurs outside it. */ +/** Closes the Share tooltip when a pointer action occurs outside it. */ function closeTooltipOnOutsideClick(event: MouseEvent): void { const target: Node = event.target as Node; const tooltipElement: Element | null = document.querySelector( @@ -145,8 +166,14 @@ function closeTooltipOnOutsideClick(event: MouseEvent): void { closeShareTooltip(); } -/** Closes the share tooltip when the page or a parent element is scrolled. */ -function closeTooltipOnScroll(): void { +/** Closes the Share tooltip when the page is scrolled. */ +function closeTooltipOnScroll(event: Event): void { + const target: EventTarget | null = event.target; + + if (target instanceof Element && target.closest('.e-spreadsheet')) { + return; + } + closeShareTooltip(); } @@ -192,32 +219,35 @@ function onActionComplete(args: unknown): void { } onMounted(() => { - if (shareButtonRef.value) { - shareButton = new Button({ - content: 'Share', - cssClass: 'e-primary share-button' - }); - shareButton.appendTo(shareButtonRef.value); + if (!shareButtonRef.value) { + return; } - if (tooltipHostRef.value) { - shareTooltip = new Tooltip({ - content: createShareTooltipContent, - opensOn: 'Click', - position: 'BottomRight', - cssClass: 'collaboration-share-tooltip', - target: '#share-collaboration-url' - }); - shareTooltip.appendTo(tooltipHostRef.value); - } + shareButton = new Button({ + content: 'Share', + cssClass: 'e-primary share-button' + }); + shareButton.appendTo(shareButtonRef.value); + + tooltipContent = createShareTooltipContent(); + shareTooltip = new Tooltip({ + content: tooltipContent, + opensOn: 'Custom', + position: 'BottomRight', + cssClass: 'collaboration-share-tooltip' + }); + shareTooltip.appendTo(shareButtonRef.value); + shareButtonRef.value.addEventListener('click', openShareTooltip); document.addEventListener('mousedown', closeTooltipOnOutsideClick); window.addEventListener('scroll', closeTooltipOnScroll, true); }); onBeforeUnmount(() => { + shareButtonRef.value?.removeEventListener('click', openShareTooltip); document.removeEventListener('mousedown', closeTooltipOnOutsideClick); window.removeEventListener('scroll', closeTooltipOnScroll, true); + copyButtonElement?.removeEventListener('click', copyUrl); if (copyResetTimer !== null) { window.clearTimeout(copyResetTimer); @@ -226,6 +256,9 @@ onBeforeUnmount(() => { shareTooltip?.destroy(); shareButton?.destroy(); copyButton?.destroy(); + tooltipContent = null; + collaborationUrlInput = null; + copyButtonElement = null; collaborationClient = null; adapter = null; initialized = false; @@ -240,7 +273,7 @@ onBeforeUnmount(() => { The link contains a unique session ID that connects all participants to the same workbook. -