From 3c39f49b618a2439554920f85725c1eae2b55c63 Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 24 Nov 2025 18:23:32 +0530 Subject: [PATCH 01/42] feat: prevent user elements like links and buttons from handling the click --- .../BrowserScripts/RemoteFunctions.js | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 0b7beceae..593ad0c7f 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3916,37 +3916,41 @@ function RemoteFunctions(config = {}) { } /** - * This function handles the click event on the live preview DOM element - * this just stops the propagation because otherwise users might not be able to edit buttons or hyperlinks etc - * @param {Event} event + * this acts as the "KILL SWITCH", it blocks all the click related events from user elements + * but we exclude all the phoenix interal elements + * we call this function from inside registerHandlers */ - function onClick(event) { - const element = event.target; + function registerInteractionBlocker() { + const eventsToBlock = ["click", "dblclick", "mousedown", "mouseup"]; - if(isElementInspectable(element)) { - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation(); + eventsToBlock.forEach(eventType => { + window.document.addEventListener(eventType, function(event) { + const element = event.target; - _selectElement(element); - activateHoverLock(); - } - } + // whitelist: phoenix internal elements + if (element.closest("[data-phcode-internal-c15r5a9]")) { + return; + } - /** - * this function handles the double click event - * @param {Event} event - */ - function onDoubleClick(event) { - const element = event.target; - if (isElementEditable(element)) { - // because we only want to allow double click text editing where we show the edit option - if (_shouldShowEditTextOption(element)) { event.preventDefault(); - event.stopPropagation(); - startEditing(element); - } - } + event.stopImmediatePropagation(); + + // handling the click and double click. + // on click we just call the selectElement as it handles everything + // on doubleClick we just need to call the startEditing + if (eventType === "click") { + // event detail is 2 for double clicks + if (event.detail !== 2) { + _selectElement(element); + activateHoverLock(); // we add hover lock on LP clicks + } + } else if (eventType === "dblclick") { + if (isElementEditable(element) && _shouldShowEditTextOption(element)) { + startEditing(element); + } + } + }, true); // true is for capture phase + }); } function onKeyUp(event) { @@ -3967,7 +3971,6 @@ function RemoteFunctions(config = {}) { window.document.addEventListener("mouseover", onMouseOver); window.document.addEventListener("mouseout", onMouseOut); window.document.addEventListener("mousemove", onMouseMove); - window.document.addEventListener("click", onClick); _localHighlight = new Highlight("#ecc", true); _setup = true; } @@ -4831,8 +4834,6 @@ function RemoteFunctions(config = {}) { // Always remove existing listeners first to avoid duplicates window.document.removeEventListener("mouseover", onElementHover); window.document.removeEventListener("mouseout", onElementHoverOut); - window.document.removeEventListener("click", onClick); - window.document.removeEventListener("dblclick", onDoubleClick); window.document.removeEventListener("dragover", onDragOver); window.document.removeEventListener("drop", onDrop); window.document.removeEventListener("dragleave", onDragLeave); @@ -4845,14 +4846,17 @@ function RemoteFunctions(config = {}) { // Initialize click highlight with animation _clickHighlight = new Highlight("#cfc", true); // Light green for click highlight + // register the event handlers window.document.addEventListener("mouseover", onElementHover); window.document.addEventListener("mouseout", onElementHoverOut); - window.document.addEventListener("click", onClick); - window.document.addEventListener("dblclick", onDoubleClick); window.document.addEventListener("dragover", onDragOver); window.document.addEventListener("drop", onDrop); window.document.addEventListener("dragleave", onDragLeave); window.document.addEventListener("keydown", onKeyDown); + + // this is to block all the interactions of the user created elements + // so that lets say user created link doesn't redirect in edit mode + registerInteractionBlocker(); } else { // Clean up any existing UI when edit features are disabled dismissUIAndCleanupState(); From 9e3478b7935d6c82c91126e84a9fc72cddd616c6 Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 24 Nov 2025 18:35:33 +0530 Subject: [PATCH 02/42] fix: internal blocker also blocking in preview mode --- .../BrowserScripts/RemoteFunctions.js | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 593ad0c7f..4dc845a65 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -33,6 +33,9 @@ function RemoteFunctions(config = {}) { // we need this so that we can remove click styling from the previous element when a new element is clicked let previouslyClickedElement = null; + // we store references to interaction blocker event handlers so we can remove them when switching modes + let _interactionBlockerHandlers = null; + var req, timeout; var animateHighlight = function (time) { if(req) { @@ -3920,39 +3923,67 @@ function RemoteFunctions(config = {}) { * but we exclude all the phoenix interal elements * we call this function from inside registerHandlers */ + /** + * Central event blocker for edit mode + * Blocks all user page interactions but allows Phoenix UI to work + * Stores handler references so they can be removed when switching modes + */ function registerInteractionBlocker() { + // Create an object to store handler references + _interactionBlockerHandlers = {}; + const eventsToBlock = ["click", "dblclick", "mousedown", "mouseup"]; eventsToBlock.forEach(eventType => { - window.document.addEventListener(eventType, function(event) { + // Create a named handler function so we can remove it later + const handler = function(event) { const element = event.target; - // whitelist: phoenix internal elements + // WHITELIST: Allow Phoenix internal UI elements to work normally if (element.closest("[data-phcode-internal-c15r5a9]")) { return; } + // BLOCK: Kill all user page interactions in edit mode event.preventDefault(); event.stopImmediatePropagation(); - // handling the click and double click. - // on click we just call the selectElement as it handles everything - // on doubleClick we just need to call the startEditing + // HANDLE: Process clicks and double-clicks for element selection/editing if (eventType === "click") { - // event detail is 2 for double clicks + // Skip click handling on the second click of a double-click + // event.detail = 1 for first click, 2 for second click (during double-click) if (event.detail !== 2) { _selectElement(element); - activateHoverLock(); // we add hover lock on LP clicks + activateHoverLock(); } } else if (eventType === "dblclick") { if (isElementEditable(element) && _shouldShowEditTextOption(element)) { startEditing(element); } } - }, true); // true is for capture phase + }; + + // Store the handler reference + _interactionBlockerHandlers[eventType] = handler; + + // Register the handler in capture phase + window.document.addEventListener(eventType, handler, true); }); } + /** + * this function is to remove all the interaction blocker + * this is needed when user is in preview/highlight mode + */ + function unregisterInteractionBlocker() { + if (_interactionBlockerHandlers) { + Object.keys(_interactionBlockerHandlers).forEach(eventType => { + window.document.removeEventListener(eventType, _interactionBlockerHandlers[eventType], true); + }); + _interactionBlockerHandlers = null; + } + } + function onKeyUp(event) { if (_setup && !_validEvent(event)) { window.document.removeEventListener("keyup", onKeyUp); @@ -4838,6 +4869,7 @@ function RemoteFunctions(config = {}) { window.document.removeEventListener("drop", onDrop); window.document.removeEventListener("dragleave", onDragLeave); window.document.removeEventListener("keydown", onKeyDown); + unregisterInteractionBlocker(); if (config.isProUser) { // Initialize hover highlight with Chrome-like colors From e5e24b1639bb87f5f3b435525f5193af6c83789f Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 24 Nov 2025 23:03:56 +0530 Subject: [PATCH 03/42] fix: cursor not getting positioned properly on element click in edit mode --- .../BrowserScripts/LiveDevProtocolRemote.js | 7 ++-- .../BrowserScripts/RemoteFunctions.js | 39 +++++++++++++++---- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js index 72b95696f..9b62b6b66 100644 --- a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js +++ b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js @@ -363,7 +363,7 @@ ProtocolManager.enable(); }); - function _getAllInheritedSelectorsInOrder(element) { + function getAllInheritedSelectorsInOrder(element) { let selectorsFound= new Map(); const selectorsList = []; while (element) { @@ -383,6 +383,7 @@ return selectorsList; } + global.getAllInheritedSelectorsInOrder = getAllInheritedSelectorsInOrder; /** * Sends the message containing tagID which is being clicked @@ -407,7 +408,7 @@ "nodeID": element.id, "nodeClassList": element.classList, "nodeName": element.nodeName, - "allSelectors": _getAllInheritedSelectorsInOrder(element), + "allSelectors": getAllInheritedSelectorsInOrder(element), "contentEditable": element.contentEditable === 'true', "clicked": true, "edit": true @@ -431,7 +432,7 @@ "nodeID": element.id, "nodeClassList": element.classList, "nodeName": element.nodeName, - "allSelectors": _getAllInheritedSelectorsInOrder(element), + "allSelectors": getAllInheritedSelectorsInOrder(element), "contentEditable": element.contentEditable === 'true', "clicked": true }); diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 4dc845a65..de6eac696 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3918,16 +3918,42 @@ function RemoteFunctions(config = {}) { }, 800); } + /** + * this function is called when user clicks on an element in the LP when in edit mode + * + * @param {HTMLElement} element - The clicked element + * @param {Event} event - The click event + */ + function handleElementClick(element, event) { + if (!isElementInspectable(element)) { return; } + + // send cursor movement message to editor so cursor jumps to clicked element + if (element.hasAttribute("data-brackets-id")) { + const selection = window.getSelection(); + + if (!selection || selection.toString().length === 0) { + window._Brackets_MessageBroker.send({ + "tagId": element.getAttribute("data-brackets-id"), + "nodeID": element.id, + "nodeClassList": element.classList, + "nodeName": element.nodeName, + "allSelectors": window.getAllInheritedSelectorsInOrder(element), + "contentEditable": element.contentEditable === "true", + "clicked": true + }); + } + } + + // call the selectElement as selectElement handles all the highlighting/boxes and all UI related stuff + _selectElement(element); + activateHoverLock(); + } + /** * this acts as the "KILL SWITCH", it blocks all the click related events from user elements * but we exclude all the phoenix interal elements * we call this function from inside registerHandlers */ - /** - * Central event blocker for edit mode - * Blocks all user page interactions but allows Phoenix UI to work - * Stores handler references so they can be removed when switching modes - */ function registerInteractionBlocker() { // Create an object to store handler references _interactionBlockerHandlers = {}; @@ -3953,8 +3979,7 @@ function RemoteFunctions(config = {}) { // Skip click handling on the second click of a double-click // event.detail = 1 for first click, 2 for second click (during double-click) if (event.detail !== 2) { - _selectElement(element); - activateHoverLock(); + handleElementClick(element, event); } } else if (eventType === "dblclick") { if (isElementEditable(element) && _shouldShowEditTextOption(element)) { From 15a8768d66ffb15049d57276b954485f40839ba5 Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 24 Nov 2025 23:19:18 +0530 Subject: [PATCH 04/42] feat: check and clear any selection if anything exists before selecting a new element --- .../BrowserScripts/RemoteFunctions.js | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index de6eac696..e622662e0 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3927,21 +3927,23 @@ function RemoteFunctions(config = {}) { function handleElementClick(element, event) { if (!isElementInspectable(element)) { return; } + // if anything is currently selected, we need to clear that + const selection = window.getSelection(); + if (selection && selection.toString().length > 0) { + selection.removeAllRanges(); + } + // send cursor movement message to editor so cursor jumps to clicked element if (element.hasAttribute("data-brackets-id")) { - const selection = window.getSelection(); - - if (!selection || selection.toString().length === 0) { - window._Brackets_MessageBroker.send({ - "tagId": element.getAttribute("data-brackets-id"), - "nodeID": element.id, - "nodeClassList": element.classList, - "nodeName": element.nodeName, - "allSelectors": window.getAllInheritedSelectorsInOrder(element), - "contentEditable": element.contentEditable === "true", - "clicked": true - }); - } + window._Brackets_MessageBroker.send({ + "tagId": element.getAttribute("data-brackets-id"), + "nodeID": element.id, + "nodeClassList": element.classList, + "nodeName": element.nodeName, + "allSelectors": window.getAllInheritedSelectorsInOrder(element), + "contentEditable": element.contentEditable === "true", + "clicked": true + }); } // call the selectElement as selectElement handles all the highlighting/boxes and all UI related stuff From 07853150fcdb8f02707313c1a665069d30c0fbc7 Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 24 Nov 2025 23:49:21 +0530 Subject: [PATCH 05/42] refactor: use better style when image gallery svg is selected --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index e622662e0..51abb46bb 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1569,7 +1569,11 @@ function RemoteFunctions(config = {}) { if (imageGallerySelected) { styles += ` .node-options span[data-action="image-gallery"] { - background-color: rgba(255, 255, 255, 0.25) !important; + background-color: rgba(50, 50, 220, 0.5) !important; + } + + .node-options span[data-action="image-gallery"]:hover { + background-color: rgba(100, 100, 230, 0.6) !important; } `; } From d80ea2b39dc6724a36b8550021bc72710dcc5088 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 25 Nov 2025 01:14:37 +0530 Subject: [PATCH 06/42] feat: show pre-filled folder in image folder dialog --- src/LiveDevelopment/LivePreviewEdit.js | 43 ++++++++++++++------------ 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 851e84d09..0af6433d1 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -1164,12 +1164,30 @@ define(function (require, exports, module) { const shouldBeChecked = persistFolder !== false; $rememberCheckbox.prop('checked', shouldBeChecked); - _scanRootDirectoriesOnly(projectRoot, rootFolders).then(() => { - stringMatcher = new StringMatch.StringMatcher({ segmentedSearch: true }); - _renderFolderSuggestions(rootFolders.slice(0, 15), $suggestions, $input); - }); + // check if any folder path exists, we pre-fill it + const savedFolder = StateManager.get(IMAGE_DOWNLOAD_FOLDER_KEY, StateManager.PROJECT_CONTEXT); + if (savedFolder !== null && savedFolder !== undefined) { + $input.val(savedFolder); + } - _scanDirectories(projectRoot, '', folderList); + // we only scan root directories if we don't have a pre-filled value + if (!savedFolder) { + _scanRootDirectoriesOnly(projectRoot, rootFolders).then(() => { + stringMatcher = new StringMatch.StringMatcher({ segmentedSearch: true }); + _renderFolderSuggestions(rootFolders.slice(0, 15), $suggestions, $input); + }); + } + + // scan all directories, and if we pre-filled a path, trigger autocomplete suggestions + _scanDirectories(projectRoot, '', folderList).then(() => { + // init stringMatcher if it wasn't created during root scan + if (!stringMatcher) { + stringMatcher = new StringMatch.StringMatcher({ segmentedSearch: true }); + } + if (savedFolder) { + _updateFolderSuggestions(savedFolder, folderList, rootFolders, stringMatcher, $suggestions, $input); + } + }); // input event handler $input.on('input', function() { @@ -1254,19 +1272,6 @@ define(function (require, exports, module) { }); } - /** - * Handles reset of image folder selection - clears the saved preference and shows the dialog - * @private - */ - function _handleResetImageFolderSelection() { - // clear the saved folder preference for this project - StateManager.set(IMAGE_DOWNLOAD_FOLDER_KEY, null, StateManager.PROJECT_CONTEXT); - - // show the folder selection dialog for the user to choose a new folder - // we pass null because we're not downloading an image, just setting the preference - _showFolderSelectionDialog(null); - } - /** * this function is responsible to save the active file (and previewed file, both might be same though) * when ctrl/cmd + s is pressed in the live preview @@ -1332,7 +1337,7 @@ define(function (require, exports, module) { // handle reset image folder selection if (message.resetImageFolderSelection) { - _handleResetImageFolderSelection(); + _showFolderSelectionDialog(null); return; } From 4abb404166ce33da3130953418e3f8c0bf740fc2 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 25 Nov 2025 17:38:39 +0530 Subject: [PATCH 07/42] fix: root directories not being displayed when user clears a pre filled input --- src/LiveDevelopment/LivePreviewEdit.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 0af6433d1..c347cf0e7 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -1186,6 +1186,8 @@ define(function (require, exports, module) { } if (savedFolder) { _updateFolderSuggestions(savedFolder, folderList, rootFolders, stringMatcher, $suggestions, $input); + // load root directories in background so they're ready when user clears input + _scanRootDirectoriesOnly(projectRoot, rootFolders); } }); From 84999265606096ab46df38ec4aa6d65b59a7f4d2 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 25 Nov 2025 17:41:39 +0530 Subject: [PATCH 08/42] fix: drag drop not working because mouse down is blocked --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 51abb46bb..65220dd27 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3964,7 +3964,7 @@ function RemoteFunctions(config = {}) { // Create an object to store handler references _interactionBlockerHandlers = {}; - const eventsToBlock = ["click", "dblclick", "mousedown", "mouseup"]; + const eventsToBlock = ["click", "dblclick"]; eventsToBlock.forEach(eventType => { // Create a named handler function so we can remove it later From 208226e9b0e6f16c6c0238ee3d215225bdae9920 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 25 Nov 2025 20:24:06 +0530 Subject: [PATCH 09/42] refactor: improve suggestions list colors to match it with tab bar --- src/styles/brackets_patterns_override.less | 61 +++++++++++++++------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less index ee4f57cb1..7a5bc286c 100644 --- a/src/styles/brackets_patterns_override.less +++ b/src/styles/brackets_patterns_override.less @@ -2522,20 +2522,20 @@ code { height: 30px; padding: 5px; box-sizing: border-box; - margin-bottom: 8px; + margin-bottom: 10px; } #folder-suggestions { max-height: 150px; overflow-y: auto; overflow-x: hidden; - border: 1px solid @bc-btn-border; - border-radius: @bc-border-radius; - background-color: @bc-panel-bg-alt; + outline: 1px solid #f5f5f5; + border-radius: 3px; + background-color: #f5f5f5; .dark & { - border: 1px solid @dark-bc-btn-border; - background-color: @dark-bc-panel-bg-alt; + background-color: #1E1E1E; + outline: 1px solid #1E1E1E; } &:empty { @@ -2550,37 +2550,60 @@ code { .folder-suggestion-item { padding: 6px 10px; + display: flex; + align-items: center; cursor: pointer; - font-size: 12px; - color: @bc-text; - border-left: 3px solid transparent; + font-size: 0.875rem; + letter-spacing: 0.4px; + word-spacing: 0.75px; + color: #555; + background-color: #f1f1f1; + border-right: 1px solid rgba(0, 0, 0, 0.05); + position: relative; + user-select: none; .dark & { - color: @dark-bc-text; + color: #aaa; + background-color: #292929; + border-right: 1px solid rgba(255, 255, 255, 0.05); } &:hover { - background-color: rgba(0, 0, 0, 0.03); + background-color: #e0e0e0; .dark & { - background-color: rgba(255, 255, 255, 0.05); + background-color: #3b3a3a; } } &.selected { - background-color: rgba(40, 142, 223, 0.08); - border-left-color: #288edf; + background-color: #fff; + color: #333; .dark & { - background-color: rgba(40, 142, 223, 0.15); - border-left-color: #3da3ff; + background-color: #1D1F21; + color: #dedede; + } + + &::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 0.15rem; + background-color: #0078D7; + + .dark & { + background-color: #75BEFF; + } } &:hover { - background-color: rgba(40, 142, 223, 0.12); + background-color: #fff; .dark & { - background-color: rgba(40, 142, 223, 0.2); + background-color: #1D1F21; } } } @@ -2596,7 +2619,7 @@ code { } .folder-help-text { - margin-top: 8px; + margin-top: 10px; margin-bottom: 0; font-size: 11px; color: @bc-text-quiet; From 3653700ac6323759fe29a86cb6b5e2c56947e1df Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 25 Nov 2025 20:58:47 +0530 Subject: [PATCH 10/42] fix: tests failing as boxes not getting dismissed on click on body or html tag --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 65220dd27..c416b8c69 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3929,7 +3929,10 @@ function RemoteFunctions(config = {}) { * @param {Event} event - The click event */ function handleElementClick(element, event) { - if (!isElementInspectable(element)) { return; } + if (!isElementInspectable(element)) { + dismissUIAndCleanupState(); + return; + } // if anything is currently selected, we need to clear that const selection = window.getSelection(); From 79b260a1bbfc4929dee535b185c32b216161722b Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 26 Nov 2025 17:50:47 +0530 Subject: [PATCH 11/42] feat: show a toast message when folder selection dialog is open --- .../BrowserScripts/RemoteFunctions.js | 66 +++++++++++++++++++ src/LiveDevelopment/LivePreviewEdit.js | 13 ++++ 2 files changed, 79 insertions(+) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index c416b8c69..f5631bf6e 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3679,6 +3679,8 @@ function RemoteFunctions(config = {}) { var _hoverLockTimer = null; const DOWNLOAD_EVENTS = { + DIALOG_OPENED: 'dialogOpened', + DIALOG_CLOSED: 'dialogClosed', STARTED: 'downloadStarted', COMPLETED: 'downloadCompleted', CANCELLED: 'downloadCancelled', @@ -3686,6 +3688,58 @@ function RemoteFunctions(config = {}) { }; let _activeDownloads = new Map(); + let _dialogOverlay = null; + + function _showDialogOverlay() { + // don't create multiple overlays + if (_dialogOverlay) { + return; + } + + // create overlay container + const overlay = window.document.createElement('div'); + overlay.className = 'phoenix-dialog-overlay'; + overlay.setAttribute('data-phcode-internal-c15r5a9', 'true'); + overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; ' + + 'background: rgba(0, 0, 0, 0.5); z-index: 10000; pointer-events: auto;'; + + // create toast card (matching existing overlay style: #666 background, #ededed text) + const toast = window.document.createElement('div'); + toast.className = 'phoenix-dialog-toast'; + toast.setAttribute('data-phcode-internal-c15r5a9', 'true'); + toast.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); ' + + 'background: #666; color: #ededed; padding: 24px 32px; border-radius: 6px; ' + + 'box-shadow: 0 4px 12px rgba(0,0,0,0.15); font-size: 16px; text-align: center;'; + + // create icon + const icon = window.document.createElement('span'); + icon.className = 'phoenix-dialog-icon'; + icon.setAttribute('data-phcode-internal-c15r5a9', 'true'); + icon.style.cssText = 'margin-right: 10px; font-size: 18px;'; + icon.textContent = 'ⓘ'; + + // create message + const message = window.document.createElement('span'); + message.className = 'phoenix-dialog-message'; + message.setAttribute('data-phcode-internal-c15r5a9', 'true'); + message.textContent = 'Select image download location in the editor to continue'; + + // assemble the structure + toast.appendChild(icon); + toast.appendChild(message); + overlay.appendChild(toast); + + // append to body + window.document.body.appendChild(overlay); + _dialogOverlay = overlay; + } + + function _hideDialogOverlay() { + if (_dialogOverlay && _dialogOverlay.parentNode) { + _dialogOverlay.parentNode.removeChild(_dialogOverlay); + _dialogOverlay = null; + } + } function handleDownloadEvent(eventType, data) { const downloadId = data && data.downloadId; @@ -3693,6 +3747,18 @@ function RemoteFunctions(config = {}) { return; } + // handle dialog events (these don't require download to exist) + if (eventType === DOWNLOAD_EVENTS.DIALOG_OPENED) { + _showDialogOverlay(); + return; + } + + if (eventType === DOWNLOAD_EVENTS.DIALOG_CLOSED) { + _hideDialogOverlay(); + return; + } + + // handle download events (these require download to exist) const download = _activeDownloads.get(downloadId); if (!download) { return; diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index c347cf0e7..d0b40142e 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -47,6 +47,8 @@ define(function (require, exports, module) { const IMAGE_DOWNLOAD_PERSIST_FOLDER_KEY = "imageGallery.persistFolder"; const DOWNLOAD_EVENTS = { + DIALOG_OPENED: 'dialogOpened', + DIALOG_CLOSED: 'dialogClosed', STARTED: 'downloadStarted', COMPLETED: 'downloadCompleted', CANCELLED: 'downloadCancelled', @@ -1156,6 +1158,11 @@ define(function (require, exports, module) { const $suggestions = $dlg.find("#folder-suggestions"); const $rememberCheckbox = $dlg.find("#remember-folder-checkbox"); + // notify live preview that dialog is now open + if (message && message.downloadId) { + _sendDownloadStatusToBrowser(DOWNLOAD_EVENTS.DIALOG_OPENED, { downloadId: message.downloadId }); + } + let folderList = []; let rootFolders = []; let stringMatcher = null; @@ -1226,6 +1233,12 @@ define(function (require, exports, module) { _sendDownloadStatusToBrowser(DOWNLOAD_EVENTS.CANCELLED, { downloadId: message.downloadId }); } } + + // notify live preview that dialog is now closed + if (message && message.downloadId) { + _sendDownloadStatusToBrowser(DOWNLOAD_EVENTS.DIALOG_CLOSED, { downloadId: message.downloadId }); + } + dialog.close(); }); } From d51696f60c688c5632abaf628561aaf6396d0dd7 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 26 Nov 2025 18:06:57 +0530 Subject: [PATCH 12/42] refactor: better UI for the toast message --- .../BrowserScripts/RemoteFunctions.js | 33 ++++++------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index f5631bf6e..361ee2cf5 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3703,31 +3703,18 @@ function RemoteFunctions(config = {}) { overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; ' + 'background: rgba(0, 0, 0, 0.5); z-index: 10000; pointer-events: auto;'; - // create toast card (matching existing overlay style: #666 background, #ededed text) - const toast = window.document.createElement('div'); - toast.className = 'phoenix-dialog-toast'; - toast.setAttribute('data-phcode-internal-c15r5a9', 'true'); - toast.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); ' + - 'background: #666; color: #ededed; padding: 24px 32px; border-radius: 6px; ' + - 'box-shadow: 0 4px 12px rgba(0,0,0,0.15); font-size: 16px; text-align: center;'; - - // create icon - const icon = window.document.createElement('span'); - icon.className = 'phoenix-dialog-icon'; - icon.setAttribute('data-phcode-internal-c15r5a9', 'true'); - icon.style.cssText = 'margin-right: 10px; font-size: 18px;'; - icon.textContent = 'ⓘ'; - - // create message - const message = window.document.createElement('span'); - message.className = 'phoenix-dialog-message'; - message.setAttribute('data-phcode-internal-c15r5a9', 'true'); - message.textContent = 'Select image download location in the editor to continue'; + // create message bar + const messageBar = window.document.createElement('div'); + messageBar.className = 'phoenix-dialog-message-bar'; + messageBar.setAttribute('data-phcode-internal-c15r5a9', 'true'); + messageBar.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); ' + + 'color: #ffffff; background-color: #333333; padding: 1em 1.5em; ' + + 'text-align: center; font-size: 16px; border-radius: 3px;' + + 'font-family: "SourceSansPro", Helvetica, Arial, sans-serif;'; + messageBar.textContent = 'Select image download location in the editor to continue'; // assemble the structure - toast.appendChild(icon); - toast.appendChild(message); - overlay.appendChild(toast); + overlay.appendChild(messageBar); // append to body window.document.body.appendChild(overlay); From 86c11f2d4d2897bce137424823916240b5d6e38d Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 26 Nov 2025 18:15:03 +0530 Subject: [PATCH 13/42] fix: localize the dialog overlay string --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 2 +- src/LiveDevelopment/main.js | 1 + src/nls/root/strings.js | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 361ee2cf5..3268d8f45 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3711,7 +3711,7 @@ function RemoteFunctions(config = {}) { 'color: #ffffff; background-color: #333333; padding: 1em 1.5em; ' + 'text-align: center; font-size: 16px; border-radius: 3px;' + 'font-family: "SourceSansPro", Helvetica, Arial, sans-serif;'; - messageBar.textContent = 'Select image download location in the editor to continue'; + messageBar.textContent = config.strings.imageGalleryDialogOverlayMessage; // assemble the structure overlay.appendChild(messageBar); diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index ad343d588..aaca637cf 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -121,6 +121,7 @@ define(function main(require, exports, module) { imageGalleryClose: Strings.LIVE_DEV_IMAGE_GALLERY_CLOSE, imageGallerySelectFromComputer: Strings.LIVE_DEV_IMAGE_GALLERY_SELECT_FROM_COMPUTER, imageGallerySelectFromComputerTooltip: Strings.LIVE_DEV_IMAGE_GALLERY_SELECT_FROM_COMPUTER_TOOLTIP, + imageGalleryDialogOverlayMessage: Strings.LIVE_DEV_IMAGE_GALLERY_DIALOG_OVERLAY_MESSAGE, toastNotEditable: Strings.LIVE_DEV_TOAST_NOT_EDITABLE } }; diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index a93026387..d17057a95 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -201,6 +201,7 @@ define({ "LIVE_DEV_IMAGE_GALLERY_CLOSE": "Close", "LIVE_DEV_IMAGE_GALLERY_SELECT_FROM_COMPUTER_TOOLTIP": "Select an image from your device", "LIVE_DEV_IMAGE_GALLERY_SELECT_FROM_COMPUTER": "Select from device", + "LIVE_DEV_IMAGE_GALLERY_DIALOG_OVERLAY_MESSAGE": "Select image download location in the editor to continue", "LIVE_DEV_TOAST_NOT_EDITABLE": "Element not editable - generated by script.", "LIVE_DEV_IMAGE_FOLDER_DIALOG_TITLE": "Select Folder to Save Image", "LIVE_DEV_IMAGE_FOLDER_DIALOG_DESCRIPTION": "Choose where to download the image:", From 9b18bafcd8de408024d2a691e1828320addf2a1d Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 26 Nov 2025 18:56:50 +0530 Subject: [PATCH 14/42] fix: use higher z-index so that overlay does not get hidden --- .../BrowserScripts/RemoteFunctions.js | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 3268d8f45..6613dbdef 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3698,25 +3698,45 @@ function RemoteFunctions(config = {}) { // create overlay container const overlay = window.document.createElement('div'); - overlay.className = 'phoenix-dialog-overlay'; overlay.setAttribute('data-phcode-internal-c15r5a9', 'true'); - overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; ' + - 'background: rgba(0, 0, 0, 0.5); z-index: 10000; pointer-events: auto;'; - - // create message bar - const messageBar = window.document.createElement('div'); - messageBar.className = 'phoenix-dialog-message-bar'; - messageBar.setAttribute('data-phcode-internal-c15r5a9', 'true'); - messageBar.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); ' + - 'color: #ffffff; background-color: #333333; padding: 1em 1.5em; ' + - 'text-align: center; font-size: 16px; border-radius: 3px;' + - 'font-family: "SourceSansPro", Helvetica, Arial, sans-serif;'; - messageBar.textContent = config.strings.imageGalleryDialogOverlayMessage; - - // assemble the structure - overlay.appendChild(messageBar); - - // append to body + + const styles = ` + + `; + + const content = ` +
+
${config.strings.imageGalleryDialogOverlayMessage}
+
+ `; + + overlay.innerHTML = styles + content; window.document.body.appendChild(overlay); _dialogOverlay = overlay; } From 79c305a747e5c5b234e5baaac22992c317fd8f9e Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 27 Nov 2025 14:20:05 +0530 Subject: [PATCH 15/42] fix: window focus handlers are never cleared which was causing repetitive calls on same element --- .../MultiBrowserImpl/documents/LiveDocument.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js index 57b63570e..8346d1ac2 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js +++ b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js @@ -71,7 +71,7 @@ define(function (require, exports, module) { // Redraw highlights when window gets focus. This ensures that the highlights // will be in sync with any DOM changes that may have occurred. - $(window).focus(this._onHighlightPrefChange); + $(window).on(`focus.LiveDocument-${this.doc.file.fullPath}`, this._onHighlightPrefChange); if (editor) { // Attach now @@ -91,6 +91,8 @@ define(function (require, exports, module) { EditorManager.off(`activeEditorChange.LiveDocument-${this.doc.file.fullPath}`); PreferencesManager.stateManager.getPreference("livedevHighlight") .off(`change.LiveDocument-${this.doc.file.fullPath}`); + + $(window).off(`focus.LiveDocument-${this.doc.file.fullPath}`); }; /** From db167400407c3864748a261177d140d3830f94d4 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 27 Nov 2025 18:20:33 +0530 Subject: [PATCH 16/42] feat: add cut copy paste buttons in options box --- .../BrowserScripts/RemoteFunctions.js | 105 +++++++++++++++++- src/LiveDevelopment/LivePreviewEdit.js | 102 +++++++++++++++++ 2 files changed, 206 insertions(+), 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 6613dbdef..e47a63688 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -358,6 +358,70 @@ function RemoteFunctions(config = {}) { } } + /** + * this is for cut button, when user clicks on cut button we copy the element's source code + * into the clipboard and remove it from the src code. read `_cutElementToClipboard` in `LivePreviewEdit.js` + * @param {Event} event + * @param {DOMElement} element - the element we need to cut + */ + function _handleCutOptionClick(event, element) { + if (isElementEditable(element)) { + const tagId = element.getAttribute("data-brackets-id"); + + window._Brackets_MessageBroker.send({ + livePreviewEditEnabled: true, + element: element, + event: event, + tagId: Number(tagId), + cut: true + }); + } else { + console.error("The TagID might be unavailable or the element tag is directly body or html"); + } + } + + /** + * this is for copy button, similar to cut just we don't remove the elements source code + * @param {Event} event + * @param {DOMElement} element + */ + function _handleCopyOptionClick(event, element) { + if (isElementEditable(element)) { + const tagId = element.getAttribute("data-brackets-id"); + + window._Brackets_MessageBroker.send({ + livePreviewEditEnabled: true, + element: element, + event: event, + tagId: Number(tagId), + copy: true + }); + } else { + console.error("The TagID might be unavailable or the element tag is directly body or html"); + } + } + + /** + * this is for paste button, this inserts the saved content from clipboard just above this element + * @param {Event} event + * @param {DOMElement} targetElement + */ + function _handlePasteOptionClick(event, targetElement) { + if (isElementEditable(targetElement)) { + const targetTagId = targetElement.getAttribute("data-brackets-id"); + + window._Brackets_MessageBroker.send({ + livePreviewEditEnabled: true, + element: targetElement, + event: event, + tagId: Number(targetTagId), + paste: true + }); + } else { + console.error("The TagID might be unavailable or the element tag is directly body or html"); + } + } + /** * this is for select-parent button * When user clicks on this option for a particular element, we get its parent element and trigger a click on it @@ -400,6 +464,12 @@ function RemoteFunctions(config = {}) { _handleDuplicateOptionClick(e, element); } else if (action === "delete") { _handleDeleteOptionClick(e, element); + } else if (action === "cut") { + _handleCutOptionClick(e, element); + } else if (action === "copy") { + _handleCopyOptionClick(e, element); + } else if (action === "paste") { + _handlePasteOptionClick(e, element); } else if (action === "ai") { _handleAIOptionClick(e, element); } else if (action === "image-gallery") { @@ -1360,6 +1430,30 @@ function RemoteFunctions(config = {}) { `, + cut: ` + + + + + + + + `, + + copy: ` + + + + + `, + + paste: ` + + + + + `, + imageGallery: ` @@ -1512,6 +1606,15 @@ function RemoteFunctions(config = {}) { ${ICONS.trash} + + ${ICONS.cut} + + + ${ICONS.copy} + + + ${ICONS.paste} + `; let styles = ` @@ -1614,7 +1717,7 @@ function RemoteFunctions(config = {}) { span.addEventListener('click', (event) => { event.stopPropagation(); event.preventDefault(); - // data-action is to differentiate between the buttons (duplicate, delete or select-parent) + // data-action is to differentiate between the buttons (duplicate, delete, select-parent etc) const action = event.currentTarget.getAttribute('data-action'); handleOptionClick(event, action, this.element); if (action !== 'duplicate') { diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index d0b40142e..a12eb4658 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -315,6 +315,102 @@ define(function (require, exports, module) { }); } + /** + * this saves the element to clipboard and deletes its source code + * @param {Number} tagId + */ + function _cutElementToClipboard(tagId) { + const editor = _getEditorAndValidate(tagId); + if (!editor) { + return; + } + + const range = _getElementRange(editor, tagId); + if (!range) { + return; + } + + const { startPos, endPos } = range; + const text = editor.getTextBetween(startPos, endPos); + + Phoenix.app.copyToClipboard(text); + + // delete the elements source code + editor.document.batchOperation(function () { + editor.replaceRange("", startPos, endPos); + + // clean up any empty line + if(startPos.line !== 0 && !(editor.getLine(startPos.line).trim())) { + const prevLineText = editor.getLine(startPos.line - 1); + const chPrevLine = prevLineText ? prevLineText.length : 0; + editor.replaceRange("", {line: startPos.line - 1, ch: chPrevLine}, startPos); + } + }); + } + + function _copyElementToClipboard(tagId) { + const editor = _getEditorAndValidate(tagId); + if (!editor) { + return; + } + + const range = _getElementRange(editor, tagId); + if (!range) { + return; + } + + const { startPos, endPos } = range; + const text = editor.getTextBetween(startPos, endPos); + + Phoenix.app.copyToClipboard(text); + } + + /** + * this function is to paste the clipboard content above the target element + * @param {Number} tagId + */ + function _pasteElementFromClipboard(tagId) { + const editor = _getEditorAndValidate(tagId); + if (!editor) { + return; + } + const range = _getElementRange(editor, tagId); + if (!range) { + return; + } + + const { startPos } = range; + + Phoenix.app.clipboardReadText().then(text => { + if (!text) { + return; + } + + // get the indentation at the target element's line + const indent = editor.getTextBetween({ line: startPos.line, ch: 0 }, startPos); + + // for proper indentation + const lines = text.split('\n'); + const indentedLines = lines.map((line, index) => { + if (index === 0) { + return indent.trim() === "" ? indent + line : line; + } + return line ? indent + line : line; + }); + const indentedContent = indentedLines.join('\n'); + + editor.document.batchOperation(function () { + if (indent.trim() === "") { + editor.replaceRange(indentedContent + "\n", startPos); + } else { + editor.replaceRange("\n" + indentedContent, { line: startPos.line, ch: 0 }); + } + }); + }).catch(err => { + console.error("Failed to read from clipboard:", err); + }); + } + /** * This function is responsible to delete an element from the source code * @param {Number} tagId - the data-brackets-id of the DOM element @@ -1387,6 +1483,12 @@ define(function (require, exports, module) { _deleteElementInSourceByTagId(message.tagId); } else if (message.duplicate) { _duplicateElementInSourceByTagId(message.tagId); + } else if (message.cut) { + _cutElementToClipboard(message.tagId); + } else if (message.copy) { + _copyElementToClipboard(message.tagId); + } else if (message.paste) { + _pasteElementFromClipboard(message.tagId); } else if (message.livePreviewTextEdit) { _editTextInSource(message); } else if (message.AISend) { From c1877c702f3dd34f59aa225df2588f69bdcb20aa Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 27 Nov 2025 21:14:58 +0530 Subject: [PATCH 17/42] refactor: move the edit options inside a dropdown --- .../BrowserScripts/RemoteFunctions.js | 218 +++++++++++++++++- 1 file changed, 207 insertions(+), 11 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index e47a63688..12ef1a6ca 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1495,6 +1495,12 @@ function RemoteFunctions(config = {}) { + `, + + verticalEllipsis: ` + + + ` }; @@ -1606,14 +1612,8 @@ function RemoteFunctions(config = {}) { ${ICONS.trash} - - ${ICONS.cut} - - - ${ICONS.copy} - - - ${ICONS.paste} + + ${ICONS.verticalEllipsis} `; @@ -1719,9 +1719,20 @@ function RemoteFunctions(config = {}) { event.preventDefault(); // data-action is to differentiate between the buttons (duplicate, delete, select-parent etc) const action = event.currentTarget.getAttribute('data-action'); - handleOptionClick(event, action, this.element); - if (action !== 'duplicate') { - this.remove(); + + if (action === 'more-options') { + // to toggle the dropdown on more options button click + if (_moreOptionsDropdown) { + _moreOptionsDropdown.remove(); + } else { + _moreOptionsDropdown = new MoreOptionsDropdown(this.element, event.currentTarget); + } + } else { + handleOptionClick(event, action, this.element); + // as we don't want to remove the options box on duplicate button click + if (action !== 'duplicate') { + this.remove(); + } } }); }); @@ -1738,6 +1749,179 @@ function RemoteFunctions(config = {}) { } }; + /** + * the more options dropdown which appears when user clicks on the ellipsis button in the options box + */ + function MoreOptionsDropdown(targetElement, ellipsisButton) { + this.targetElement = targetElement; + this.ellipsisButton = ellipsisButton; + this.remove = this.remove.bind(this); + this.create(); + } + + MoreOptionsDropdown.prototype = { + _getDropdownPosition: function(dropdownWidth, dropdownHeight) { + const buttonBounds = this.ellipsisButton.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + + let topPos, leftPos; + + // Check if there's enough space below the button + const spaceBelow = viewportHeight - buttonBounds.bottom; + const spaceAbove = buttonBounds.top; + + if (spaceBelow >= dropdownHeight + 6) { + // Show below the ellipsis button + topPos = buttonBounds.bottom + window.pageYOffset + 6; + } else if (spaceAbove >= dropdownHeight + 6) { + // Show above the ellipsis button + topPos = buttonBounds.top + window.pageYOffset - dropdownHeight - 6; + } else { + // Not enough space either way, default to below + topPos = buttonBounds.bottom + window.pageYOffset + 6; + } + + // Align dropdown to the right edge of the button + leftPos = buttonBounds.right + window.pageXOffset - dropdownWidth; + + // Make sure dropdown doesn't go off the left edge of viewport + if (leftPos < 0) { + leftPos = buttonBounds.left + window.pageXOffset; + } + + return {topPos: topPos, leftPos: leftPos}; + }, + + _style: function() { + this.body = window.document.createElement("div"); + this.body.setAttribute("data-phcode-internal-c15r5a9", "true"); + + const shadow = this.body.attachShadow({ mode: "open" }); + + let content = ` +
+ + + +
+ `; + + let styles = ` + :host { + all: initial !important; + } + + .phoenix-dropdown { + background-color: #ffffff !important; + color: #1f2933 !important; + border: 1px solid #1a73e8 !important; + border-radius: 6px !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25) !important; + font-size: 13px !important; + font-family: Arial, sans-serif !important; + z-index: 2147483647 !important; + position: absolute !important; + left: -1000px; + top: -1000px; + box-sizing: border-box !important; + min-width: 150px !important; + padding: 4px 0 !important; + overflow: hidden !important; + } + + .more-options-dropdown { + display: flex !important; + flex-direction: column !important; + } + + .dropdown-item { + padding: 7px 14px !important; + cursor: pointer !important; + white-space: nowrap !important; + user-select: none !important; + display: flex !important; + align-items: center !important; + gap: 8px !important; + } + + .dropdown-item:hover { + background-color: #e8f1ff !important; + } + + .item-icon { + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 16px !important; + height: 16px !important; + flex-shrink: 0 !important; + } + + .item-icon svg { + width: 16px !important; + height: 16px !important; + display: block !important; + } + + .item-label { + flex: 1 !important; + } + `; + + shadow.innerHTML = `
${content}
`; + this._shadow = shadow; + }, + + create: function() { + this.remove(); + this._style(); + window.document.body.appendChild(this.body); + + // to position the dropdown element at the right position + const dropdownElement = this._shadow.querySelector('.phoenix-dropdown'); + if (dropdownElement) { + const dropdownRect = dropdownElement.getBoundingClientRect(); + const pos = this._getDropdownPosition(dropdownRect.width, dropdownRect.height); + + dropdownElement.style.left = pos.leftPos + 'px'; + dropdownElement.style.top = pos.topPos + 'px'; + } + + // click handlers for the dropdown items + const items = this._shadow.querySelectorAll('.dropdown-item'); + items.forEach(item => { + item.addEventListener('click', (event) => { + event.stopPropagation(); + event.preventDefault(); + const action = event.currentTarget.getAttribute('data-action'); + handleOptionClick(event, action, this.targetElement); + // when an option is selected we close both the dropdown as well as the options box + this.remove(); + if (_nodeMoreOptionsBox) { + _nodeMoreOptionsBox.remove(); + } + }); + }); + }, + + remove: function() { + if (this.body && this.body.parentNode && this.body.parentNode === window.document.body) { + window.document.body.removeChild(this.body); + this.body = null; + _moreOptionsDropdown = null; + } + } + }; + // Node info box to display DOM node ID and classes on hover function NodeInfoBox(element) { this.element = element; @@ -3776,6 +3960,7 @@ function RemoteFunctions(config = {}) { var _clickHighlight; var _nodeInfoBox; var _nodeMoreOptionsBox; + var _moreOptionsDropdown; var _aiPromptBox; var _imageRibbonGallery; var _setup = false; @@ -4750,6 +4935,16 @@ function RemoteFunctions(config = {}) { } } + /** + * Helper function to dismiss MoreOptionsDropdown if it exists + */ + function dismissMoreOptionsDropdown() { + if (_moreOptionsDropdown) { + _moreOptionsDropdown.remove(); + _moreOptionsDropdown = null; + } + } + /** * Helper function to dismiss NodeInfoBox if it exists */ @@ -4785,6 +4980,7 @@ function RemoteFunctions(config = {}) { */ function dismissAllUIBoxes() { dismissNodeMoreOptionsBox(); + dismissMoreOptionsDropdown(); dismissAIPromptBox(); dismissNodeInfoBox(); dismissImageRibbonGallery(); From fb08a2bc6d62e4d21509399755eb49d7a1d4a613 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 27 Nov 2025 21:20:56 +0530 Subject: [PATCH 18/42] feat: localize the strings used in the dropdown --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 8 ++++---- src/LiveDevelopment/main.js | 4 ++++ src/nls/root/strings.js | 4 ++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 12ef1a6ca..ab07c805b 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1612,7 +1612,7 @@ function RemoteFunctions(config = {}) { ${ICONS.trash} - + ${ICONS.verticalEllipsis} `; @@ -1802,15 +1802,15 @@ function RemoteFunctions(config = {}) {
`; diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index aaca637cf..72900b539 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -109,6 +109,10 @@ define(function main(require, exports, module) { delete: Strings.LIVE_DEV_MORE_OPTIONS_DELETE, ai: Strings.LIVE_DEV_MORE_OPTIONS_AI, imageGallery: Strings.LIVE_DEV_MORE_OPTIONS_IMAGE_GALLERY, + moreOptions: Strings.LIVE_DEV_MORE_OPTIONS_MORE, + cut: Strings.LIVE_DEV_MORE_OPTIONS_CUT, + copy: Strings.LIVE_DEV_MORE_OPTIONS_COPY, + paste: Strings.LIVE_DEV_MORE_OPTIONS_PASTE, aiPromptPlaceholder: Strings.LIVE_DEV_AI_PROMPT_PLACEHOLDER, imageGalleryUseImage: Strings.LIVE_DEV_IMAGE_GALLERY_USE_IMAGE, imageGallerySelectDownloadFolder: Strings.LIVE_DEV_IMAGE_GALLERY_SELECT_DOWNLOAD_FOLDER, diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index d17057a95..31b7309f0 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -190,6 +190,10 @@ define({ "LIVE_DEV_MORE_OPTIONS_DELETE": "Delete", "LIVE_DEV_MORE_OPTIONS_AI": "Edit with AI", "LIVE_DEV_MORE_OPTIONS_IMAGE_GALLERY": "Image Gallery", + "LIVE_DEV_MORE_OPTIONS_MORE": "More Options", + "LIVE_DEV_MORE_OPTIONS_CUT": "Cut", + "LIVE_DEV_MORE_OPTIONS_COPY": "Copy", + "LIVE_DEV_MORE_OPTIONS_PASTE": "Paste", "LIVE_DEV_IMAGE_GALLERY_USE_IMAGE": "Use this image", "LIVE_DEV_IMAGE_GALLERY_SELECT_DOWNLOAD_FOLDER": "Choose image download folder", "LIVE_DEV_IMAGE_GALLERY_SEARCH_PLACEHOLDER": "Search images\u2026", From dc05338c9f55433d8d31db68025b97069a0fd047 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 27 Nov 2025 22:32:51 +0530 Subject: [PATCH 19/42] refactor: make dropdown consistent to existing phoenix colors --- .../BrowserScripts/RemoteFunctions.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index ab07c805b..fe4a5e33f 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1821,9 +1821,9 @@ function RemoteFunctions(config = {}) { } .phoenix-dropdown { - background-color: #ffffff !important; - color: #1f2933 !important; - border: 1px solid #1a73e8 !important; + background-color: #2c2c2c !important; + color: #cdcdcd !important; + border: 1px solid #4285F4 !important; border-radius: 6px !important; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25) !important; font-size: 13px !important; @@ -1833,8 +1833,8 @@ function RemoteFunctions(config = {}) { left: -1000px; top: -1000px; box-sizing: border-box !important; - min-width: 150px !important; - padding: 4px 0 !important; + min-width: 130px !important; + padding: 2px 0 !important; overflow: hidden !important; } @@ -1854,7 +1854,7 @@ function RemoteFunctions(config = {}) { } .dropdown-item:hover { - background-color: #e8f1ff !important; + background-color: #3c3f41 !important; } .item-icon { From c714d96ce5adcb3784aa3ae967c61ccfa23a8a56 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 27 Nov 2025 22:39:55 +0530 Subject: [PATCH 20/42] fix: dropdown not getting dismissed on other element click --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index fe4a5e33f..e10bbf99a 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -4190,6 +4190,7 @@ function RemoteFunctions(config = {}) { */ function _selectElement(element) { dismissNodeMoreOptionsBox(); + dismissMoreOptionsDropdown(); dismissAIPromptBox(); dismissNodeInfoBox(); dismissToastMessage(); From e7ec0e589400d98b5fe3ffb74222207618dadda1 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 27 Nov 2025 23:26:44 +0530 Subject: [PATCH 21/42] refactor: use font awesome icons for cut copy paste --- .../BrowserScripts/RemoteFunctions.js | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index e10bbf99a..001a5172a 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1431,28 +1431,22 @@ function RemoteFunctions(config = {}) { `, cut: ` - - - - - - + + - `, + `, copy: ` - - - + + - `, + `, paste: ` - - - + + - `, + `, imageGallery: ` From e76b50cc8a7ce5af270f6c417b34e9e9d8781bd3 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 28 Nov 2025 00:18:59 +0530 Subject: [PATCH 22/42] fix: improvised positioning of the dropdown --- .../BrowserScripts/RemoteFunctions.js | 75 +++++++++++++++---- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 001a5172a..1f2aa65d5 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1757,33 +1757,76 @@ function RemoteFunctions(config = {}) { _getDropdownPosition: function(dropdownWidth, dropdownHeight) { const buttonBounds = this.ellipsisButton.getBoundingClientRect(); const viewportHeight = window.innerHeight; + const viewportWidth = window.innerWidth; + + const optionsBox = _nodeMoreOptionsBox._shadow.querySelector(".phoenix-more-options-box"); + const optionsBoxBounds = optionsBox.getBoundingClientRect(); + const targetElementBounds = this.targetElement.getBoundingClientRect(); let topPos, leftPos; - // Check if there's enough space below the button - const spaceBelow = viewportHeight - buttonBounds.bottom; - const spaceAbove = buttonBounds.top; + const checkOverlap = function (dTop, dLeft, dWidth, dHeight) { + const dropdownRight = dLeft + dWidth; + const dropdownBottom = dTop + dHeight; + const elemRight = targetElementBounds.left + targetElementBounds.width; + const elemBottom = targetElementBounds.top + targetElementBounds.height; + + return !( + dLeft > elemRight || + dropdownRight < targetElementBounds.left || + dTop > elemBottom || + dropdownBottom < targetElementBounds.top + ); + }; + + const isOptionsBoxAboveElement = optionsBoxBounds.bottom < targetElementBounds.top; + + if (isOptionsBoxAboveElement) { + const spaceAbove = optionsBoxBounds.top; + + if (spaceAbove >= dropdownHeight + 6) { + topPos = optionsBoxBounds.top + window.pageYOffset - dropdownHeight - 6; + } else { + topPos = optionsBoxBounds.bottom + window.pageYOffset + 6; + + const tempTop = optionsBoxBounds.bottom; + const tempLeft = buttonBounds.right - dropdownWidth; + + if (checkOverlap(tempTop, tempLeft, dropdownWidth, dropdownHeight)) { + let shiftedLeft = targetElementBounds.right + 6; + if (shiftedLeft + dropdownWidth <= viewportWidth - 6) { + leftPos = shiftedLeft + window.pageXOffset; + return { topPos: topPos, leftPos: leftPos }; + } - if (spaceBelow >= dropdownHeight + 6) { - // Show below the ellipsis button - topPos = buttonBounds.bottom + window.pageYOffset + 6; - } else if (spaceAbove >= dropdownHeight + 6) { - // Show above the ellipsis button - topPos = buttonBounds.top + window.pageYOffset - dropdownHeight - 6; + shiftedLeft = targetElementBounds.left - dropdownWidth - 6; + if (shiftedLeft >= 6) { + leftPos = shiftedLeft + window.pageXOffset; + return { topPos: topPos, leftPos: leftPos }; + } + } + } } else { - // Not enough space either way, default to below - topPos = buttonBounds.bottom + window.pageYOffset + 6; + const spaceBelow = viewportHeight - optionsBoxBounds.bottom; + + if (spaceBelow >= dropdownHeight + 6) { + topPos = optionsBoxBounds.bottom + window.pageYOffset + 6; + } else { + topPos = optionsBoxBounds.top + window.pageYOffset - dropdownHeight - 6; + } } - // Align dropdown to the right edge of the button leftPos = buttonBounds.right + window.pageXOffset - dropdownWidth; - // Make sure dropdown doesn't go off the left edge of viewport - if (leftPos < 0) { - leftPos = buttonBounds.left + window.pageXOffset; + if (leftPos < 6) { + leftPos = 6; } - return {topPos: topPos, leftPos: leftPos}; + if (leftPos + dropdownWidth > viewportWidth - 6) { + leftPos = viewportWidth - dropdownWidth - 6; + } + + return { topPos: topPos, leftPos: leftPos }; }, _style: function() { From 3537382ffd91d318c6b957e799f0251794b29c4b Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 29 Nov 2025 14:05:17 +0530 Subject: [PATCH 23/42] feat: add ruler lines feature when element is clicked --- .../BrowserScripts/RemoteFunctions.js | 138 ++++++++++++++++++ src/LiveDevelopment/main.js | 10 ++ .../Phoenix-live-preview/main.js | 30 +++- src/nls/root/strings.js | 4 +- 4 files changed, 179 insertions(+), 3 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 1f2aa65d5..59b263728 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3992,6 +3992,121 @@ function RemoteFunctions(config = {}) { } }; + /** + * Ruler lines class, this creates the rulers across the edges of the element (hori as well as vert) + */ + function RulerLines(element) { + this.element = element; + this.lineElements = { + left: null, + right: null, + top: null, + bottom: null + }; + this.create(); + this.update(); + } + + RulerLines.prototype = { + create: function() { + let body = window.document.body; + + this.lineElements.left = window.document.createElement("div"); + this.lineElements.right = window.document.createElement("div"); + this.lineElements.top = window.document.createElement("div"); + this.lineElements.bottom = window.document.createElement("div"); + + this.lineElements.left.setAttribute("data-phcode-internal-c15r5a9", "true"); + this.lineElements.right.setAttribute("data-phcode-internal-c15r5a9", "true"); + this.lineElements.top.setAttribute("data-phcode-internal-c15r5a9", "true"); + this.lineElements.bottom.setAttribute("data-phcode-internal-c15r5a9", "true"); + + let applyStyles = function (element) { + element.style.position = "absolute"; + element.style.backgroundColor = "rgba(66, 133, 244, 0.5)"; + element.style.pointerEvents = "none"; + element.style.zIndex = "2147483645"; + }; + + applyStyles(this.lineElements.left); + applyStyles(this.lineElements.right); + applyStyles(this.lineElements.top); + applyStyles(this.lineElements.bottom); + + body.appendChild(this.lineElements.left); + body.appendChild(this.lineElements.right); + body.appendChild(this.lineElements.top); + body.appendChild(this.lineElements.bottom); + }, + + update: function() { + if (!this.element) { + return; + } + + let rect = this.element.getBoundingClientRect(); + let scrollTop = window.pageYOffset; + let scrollLeft = window.pageXOffset; + + var edges = { + left: rect.left + scrollLeft, + right: rect.right + scrollLeft, + top: rect.top + scrollTop, + bottom: rect.bottom + scrollTop + }; + + // get the doc dimensions as we need to put the ruler lines in the whole document + var docHeight = window.document.documentElement.scrollHeight; + var docWidth = window.document.documentElement.scrollWidth; + + // for vertical lines + this.lineElements.left.style.width = '1px'; + this.lineElements.left.style.height = docHeight + 'px'; + this.lineElements.left.style.left = edges.left + 'px'; + this.lineElements.left.style.top = '0px'; + + this.lineElements.right.style.width = '1px'; + this.lineElements.right.style.height = docHeight + 'px'; + this.lineElements.right.style.left = edges.right + 'px'; + this.lineElements.right.style.top = '0px'; + + // for horizontal lines + this.lineElements.top.style.height = '1px'; + this.lineElements.top.style.width = docWidth + 'px'; + this.lineElements.top.style.top = edges.top + 'px'; + this.lineElements.top.style.left = '0px'; + + this.lineElements.bottom.style.height = '1px'; + this.lineElements.bottom.style.width = docWidth + 'px'; + this.lineElements.bottom.style.top = edges.bottom + 'px'; + this.lineElements.bottom.style.left = '0px'; + }, + + remove: function() { + var body = window.document.body; + + if (this.lineElements.left && this.lineElements.left.parentNode) { + body.removeChild(this.lineElements.left); + } + if (this.lineElements.right && this.lineElements.right.parentNode) { + body.removeChild(this.lineElements.right); + } + if (this.lineElements.top && this.lineElements.top.parentNode) { + body.removeChild(this.lineElements.top); + } + if (this.lineElements.bottom && this.lineElements.bottom.parentNode) { + body.removeChild(this.lineElements.bottom); + } + + this.lineElements = { + left: null, + right: null, + top: null, + bottom: null + }; + } + }; + var _localHighlight; var _hoverHighlight; var _clickHighlight; @@ -4000,6 +4115,7 @@ function RemoteFunctions(config = {}) { var _moreOptionsDropdown; var _aiPromptBox; var _imageRibbonGallery; + var _currentRulerLines; var _setup = false; var _hoverLockTimer = null; @@ -4287,6 +4403,11 @@ function RemoteFunctions(config = {}) { _hoverHighlight.add(element, true); } + // to show ruler lines (only when its enabled) + if (config.showRulerLines) { + _currentRulerLines = new RulerLines(element); + } + previouslyClickedElement = element; } @@ -4527,10 +4648,18 @@ function RemoteFunctions(config = {}) { } } + // redraw ruler lines when element is selected + function redrawRulerLines() { + if (_currentRulerLines) { + _currentRulerLines.update(); + } + } + // just a wrapper function when we need to redraw highlights as well as UI boxes function redrawEverything() { redrawHighlights(); redrawUIBoxes(); + redrawRulerLines(); } window.addEventListener("resize", redrawEverything); @@ -4629,6 +4758,7 @@ function RemoteFunctions(config = {}) { // need to be updated on a timer to ensure the layout is correct. if (e.target === window.document) { redrawHighlights(); + redrawRulerLines(); // need to dismiss the box if the elements are fixed, otherwise they drift at times _dismissBoxesForFixedElements(); _repositionAIBox(); // and reposition the AI box @@ -4636,6 +4766,9 @@ function RemoteFunctions(config = {}) { if (_localHighlight || _clickHighlight || _hoverHighlight) { window.setTimeout(redrawHighlights, 0); } + if (_currentRulerLines) { + window.setTimeout(redrawRulerLines, 0); + } _dismissBoxesForFixedElements(); _repositionAIBox(); } @@ -5129,6 +5262,11 @@ function RemoteFunctions(config = {}) { _hoverHighlight.clear(); } + if (_currentRulerLines) { + _currentRulerLines.remove(); + _currentRulerLines = null; + } + previouslyClickedElement = null; } } diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 72900b539..5b0345c2d 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -98,6 +98,7 @@ define(function main(require, exports, module) { }, isProUser: isProUser, elemHighlights: "hover", // default value, this will get updated when the extension loads + showRulerLines: false, // default value, this will get updated when the extension loads imageGalleryState: _getImageGalleryState(), // image gallery selected state // this strings are used in RemoteFunctions.js // we need to pass this through config as remoteFunctions runs in browser context and cannot @@ -488,6 +489,14 @@ define(function main(require, exports, module) { } } + function updateRulerLinesConfig() { + const prefValue = PreferencesManager.get("livePreviewShowRulerLines"); + config.showRulerLines = prefValue || false; + if (MultiBrowserLiveDev && MultiBrowserLiveDev.status >= MultiBrowserLiveDev.STATUS_ACTIVE) { + MultiBrowserLiveDev.updateConfig(JSON.stringify(config)); + } + } + // init commands CommandManager.register(Strings.CMD_LIVE_HIGHLIGHT, Commands.FILE_LIVE_HIGHLIGHT, togglePreviewHighlight); CommandManager.register(Strings.CMD_RELOAD_LIVE_PREVIEW, Commands.CMD_RELOAD_LIVE_PREVIEW, _handleReloadLivePreviewCommand); @@ -517,6 +526,7 @@ define(function main(require, exports, module) { exports.setLivePreviewEditFeaturesActive = setLivePreviewEditFeaturesActive; exports.setImageGalleryState = setImageGalleryState; exports.updateElementHighlightConfig = updateElementHighlightConfig; + exports.updateRulerLinesConfig = updateRulerLinesConfig; exports.getConnectionIds = MultiBrowserLiveDev.getConnectionIds; exports.getLivePreviewDetails = MultiBrowserLiveDev.getLivePreviewDetails; exports.hideHighlight = MultiBrowserLiveDev.hideHighlight; diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js index 547851b0e..714fa6598 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/main.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js @@ -98,6 +98,12 @@ define(function (require, exports, module) { description: Strings.LIVE_DEV_SETTINGS_ELEMENT_HIGHLIGHT_PREFERENCE }); + // live preview ruler lines preference (show/hide ruler lines on element selection) + const PREFERENCE_SHOW_RULER_LINES = "livePreviewShowRulerLines"; + PreferencesManager.definePreference(PREFERENCE_SHOW_RULER_LINES, "boolean", false, { + description: Strings.LIVE_DEV_SETTINGS_SHOW_RULER_LINES_PREFERENCE + }); + const LIVE_PREVIEW_PANEL_ID = "live-preview-panel"; const LIVE_PREVIEW_IFRAME_ID = "panel-live-preview-frame"; const LIVE_PREVIEW_IFRAME_HTML = ` @@ -357,6 +363,7 @@ define(function (require, exports, module) { if (isEditFeaturesActive) { items.push("---"); items.push(Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON); + items.push(Strings.LIVE_PREVIEW_SHOW_RULER_LINES); } const currentMode = LiveDevelopment.getCurrentMode(); @@ -380,6 +387,12 @@ define(function (require, exports, module) { return `✓ ${Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON}`; } return `${'\u00A0'.repeat(4)}${Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON}`; + } else if (item === Strings.LIVE_PREVIEW_SHOW_RULER_LINES) { + const isEnabled = PreferencesManager.get(PREFERENCE_SHOW_RULER_LINES); + if(isEnabled) { + return `✓ ${Strings.LIVE_PREVIEW_SHOW_RULER_LINES}`; + } + return `${'\u00A0'.repeat(4)}${Strings.LIVE_PREVIEW_SHOW_RULER_LINES}`; } return item; }); @@ -422,6 +435,15 @@ define(function (require, exports, module) { const newMode = currMode !== "click" ? "click" : "hover"; PreferencesManager.set(PREFERENCE_PROJECT_ELEMENT_HIGHLIGHT, newMode); return; // Don't dismiss highlights for this option + } else if (item === Strings.LIVE_PREVIEW_SHOW_RULER_LINES) { + // Don't allow ruler lines toggle if edit features are not active + if (!isEditFeaturesActive) { + return; + } + // Toggle ruler lines on/off + const currentValue = PreferencesManager.get(PREFERENCE_SHOW_RULER_LINES); + PreferencesManager.set(PREFERENCE_SHOW_RULER_LINES, !currentValue); + return; // Don't dismiss highlights for this option } // need to dismiss the previous highlighting and stuff @@ -1205,13 +1227,17 @@ define(function (require, exports, module) { _initializeMode(); }); - // Handle element highlight preference changes from this extension + // Handle element highlight & ruler lines preference changes PreferencesManager.on("change", PREFERENCE_PROJECT_ELEMENT_HIGHLIGHT, function() { LiveDevelopment.updateElementHighlightConfig(); }); + PreferencesManager.on("change", PREFERENCE_SHOW_RULER_LINES, function() { + LiveDevelopment.updateRulerLinesConfig(); + }); - // Initialize element highlight config on startup + // Initialize element highlight and ruler lines config on startup LiveDevelopment.updateElementHighlightConfig(); + LiveDevelopment.updateRulerLinesConfig(); LiveDevelopment.openLivePreview(); LiveDevelopment.on(LiveDevelopment.EVENT_OPEN_PREVIEW_URL, _openLivePreviewURL); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 31b7309f0..887740c97 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -183,7 +183,8 @@ define({ "LIVE_DEV_SETTINGS_ELEMENT_HIGHLIGHT": "Show Live Preview Element Highlights on:", "LIVE_DEV_SETTINGS_ELEMENT_HIGHLIGHT_HOVER": "hover", "LIVE_DEV_SETTINGS_ELEMENT_HIGHLIGHT_CLICK": "click", - "LIVE_DEV_SETTINGS_ELEMENT_HIGHLIGHT_PREFERENCE": "show live preview element highlights on 'hover' or 'click'. Defaults to 'hover'", + "LIVE_DEV_SETTINGS_ELEMENT_HIGHLIGHT_PREFERENCE": "Show live preview element highlights on 'hover' or 'click'. Defaults to 'hover'", + "LIVE_DEV_SETTINGS_SHOW_RULER_LINES_PREFERENCE": "Show ruler lines when elements are selected in live preview. Defaults to 'false'", "LIVE_DEV_MORE_OPTIONS_SELECT_PARENT": "Select Parent", "LIVE_DEV_MORE_OPTIONS_EDIT_TEXT": "Edit Text", "LIVE_DEV_MORE_OPTIONS_DUPLICATE": "Duplicate", @@ -219,6 +220,7 @@ define({ "LIVE_PREVIEW_MODE_HIGHLIGHT": "Highlight Mode", "LIVE_PREVIEW_MODE_EDIT": "Edit Mode", "LIVE_PREVIEW_EDIT_HIGHLIGHT_ON": "Edit Highlights on Hover", + "LIVE_PREVIEW_SHOW_RULER_LINES": "Show Ruler Lines", "LIVE_PREVIEW_MODE_PREFERENCE": "{0} shows only the webpage, {1} connects the webpage to your code - click on elements to jump to their code and vice versa, {2} provides highlighting along with advanced element manipulation", "LIVE_PREVIEW_CONFIGURE_MODES": "Configure Live Preview Modes", From 2b9a071d59d30be1cf45677a2236789b9460d9df Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 29 Nov 2025 14:14:29 +0530 Subject: [PATCH 24/42] fix: ruler lines not getting updated unless when clicked element changes --- .../BrowserScripts/RemoteFunctions.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 59b263728..3a245df88 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -5032,6 +5032,28 @@ function RemoteFunctions(config = {}) { _editHandler.apply(edits); } + /** + * Handle ruler lines visibility toggle when config changes + * @param {Object} oldConfig - the prev config state + */ + function _handleRulerLinesConfigChange(oldConfig) { + const rulerLinesChanged = oldConfig.showRulerLines !== config.showRulerLines; + if (rulerLinesChanged && previouslyClickedElement) { + if (config.showRulerLines) { + // if user turned it on: create ruler lines for the element + if (!_currentRulerLines) { + _currentRulerLines = new RulerLines(previouslyClickedElement); + } + } else { + // if user turned it off: remove the lines + if (_currentRulerLines) { + _currentRulerLines.remove(); + _currentRulerLines = null; + } + } + } + } + function updateConfig(newConfig) { const oldConfig = config; config = JSON.parse(newConfig); @@ -5041,6 +5063,9 @@ function RemoteFunctions(config = {}) { imageGallerySelected = config.imageGalleryState; } + // handle ruler lines visibility toggle + _handleRulerLinesConfigChange(oldConfig); + // Determine if configuration has changed significantly const oldHighlightMode = oldConfig.elemHighlights ? oldConfig.elemHighlights.toLowerCase() : "hover"; const newHighlightMode = getHighlightMode(); From ec2ae2c7a9db6c9249c41c440ab05c2caf7d98ec Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 29 Nov 2025 14:30:41 +0530 Subject: [PATCH 25/42] feat: ruler lines and outline to be shown in blue for non-editable elements --- .../BrowserScripts/RemoteFunctions.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 3a245df88..e153505a0 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -4003,6 +4003,10 @@ function RemoteFunctions(config = {}) { top: null, bottom: null }; + // gray color for non-editable elements, blue for editable + this.color = element.hasAttribute("data-brackets-id") + ? "#4285F4" + : "#3C3F41"; this.create(); this.update(); } @@ -4021,17 +4025,17 @@ function RemoteFunctions(config = {}) { this.lineElements.top.setAttribute("data-phcode-internal-c15r5a9", "true"); this.lineElements.bottom.setAttribute("data-phcode-internal-c15r5a9", "true"); - let applyStyles = function (element) { + let applyStyles = function (element, color) { element.style.position = "absolute"; - element.style.backgroundColor = "rgba(66, 133, 244, 0.5)"; + element.style.backgroundColor = color; element.style.pointerEvents = "none"; element.style.zIndex = "2147483645"; }; - applyStyles(this.lineElements.left); - applyStyles(this.lineElements.right); - applyStyles(this.lineElements.top); - applyStyles(this.lineElements.bottom); + applyStyles(this.lineElements.left, this.color); + applyStyles(this.lineElements.right, this.color); + applyStyles(this.lineElements.top, this.color); + applyStyles(this.lineElements.bottom, this.color); body.appendChild(this.lineElements.left); body.appendChild(this.lineElements.right); @@ -4388,7 +4392,8 @@ function RemoteFunctions(config = {}) { } element._originalOutline = element.style.outline; - element.style.outline = "1px solid #4285F4"; + const outlineColor = element.hasAttribute("data-brackets-id") ? "#4285F4" : "#3C3F41"; + element.style.outline = `1px solid ${outlineColor}`; // Only apply background tint for editable elements (not for dynamic/read-only) if (element.hasAttribute("data-brackets-id")) { From 37c12618e6d50ffc96752470e0bcfbfca2f9013e Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 29 Nov 2025 17:17:50 +0530 Subject: [PATCH 26/42] refactor: use rgba vals with opacity to ruler lines to make it less distracting --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index e153505a0..b0dd32849 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -4005,8 +4005,8 @@ function RemoteFunctions(config = {}) { }; // gray color for non-editable elements, blue for editable this.color = element.hasAttribute("data-brackets-id") - ? "#4285F4" - : "#3C3F41"; + ? "rgba(66, 133, 244, 0.4)" + : "rgba(60, 63, 65, 0.8)"; this.create(); this.update(); } From 42f9552993da5b18a5c58c8184d6d262ba3e3961 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 29 Nov 2025 21:30:02 +0530 Subject: [PATCH 27/42] feat: also add show ruler lines option in the more options dropdown --- .../BrowserScripts/RemoteFunctions.js | 79 +++++++++++++++++-- src/LiveDevelopment/LivePreviewEdit.js | 7 ++ src/LiveDevelopment/main.js | 1 + 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index b0dd32849..2815f2d8c 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1448,6 +1448,12 @@ function RemoteFunctions(config = {}) { `, + ruler: ` + + + + `, + imageGallery: ` @@ -1743,6 +1749,39 @@ function RemoteFunctions(config = {}) { } }; + /** + * this is called when user clicks on the Show Ruler lines option in the more options dropdown + * @param {Event} event - click event + * @param {MoreOptionsDropdown} dropdown - the dropdown instance + */ + function _handleToggleRulerLines(event, dropdown) { + config.showRulerLines = !config.showRulerLines; + + window._Brackets_MessageBroker.send({ + livePreviewEditEnabled: true, + type: "toggleRulerLines", + enabled: config.showRulerLines + }); + + // add checkmark in the dropdown + const checkmark = dropdown._shadow.querySelector('[data-action="toggle-ruler-lines"] .item-checkmark'); + if (checkmark) { + checkmark.textContent = config.showRulerLines ? '✓' : ''; + } + + // to apply the ruler lines or remove it when option is toggled + if (config.showRulerLines && previouslyClickedElement) { + if (!_currentRulerLines) { + _currentRulerLines = new RulerLines(previouslyClickedElement); + } + } else { + if (_currentRulerLines) { + _currentRulerLines.remove(); + _currentRulerLines = null; + } + } + } + /** * the more options dropdown which appears when user clicks on the ellipsis button in the options box */ @@ -1849,6 +1888,11 @@ function RemoteFunctions(config = {}) { ${ICONS.paste} ${config.strings.paste} + `; @@ -1912,6 +1956,12 @@ function RemoteFunctions(config = {}) { .item-label { flex: 1 !important; } + + .item-checkmark { + margin-left: auto !important; + padding-left: 12px !important; + font-size: 14px !important; + } `; shadow.innerHTML = `
${content}
`; @@ -1940,11 +1990,17 @@ function RemoteFunctions(config = {}) { event.stopPropagation(); event.preventDefault(); const action = event.currentTarget.getAttribute('data-action'); - handleOptionClick(event, action, this.targetElement); - // when an option is selected we close both the dropdown as well as the options box - this.remove(); - if (_nodeMoreOptionsBox) { - _nodeMoreOptionsBox.remove(); + + if (action === 'toggle-ruler-lines') { + // when ruler lines option is clicked we need to keep the dropdown open + _handleToggleRulerLines(event, this); + } else { + // for other options, we close both the dropdown as well as the options box + handleOptionClick(event, action, this.targetElement); + this.remove(); + if (_nodeMoreOptionsBox) { + _nodeMoreOptionsBox.remove(); + } } }); }); @@ -1956,6 +2012,14 @@ function RemoteFunctions(config = {}) { this.body = null; _moreOptionsDropdown = null; } + }, + + refresh: function() { + // update the checkmark state when config changes + const checkmark = this._shadow.querySelector('[data-action="toggle-ruler-lines"] .item-checkmark'); + if (checkmark) { + checkmark.textContent = config.showRulerLines ? '✓' : ''; + } } }; @@ -5068,8 +5132,11 @@ function RemoteFunctions(config = {}) { imageGallerySelected = config.imageGalleryState; } - // handle ruler lines visibility toggle + // handle ruler lines visibility toggle and refresh the more options dropdown if its open _handleRulerLinesConfigChange(oldConfig); + if (_moreOptionsDropdown) { + _moreOptionsDropdown.refresh(); + } // Determine if configuration has changed significantly const oldHighlightMode = oldConfig.elemHighlights ? oldConfig.elemHighlights.toLowerCase() : "hover"; diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index a12eb4658..6a22e1c5a 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -30,6 +30,7 @@ define(function (require, exports, module) { const LiveDevelopment = require("LiveDevelopment/main"); const CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"); const ProjectManager = require("project/ProjectManager"); + const PreferencesManager = require("preferences/PreferencesManager"); const CommandManager = require("command/CommandManager"); const Commands = require("command/Commands"); const FileSystem = require("filesystem/FileSystem"); @@ -1458,6 +1459,12 @@ define(function (require, exports, module) { return; } + // handle ruler lines toggle message + if (message.type === "toggleRulerLines") { + PreferencesManager.set("livePreviewShowRulerLines", message.enabled); + return; + } + // handle move(drag & drop) if (message.move && message.sourceId && message.targetId) { _moveElementInSource(message.sourceId, message.targetId, message.insertAfter, message.insertInside); diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 5b0345c2d..8f82ac2a6 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -114,6 +114,7 @@ define(function main(require, exports, module) { cut: Strings.LIVE_DEV_MORE_OPTIONS_CUT, copy: Strings.LIVE_DEV_MORE_OPTIONS_COPY, paste: Strings.LIVE_DEV_MORE_OPTIONS_PASTE, + showRulerLines: Strings.LIVE_PREVIEW_SHOW_RULER_LINES, aiPromptPlaceholder: Strings.LIVE_DEV_AI_PROMPT_PLACEHOLDER, imageGalleryUseImage: Strings.LIVE_DEV_IMAGE_GALLERY_USE_IMAGE, imageGallerySelectDownloadFolder: Strings.LIVE_DEV_IMAGE_GALLERY_SELECT_DOWNLOAD_FOLDER, From 403e7bb931a5e29047e77db1d66d91752dce643f Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 29 Nov 2025 21:45:43 +0530 Subject: [PATCH 28/42] refactor: improve checkmark styles in more options dropdown --- .../BrowserScripts/RemoteFunctions.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 2815f2d8c..337444c71 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1888,6 +1888,7 @@ function RemoteFunctions(config = {}) { ${ICONS.paste} ${config.strings.paste} +