From 3080fb557b1f170dfcf9dcd85359919c80b9dc17 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 22 Aug 2025 23:36:59 +0530 Subject: [PATCH 001/849] feat: add placeholder tab to working set when save is clicked --- src/command/Menus.js | 4 +-- src/extensionsIntegrated/TabBar/main.js | 42 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/command/Menus.js b/src/command/Menus.js index b1c137b621..30e358df89 100644 --- a/src/command/Menus.js +++ b/src/command/Menus.js @@ -737,12 +737,12 @@ define(function (require, exports, module) { logger.leaveTrail("UI Menu Click: " + menuItem._command.getID()); MainViewManager.focusActivePane(); if (menuItem._command._options.eventSource) { - menuItem._command.execute({ + CommandManager.execute(menuItem._command.getID(), { eventSource: CommandManager.SOURCE_UI_MENU_CLICK, sourceType: self.id }); } else { - menuItem._command.execute(); + CommandManager.execute(menuItem._command.getID()); } }); diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 9c43888f3f..5f8f1b76f0 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -547,6 +547,44 @@ define(function (require, exports, module) { // the event fires too frequently when switching editors, leading to unexpected behavior const debounceUpdateTabs = _.debounce(updateTabs, 2); + /** + * This function is responsible to add the placeholder tab to the working set (if user press save on it) + * @param {Event} event + * @param {String} commandId - the command id, to make sure we check it do the operation only on file save + */ + function onFileSave(event, commandId) { + if (commandId === Commands.FILE_SAVE || commandId === Commands.FILE_SAVE_ALL) { + const activePane = MainViewManager.getActivePaneId(); + const currentFile = MainViewManager.getCurrentlyViewedFile(activePane); + + if (currentFile) { + const filePath = currentFile.fullPath; + + // check if this file is currently shown as a placeholder in any pane + const isFirstPanePlaceholder = + MainViewManager.getCurrentlyViewedFile("first-pane") && + MainViewManager.getCurrentlyViewedFile("first-pane").fullPath === filePath && + !Global.firstPaneWorkingSet.some((entry) => entry.path === filePath); + + const isSecondPanePlaceholder = + MainViewManager.getCurrentlyViewedFile("second-pane") && + MainViewManager.getCurrentlyViewedFile("second-pane").fullPath === filePath && + !Global.secondPaneWorkingSet.some((entry) => entry.path === filePath); + + // if it's a placeholder tab, we add it to the working set + if (isFirstPanePlaceholder) { + const fileObj = FileSystem.getFileForPath(filePath); + MainViewManager.addToWorkingSet("first-pane", fileObj); + } + + if (isSecondPanePlaceholder) { + const fileObj = FileSystem.getFileForPath(filePath); + MainViewManager.addToWorkingSet("second-pane", fileObj); + } + } + } + } + /** * Registers the event handlers */ @@ -588,6 +626,10 @@ define(function (require, exports, module) { // main-plugin-panel[0] = live preview panel new ResizeObserver(updateTabs).observe($("#main-plugin-panel")[0]); + // listen for file save commands, needed to add placeholder tab to the working set + CommandManager.off("beforeExecuteCommand", onFileSave); + CommandManager.on("beforeExecuteCommand", onFileSave); + // File dirty flag change handling DocumentManager.on("dirtyFlagChange", function (event, doc) { const filePath = doc.file.fullPath; From 6e2c3dc93b45fd08858ccb0891ea315af38d01d5 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 3 Jul 2025 10:37:48 +0530 Subject: [PATCH 002/849] feat: live preview hover inspect ui initial --- .../BrowserScripts/RemoteFunctions.js | 136 +++++++++++------- src/LiveDevelopment/main.js | 8 +- src/nls/root/strings.js | 2 +- 3 files changed, 91 insertions(+), 55 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index bec0f1905e..95c283b055 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -33,20 +33,20 @@ function RemoteFunctions(config) { var experimental; if (!config) { - experimental = false; + experimental = false; } else { - experimental = config.experimental; + experimental = config.experimental; } var req, timeout; var animateHighlight = function (time) { if(req) { - window.cancelAnimationFrame(req); + window.cancelAnimationFrame(req); window.clearTimeout(timeout); } req = window.requestAnimationFrame(redrawHighlights); timeout = setTimeout(function () { - window.cancelAnimationFrame(req); + window.cancelAnimationFrame(req); req = null; }, time * 1000); }; @@ -113,7 +113,7 @@ function RemoteFunctions(config) { element.removeAttribute(key); } } - + // Checks if the element is in Viewport in the client browser function isInViewport(element) { var rect = element.getBoundingClientRect(); @@ -125,7 +125,7 @@ function RemoteFunctions(config) { rect.right <= (window.innerWidth || html.clientWidth) ); } - + // returns the distance from the top of the closest relatively positioned parent element function getDocumentOffsetTop(element) { return element.offsetTop + (element.offsetParent ? getDocumentOffsetTop(element.offsetParent) : 0); @@ -273,7 +273,7 @@ function RemoteFunctions(config) { animationDuration = parseFloat(elementStyling.getPropertyValue('animation-duration')); highlight.trackingElement = element; // save which node are we highlighting - + if (transitionDuration) { animateHighlight(transitionDuration); } @@ -286,21 +286,21 @@ function RemoteFunctions(config) { if (elementBounds.width === 0 && elementBounds.height === 0) { return; } - + var realElBorder = { right: elementStyling.getPropertyValue('border-right-width'), left: elementStyling.getPropertyValue('border-left-width'), top: elementStyling.getPropertyValue('border-top-width'), bottom: elementStyling.getPropertyValue('border-bottom-width') }; - + var borderBox = elementStyling.boxSizing === 'border-box'; - + var innerWidth = parseFloat(elementStyling.width), innerHeight = parseFloat(elementStyling.height), outerHeight = innerHeight, outerWidth = innerWidth; - + if (!borderBox) { innerWidth += parseFloat(elementStyling.paddingLeft) + parseFloat(elementStyling.paddingRight); innerHeight += parseFloat(elementStyling.paddingTop) + parseFloat(elementStyling.paddingBottom); @@ -309,49 +309,49 @@ function RemoteFunctions(config) { outerHeight = innerHeight + parseFloat(realElBorder.bottom) + parseFloat(realElBorder.top); } - + var visualisations = { horizontal: "left, right", vertical: "top, bottom" }; - + var drawPaddingRect = function(side) { var elStyling = {}; - + if (visualisations.horizontal.indexOf(side) >= 0) { elStyling['width'] = elementStyling.getPropertyValue('padding-' + side); elStyling['height'] = innerHeight + "px"; elStyling['top'] = 0; - + if (borderBox) { elStyling['height'] = innerHeight - parseFloat(realElBorder.top) - parseFloat(realElBorder.bottom) + "px"; } - + } else { - elStyling['height'] = elementStyling.getPropertyValue('padding-' + side); + elStyling['height'] = elementStyling.getPropertyValue('padding-' + side); elStyling['width'] = innerWidth + "px"; elStyling['left'] = 0; - + if (borderBox) { elStyling['width'] = innerWidth - parseFloat(realElBorder.left) - parseFloat(realElBorder.right) + "px"; } } - + elStyling[side] = 0; elStyling['position'] = 'absolute'; - + return elStyling; }; - + var drawMarginRect = function(side) { var elStyling = {}; - + var margin = []; margin['right'] = parseFloat(elementStyling.getPropertyValue('margin-right')); margin['top'] = parseFloat(elementStyling.getPropertyValue('margin-top')); margin['bottom'] = parseFloat(elementStyling.getPropertyValue('margin-bottom')); margin['left'] = parseFloat(elementStyling.getPropertyValue('margin-left')); - + if(visualisations['horizontal'].indexOf(side) >= 0) { elStyling['width'] = elementStyling.getPropertyValue('margin-' + side); @@ -371,37 +371,37 @@ function RemoteFunctions(config) { var setVisibility = function (el) { if ( - !config.remoteHighlight.showPaddingMargin || - parseInt(el.height, 10) <= 0 || - parseInt(el.width, 10) <= 0 + !config.remoteHighlight.showPaddingMargin || + parseInt(el.height, 10) <= 0 || + parseInt(el.width, 10) <= 0 ) { el.display = 'none'; } else { el.display = 'block'; } }; - + var mainBoxStyles = config.remoteHighlight.stylesToSet; - + var paddingVisualisations = [ drawPaddingRect('top'), drawPaddingRect('right'), drawPaddingRect('bottom'), - drawPaddingRect('left') + drawPaddingRect('left') ]; - + var marginVisualisations = [ drawMarginRect('top'), drawMarginRect('right'), drawMarginRect('bottom'), - drawMarginRect('left') + drawMarginRect('left') ]; - + var setupVisualisations = function (arr, config) { var i; for (i = 0; i < arr.length; i++) { setVisibility(arr[i]); - + // Applies to every visualisationElement (padding or margin div) arr[i]["transform"] = "none"; var el = window.document.createElement("div"), @@ -416,7 +416,7 @@ function RemoteFunctions(config) { highlight.appendChild(el); } }; - + setupVisualisations( marginVisualisations, config.remoteHighlight.marginStyling @@ -425,11 +425,11 @@ function RemoteFunctions(config) { paddingVisualisations, config.remoteHighlight.paddingStyling ); - + highlight.className = HIGHLIGHT_CLASSNAME; var offset = _screenOffset(element); - + // some code to find element left/top was removed here. This seems to be relevant to box model // live highlights. firether reading: https://github.com/adobe/brackets/pull/13357/files // we removed this in phoenix because it was throwing the rendering of live highlight boxes in phonix @@ -448,14 +448,14 @@ function RemoteFunctions(config) { "position": "absolute", "pointer-events": "none", "box-shadow": "0 0 1px #fff", - "box-sizing": elementStyling.getPropertyValue('box-sizing'), - "border-right": elementStyling.getPropertyValue('border-right'), - "border-left": elementStyling.getPropertyValue('border-left'), - "border-top": elementStyling.getPropertyValue('border-top'), + "box-sizing": elementStyling.getPropertyValue('box-sizing'), + "border-right": elementStyling.getPropertyValue('border-right'), + "border-left": elementStyling.getPropertyValue('border-left'), + "border-top": elementStyling.getPropertyValue('border-top'), "border-bottom": elementStyling.getPropertyValue('border-bottom'), "border-color": config.remoteHighlight.borderColor }; - + var mergedStyles = Object.assign({}, stylesToSet, config.remoteHighlight.stylesToSet); var animateStartValues = config.remoteHighlight.animateStartValue; @@ -500,7 +500,7 @@ function RemoteFunctions(config) { if (this.trigger) { _trigger(element, "highlight", 1); } - + if ((!window.event || window.event instanceof MessageEvent) && !isInViewport(element)) { var top = getDocumentOffsetTop(element); if (top) { @@ -563,6 +563,8 @@ function RemoteFunctions(config) { var _localHighlight; var _remoteHighlight; + var _hoverHighlight; + var _clickHighlight; var _setup = false; @@ -585,6 +587,19 @@ function RemoteFunctions(config) { window.document.removeEventListener("mousemove", onMouseMove); } + function onElementHover(event) { + if (_hoverHighlight) { + _hoverHighlight.clear(); + _hoverHighlight.add(event.target, false); + } + } + + function onElementHoverOut() { + if (_hoverHighlight) { + _hoverHighlight.clear(); + } + } + function onClick(event) { if (_validEvent(event)) { event.preventDefault(); @@ -643,17 +658,24 @@ function RemoteFunctions(config) { _remoteHighlight.clear(); _remoteHighlight = null; } + if (_clickHighlight) { + _clickHighlight.clear(); + _clickHighlight = null; + } + if (_hoverHighlight) { + _hoverHighlight.clear(); + } } // highlight a node function highlight(node, clear) { - if (!_remoteHighlight) { - _remoteHighlight = new Highlight("#cfc"); + if (!_clickHighlight) { + _clickHighlight = new Highlight("#cfc"); } if (clear) { - _remoteHighlight.clear(); + _clickHighlight.clear(); } - _remoteHighlight.add(node, true); + _clickHighlight.add(node, true); } // highlight a rule @@ -663,7 +685,7 @@ function RemoteFunctions(config) { for (i = 0; i < nodes.length; i++) { highlight(nodes[i]); } - _remoteHighlight.selector = rule; + _clickHighlight.selector = rule; } // redraw active highlights @@ -671,6 +693,12 @@ function RemoteFunctions(config) { if (_remoteHighlight) { _remoteHighlight.redraw(); } + if (_clickHighlight) { + _clickHighlight.redraw(); + } + if (_hoverHighlight) { + _hoverHighlight.redraw(); + } } window.addEventListener("resize", redrawHighlights); @@ -683,7 +711,7 @@ function RemoteFunctions(config) { if (e.target === window.document) { redrawHighlights(); } else { - if (_remoteHighlight || _localHighlight) { + if (_remoteHighlight || _localHighlight || _clickHighlight || _hoverHighlight) { window.setTimeout(redrawHighlights, 0); } } @@ -985,7 +1013,7 @@ function RemoteFunctions(config) { function getSimpleDOM() { return JSON.stringify(_domElementToJSON(window.document.documentElement)); } - + function updateConfig(newConfig) { config = JSON.parse(newConfig); return JSON.stringify(config); @@ -994,6 +1022,16 @@ function RemoteFunctions(config) { // init _editHandler = new DOMEditHandler(window.document); + // Initialize hover highlight with Chrome-like colors + _hoverHighlight = new Highlight("#c8f9c5", true); // Green similar to Chrome's padding color + + // Initialize click highlight with animation + _clickHighlight = new Highlight("#cfc", true); // Light green for click highlight + + // Add event listeners for hover + window.document.addEventListener("mouseover", onElementHover); + window.document.addEventListener("mouseout", onElementHoverOut); + if (experimental) { window.document.addEventListener("keydown", onKeyDown); } diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 7d85eeab5f..3a5c27405a 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -79,14 +79,12 @@ define(function main(require, exports, module) { "opacity": 0.6 }, "paddingStyling": { - "border-width": "1px", - "border-style": "dashed", - "border-color": "rgba(0, 162, 255, 0.5)" + "background-color": "rgba(200, 249, 197, 0.7)" }, "marginStyling": { - "background-color": "rgba(21, 165, 255, 0.58)" + "background-color": "rgba(249, 204, 157, 0.7)" }, - "borderColor": "rgba(21, 165, 255, 0.85)", + "borderColor": "rgba(200, 249, 197, 0.85)", "showPaddingMargin": true }, { description: Strings.DESCRIPTION_LIVE_DEV_HIGHLIGHT_SETTINGS diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 1155d661b3..88c738c16d 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -328,7 +328,7 @@ define({ "SPLITVIEW_MENU_TOOLTIP": "Split the editor vertically or horizontally", "GEAR_MENU_TOOLTIP": "Configure Working Set", - "CMD_TOGGLE_SHOW_WORKING_SET": "Show Working Set", + "CMD_TOGGLE_SHOW_WORKING_SET": "Show Working Files", "CMD_TOGGLE_SHOW_FILE_TABS": "Show File Tab Bar", "SPLITVIEW_INFO_TITLE": "Already Open", From c8a071060a8e794372f3b606ea4578df41b8c2e9 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 3 Jul 2025 11:24:32 +0530 Subject: [PATCH 003/849] feat: live preview hover highlights improved --- .../BrowserScripts/RemoteFunctions.js | 145 +++++++++++++++++- src/LiveDevelopment/main.js | 3 + 2 files changed, 144 insertions(+), 4 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 95c283b055..3af92e25b5 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -212,6 +212,80 @@ function RemoteFunctions(config) { }; + // Node info box to display DOM node ID and classes on hover + function NodeInfoBox(element) { + this.element = element; + this.remove = this.remove.bind(this); + this.create(); + } + + NodeInfoBox.prototype = { + create: function() { + // Remove existing info box if any + this.remove(); + + // compute the position on screen + var offset = _screenOffset(this.element), + x = offset.left, + y = offset.top - 30; // Position above the element + + // create the container + this.body = window.document.createElement("div"); + this.body.style.setProperty("z-index", 2147483647); + this.body.style.setProperty("position", "fixed"); + this.body.style.setProperty("left", (offset.left) + "px"); + this.body.style.setProperty("top", (offset.top - 30 < 0 ? offset.top + this.element.offsetHeight + 5 : offset.top - 30) + "px"); + this.body.style.setProperty("font-size", "12px"); + this.body.style.setProperty("font-family", "Arial, sans-serif"); + + // Style the info box with a blue background + this.body.style.setProperty("background", "#4285F4"); + this.body.style.setProperty("color", "white"); + this.body.style.setProperty("border-radius", "3px"); + this.body.style.setProperty("padding", "5px 8px"); + this.body.style.setProperty("box-shadow", "0 2px 5px rgba(0,0,0,0.2)"); + this.body.style.setProperty("max-width", "300px"); + this.body.style.setProperty("pointer-events", "none"); // Make it non-interactive + + // Get element ID and classes + var id = this.element.id; + var classes = this.element.className ? this.element.className.split(/\s+/).filter(Boolean) : []; + + // Create content for the info box + var content = ""; + + // Add element tag name + content += "
" + this.element.tagName.toLowerCase() + "
"; + + // Add ID if present + if (id) { + content += "
#" + id + "
"; + } + + // Add classes (limit to 3 with dropdown indicator) + if (classes.length > 0) { + content += "
"; + for (var i = 0; i < Math.min(classes.length, 3); i++) { + content += "." + classes[i] + " "; + } + if (classes.length > 3) { + content += "+" + (classes.length - 3) + " more"; + } + content += "
"; + } + + this.body.innerHTML = content; + window.document.body.appendChild(this.body); + }, + + remove: function() { + if (this.body && this.body.parentNode) { + window.document.body.removeChild(this.body); + this.body = null; + } + } + }; + function Editor(element) { this.onBlur = this.onBlur.bind(this); this.onKeyPress = this.onKeyPress.bind(this); @@ -565,6 +639,7 @@ function RemoteFunctions(config) { var _remoteHighlight; var _hoverHighlight; var _clickHighlight; + var _nodeInfoBox; var _setup = false; @@ -572,7 +647,11 @@ function RemoteFunctions(config) { function onMouseOver(event) { if (_validEvent(event)) { - _localHighlight.add(event.target, true); + // Skip highlighting for HTML and BODY tags + if (event.target && event.target.nodeType === Node.ELEMENT_NODE && + event.target.tagName !== "HTML" && event.target.tagName !== "BODY") { + _localHighlight.add(event.target, true); + } } } @@ -590,14 +669,45 @@ function RemoteFunctions(config) { function onElementHover(event) { if (_hoverHighlight) { _hoverHighlight.clear(); - _hoverHighlight.add(event.target, false); + + // Skip highlighting for HTML and BODY tags + if (event.target && event.target.nodeType === Node.ELEMENT_NODE && + event.target.tagName !== "HTML" && event.target.tagName !== "BODY") { + // Store original background color to restore on hover out + event.target._originalBackgroundColor = event.target.style.backgroundColor; + event.target.style.backgroundColor = "rgba(0, 162, 255, 0.2)"; + + _hoverHighlight.add(event.target, false); + + // Create info box for the hovered element + if (_nodeInfoBox) { + _nodeInfoBox.remove(); + } + _nodeInfoBox = new NodeInfoBox(event.target); + } } } - function onElementHoverOut() { + function onElementHoverOut(event) { if (_hoverHighlight) { _hoverHighlight.clear(); } + + // Restore original background color + if (event && event.target && event.target.nodeType === Node.ELEMENT_NODE) { + if (event.target._originalBackgroundColor !== undefined) { + event.target.style.backgroundColor = event.target._originalBackgroundColor; + delete event.target._originalBackgroundColor; + } else { + event.target.style.backgroundColor = ""; + } + } + + // Remove info box when mouse leaves the element + if (_nodeInfoBox) { + _nodeInfoBox.remove(); + _nodeInfoBox = null; + } } function onClick(event) { @@ -665,6 +775,10 @@ function RemoteFunctions(config) { if (_hoverHighlight) { _hoverHighlight.clear(); } + if (_nodeInfoBox) { + _nodeInfoBox.remove(); + _nodeInfoBox = null; + } } // highlight a node @@ -675,7 +789,11 @@ function RemoteFunctions(config) { if (clear) { _clickHighlight.clear(); } - _clickHighlight.add(node, true); + // Skip highlighting for HTML and BODY tags + if (node && node.nodeType === Node.ELEMENT_NODE && + node.tagName !== "HTML" && node.tagName !== "BODY") { + _clickHighlight.add(node, true); + } } // highlight a rule @@ -1015,7 +1133,26 @@ function RemoteFunctions(config) { } function updateConfig(newConfig) { + var oldConfig = config; config = JSON.parse(newConfig); + + if (config.highlight) { + // Add hover event listeners if highlight is enabled + window.document.removeEventListener("mouseover", onElementHover); + window.document.removeEventListener("mouseout", onElementHoverOut); + window.document.addEventListener("mouseover", onElementHover); + window.document.addEventListener("mouseout", onElementHoverOut); + } else { + // Remove hover event listeners if highlight is disabled + window.document.removeEventListener("mouseover", onElementHover); + window.document.removeEventListener("mouseout", onElementHoverOut); + + // Remove info box if highlight is disabled + if (_nodeInfoBox) { + _nodeInfoBox.remove(); + _nodeInfoBox = null; + } + } return JSON.stringify(config); } diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 3a5c27405a..6d7cb962c9 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -298,6 +298,9 @@ define(function main(require, exports, module) { .on("change", function () { config.highlight = PreferencesManager.getViewState("livedevHighlight"); _updateHighlightCheckmark(); + if (MultiBrowserLiveDev && MultiBrowserLiveDev.status >= MultiBrowserLiveDev.STATUS_ACTIVE) { + MultiBrowserLiveDev.agents.remote.call("updateConfig",JSON.stringify(config)); + } }); config.highlight = PreferencesManager.getViewState("livedevHighlight"); From addf4c2d1d83c07d67d83513d86e56f92feb3b07 Mon Sep 17 00:00:00 2001 From: abose Date: Sun, 6 Jul 2025 12:56:52 +0530 Subject: [PATCH 004/849] feat: element selection and content editable setting --- .../BrowserScripts/LiveDevProtocolRemote.js | 59 +++++++++---- .../BrowserScripts/RemoteFunctions.js | 83 ++++++++++++++++++- 2 files changed, 124 insertions(+), 18 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js index 0731a1f86e..f83477f7ad 100644 --- a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js +++ b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js @@ -390,25 +390,50 @@ function onDocumentClick(event) { // Get the user's current selection const selection = window.getSelection(); - - // Check if there is a selection - if (selection.toString().length > 0) { - // if there is any selection like text or others, we don't see it as a live selection event - // Eg: user may selects ome text in live preview to copy, in which case we should nt treat it - // as a live select. - return; - } var element = event.target; if (element && element.hasAttribute('data-brackets-id')) { - MessageBroker.send({ - "tagId": element.getAttribute('data-brackets-id'), - "nodeID": element.id, - "nodeClassList": element.classList, - "nodeName": element.nodeName, - "allSelectors": _getAllInheritedSelectorsInOrder(element), - "contentEditable": element.contentEditable === 'true', - "clicked": true - }); + // Check if it's a double-click for direct editing + if (event.detail === 2 && element.textContent && !['INPUT', 'TEXTAREA', 'SELECT'].includes(element.tagName)) { + // Double-click detected, enable direct editing + // Make the element editable + if (window._LD && window._LD.DOMEditHandler) { + // Use the existing DOMEditHandler to handle the edit + window._LD.startEditing(element); + } else { + MessageBroker.send({ + "tagId": element.getAttribute('data-brackets-id'), + "nodeID": element.id, + "nodeClassList": element.classList, + "nodeName": element.nodeName, + "allSelectors": _getAllInheritedSelectorsInOrder(element), + "contentEditable": element.contentEditable === 'true', + "clicked": true, + "edit": true + }); + } + + // Prevent default behavior and stop propagation + event.preventDefault(); + event.stopPropagation(); + } else { + // Regular click, just send the information + // Check if there is a selection + if (selection.toString().length > 0) { + // if there is any selection like text or others, we don't see it as a live selection event + // Eg: user may selects ome text in live preview to copy, in which case we should nt treat it + // as a live select. + return; + } + MessageBroker.send({ + "tagId": element.getAttribute('data-brackets-id'), + "nodeID": element.id, + "nodeClassList": element.classList, + "nodeName": element.nodeName, + "allSelectors": _getAllInheritedSelectorsInOrder(element), + "contentEditable": element.contentEditable === 'true', + "clicked": true + }); + } } } window.document.addEventListener("click", onDocumentClick); diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 3af92e25b5..5bfacb94cf 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1156,6 +1156,85 @@ function RemoteFunctions(config) { return JSON.stringify(config); } + // Function to handle direct editing of elements in the live preview + function startEditing(element) { + if (!element) { + return; + } + + // Make the element editable + element.setAttribute("contenteditable", "true"); + element.focus(); + + // Save the original content for potential cancellation + element._originalContent = element.innerHTML; + + // Add event listeners for editing + function onBlur() { + finishEditing(element); + } + + function onKeyDown(event) { + if (event.key === "Escape") { + // Cancel editing + element.innerHTML = element._originalContent; + finishEditing(element); + event.preventDefault(); + } else if (event.key === "Enter" && !event.shiftKey) { + // Finish editing on Enter (unless Shift is held) + finishEditing(element); + event.preventDefault(); + } + } + + element.addEventListener("blur", onBlur); + element.addEventListener("keydown", onKeyDown); + + // Store the event listeners for later removal + element._editListeners = { + blur: onBlur, + keydown: onKeyDown + }; + } + + // Function to finish editing and apply changes + function finishEditing(element) { + if (!element || !element.hasAttribute("contenteditable")) { + return; + } + + // Remove contenteditable attribute + element.removeAttribute("contenteditable"); + + // Remove event listeners + if (element._editListeners) { + element.removeEventListener("blur", element._editListeners.blur); + element.removeEventListener("keydown", element._editListeners.keydown); + delete element._editListeners; + } + + // Get the new content + const newContent = element.innerHTML; + + // If content has changed, send the edit to the editor + if (newContent !== element._originalContent && element.hasAttribute("data-brackets-id")) { + const tagId = element.getAttribute("data-brackets-id"); + + // Create a text edit operation + // const edit = { + // type: "textReplace", + // parentID: element.parentNode.getAttribute("data-brackets-id"), + // tagID: tagId, + // content: newContent + // }; + // + // todo: send the edited text to phoenix to change in text editor + } + + // Clean up + delete element._originalContent; + } + // init _editHandler = new DOMEditHandler(window.document); @@ -1182,6 +1261,8 @@ function RemoteFunctions(config) { "redrawHighlights" : redrawHighlights, "applyDOMEdits" : applyDOMEdits, "getSimpleDOM" : getSimpleDOM, - "updateConfig" : updateConfig + "updateConfig" : updateConfig, + "startEditing" : startEditing, + "finishEditing" : finishEditing }; } From 98ddfb9bcd8df80627150b2738d3fa42f6b39cab Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 16 Jul 2025 21:34:53 +0530 Subject: [PATCH 005/849] feat: create the DOM manipulation option box when an element is clicked in the live preview --- .../BrowserScripts/RemoteFunctions.js | 116 ++++++++++++++++-- 1 file changed, 109 insertions(+), 7 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 5bfacb94cf..6063d0f8d6 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -30,6 +30,9 @@ */ function RemoteFunctions(config) { + // this is responsible to make the advanced DOM features active or inactive + // TODO: give this var a better name + let isFlagActive = true; var experimental; if (!config) { @@ -212,6 +215,98 @@ function RemoteFunctions(config) { }; + /** + * This is for the advanced DOM options that appears when a DOM element is clicked + * advanced options like: 'select parent', 'duplicate', 'delete' + */ + function NodeMoreOptionsBox(element) { + this.element = element; + this.remove = this.remove.bind(this); + this.create(); + } + + NodeMoreOptionsBox.prototype = { + create: function() { + // Remove existing more options box if any + this.remove(); + + // compute the position on screen (this gives us the left and the top offset) + var offset = _screenOffset(this.element); + // we need to fetch the height & width of the element to place the box at the correct position + var elementSize = this.element.getBoundingClientRect(); + + // for styling the svg's + if (!document.getElementById("node-more-options-style")) { + const style = document.createElement("style"); + style.id = "node-more-options-style"; + style.textContent = ` + .node-options span > svg { + width: 16px; + height: 16px; + display: block; + } + `; + document.head.appendChild(style); + } + + // create the container + this.body = window.document.createElement("div"); + this.body.style.setProperty("z-index", 2147483647); + this.body.style.setProperty("position", "fixed"); + this.body.style.setProperty("left", (offset.left + (elementSize.width - 40)) + "px"); + this.body.style.setProperty("top", (offset.top - 30 < 0 ? offset.top + this.element.offsetHeight + 5 : offset.top - 30) + "px"); + this.body.style.setProperty("font-size", "12px"); + this.body.style.setProperty("font-family", "Arial, sans-serif"); + + // style the box with a blue background. this will appear on the right side of the clicked DOM element + this.body.style.setProperty("background", "#4285F4"); + this.body.style.setProperty("color", "white"); + this.body.style.setProperty("border-radius", "3px"); + this.body.style.setProperty("padding", "5px 8px"); + this.body.style.setProperty("box-shadow", "0 2px 5px rgba(0,0,0,0.2)"); + this.body.style.setProperty("max-width", "82px"); + this.body.style.setProperty("width", "82px"); + this.body.style.setProperty("pointer-events", "none"); + + const ICONS = { + arrowUp: ` + + `, + + copy: ` + + `, + + trash: ` + + ` + }; + + let content = `
+ + ${ICONS.arrowUp} + + + ${ICONS.copy} + + + ${ICONS.trash} + +
`; + + + this.body.innerHTML = content; + window.document.body.appendChild(this.body); + }, + + remove: function() { + if (this.body && this.body.parentNode) { + window.document.body.removeChild(this.body); + this.body = null; + } + } + }; + // Node info box to display DOM node ID and classes on hover function NodeInfoBox(element) { this.element = element; @@ -640,6 +735,7 @@ function RemoteFunctions(config) { var _hoverHighlight; var _clickHighlight; var _nodeInfoBox; + var _nodeMoreOptionsBox; var _setup = false; @@ -710,15 +806,20 @@ function RemoteFunctions(config) { } } + /** + * This function handles the click event on the live preview DOM element + * it is to show the advanced DOM manipulation options in the live preview + * @param {Event} event + */ function onClick(event) { - if (_validEvent(event)) { - event.preventDefault(); - event.stopPropagation(); - if (event.altKey) { - _toggleEditor(event.target); - } else { - _toggleMenu(event.target); + // make sure that the feature is enabled and also the clicked element has the attribute 'data-brackets-id' + if(isFlagActive && event.target.hasAttribute('data-brackets-id')) { + console.log("event:", event); + if(_nodeMoreOptionsBox) { + _nodeMoreOptionsBox.remove(); + _nodeMoreOptionsBox = null; } + _nodeMoreOptionsBox = new NodeMoreOptionsBox(event.target); } } @@ -1247,6 +1348,7 @@ function RemoteFunctions(config) { // Add event listeners for hover window.document.addEventListener("mouseover", onElementHover); window.document.addEventListener("mouseout", onElementHoverOut); + window.document.addEventListener("click", onClick); if (experimental) { window.document.addEventListener("keydown", onKeyDown); From 07c32a08d9e31df3f0b3176a696f23c9fb435618 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 16 Jul 2025 23:25:36 +0530 Subject: [PATCH 006/849] feat: add click handler to the DOM advance option buttons --- .../BrowserScripts/RemoteFunctions.js | 59 +++++++++++++++++-- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 6063d0f8d6..345e48b778 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -215,6 +215,37 @@ function RemoteFunctions(config) { }; + // TODO: need to implement + function _handleSelectParentOptionClick(e) { + console.log("handle select parent option button was clicked"); + } + + // TODO: need to implement + function _handleDuplicateOptionClick(e) { + console.log("handle duplicate option button was clicked"); + } + + // TODO: need to implement + function _handleDeleteOptionClick(e) { + console.log("handle delete option button was clicked"); + } + + + /** + * This function will get triggered when from the multiple advance DOM buttons, one is clicked + * this function just checks which exact button was clicked and call the required function + * @param {Event} e + * @param {String} action - the data-action attribute to differentiate between buttons + */ + function handleOptionClick(e, action) { + if (action === "select-parent") { + _handleSelectParentOptionClick(e); + } else if (action === "duplicate") { + _handleDuplicateOptionClick(e); + } else if (action === "delete") { + _handleDeleteOptionClick(e); + } + } /** * This is for the advanced DOM options that appears when a DOM element is clicked * advanced options like: 'select parent', 'duplicate', 'delete' @@ -266,7 +297,6 @@ function RemoteFunctions(config) { this.body.style.setProperty("box-shadow", "0 2px 5px rgba(0,0,0,0.2)"); this.body.style.setProperty("max-width", "82px"); this.body.style.setProperty("width", "82px"); - this.body.style.setProperty("pointer-events", "none"); const ICONS = { arrowUp: ` @@ -294,9 +324,20 @@ function RemoteFunctions(config) { `; - this.body.innerHTML = content; window.document.body.appendChild(this.body); + + // add the click handler to all the buttons + const spans = this.body.querySelectorAll('.node-options span'); + spans.forEach(span => { + // to differentiate between each button click we can use the data-action attribute + span.addEventListener('click', (event) => { + event.stopPropagation(); + event.preventDefault(); + const action = event.currentTarget.getAttribute('data-action'); + handleOptionClick(event, action); + }); + }); }, remove: function() { @@ -766,9 +807,15 @@ function RemoteFunctions(config) { if (_hoverHighlight) { _hoverHighlight.clear(); - // Skip highlighting for HTML and BODY tags - if (event.target && event.target.nodeType === Node.ELEMENT_NODE && - event.target.tagName !== "HTML" && event.target.tagName !== "BODY") { + // Skip highlighting for HTML and BODY tags and for DOM elements which doesn't have 'data-brackets-id' + // NOTE: Don't remove 'data-brackets-id' check else hover will also target internal live preview elements + if ( + event.target && + event.target.nodeType === Node.ELEMENT_NODE && + event.target.tagName !== "HTML" && + event.target.tagName !== "BODY" && + event.target.hasAttribute("data-brackets-id") + ) { // Store original background color to restore on hover out event.target._originalBackgroundColor = event.target.style.backgroundColor; event.target.style.backgroundColor = "rgba(0, 162, 255, 0.2)"; @@ -814,7 +861,7 @@ function RemoteFunctions(config) { function onClick(event) { // make sure that the feature is enabled and also the clicked element has the attribute 'data-brackets-id' if(isFlagActive && event.target.hasAttribute('data-brackets-id')) { - console.log("event:", event); + //console.log("event:", event); if(_nodeMoreOptionsBox) { _nodeMoreOptionsBox.remove(); _nodeMoreOptionsBox = null; From d84ae169ce017cbfdc467375eadb67ebb0d75f9b Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 16 Jul 2025 23:59:27 +0530 Subject: [PATCH 007/849] fix: dom more options box background color changing when hovered --- 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 345e48b778..271e118418 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -837,7 +837,7 @@ function RemoteFunctions(config) { } // Restore original background color - if (event && event.target && event.target.nodeType === Node.ELEMENT_NODE) { + if (event && event.target && event.target.nodeType === Node.ELEMENT_NODE && event.target.hasAttribute("data-brackets-id")) { if (event.target._originalBackgroundColor !== undefined) { event.target.style.backgroundColor = event.target._originalBackgroundColor; delete event.target._originalBackgroundColor; From 24abd1f21e8a77a31f3c3f3e43c7052dfbfae433 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 17 Jul 2025 10:48:47 +0530 Subject: [PATCH 008/849] feat: add border when a element is clicked --- .../BrowserScripts/RemoteFunctions.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 271e118418..53ad51daa5 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -34,6 +34,10 @@ function RemoteFunctions(config) { // TODO: give this var a better name let isFlagActive = true; + // this will store the element that was clicked previously (before the new click) + // we need this so that we can remove click styling from the previous element when a new element is clicked + let previouslyClickedElement = null; + var experimental; if (!config) { experimental = false; @@ -866,7 +870,21 @@ function RemoteFunctions(config) { _nodeMoreOptionsBox.remove(); _nodeMoreOptionsBox = null; } + + // to remove the border styling from the previously clicked element + if(previouslyClickedElement) { + if(previouslyClickedElement._originalBorder !== undefined) { + previouslyClickedElement.style.border = previouslyClickedElement._originalBorder; + delete previouslyClickedElement._originalBorder; + } else { + previouslyClickedElement.style.border = ""; + } + } + _nodeMoreOptionsBox = new NodeMoreOptionsBox(event.target); + event.target._originalBorder = event.target.style.border; + event.target.style.border = "1px solid #4285F4"; + previouslyClickedElement = event.target; // add the current element to the previouslyClickedElement } } From f7cd3f7d51a0b2da3d2518ac8c95b0467c154a4c Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 17 Jul 2025 12:01:41 +0530 Subject: [PATCH 009/849] fix: change border to outline to prevent content shifting --- .../BrowserScripts/RemoteFunctions.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 53ad51daa5..385e59a060 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -871,19 +871,19 @@ function RemoteFunctions(config) { _nodeMoreOptionsBox = null; } - // to remove the border styling from the previously clicked element - if(previouslyClickedElement) { - if(previouslyClickedElement._originalBorder !== undefined) { - previouslyClickedElement.style.border = previouslyClickedElement._originalBorder; - delete previouslyClickedElement._originalBorder; + // to remove the outline styling from the previously clicked element + if (previouslyClickedElement) { + if (previouslyClickedElement._originalOutline !== undefined) { + previouslyClickedElement.style.outline = previouslyClickedElement._originalOutline; + delete previouslyClickedElement._originalOutline; } else { - previouslyClickedElement.style.border = ""; + previouslyClickedElement.style.outline = ""; } } _nodeMoreOptionsBox = new NodeMoreOptionsBox(event.target); - event.target._originalBorder = event.target.style.border; - event.target.style.border = "1px solid #4285F4"; + event.target._originalOutline = event.target.style.outline; + event.target.style.outline = "1px solid #4285F4"; previouslyClickedElement = event.target; // add the current element to the previouslyClickedElement } } From 4451767f8de1b555c55399aa1865401679834f1e Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 17 Jul 2025 15:05:34 +0530 Subject: [PATCH 010/849] fix: more options box positioning when live preview is scrolled --- .../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 385e59a060..3648b88f46 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -265,10 +265,7 @@ function RemoteFunctions(config) { // Remove existing more options box if any this.remove(); - // compute the position on screen (this gives us the left and the top offset) - var offset = _screenOffset(this.element); - // we need to fetch the height & width of the element to place the box at the correct position - var elementSize = this.element.getBoundingClientRect(); + let elemBounds = this.element.getBoundingClientRect(); // for styling the svg's if (!document.getElementById("node-more-options-style")) { @@ -288,8 +285,11 @@ function RemoteFunctions(config) { this.body = window.document.createElement("div"); this.body.style.setProperty("z-index", 2147483647); this.body.style.setProperty("position", "fixed"); - this.body.style.setProperty("left", (offset.left + (elementSize.width - 40)) + "px"); - this.body.style.setProperty("top", (offset.top - 30 < 0 ? offset.top + this.element.offsetHeight + 5 : offset.top - 30) + "px"); + this.body.style.setProperty("left", elemBounds.left + (elemBounds.width - 40) + "px"); + this.body.style.setProperty( + "top", + (elemBounds.top - 30 < 0 ? elemBounds.top + elemBounds.offsetHeight + 5 : elemBounds.top - 30) + "px" + ); this.body.style.setProperty("font-size", "12px"); this.body.style.setProperty("font-family", "Arial, sans-serif"); From b6f481a12fa593a9fe1bc786e5ce0312edcb51aa Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 17 Jul 2025 15:17:42 +0530 Subject: [PATCH 011/849] fix: offset height gives invalid px and puts the more options box below the screen --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 3648b88f46..0fd7aed4b6 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -288,7 +288,9 @@ function RemoteFunctions(config) { this.body.style.setProperty("left", elemBounds.left + (elemBounds.width - 40) + "px"); this.body.style.setProperty( "top", - (elemBounds.top - 30 < 0 ? elemBounds.top + elemBounds.offsetHeight + 5 : elemBounds.top - 30) + "px" + // if there's not enough space to show the box above the element, + // we show it below the element + (elemBounds.top - 30 < 0 ? elemBounds.top + elemBounds.height + 5 : elemBounds.top - 30) + "px" ); this.body.style.setProperty("font-size", "12px"); this.body.style.setProperty("font-family", "Arial, sans-serif"); From e10e89d1e3758d85c692dfc28b9f61cf203bdbd4 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 17 Jul 2025 15:37:48 +0530 Subject: [PATCH 012/849] fix: box getting out of viewport horizontally --- .../BrowserScripts/RemoteFunctions.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 0fd7aed4b6..6058bc0995 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -285,7 +285,17 @@ function RemoteFunctions(config) { this.body = window.document.createElement("div"); this.body.style.setProperty("z-index", 2147483647); this.body.style.setProperty("position", "fixed"); - this.body.style.setProperty("left", elemBounds.left + (elemBounds.width - 40) + "px"); + + // the more options box width + const boxWidth = 82 + 16; // 82px width + 8px padding on each side + let leftPos = elemBounds.left + (elemBounds.width - 40); // the potential left position + // make sure that box wouldn't go out of the viewport + const viewportWidth = window.innerWidth || document.documentElement.clientWidth; + if (leftPos + boxWidth > viewportWidth) { + leftPos = viewportWidth - boxWidth; + } + + this.body.style.setProperty("left", leftPos + "px"); this.body.style.setProperty( "top", // if there's not enough space to show the box above the element, @@ -302,7 +312,7 @@ function RemoteFunctions(config) { this.body.style.setProperty("padding", "5px 8px"); this.body.style.setProperty("box-shadow", "0 2px 5px rgba(0,0,0,0.2)"); this.body.style.setProperty("max-width", "82px"); - this.body.style.setProperty("width", "82px"); + this.body.style.setProperty("width", "82px"); // if changing this, also update the boxWidth variable const ICONS = { arrowUp: ` From 22a16a44e9c69932022fef6f43375bef0017b442 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 17 Jul 2025 18:33:09 +0530 Subject: [PATCH 013/849] fix: positioning of the element info box --- .../BrowserScripts/RemoteFunctions.js | 72 +++++++++++-------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 6058bc0995..30e07a53a3 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -376,17 +376,60 @@ function RemoteFunctions(config) { // Remove existing info box if any this.remove(); + // this value decides where we need to show the box in the UI + // we are creating this here, because if the element has IDs and Classes then we need to increase the value + // so that the box doesn't obscure the element + let pushBoxUp = 29; // px value + + // get the ID and classes for that element, as we need to display it in the box + const id = this.element.id; + const classes = this.element.className ? this.element.className.split(/\s+/).filter(Boolean) : []; + + let content = ""; // this will hold the main content that will be displayed + // add element tag name + content += "
" + this.element.tagName.toLowerCase() + "
"; + + // Add ID if present + if (id) { + content += "
#" + id + "
"; + pushBoxUp += 17; + } + + // Add classes (limit to 3 with dropdown indicator) + if (classes.length > 0) { + content += "
"; + for (var i = 0; i < Math.min(classes.length, 3); i++) { + content += "." + classes[i] + " "; + } + if (classes.length > 3) { + content += "+" + (classes.length - 3) + " more"; + } + content += "
"; + + pushBoxUp += 17; + } + // compute the position on screen var offset = _screenOffset(this.element), x = offset.left, y = offset.top - 30; // Position above the element + let elemBounds = this.element.getBoundingClientRect(); + + console.log("elemBounds: ", elemBounds); + // create the container this.body = window.document.createElement("div"); this.body.style.setProperty("z-index", 2147483647); this.body.style.setProperty("position", "fixed"); this.body.style.setProperty("left", (offset.left) + "px"); - this.body.style.setProperty("top", (offset.top - 30 < 0 ? offset.top + this.element.offsetHeight + 5 : offset.top - 30) + "px"); + this.body.style.setProperty( + "top", + // if there's not enough space to show the box above the element, + // we show it below the element + (elemBounds.top - pushBoxUp < 0 ? elemBounds.top + elemBounds.height + 5 : elemBounds.top - pushBoxUp) + + "px" + ); this.body.style.setProperty("font-size", "12px"); this.body.style.setProperty("font-family", "Arial, sans-serif"); @@ -399,33 +442,6 @@ function RemoteFunctions(config) { this.body.style.setProperty("max-width", "300px"); this.body.style.setProperty("pointer-events", "none"); // Make it non-interactive - // Get element ID and classes - var id = this.element.id; - var classes = this.element.className ? this.element.className.split(/\s+/).filter(Boolean) : []; - - // Create content for the info box - var content = ""; - - // Add element tag name - content += "
" + this.element.tagName.toLowerCase() + "
"; - - // Add ID if present - if (id) { - content += "
#" + id + "
"; - } - - // Add classes (limit to 3 with dropdown indicator) - if (classes.length > 0) { - content += "
"; - for (var i = 0; i < Math.min(classes.length, 3); i++) { - content += "." + classes[i] + " "; - } - if (classes.length > 3) { - content += "+" + (classes.length - 3) + " more"; - } - content += "
"; - } - this.body.innerHTML = content; window.document.body.appendChild(this.body); }, From 6a661bde19d96f70f5899706e6399de9250d6903 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 17 Jul 2025 18:48:07 +0530 Subject: [PATCH 014/849] fix: horizontal positioning of both the boxes --- .../BrowserScripts/RemoteFunctions.js | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 30e07a53a3..a563fa1f17 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -286,16 +286,9 @@ function RemoteFunctions(config) { this.body.style.setProperty("z-index", 2147483647); this.body.style.setProperty("position", "fixed"); - // the more options box width - const boxWidth = 82 + 16; // 82px width + 8px padding on each side - let leftPos = elemBounds.left + (elemBounds.width - 40); // the potential left position - // make sure that box wouldn't go out of the viewport - const viewportWidth = window.innerWidth || document.documentElement.clientWidth; - if (leftPos + boxWidth > viewportWidth) { - leftPos = viewportWidth - boxWidth; - } + const boxWidth = 82; - this.body.style.setProperty("left", leftPos + "px"); + this.body.style.setProperty("left", (elemBounds.left + (elemBounds.width - boxWidth)) + "px"); this.body.style.setProperty( "top", // if there's not enough space to show the box above the element, @@ -311,8 +304,7 @@ function RemoteFunctions(config) { this.body.style.setProperty("border-radius", "3px"); this.body.style.setProperty("padding", "5px 8px"); this.body.style.setProperty("box-shadow", "0 2px 5px rgba(0,0,0,0.2)"); - this.body.style.setProperty("max-width", "82px"); - this.body.style.setProperty("width", "82px"); // if changing this, also update the boxWidth variable + this.body.style.setProperty("width", boxWidth + "px"); const ICONS = { arrowUp: ` @@ -409,20 +401,13 @@ function RemoteFunctions(config) { pushBoxUp += 17; } - // compute the position on screen - var offset = _screenOffset(this.element), - x = offset.left, - y = offset.top - 30; // Position above the element - let elemBounds = this.element.getBoundingClientRect(); - console.log("elemBounds: ", elemBounds); - // create the container this.body = window.document.createElement("div"); this.body.style.setProperty("z-index", 2147483647); this.body.style.setProperty("position", "fixed"); - this.body.style.setProperty("left", (offset.left) + "px"); + this.body.style.setProperty("left", elemBounds.left + "px"); this.body.style.setProperty( "top", // if there's not enough space to show the box above the element, From 7fe3498ebfe8c1676cca50b3977bd777f29faddf Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 18 Jul 2025 18:32:13 +0530 Subject: [PATCH 015/849] feat: enable deletion of elements in source code from live preview --- .../BrowserScripts/LiveDevProtocolRemote.js | 2 ++ .../BrowserScripts/RemoteFunctions.js | 34 +++++++++++++------ .../protocol/LiveDevProtocol.js | 7 ++-- src/LiveDevelopment/livePreviewEdit.js | 27 +++++++++++++++ 4 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 src/LiveDevelopment/livePreviewEdit.js diff --git a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js index f83477f7ad..74e007de0b 100644 --- a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js +++ b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js @@ -128,6 +128,8 @@ } }; + global._Brackets_MessageBroker = MessageBroker; + /** * Runtime Domain. Implements remote commands for "Runtime.*" */ diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index a563fa1f17..bd4f90a3b8 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -229,25 +229,39 @@ function RemoteFunctions(config) { console.log("handle duplicate option button was clicked"); } - // TODO: need to implement - function _handleDeleteOptionClick(e) { - console.log("handle delete option button was clicked"); + /** + * This function gets called when the delete button is clicked + * it sends a message to the editor using postMessage to delete the element from the source code + * @param {Event} e + * @param {DOMElement} element - the HTML DOM element that was clicked. it is to get the data-brackets-id attribute + */ + function _handleDeleteOptionClick(e, element) { + const tagId = element.getAttribute("data-brackets-id"); + if (tagId) { + window._Brackets_MessageBroker.send({ + livePreviewEditEnabled: true, + element: element, + event: e, + tagId: tagId, + delete: true + }); + } } - /** * This function will get triggered when from the multiple advance DOM buttons, one is clicked * this function just checks which exact button was clicked and call the required function * @param {Event} e * @param {String} action - the data-action attribute to differentiate between buttons + * @param {DOMElement} element - the selected DOM element */ - function handleOptionClick(e, action) { + function handleOptionClick(e, action, element) { if (action === "select-parent") { - _handleSelectParentOptionClick(e); + _handleSelectParentOptionClick(e, element); } else if (action === "duplicate") { - _handleDuplicateOptionClick(e); + _handleDuplicateOptionClick(e, element); } else if (action === "delete") { - _handleDeleteOptionClick(e); + _handleDeleteOptionClick(e, element); } } /** @@ -288,7 +302,7 @@ function RemoteFunctions(config) { const boxWidth = 82; - this.body.style.setProperty("left", (elemBounds.left + (elemBounds.width - boxWidth)) + "px"); + this.body.style.setProperty("left", (elemBounds.right - boxWidth) + "px"); this.body.style.setProperty( "top", // if there's not enough space to show the box above the element, @@ -343,7 +357,7 @@ function RemoteFunctions(config) { event.stopPropagation(); event.preventDefault(); const action = event.currentTarget.getAttribute('data-action'); - handleOptionClick(event, action); + handleOptionClick(event, action, this.element); }); }); }, diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index f6b9c39108..57ed132e21 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -52,7 +52,8 @@ define(function (require, exports, module) { HTMLInstrumentation = require("LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation"), StringUtils = require("utils/StringUtils"), FileViewController = require("project/FileViewController"), - MainViewManager = require("view/MainViewManager"); + MainViewManager = require("view/MainViewManager"), + LivePreviewEdit = require("LiveDevelopment/livePreviewEdit"); const LIVE_DEV_REMOTE_SCRIPTS_FILE_NAME = `phoenix_live_preview_scripts_instrumented_${StringUtils.randomString(8)}.js`; const LIVE_DEV_REMOTE_WORKER_SCRIPTS_FILE_NAME = `pageLoaderWorker_${StringUtils.randomString(8)}.js`; @@ -207,7 +208,9 @@ define(function (require, exports, module) { var msg = JSON.parse(msgStr), event = msg.method || "event", deferred; - if (msg.id) { + if (msg.livePreviewEditEnabled) { + LivePreviewEdit.deleteElementInSourceByTagId(msg); + } else if (msg.id) { deferred = _responseDeferreds[msg.id]; if (deferred) { delete _responseDeferreds[msg.id]; diff --git a/src/LiveDevelopment/livePreviewEdit.js b/src/LiveDevelopment/livePreviewEdit.js new file mode 100644 index 0000000000..6fe688ab3d --- /dev/null +++ b/src/LiveDevelopment/livePreviewEdit.js @@ -0,0 +1,27 @@ +define(function (require, exports, module) { + const EditorManager = require("editor/EditorManager"); + + function deleteElementInSourceByTagId(tagId) { + const editor = EditorManager.getActiveEditor(); + if (!editor || !tagId) { + return; + } + + // this will give us the text marker object + const mark = editor.getAllMarks().find((m) => m.tagID === Number(tagId)); + if (!mark) { + return; + } + + // this give us the start position to the end position of the code. + // we just need to delete it + const range = mark.find(); + if (!range) { + return; + } + + editor.replaceRange("", range.from, range.to); + } + + exports.deleteElementInSourceByTagId = deleteElementInSourceByTagId; +}); From 8451ee05d27c8eb988b9c57d64404ee0e0a3148a Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 18 Jul 2025 20:16:56 +0530 Subject: [PATCH 016/849] fix: use existing APIs to reference the source code of the element given its tagId --- .../BrowserScripts/RemoteFunctions.js | 6 +-- .../language/HTMLInstrumentation.js | 6 ++- .../protocol/LiveDevProtocol.js | 8 ++-- src/LiveDevelopment/livePreviewEdit.js | 48 +++++++++++++++---- 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index bd4f90a3b8..1f66ee94e1 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -232,16 +232,16 @@ function RemoteFunctions(config) { /** * This function gets called when the delete button is clicked * it sends a message to the editor using postMessage to delete the element from the source code - * @param {Event} e + * @param {Event} event * @param {DOMElement} element - the HTML DOM element that was clicked. it is to get the data-brackets-id attribute */ - function _handleDeleteOptionClick(e, element) { + function _handleDeleteOptionClick(event, element) { const tagId = element.getAttribute("data-brackets-id"); if (tagId) { window._Brackets_MessageBroker.send({ livePreviewEditEnabled: true, element: element, - event: e, + event: event, tagId: tagId, delete: true }); diff --git a/src/LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation.js b/src/LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation.js index f467cd6ad0..13934ac7ee 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation.js +++ b/src/LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation.js @@ -851,10 +851,12 @@ define(function (require, exports, module) { return (mark.tagID === tagId); }); if (markFound) { - return markFound.find().from; + return { + from: markFound.find().from, + to: markFound.find().to + }; } return null; - } // private methods diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index 57ed132e21..67aa80f5d5 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -166,7 +166,7 @@ define(function (require, exports, module) { } const allOpenFileCount = MainViewManager.getWorkingSetSize(MainViewManager.ALL_PANES); function selectInHTMLEditor(fullHtmlEditor) { - const position = HTMLInstrumentation.getPositionFromTagId(fullHtmlEditor, parseInt(tagId, 10)); + const position = HTMLInstrumentation.getPositionFromTagId(fullHtmlEditor, parseInt(tagId, 10)).from; if(position && fullHtmlEditor) { const masterEditor = fullHtmlEditor.document._masterEditor || fullHtmlEditor; masterEditor.setCursorPos(position.line, position.ch, true); @@ -209,8 +209,10 @@ define(function (require, exports, module) { event = msg.method || "event", deferred; if (msg.livePreviewEditEnabled) { - LivePreviewEdit.deleteElementInSourceByTagId(msg); - } else if (msg.id) { + LivePreviewEdit.handleLivePreviewEditOperation(msg); + } + + if (msg.id) { deferred = _responseDeferreds[msg.id]; if (deferred) { delete _responseDeferreds[msg.id]; diff --git a/src/LiveDevelopment/livePreviewEdit.js b/src/LiveDevelopment/livePreviewEdit.js index 6fe688ab3d..abadd1d77c 100644 --- a/src/LiveDevelopment/livePreviewEdit.js +++ b/src/LiveDevelopment/livePreviewEdit.js @@ -1,27 +1,55 @@ define(function (require, exports, module) { const EditorManager = require("editor/EditorManager"); + const HTMLInstrumentation = require("LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation"); - function deleteElementInSourceByTagId(tagId) { + + /** + * This function is responsible to delete an element from the source code + * @param {Number} tagId - the data-brackets-id of the DOM element + */ + function _deleteElementInSourceByTagId(tagId) { const editor = EditorManager.getActiveEditor(); if (!editor || !tagId) { return; } - // this will give us the text marker object - const mark = editor.getAllMarks().find((m) => m.tagID === Number(tagId)); - if (!mark) { + // this will give us the start pos and end pos of the DOM element in the source code + // can be referenced using range.from and range.to + const range = HTMLInstrumentation.getPositionFromTagId(editor, tagId); + if(!range) { return; } - // this give us the start position to the end position of the code. - // we just need to delete it - const range = mark.find(); - if (!range) { + editor.replaceRange("", range.from, range.to); + } + + /** + * This is the main function that is exported. + * it will be called by LiveDevProtocol when it receives a message from RemoteFunctions.js using MessageBroker + * Refer to: `handleOptionClick` function in the RemoteFunctions.js and `_receive` function in LiveDevProtocol.js + * + * @param {Object} message - this is the object that is passed by RemoteFunctions.js using MessageBroker + * this object will be in the format + * { + livePreviewEditEnabled: true, + element: element, + event: event, + tagId: tagId, + delete: true + } + * here element is the actual DOM element that is clicked, and tagId is the data-brackets-id + * and 'delete: true is just an example, it might be 'duplicate' or 'select-parent' also + */ + function handleLivePreviewEditOperation(message) { + if (!message.element || !message.tagId) { return; } - editor.replaceRange("", range.from, range.to); + // just call the required functions + if (message.delete) { + _deleteElementInSourceByTagId(Number(message.tagId)); + } } - exports.deleteElementInSourceByTagId = deleteElementInSourceByTagId; + exports.handleLivePreviewEditOperation = handleLivePreviewEditOperation; }); From 42c5178526db5fd3b617fdadefa6881d8fa4ae83 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 18 Jul 2025 20:22:06 +0530 Subject: [PATCH 017/849] fix: remove the popup after it is handled --- 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 1f66ee94e1..95fd656e71 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -358,6 +358,7 @@ function RemoteFunctions(config) { event.preventDefault(); const action = event.currentTarget.getAttribute('data-action'); handleOptionClick(event, action, this.element); + this.remove(); }); }); }, From ff22ce62240987c228fb4b30ec64061a2ecaead1 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 19 Jul 2025 00:19:51 +0530 Subject: [PATCH 018/849] feat: implement live preview duplicate option functionality --- .../BrowserScripts/RemoteFunctions.js | 26 +++++++++--- src/LiveDevelopment/livePreviewEdit.js | 41 ++++++++++++++++++- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 95fd656e71..b6a805528c 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -224,11 +224,6 @@ function RemoteFunctions(config) { console.log("handle select parent option button was clicked"); } - // TODO: need to implement - function _handleDuplicateOptionClick(e) { - console.log("handle duplicate option button was clicked"); - } - /** * This function gets called when the delete button is clicked * it sends a message to the editor using postMessage to delete the element from the source code @@ -242,12 +237,31 @@ function RemoteFunctions(config) { livePreviewEditEnabled: true, element: element, event: event, - tagId: tagId, + tagId: Number(tagId), delete: true }); } } + /** + * For duplicate button. Read '_handleDeleteOptionClick' jsdoc to understand more on how this works + * @param {Event} event + * @param {DOMElement} element - the HTML DOM element that was clicked. it is to get the data-brackets-id attribute + */ + function _handleDuplicateOptionClick(event, element) { + const tagId = element.getAttribute("data-brackets-id"); + if (tagId) { + window._Brackets_MessageBroker.send({ + livePreviewEditEnabled: true, + element: element, + event: event, + tagId: Number(tagId), + duplicate: true + }); + } + } + + /** * This function will get triggered when from the multiple advance DOM buttons, one is clicked * this function just checks which exact button was clicked and call the required function diff --git a/src/LiveDevelopment/livePreviewEdit.js b/src/LiveDevelopment/livePreviewEdit.js index abadd1d77c..5503041aad 100644 --- a/src/LiveDevelopment/livePreviewEdit.js +++ b/src/LiveDevelopment/livePreviewEdit.js @@ -2,6 +2,41 @@ define(function (require, exports, module) { const EditorManager = require("editor/EditorManager"); const HTMLInstrumentation = require("LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation"); + /** + * This function is responsible to duplicate an element from the source code + * @param {Number} tagId - the data-brackets-id of the DOM element + */ + function _duplicateElementInSourceByTagId(tagId) { + const editor = EditorManager.getActiveEditor(); + if (!editor || !tagId) { + return; + } + + // this will give us the start pos and end pos of the DOM element in the source code + // can be referenced using range.from and range.to + const range = HTMLInstrumentation.getPositionFromTagId(editor, tagId); + if (!range) { + return; + } + + // this is the actual source code for the element that we need to duplicate + const text = editor.getTextBetween(range.from, range.to); + // this is the indentation on the line + const indent = editor.getTextBetween({line: range.from.line, ch: 0}, range.from); + + // this is the position where we need to insert + // we're giving the char as 0 because since we insert a new line using '\n' + // that's why writing any char value will not work, as the line is empty + // and codemirror doesn't allow to insert at a column (ch) greater than the length of the line + // So, the logic is to just append the indent before the text at this insertPos + const insertPos = { + line: range.from.line + (range.to.line - range.from.line + 1), + ch: 0 + }; + + editor.replaceRange('\n', range.to); + editor.replaceRange(indent + text, insertPos); + } /** * This function is responsible to delete an element from the source code @@ -16,7 +51,7 @@ define(function (require, exports, module) { // this will give us the start pos and end pos of the DOM element in the source code // can be referenced using range.from and range.to const range = HTMLInstrumentation.getPositionFromTagId(editor, tagId); - if(!range) { + if (!range) { return; } @@ -47,7 +82,9 @@ define(function (require, exports, module) { // just call the required functions if (message.delete) { - _deleteElementInSourceByTagId(Number(message.tagId)); + _deleteElementInSourceByTagId(message.tagId); + } else if (message.duplicate) { + _duplicateElementInSourceByTagId(message.tagId); } } From a5d982f186deffe847e28e279c895ea939bd00a2 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 19 Jul 2025 21:31:39 +0530 Subject: [PATCH 019/849] feat: implement select-parent option in live preview edit --- .../BrowserScripts/RemoteFunctions.js | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index b6a805528c..2cae9262fb 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -219,11 +219,6 @@ function RemoteFunctions(config) { }; - // TODO: need to implement - function _handleSelectParentOptionClick(e) { - console.log("handle select parent option button was clicked"); - } - /** * This function gets called when the delete button is clicked * it sends a message to the editor using postMessage to delete the element from the source code @@ -244,7 +239,7 @@ function RemoteFunctions(config) { } /** - * For duplicate button. Read '_handleDeleteOptionClick' jsdoc to understand more on how this works + * this is for duplicate button. Read '_handleDeleteOptionClick' jsdoc to understand more on how this works * @param {Event} event * @param {DOMElement} element - the HTML DOM element that was clicked. it is to get the data-brackets-id attribute */ @@ -261,6 +256,32 @@ function RemoteFunctions(config) { } } + /** + * 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 + * @param {Event} event + * @param {DOMElement} element - the HTML DOM element that was clicked. it is to get the data-brackets-id attribute + */ + function _handleSelectParentOptionClick(event, element) { + if (!element) { + return; + } + + const parentElement = element.parentElement; + if (!parentElement) { + return; + } + + // we need to make sure that the parent element is not the body tag or the html. + // also we expect it to have the 'data-brackets-id' + if ( + parentElement.tagName !== "BODY" && + parentElement.tagName !== "HTML" && + parentElement.hasAttribute("data-brackets-id") + ) { + parentElement.click(); + } + } /** * This function will get triggered when from the multiple advance DOM buttons, one is clicked From 0cc7413f8b32454af93d8e020f5fa29fd63ade69 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 20 Jul 2025 01:05:38 +0530 Subject: [PATCH 020/849] fix: implement shadow DOM to prevent box styles being overridden by user stylesheets --- .../BrowserScripts/RemoteFunctions.js | 142 ++++++++++-------- 1 file changed, 83 insertions(+), 59 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 2cae9262fb..7a689be439 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -310,87 +310,111 @@ function RemoteFunctions(config) { } NodeMoreOptionsBox.prototype = { - create: function() { - // Remove existing more options box if any - this.remove(); - - let elemBounds = this.element.getBoundingClientRect(); - - // for styling the svg's - if (!document.getElementById("node-more-options-style")) { - const style = document.createElement("style"); - style.id = "node-more-options-style"; - style.textContent = ` - .node-options span > svg { - width: 16px; - height: 16px; - display: block; - } - `; - document.head.appendChild(style); - } - - // create the container + _style: function() { this.body = window.document.createElement("div"); - this.body.style.setProperty("z-index", 2147483647); - this.body.style.setProperty("position", "fixed"); - const boxWidth = 82; + // this is shadow DOM. + // we need it because if we add the box directly to the DOM then users style might override it. + // {mode: "closed"} means that users will not be able to access the shadow DOM + const shadow = this.body.attachShadow({ mode: "closed" }); - this.body.style.setProperty("left", (elemBounds.right - boxWidth) + "px"); - this.body.style.setProperty( - "top", - // if there's not enough space to show the box above the element, - // we show it below the element - (elemBounds.top - 30 < 0 ? elemBounds.top + elemBounds.height + 5 : elemBounds.top - 30) + "px" - ); - this.body.style.setProperty("font-size", "12px"); - this.body.style.setProperty("font-family", "Arial, sans-serif"); + // the element that was clicked + let elemBounds = this.element.getBoundingClientRect(); - // style the box with a blue background. this will appear on the right side of the clicked DOM element - this.body.style.setProperty("background", "#4285F4"); - this.body.style.setProperty("color", "white"); - this.body.style.setProperty("border-radius", "3px"); - this.body.style.setProperty("padding", "5px 8px"); - this.body.style.setProperty("box-shadow", "0 2px 5px rgba(0,0,0,0.2)"); - this.body.style.setProperty("width", boxWidth + "px"); + // the box width and the positions where it should be placed + const boxWidth = 82; + const leftPos = (elemBounds.right - boxWidth); + const topPos = (elemBounds.top - 30 < 0 ? elemBounds.top + elemBounds.height + 5 : elemBounds.top - 30); + // the icons that is displayed in the box const ICONS = { - arrowUp: ` - - `, - - copy: ` - - `, - - trash: ` - - ` + arrowUp: ` + + + + `, + + copy: ` + + + + `, + + trash: ` + + + + ` }; - let content = `
- + let content = `
+ ${ICONS.arrowUp} - + ${ICONS.copy} - + ${ICONS.trash}
`; - this.body.innerHTML = content; + const styles = ` + .box { + background-color: #4285F4; + color: white; + border-radius: 3px; + padding: 5px 8px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + font-size: 12px; + font-family: Arial, sans-serif; + z-index: 2147483647; + position: fixed; + left: ${leftPos}px; + top: ${topPos}px; + width: ${boxWidth}px; + box-sizing: border-box; + } + + .node-options { + display: flex; + gap: 8px; + align-items: center; + } + + .node-options span { + cursor: pointer; + display: flex; + align-items: center; + } + + .node-options span > svg { + width: 16px; + height: 16px; + display: block; + } + `; + + // add everything to the shadow box + shadow.innerHTML = `
${content}
`; + this._shadow = shadow; + }, + + create: function() { + this.remove(); // remove existing box if already present + this._style(); // style the box + window.document.body.appendChild(this.body); - // add the click handler to all the buttons - const spans = this.body.querySelectorAll('.node-options span'); + // add click handler to all the buttons + const spans = this._shadow.querySelectorAll('.node-options span'); spans.forEach(span => { - // to differentiate between each button click we can use the data-action attribute span.addEventListener('click', (event) => { event.stopPropagation(); event.preventDefault(); + // data-action is to differentiate between the buttons (duplicate, delete or select-parent) const action = event.currentTarget.getAttribute('data-action'); handleOptionClick(event, action, this.element); this.remove(); From 444d0092a0484099bf7ce6792ed4da251811ec85 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 20 Jul 2025 01:25:30 +0530 Subject: [PATCH 021/849] fix: box now moves along with the selected element when scrolled --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 7a689be439..4860fc4ef9 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -323,8 +323,13 @@ function RemoteFunctions(config) { // the box width and the positions where it should be placed const boxWidth = 82; - const leftPos = (elemBounds.right - boxWidth); - const topPos = (elemBounds.top - 30 < 0 ? elemBounds.top + elemBounds.height + 5 : elemBounds.top - 30); + const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + + const leftPos = elemBounds.right - boxWidth + scrollLeft; + const topPos = (elemBounds.top - 30 < 0 + ? elemBounds.top + elemBounds.height + 5 + : elemBounds.top - 30) + scrollTop; // the icons that is displayed in the box const ICONS = { @@ -371,7 +376,7 @@ function RemoteFunctions(config) { font-size: 12px; font-family: Arial, sans-serif; z-index: 2147483647; - position: fixed; + position: absolute; left: ${leftPos}px; top: ${topPos}px; width: ${boxWidth}px; From 735627fa7b29640f2996838b688f42d10768185f Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 20 Jul 2025 01:55:59 +0530 Subject: [PATCH 022/849] fix: positioning of the info box --- .../BrowserScripts/RemoteFunctions.js | 105 +++++++++++------- 1 file changed, 67 insertions(+), 38 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 4860fc4ef9..fc57b70bff 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -443,70 +443,99 @@ function RemoteFunctions(config) { } NodeInfoBox.prototype = { - create: function() { - // Remove existing info box if any - this.remove(); + _style: function() { + this.body = window.document.createElement("div"); + + // this is shadow DOM. + // we need it because if we add the box directly to the DOM then users style might override it. + // {mode: "closed"} means that users will not be able to access the shadow DOM + const shadow = this.body.attachShadow({ mode: "closed" }); + + // the element that was clicked + let elemBounds = this.element.getBoundingClientRect(); + + // the positions where it should be placed + const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; // this value decides where we need to show the box in the UI // we are creating this here, because if the element has IDs and Classes then we need to increase the value // so that the box doesn't obscure the element - let pushBoxUp = 29; // px value + let pushBoxUp = 28; // px value // get the ID and classes for that element, as we need to display it in the box const id = this.element.id; const classes = this.element.className ? this.element.className.split(/\s+/).filter(Boolean) : []; let content = ""; // this will hold the main content that will be displayed - // add element tag name - content += "
" + this.element.tagName.toLowerCase() + "
"; + content += "
" + this.element.tagName.toLowerCase() + "
"; // add element tag name // Add ID if present if (id) { - content += "
#" + id + "
"; - pushBoxUp += 17; + content += "
#" + id + "
"; + pushBoxUp += 16; } // Add classes (limit to 3 with dropdown indicator) if (classes.length > 0) { - content += "
"; + content += "
"; for (var i = 0; i < Math.min(classes.length, 3); i++) { content += "." + classes[i] + " "; } if (classes.length > 3) { - content += "+" + (classes.length - 3) + " more"; + content += "+" + (classes.length - 3) + " more"; } content += "
"; - - pushBoxUp += 17; + pushBoxUp += 16; } - let elemBounds = this.element.getBoundingClientRect(); + // Now calculate topPos using the final pushBoxUp value + const leftPos = elemBounds.left + scrollLeft; + const topPos = (elemBounds.top - pushBoxUp < 0 + ? elemBounds.top + elemBounds.height + 5 + : elemBounds.top - pushBoxUp) + scrollTop; + + const styles = ` + .box { + background-color: #4285F4; + color: white; + border-radius: 3px; + padding: 5px 8px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + font-size: 12px; + font-family: Arial, sans-serif; + z-index: 2147483647; + position: absolute; + left: ${leftPos}px; + top: ${topPos}px; + max-width: 300px; + box-sizing: border-box; + pointer-events: none; + } + + .tag-name { + font-weight: bold; + } + + .id-name, + .class-name { + margin-top: 3px; + } + + .exceeded-classes { + opacity: 0.8; + } + `; + + // add everything to the shadow box + shadow.innerHTML = `
${content}
`; + this._shadow = shadow; + }, + + create: function() { + this.remove(); // remove existing box if already present + this._style(); // style the box - // create the container - this.body = window.document.createElement("div"); - this.body.style.setProperty("z-index", 2147483647); - this.body.style.setProperty("position", "fixed"); - this.body.style.setProperty("left", elemBounds.left + "px"); - this.body.style.setProperty( - "top", - // if there's not enough space to show the box above the element, - // we show it below the element - (elemBounds.top - pushBoxUp < 0 ? elemBounds.top + elemBounds.height + 5 : elemBounds.top - pushBoxUp) + - "px" - ); - this.body.style.setProperty("font-size", "12px"); - this.body.style.setProperty("font-family", "Arial, sans-serif"); - - // Style the info box with a blue background - this.body.style.setProperty("background", "#4285F4"); - this.body.style.setProperty("color", "white"); - this.body.style.setProperty("border-radius", "3px"); - this.body.style.setProperty("padding", "5px 8px"); - this.body.style.setProperty("box-shadow", "0 2px 5px rgba(0,0,0,0.2)"); - this.body.style.setProperty("max-width", "300px"); - this.body.style.setProperty("pointer-events", "none"); // Make it non-interactive - - this.body.innerHTML = content; window.document.body.appendChild(this.body); }, From c2f8cadf4bf9ff43ca4a71dd0d18036cbbd8d24d Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 20 Jul 2025 14:03:23 +0530 Subject: [PATCH 023/849] fix: remove properties after setting values --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index fc57b70bff..0094b1e1e0 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -965,10 +965,10 @@ function RemoteFunctions(config) { if (event && event.target && event.target.nodeType === Node.ELEMENT_NODE && event.target.hasAttribute("data-brackets-id")) { if (event.target._originalBackgroundColor !== undefined) { event.target.style.backgroundColor = event.target._originalBackgroundColor; - delete event.target._originalBackgroundColor; } else { event.target.style.backgroundColor = ""; } + delete event.target._originalBackgroundColor; } // Remove info box when mouse leaves the element @@ -986,7 +986,6 @@ function RemoteFunctions(config) { function onClick(event) { // make sure that the feature is enabled and also the clicked element has the attribute 'data-brackets-id' if(isFlagActive && event.target.hasAttribute('data-brackets-id')) { - //console.log("event:", event); if(_nodeMoreOptionsBox) { _nodeMoreOptionsBox.remove(); _nodeMoreOptionsBox = null; @@ -996,10 +995,10 @@ function RemoteFunctions(config) { if (previouslyClickedElement) { if (previouslyClickedElement._originalOutline !== undefined) { previouslyClickedElement.style.outline = previouslyClickedElement._originalOutline; - delete previouslyClickedElement._originalOutline; } else { previouslyClickedElement.style.outline = ""; } + delete previouslyClickedElement._originalOutline; } _nodeMoreOptionsBox = new NodeMoreOptionsBox(event.target); From 25d823c82423cd568609dc5405813cc862a060ad Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 20 Jul 2025 15:46:08 +0530 Subject: [PATCH 024/849] feat: add event listener for dragging inside live preview --- .../BrowserScripts/RemoteFunctions.js | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 0094b1e1e0..13af035291 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -299,6 +299,22 @@ function RemoteFunctions(config) { _handleDeleteOptionClick(e, element); } } + + function _dragStartChores(element) { + element._originalDragOpacity = element.style.opacity; + element.style.opacity = 0.3; + } + + + function _dragEndChores(element) { + if (element._originalDragOpacity) { + element.style.opacity = element._originalDragOpacity; + } else { + element.style.opacity = 1; + } + delete element._originalDragOpacity; + } + /** * This is for the advanced DOM options that appears when a DOM element is clicked * advanced options like: 'select parent', 'duplicate', 'delete' @@ -310,6 +326,36 @@ function RemoteFunctions(config) { } NodeMoreOptionsBox.prototype = { + _registerDragDrop: function() { + this.element.setAttribute("draggable", true); + + this.element.addEventListener("dragstart", (event) => { + event.stopPropagation(); + event.dataTransfer.setData("text/plain", this.element.getAttribute("data-brackets-id")); + _dragStartChores(this.element); + console.log("pluto- dragstart: ", this.element.getAttribute("data-brackets-id")); + }); + + this.element.addEventListener("dragover", (event) => { + event.preventDefault(); + event.stopPropagation(); + console.log("pluto- dragover"); + }); + + this.element.addEventListener("dragend", (event) => { + event.preventDefault(); + event.stopPropagation(); + _dragEndChores(this.element); + console.log("pluto- dragend"); + }); + + this.element.addEventListener("drop", (event) => { + event.preventDefault(); + event.stopPropagation(); + console.log("pluto- drop"); + }); + }, + _style: function() { this.body = window.document.createElement("div"); @@ -425,6 +471,8 @@ function RemoteFunctions(config) { this.remove(); }); }); + + this._registerDragDrop(); }, remove: function() { From 29df4b8476c1439af295c49eb8389bb59a4ba4b0 Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 21 Jul 2025 01:22:47 +0530 Subject: [PATCH 025/849] feat: implement live preview edit text functionality --- .../BrowserScripts/RemoteFunctions.js | 18 +++---- src/LiveDevelopment/livePreviewEdit.js | 49 +++++++++++++++++-- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 13af035291..ff97e3c29b 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1553,16 +1553,14 @@ function RemoteFunctions(config) { // If content has changed, send the edit to the editor if (newContent !== element._originalContent && element.hasAttribute("data-brackets-id")) { const tagId = element.getAttribute("data-brackets-id"); - - // Create a text edit operation - // const edit = { - // type: "textReplace", - // parentID: element.parentNode.getAttribute("data-brackets-id"), - // tagID: tagId, - // content: newContent - // }; - // - // todo: send the edited text to phoenix to change in text editor + window._Brackets_MessageBroker.send({ + livePreviewEditEnabled: true, + element: element, + oldContent: element._originalContent, + newContent: newContent, + tagId: Number(tagId), + livePreviewTextEdit: true + }); } // Clean up diff --git a/src/LiveDevelopment/livePreviewEdit.js b/src/LiveDevelopment/livePreviewEdit.js index 5503041aad..b6952c79eb 100644 --- a/src/LiveDevelopment/livePreviewEdit.js +++ b/src/LiveDevelopment/livePreviewEdit.js @@ -2,6 +2,46 @@ define(function (require, exports, module) { const EditorManager = require("editor/EditorManager"); const HTMLInstrumentation = require("LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation"); + /** + * this function handles the text edit in the source code when user updates the text in the live preview + * @param {Object} message - the message object + * { + * livePreviewEditEnabled: true, + element: the DOM element that was modified, + oldContent: the text that was present before the edit, + newContent: the new text, + tagId: data-brackets-id of the DOM element, + livePreviewTextEdit: true + } + * + * The logic is: get the text in the editor using the tagId. split that text using the old content + * join the text back and add the new content in between + */ + function _editTextInSource(message) { + const editor = EditorManager.getActiveEditor(); + if (!editor || !message.tagId) { + return; + } + + // this will give us the start pos and end pos of the DOM element in the source code + // can be referenced using range.from and range.to + const range = HTMLInstrumentation.getPositionFromTagId(editor, message.tagId); + if (!range) { + return; + } + + // this is the actual source code for the element that we need to duplicate + const text = editor.getTextBetween(range.from, range.to); + // split the text as we want to remove the old content from the source code + // for ex: if we have

hello

then splitting from hello will give us [

,

] + const splittedText = text.split(message.oldContent); + + // so now we just merge the whole thing back replacing the old content with the new one + const finalText = splittedText[0] + message.newContent + splittedText[1]; + + editor.replaceRange(finalText, range.from, range.to); + } + /** * This function is responsible to duplicate an element from the source code * @param {Number} tagId - the data-brackets-id of the DOM element @@ -67,13 +107,10 @@ define(function (require, exports, module) { * this object will be in the format * { livePreviewEditEnabled: true, - element: element, - event: event, tagId: tagId, - delete: true + delete || duplicate || livePreviewTextEdit: true } - * here element is the actual DOM element that is clicked, and tagId is the data-brackets-id - * and 'delete: true is just an example, it might be 'duplicate' or 'select-parent' also + * these are the main properties that are passed through the message */ function handleLivePreviewEditOperation(message) { if (!message.element || !message.tagId) { @@ -85,6 +122,8 @@ define(function (require, exports, module) { _deleteElementInSourceByTagId(message.tagId); } else if (message.duplicate) { _duplicateElementInSourceByTagId(message.tagId); + } else if (message.livePreviewTextEdit) { + _editTextInSource(message); } } From db02790cf3f93652de4c7e0c947f986ae88e11e3 Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 21 Jul 2025 12:42:08 +0530 Subject: [PATCH 026/849] fix: duplication issue when there is some content before the element --- src/LiveDevelopment/livePreviewEdit.js | 34 +++++++++++++++----------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/LiveDevelopment/livePreviewEdit.js b/src/LiveDevelopment/livePreviewEdit.js index b6952c79eb..277589a861 100644 --- a/src/LiveDevelopment/livePreviewEdit.js +++ b/src/LiveDevelopment/livePreviewEdit.js @@ -62,20 +62,26 @@ define(function (require, exports, module) { // this is the actual source code for the element that we need to duplicate const text = editor.getTextBetween(range.from, range.to); // this is the indentation on the line - const indent = editor.getTextBetween({line: range.from.line, ch: 0}, range.from); - - // this is the position where we need to insert - // we're giving the char as 0 because since we insert a new line using '\n' - // that's why writing any char value will not work, as the line is empty - // and codemirror doesn't allow to insert at a column (ch) greater than the length of the line - // So, the logic is to just append the indent before the text at this insertPos - const insertPos = { - line: range.from.line + (range.to.line - range.from.line + 1), - ch: 0 - }; - - editor.replaceRange('\n', range.to); - editor.replaceRange(indent + text, insertPos); + const indent = editor.getTextBetween({ line: range.from.line, ch: 0 }, range.from); + + // make sure there is only indentation and no text before it + if (indent.trim() === "") { + // this is the position where we need to insert + // we're giving the char as 0 because since we insert a new line using '\n' + // that's why writing any char value will not work, as the line is empty + // and codemirror doesn't allow to insert at a column (ch) greater than the length of the line + // So, the logic is to just append the indent before the text at this insertPos + const insertPos = { + line: range.from.line + (range.to.line - range.from.line + 1), + ch: 0 + }; + + editor.replaceRange("\n", range.to); + editor.replaceRange(indent + text, insertPos); + } else { + // if there is some text, we just add the duplicated text right next to it + editor.replaceRange(text, range.from); + } } /** From 1794526da5174c3e8a961a8b82af0a83acf4e0c5 Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 21 Jul 2025 13:23:49 +0530 Subject: [PATCH 027/849] fix: disbale live preview edit for html & body tags --- .../BrowserScripts/RemoteFunctions.js | 21 +++++++++++++++---- .../protocol/LiveDevProtocol.js | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index ff97e3c29b..9ba394b139 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -227,7 +227,8 @@ function RemoteFunctions(config) { */ function _handleDeleteOptionClick(event, element) { const tagId = element.getAttribute("data-brackets-id"); - if (tagId) { + + if (tagId && element.tagName !== "BODY" && element.tagName !== "HTML") { window._Brackets_MessageBroker.send({ livePreviewEditEnabled: true, element: element, @@ -235,6 +236,8 @@ function RemoteFunctions(config) { tagId: Number(tagId), delete: true }); + } else { + console.error("The TagID might be unavailable or the element tag is directly body or html"); } } @@ -245,7 +248,8 @@ function RemoteFunctions(config) { */ function _handleDuplicateOptionClick(event, element) { const tagId = element.getAttribute("data-brackets-id"); - if (tagId) { + + if (tagId && element.tagName !== "BODY" && element.tagName !== "HTML") { window._Brackets_MessageBroker.send({ livePreviewEditEnabled: true, element: element, @@ -253,6 +257,8 @@ function RemoteFunctions(config) { tagId: Number(tagId), duplicate: true }); + } else { + console.error("The TagID might be unavailable or the element tag is directly body or html"); } } @@ -280,6 +286,8 @@ function RemoteFunctions(config) { parentElement.hasAttribute("data-brackets-id") ) { parentElement.click(); + } else { + console.error("The TagID might be unavailable or the parent element tag is directly body or html"); } } @@ -1033,8 +1041,13 @@ function RemoteFunctions(config) { */ function onClick(event) { // make sure that the feature is enabled and also the clicked element has the attribute 'data-brackets-id' - if(isFlagActive && event.target.hasAttribute('data-brackets-id')) { - if(_nodeMoreOptionsBox) { + if ( + isFlagActive && + event.target.hasAttribute("data-brackets-id") && + event.target.tagName !== "BODY" && + event.target.tagName !== "HTML" + ) { + if (_nodeMoreOptionsBox) { _nodeMoreOptionsBox.remove(); _nodeMoreOptionsBox = null; } diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index 67aa80f5d5..b3206978ca 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -53,7 +53,7 @@ define(function (require, exports, module) { StringUtils = require("utils/StringUtils"), FileViewController = require("project/FileViewController"), MainViewManager = require("view/MainViewManager"), - LivePreviewEdit = require("LiveDevelopment/livePreviewEdit"); + LivePreviewEdit = require("LiveDevelopment/LivePreviewEdit"); const LIVE_DEV_REMOTE_SCRIPTS_FILE_NAME = `phoenix_live_preview_scripts_instrumented_${StringUtils.randomString(8)}.js`; const LIVE_DEV_REMOTE_WORKER_SCRIPTS_FILE_NAME = `pageLoaderWorker_${StringUtils.randomString(8)}.js`; From d4b0e201aa1fdb36ca77ab4ef1af9ed2b061074c Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 21 Jul 2025 15:30:00 +0530 Subject: [PATCH 028/849] fix: show more options box at the correct position when live preview panel is resized --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 9ba394b139..3957a8ffe2 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1164,6 +1164,11 @@ function RemoteFunctions(config) { if (_hoverHighlight) { _hoverHighlight.redraw(); } + if (_nodeMoreOptionsBox) { + const element = _nodeMoreOptionsBox.element; + _nodeMoreOptionsBox.remove(); + _nodeMoreOptionsBox = new NodeMoreOptionsBox(element); + } } window.addEventListener("resize", redrawHighlights); From 216c83d3f7aa9f2b8f460dadff8b5b51ab5a7d0e Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 21 Jul 2025 23:52:21 +0530 Subject: [PATCH 029/849] fix: bold italic type of tags break inside live preview and leaks internal attributes --- .../BrowserScripts/RemoteFunctions.js | 54 ++++++++++++++++++- src/LiveDevelopment/livePreviewEdit.js | 17 ++++-- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 3957a8ffe2..b7d1212184 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1520,6 +1520,7 @@ function RemoteFunctions(config) { // Save the original content for potential cancellation element._originalContent = element.innerHTML; + element._originalTextContent = element.textContent; // Add event listeners for editing function onBlur() { @@ -1549,6 +1550,53 @@ function RemoteFunctions(config) { }; } + // this function is to remove the properties from elements before getting the innerHTML + // then add all the properties back to the elements + function cleanupElementProperties(element) { + // a temporary container to hold a clean copy + const tempContainer = document.createElement('div'); + tempContainer.innerHTML = element.innerHTML; + + // get all elements in the temporary container + const allElements = tempContainer.querySelectorAll('*'); + + // Store original attributes for later restoration + const originalAttributes = new Map(); + const elementsInOriginal = element.querySelectorAll('*'); + + // Save original attributes + elementsInOriginal.forEach((el, index) => { + const attrs = {}; + for (let i = 0; i < el.attributes.length; i++) { + const attr = el.attributes[i]; + attrs[attr.name] = attr.value; + } + originalAttributes.set(index, attrs); + }); + + // Remove all attributes from elements in the temp container + allElements.forEach(el => { + while (el.attributes.length > 0) { + el.removeAttribute(el.attributes[0].name); + } + }); + + // Get the clean HTML content + const cleanContent = tempContainer.innerHTML; + + // Restore original attributes to the actual elements + elementsInOriginal.forEach((el, index) => { + if (originalAttributes.has(index)) { + const attrs = originalAttributes.get(index); + for (const [name, value] of Object.entries(attrs)) { + el.setAttribute(name, value); + } + } + }); + + return cleanContent; + } + // Function to finish editing and apply changes function finishEditing(element) { if (!element || !element.hasAttribute("contenteditable")) { @@ -1565,8 +1613,8 @@ function RemoteFunctions(config) { delete element._editListeners; } - // Get the new content - const newContent = element.innerHTML; + // Get the new content after cleaning up unwanted properties + const newContent = cleanupElementProperties(element); // If content has changed, send the edit to the editor if (newContent !== element._originalContent && element.hasAttribute("data-brackets-id")) { @@ -1575,6 +1623,7 @@ function RemoteFunctions(config) { livePreviewEditEnabled: true, element: element, oldContent: element._originalContent, + oldTextContent: element._originalTextContent, newContent: newContent, tagId: Number(tagId), livePreviewTextEdit: true @@ -1583,6 +1632,7 @@ function RemoteFunctions(config) { // Clean up delete element._originalContent; + delete element._originalTextContent; } // init diff --git a/src/LiveDevelopment/livePreviewEdit.js b/src/LiveDevelopment/livePreviewEdit.js index 277589a861..8929fbc97f 100644 --- a/src/LiveDevelopment/livePreviewEdit.js +++ b/src/LiveDevelopment/livePreviewEdit.js @@ -32,14 +32,21 @@ define(function (require, exports, module) { // this is the actual source code for the element that we need to duplicate const text = editor.getTextBetween(range.from, range.to); + // remove the , and tags from the text, + // this is done because we split the text using the textContent and not the innerHTML + // and textContent doesn't have all these tags + const cleanedText = text.replace(/<\/?(b|i|u)>/gi, ""); + // split the text as we want to remove the old content from the source code // for ex: if we have

hello

then splitting from hello will give us [

,

] - const splittedText = text.split(message.oldContent); - - // so now we just merge the whole thing back replacing the old content with the new one - const finalText = splittedText[0] + message.newContent + splittedText[1]; + const splittedText = cleanedText.split(message.oldTextContent); - editor.replaceRange(finalText, range.from, range.to); + // make sure that the split was successful + if (splittedText.length === 2) { + // so now we just merge the whole thing back replacing the old text content with the new one + const finalText = splittedText[0] + message.newContent + splittedText[1]; + editor.replaceRange(finalText, range.from, range.to); + } } /** From d37c0d37ac7e88fa00a72bc29ac479b712fa2f09 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 22 Jul 2025 00:18:22 +0530 Subject: [PATCH 030/849] fix: use current live doc instead of active editor --- src/LiveDevelopment/livePreviewEdit.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/LiveDevelopment/livePreviewEdit.js b/src/LiveDevelopment/livePreviewEdit.js index 8929fbc97f..e097d68864 100644 --- a/src/LiveDevelopment/livePreviewEdit.js +++ b/src/LiveDevelopment/livePreviewEdit.js @@ -1,6 +1,6 @@ define(function (require, exports, module) { - const EditorManager = require("editor/EditorManager"); const HTMLInstrumentation = require("LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation"); + const LiveDevMultiBrowser = require("LiveDevelopment/LiveDevMultiBrowser"); /** * this function handles the text edit in the source code when user updates the text in the live preview @@ -18,7 +18,13 @@ define(function (require, exports, module) { * join the text back and add the new content in between */ function _editTextInSource(message) { - const editor = EditorManager.getActiveEditor(); + // this is to get the currently live document that is being served in the live preview + const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); + if(!currLiveDoc) { + return; + } + + const editor = currLiveDoc.editor; if (!editor || !message.tagId) { return; } @@ -54,7 +60,13 @@ define(function (require, exports, module) { * @param {Number} tagId - the data-brackets-id of the DOM element */ function _duplicateElementInSourceByTagId(tagId) { - const editor = EditorManager.getActiveEditor(); + // this is to get the currently live document that is being served in the live preview + const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); + if(!currLiveDoc) { + return; + } + + const editor = currLiveDoc.editor; if (!editor || !tagId) { return; } @@ -96,7 +108,13 @@ define(function (require, exports, module) { * @param {Number} tagId - the data-brackets-id of the DOM element */ function _deleteElementInSourceByTagId(tagId) { - const editor = EditorManager.getActiveEditor(); + // this is to get the currently live document that is being served in the live preview + const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); + if(!currLiveDoc) { + return; + } + + const editor = currLiveDoc.editor; if (!editor || !tagId) { return; } From 186c5ae28eb457e634bf983062d56b3523f03ab6 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 22 Jul 2025 13:57:42 +0530 Subject: [PATCH 031/849] fix: editing text inside live preview leaks phoenix internal attributes --- .../BrowserScripts/LiveDevProtocolRemote.js | 2 +- .../BrowserScripts/RemoteFunctions.js | 55 +++++-------------- src/LiveDevelopment/livePreviewEdit.js | 53 +++++++++++------- 3 files changed, 47 insertions(+), 63 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js index 74e007de0b..551ae1ac04 100644 --- a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js +++ b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js @@ -395,7 +395,7 @@ var element = event.target; if (element && element.hasAttribute('data-brackets-id')) { // Check if it's a double-click for direct editing - if (event.detail === 2 && element.textContent && !['INPUT', 'TEXTAREA', 'SELECT'].includes(element.tagName)) { + if (event.detail === 2 && !['INPUT', 'TEXTAREA', 'SELECT'].includes(element.tagName)) { // Double-click detected, enable direct editing // Make the element editable if (window._LD && window._LD.DOMEditHandler) { diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index b7d1212184..2ec2f6903d 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1518,9 +1518,7 @@ function RemoteFunctions(config) { element.setAttribute("contenteditable", "true"); element.focus(); - // Save the original content for potential cancellation element._originalContent = element.innerHTML; - element._originalTextContent = element.textContent; // Add event listeners for editing function onBlur() { @@ -1550,51 +1548,29 @@ function RemoteFunctions(config) { }; } - // this function is to remove the properties from elements before getting the innerHTML + // this function is to remove the internal properties from elements before getting the innerHTML // then add all the properties back to the elements + // internal properties such as 'data-brackets-id', 'data-ld-highlight' etc function cleanupElementProperties(element) { - // a temporary container to hold a clean copy - const tempContainer = document.createElement('div'); - tempContainer.innerHTML = element.innerHTML; - - // get all elements in the temporary container - const allElements = tempContainer.querySelectorAll('*'); - - // Store original attributes for later restoration - const originalAttributes = new Map(); - const elementsInOriginal = element.querySelectorAll('*'); - - // Save original attributes - elementsInOriginal.forEach((el, index) => { - const attrs = {}; - for (let i = 0; i < el.attributes.length; i++) { - const attr = el.attributes[i]; - attrs[attr.name] = attr.value; - } - originalAttributes.set(index, attrs); - }); + const clone = element.cloneNode(true); + const allElements = [clone, ...clone.querySelectorAll('*')]; - // Remove all attributes from elements in the temp container allElements.forEach(el => { - while (el.attributes.length > 0) { - el.removeAttribute(el.attributes[0].name); + // Remove Phoenix internal attributes + if (el.hasAttribute('data-brackets-id')) { + el.removeAttribute('data-brackets-id'); + } + if (el.hasAttribute('data-ld-highlight')) { + el.removeAttribute('data-ld-highlight'); } - }); - - // Get the clean HTML content - const cleanContent = tempContainer.innerHTML; - // Restore original attributes to the actual elements - elementsInOriginal.forEach((el, index) => { - if (originalAttributes.has(index)) { - const attrs = originalAttributes.get(index); - for (const [name, value] of Object.entries(attrs)) { - el.setAttribute(name, value); - } + // Remove empty style attribute + if (el.hasAttribute('style') && el.getAttribute('style').trim() === '') { + el.removeAttribute('style'); } }); - return cleanContent; + return clone.innerHTML; } // Function to finish editing and apply changes @@ -1622,8 +1598,6 @@ function RemoteFunctions(config) { window._Brackets_MessageBroker.send({ livePreviewEditEnabled: true, element: element, - oldContent: element._originalContent, - oldTextContent: element._originalTextContent, newContent: newContent, tagId: Number(tagId), livePreviewTextEdit: true @@ -1632,7 +1606,6 @@ function RemoteFunctions(config) { // Clean up delete element._originalContent; - delete element._originalTextContent; } // init diff --git a/src/LiveDevelopment/livePreviewEdit.js b/src/LiveDevelopment/livePreviewEdit.js index e097d68864..4594117ba8 100644 --- a/src/LiveDevelopment/livePreviewEdit.js +++ b/src/LiveDevelopment/livePreviewEdit.js @@ -2,6 +2,25 @@ define(function (require, exports, module) { const HTMLInstrumentation = require("LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation"); const LiveDevMultiBrowser = require("LiveDevelopment/LiveDevMultiBrowser"); + /** + * this is a helper function to find the content boundaries in HTML + * @param {string} html - The HTML string to parse + * @return {Object} - Object with openTag and closeTag properties + */ + function _findContentBoundaries(html) { + const openTagEnd = html.indexOf('>') + 1; + const closeTagStart = html.lastIndexOf('<'); + + if (openTagEnd > 0 && closeTagStart > openTagEnd) { + return { + openTag: html.substring(0, openTagEnd), + closeTag: html.substring(closeTagStart) + }; + } + + return null; + } + /** * this function handles the text edit in the source code when user updates the text in the live preview * @param {Object} message - the message object @@ -18,40 +37,32 @@ define(function (require, exports, module) { * join the text back and add the new content in between */ function _editTextInSource(message) { - // this is to get the currently live document that is being served in the live preview const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); - if(!currLiveDoc) { + if(!currLiveDoc || !currLiveDoc.editor || !message.tagId) { return; } const editor = currLiveDoc.editor; - if (!editor || !message.tagId) { - return; - } - - // this will give us the start pos and end pos of the DOM element in the source code - // can be referenced using range.from and range.to const range = HTMLInstrumentation.getPositionFromTagId(editor, message.tagId); if (!range) { return; } - // this is the actual source code for the element that we need to duplicate const text = editor.getTextBetween(range.from, range.to); - // remove the , and tags from the text, - // this is done because we split the text using the textContent and not the innerHTML - // and textContent doesn't have all these tags - const cleanedText = text.replace(/<\/?(b|i|u)>/gi, ""); - - // split the text as we want to remove the old content from the source code - // for ex: if we have

hello

then splitting from hello will give us [

,

] - const splittedText = cleanedText.split(message.oldTextContent); - - // make sure that the split was successful - if (splittedText.length === 2) { - // so now we just merge the whole thing back replacing the old text content with the new one + let splittedText; + + // we need to find the content boundaries to find exactly where the content starts and where it ends + const boundaries = _findContentBoundaries(text); + if (boundaries) { + splittedText = [boundaries.openTag, boundaries.closeTag]; + } + + // if the text split was done successfully, apply the edit + if (splittedText && splittedText.length === 2) { const finalText = splittedText[0] + message.newContent + splittedText[1]; editor.replaceRange(finalText, range.from, range.to); + } else { + console.error("Live preview text edit operation failed."); } } From baae73607d0926f91ee026f3189d4e8a62015c8a Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 22 Jul 2025 14:03:21 +0530 Subject: [PATCH 032/849] fix: file rename to make it consistent --- src/LiveDevelopment/{livePreviewEdit.js => LivePreviewEditt.js} | 0 .../MultiBrowserImpl/protocol/LiveDevProtocol.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/LiveDevelopment/{livePreviewEdit.js => LivePreviewEditt.js} (100%) diff --git a/src/LiveDevelopment/livePreviewEdit.js b/src/LiveDevelopment/LivePreviewEditt.js similarity index 100% rename from src/LiveDevelopment/livePreviewEdit.js rename to src/LiveDevelopment/LivePreviewEditt.js diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index b3206978ca..d47fea466f 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -53,7 +53,7 @@ define(function (require, exports, module) { StringUtils = require("utils/StringUtils"), FileViewController = require("project/FileViewController"), MainViewManager = require("view/MainViewManager"), - LivePreviewEdit = require("LiveDevelopment/LivePreviewEdit"); + LivePreviewEdit = require("LiveDevelopment/LivePreviewEditt"); const LIVE_DEV_REMOTE_SCRIPTS_FILE_NAME = `phoenix_live_preview_scripts_instrumented_${StringUtils.randomString(8)}.js`; const LIVE_DEV_REMOTE_WORKER_SCRIPTS_FILE_NAME = `pageLoaderWorker_${StringUtils.randomString(8)}.js`; From 1159d1a2be7197736189bfd60793d47f1f5564c7 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 22 Jul 2025 14:04:08 +0530 Subject: [PATCH 033/849] fix: typo in file name --- src/LiveDevelopment/{LivePreviewEditt.js => LivePreviewEdit.js} | 0 .../MultiBrowserImpl/protocol/LiveDevProtocol.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/LiveDevelopment/{LivePreviewEditt.js => LivePreviewEdit.js} (100%) diff --git a/src/LiveDevelopment/LivePreviewEditt.js b/src/LiveDevelopment/LivePreviewEdit.js similarity index 100% rename from src/LiveDevelopment/LivePreviewEditt.js rename to src/LiveDevelopment/LivePreviewEdit.js diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index d47fea466f..b3206978ca 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -53,7 +53,7 @@ define(function (require, exports, module) { StringUtils = require("utils/StringUtils"), FileViewController = require("project/FileViewController"), MainViewManager = require("view/MainViewManager"), - LivePreviewEdit = require("LiveDevelopment/LivePreviewEditt"); + LivePreviewEdit = require("LiveDevelopment/LivePreviewEdit"); const LIVE_DEV_REMOTE_SCRIPTS_FILE_NAME = `phoenix_live_preview_scripts_instrumented_${StringUtils.randomString(8)}.js`; const LIVE_DEV_REMOTE_WORKER_SCRIPTS_FILE_NAME = `pageLoaderWorker_${StringUtils.randomString(8)}.js`; From ebc68ae20ce12991ade261348a448ab391c4aefd Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 22 Jul 2025 15:35:23 +0530 Subject: [PATCH 034/849] fix: show info box along with more option box when a DOM element is clicked --- .../BrowserScripts/RemoteFunctions.js | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 2ec2f6903d..c98d97ff8f 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1047,6 +1047,7 @@ function RemoteFunctions(config) { event.target.tagName !== "BODY" && event.target.tagName !== "HTML" ) { + event.stopPropagation(); if (_nodeMoreOptionsBox) { _nodeMoreOptionsBox.remove(); _nodeMoreOptionsBox = null; @@ -1063,6 +1064,13 @@ function RemoteFunctions(config) { } _nodeMoreOptionsBox = new NodeMoreOptionsBox(event.target); + + // show the info box when a DOM element is clicked + if (_nodeInfoBox) { + _nodeInfoBox.remove(); + } + _nodeInfoBox = new NodeInfoBox(event.target); + event.target._originalOutline = event.target.style.outline; event.target.style.outline = "1px solid #4285F4"; previouslyClickedElement = event.target; // add the current element to the previouslyClickedElement @@ -1122,10 +1130,6 @@ function RemoteFunctions(config) { if (_hoverHighlight) { _hoverHighlight.clear(); } - if (_nodeInfoBox) { - _nodeInfoBox.remove(); - _nodeInfoBox = null; - } } // highlight a node @@ -1168,6 +1172,11 @@ function RemoteFunctions(config) { const element = _nodeMoreOptionsBox.element; _nodeMoreOptionsBox.remove(); _nodeMoreOptionsBox = new NodeMoreOptionsBox(element); + + if (_nodeInfoBox) { + _nodeInfoBox.remove(); + _nodeInfoBox = new NodeInfoBox(element); + } } } @@ -1499,11 +1508,15 @@ function RemoteFunctions(config) { window.document.removeEventListener("mouseover", onElementHover); window.document.removeEventListener("mouseout", onElementHoverOut); - // Remove info box if highlight is disabled + // Remove info box and more options box if highlight is disabled if (_nodeInfoBox) { _nodeInfoBox.remove(); _nodeInfoBox = null; } + if (_nodeMoreOptionsBox) { + _nodeMoreOptionsBox.remove(); + _nodeMoreOptionsBox = null; + } } return JSON.stringify(config); } From 3fb717564a6ea1d9dfce472b9b01114751bf2924 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 23 Jul 2025 14:26:14 +0530 Subject: [PATCH 035/849] fix: overlapping issue between the info box and the more options box --- .../BrowserScripts/RemoteFunctions.js | 101 +++++++++++++++--- 1 file changed, 89 insertions(+), 12 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index c98d97ff8f..f75a5b3bf0 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -323,6 +323,22 @@ function RemoteFunctions(config) { delete element._originalDragOpacity; } + function checkOverlap(elemWidth, id, classes) { + if(elemWidth > 280) { + return false; + } + if(classes.length >= 3 && elemWidth <= 280) { + return true; + } + if((id || classes.length <= 2) && elemWidth <= 250) { + return true; + } + if(elemWidth <= 180) { + return true; + } + return false; + } + /** * This is for the advanced DOM options that appears when a DOM element is clicked * advanced options like: 'select parent', 'duplicate', 'delete' @@ -380,10 +396,34 @@ function RemoteFunctions(config) { const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; const scrollTop = window.pageYOffset || document.documentElement.scrollTop; - const leftPos = elemBounds.right - boxWidth + scrollLeft; - const topPos = (elemBounds.top - 30 < 0 - ? elemBounds.top + elemBounds.height + 5 - : elemBounds.top - 30) + scrollTop; + // get the ID and classes for the element + // we need this to check for overlap issue between the info box and this box + // because when we have classes and ids then the info box tends to stretch in width + const id = this.element.id; + const classes = this.element.className ? this.element.className.split(/\s+/).filter(Boolean) : []; + + const isOverlap = checkOverlap(elemBounds.width, id, classes); + + // default position (right aligned with element) + let leftPos = elemBounds.right - boxWidth + scrollLeft; + + // this will be calculated based on whether we have overlap issue or not + let topPos; + + if (isOverlap) { + if (elemBounds.top > 40) { // check if there's enough space at the top + // place at the top + topPos = elemBounds.top - 30 + scrollTop; + } else { + // at the bottom + topPos = elemBounds.top + elemBounds.height + 5 + scrollTop; + } + } else { + // no overlap, so it comes just above the element + topPos = (elemBounds.top - 30 < 0 + ? elemBounds.top + elemBounds.height + 5 + : elemBounds.top - 30) + scrollTop; + } // the icons that is displayed in the box const ICONS = { @@ -492,8 +532,9 @@ function RemoteFunctions(config) { }; // Node info box to display DOM node ID and classes on hover - function NodeInfoBox(element) { + function NodeInfoBox(element, isFromClick) { this.element = element; + this.isFromClick = isFromClick || false; this.remove = this.remove.bind(this); this.create(); } @@ -545,12 +586,45 @@ function RemoteFunctions(config) { pushBoxUp += 16; } - // Now calculate topPos using the final pushBoxUp value - const leftPos = elemBounds.left + scrollLeft; - const topPos = (elemBounds.top - pushBoxUp < 0 + let leftPos = elemBounds.left + scrollLeft; + let topPos = (elemBounds.top - pushBoxUp < 0 ? elemBounds.top + elemBounds.height + 5 : elemBounds.top - pushBoxUp) + scrollTop; + // we need to check for overlap if this is from a click + if (this.isFromClick) { + const isOverlap = checkOverlap(elemBounds.width, id, classes); + + if (isOverlap) { + const windowWidth = window.innerWidth; + const boxWidth = 300; // max-width of the box + + // Estimate the height of the info box based on its content + // base height for tag name + padding + let estimatedHeight = 20; + + // height adjustment if ID is present + if (id) { + estimatedHeight += 15; + } + + // height adjustment if classes are present + if (classes.length > 0) { + estimatedHeight += 15; + } + + // align with the bottom of the info box + topPos = (elemBounds.top + elemBounds.height - estimatedHeight) + scrollTop; + + // decide whether position at left or right + if (elemBounds.left > boxWidth + 10) { + leftPos = elemBounds.left - boxWidth - 10 + scrollLeft; + } else if (windowWidth - elemBounds.right > boxWidth + 10) { + leftPos = elemBounds.right + 10 + scrollLeft; + } + } + } + const styles = ` .box { background-color: #4285F4; @@ -1007,7 +1081,10 @@ function RemoteFunctions(config) { if (_nodeInfoBox) { _nodeInfoBox.remove(); } - _nodeInfoBox = new NodeInfoBox(event.target); + // check if this element is already clicked (has more options box) + // this is needed so that we can check for overlapping issue among the boxes + const isAlreadyClicked = previouslyClickedElement === event.target && _nodeMoreOptionsBox !== null; + _nodeInfoBox = new NodeInfoBox(event.target, isAlreadyClicked); } } } @@ -1069,11 +1146,11 @@ function RemoteFunctions(config) { if (_nodeInfoBox) { _nodeInfoBox.remove(); } - _nodeInfoBox = new NodeInfoBox(event.target); + _nodeInfoBox = new NodeInfoBox(event.target, true); // true means that the element was clicked event.target._originalOutline = event.target.style.outline; event.target.style.outline = "1px solid #4285F4"; - previouslyClickedElement = event.target; // add the current element to the previouslyClickedElement + previouslyClickedElement = event.target; } } @@ -1175,7 +1252,7 @@ function RemoteFunctions(config) { if (_nodeInfoBox) { _nodeInfoBox.remove(); - _nodeInfoBox = new NodeInfoBox(element); + _nodeInfoBox = new NodeInfoBox(element, true); // true means it came from a click } } } From d222ea63dbec713832dccaf88593d08aeadcbff8 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 23 Jul 2025 15:33:27 +0530 Subject: [PATCH 036/849] fix: calculate info box width based on char count --- .../BrowserScripts/RemoteFunctions.js | 92 +++++++++++++++---- 1 file changed, 76 insertions(+), 16 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index f75a5b3bf0..4b08b9932a 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -323,20 +323,65 @@ function RemoteFunctions(config) { delete element._originalDragOpacity; } - function checkOverlap(elemWidth, id, classes) { - if(elemWidth > 280) { - return false; - } - if(classes.length >= 3 && elemWidth <= 280) { - return true; + /** + * This function is to calculate the width of the info box based on the number of chars in the box + * @param {String} tagName - the element's tag name + * @param {String} id - the element's id + * @param {Array} classes - the array of class names + * @returns {Number} - the total char count + */ + function _calculateInfoBoxCharCount(tagName, id, classes) { + // char count for tag name + let charCount = tagName.length; + + // char count for id + if (id) { + charCount += id.length + 1; // +1 for # } - if((id || classes.length <= 2) && elemWidth <= 250) { - return true; + + // char count for classes + if (classes.length > 0) { + for (let i = 0; i < Math.min(classes.length, 3); i++) { + charCount += classes[i].length + 1; // +1 for . + } + + if (classes.length > 3) { + // "+ X more" for more than 3 classes + const moreText = `+${classes.length - 3} more`; + charCount += moreText.length; + } } - if(elemWidth <= 180) { - return true; + return charCount; + } + + /** + * This function checks whether there is overlap between the info and the more options box + * @param {Number} elemWidth - the width of the DOM element + * @param {String} tagName - the element's tag name + * @param {String} id - the element's id + * @param {Array} classes - the array of class names + * @returns {Number} - the total char count + */ + function checkOverlap(elemWidth, tagName, id, classes) { + const avgCharWidth = 7; + const basePadding = 16; + + // char count for tag name, id, and classes + let charCount = _calculateInfoBoxCharCount(tagName, id, classes); + + // calc estimate width based on the char count + const infoBoxWidth = basePadding + (charCount * avgCharWidth); + + // more options box is 82px + const moreOptionsBoxWidth = 82; + + // check if there's enough space for both boxes + // 20px buffer for spacing between boxes + if (elemWidth > (infoBoxWidth + moreOptionsBoxWidth + 20)) { + return false; // No overlap } - return false; + + return true; } /** @@ -401,8 +446,9 @@ function RemoteFunctions(config) { // because when we have classes and ids then the info box tends to stretch in width const id = this.element.id; const classes = this.element.className ? this.element.className.split(/\s+/).filter(Boolean) : []; + const tagName = this.element.tagName.toLowerCase(); - const isOverlap = checkOverlap(elemBounds.width, id, classes); + const isOverlap = checkOverlap(elemBounds.width, tagName, id, classes); // default position (right aligned with element) let leftPos = elemBounds.right - boxWidth + scrollLeft; @@ -591,13 +637,25 @@ function RemoteFunctions(config) { ? elemBounds.top + elemBounds.height + 5 : elemBounds.top - pushBoxUp) + scrollTop; + const avgCharWidth = 7; + const basePadding = 16; + + // Get the tag name + const tagName = this.element.tagName.toLowerCase(); + + // Count characters in tag name, id, and classes + let charCount = _calculateInfoBoxCharCount(tagName, id, classes); + + // Calculate estimated width based on character count + // Formula: base padding + (character count * average character width) + const boxWidth = basePadding + (charCount * avgCharWidth); + // we need to check for overlap if this is from a click if (this.isFromClick) { - const isOverlap = checkOverlap(elemBounds.width, id, classes); + const isOverlap = checkOverlap(elemBounds.width, tagName, id, classes); if (isOverlap) { const windowWidth = window.innerWidth; - const boxWidth = 300; // max-width of the box // Estimate the height of the info box based on its content // base height for tag name + padding @@ -616,10 +674,12 @@ function RemoteFunctions(config) { // align with the bottom of the info box topPos = (elemBounds.top + elemBounds.height - estimatedHeight) + scrollTop; - // decide whether position at left or right + // decide whether position at left or right based on available space + // check if there's enough space on the left side if (elemBounds.left > boxWidth + 10) { leftPos = elemBounds.left - boxWidth - 10 + scrollLeft; } else if (windowWidth - elemBounds.right > boxWidth + 10) { + // position on the right leftPos = elemBounds.right + 10 + scrollLeft; } } @@ -638,7 +698,7 @@ function RemoteFunctions(config) { position: absolute; left: ${leftPos}px; top: ${topPos}px; - max-width: 300px; + max-width: ${boxWidth}px; box-sizing: border-box; pointer-events: none; } From fdaa01a9e52e513bf75ae8961c067acb5f3913b7 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 23 Jul 2025 16:23:01 +0530 Subject: [PATCH 037/849] fix: positioning of the info box --- .../BrowserScripts/RemoteFunctions.js | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 4b08b9932a..1bac39ad52 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -332,26 +332,27 @@ function RemoteFunctions(config) { */ function _calculateInfoBoxCharCount(tagName, id, classes) { // char count for tag name - let charCount = tagName.length; - + let tagNameCharCount = tagName.length; + let idNameCharCount = 0; + let classNameCharCount = 0; // char count for id if (id) { - charCount += id.length + 1; // +1 for # + idNameCharCount = id.length + 1; // +1 for # } // char count for classes if (classes.length > 0) { for (let i = 0; i < Math.min(classes.length, 3); i++) { - charCount += classes[i].length + 1; // +1 for . + classNameCharCount += classes[i].length + 1; // +1 for . } if (classes.length > 3) { // "+ X more" for more than 3 classes const moreText = `+${classes.length - 3} more`; - charCount += moreText.length; + classNameCharCount += moreText.length; } } - return charCount; + return Math.max(tagNameCharCount, idNameCharCount, classNameCharCount); } /** @@ -363,11 +364,14 @@ function RemoteFunctions(config) { * @returns {Number} - the total char count */ function checkOverlap(elemWidth, tagName, id, classes) { - const avgCharWidth = 7; + let avgCharWidth = 6; const basePadding = 16; // char count for tag name, id, and classes let charCount = _calculateInfoBoxCharCount(tagName, id, classes); + if(charCount <= 10) { + avgCharWidth = 7.5; + } // calc estimate width based on the char count const infoBoxWidth = basePadding + (charCount * avgCharWidth); @@ -637,7 +641,7 @@ function RemoteFunctions(config) { ? elemBounds.top + elemBounds.height + 5 : elemBounds.top - pushBoxUp) + scrollTop; - const avgCharWidth = 7; + let avgCharWidth = 6; const basePadding = 16; // Get the tag name @@ -645,6 +649,9 @@ function RemoteFunctions(config) { // Count characters in tag name, id, and classes let charCount = _calculateInfoBoxCharCount(tagName, id, classes); + if(charCount <= 10) { + avgCharWidth = 7.5; + } // Calculate estimated width based on character count // Formula: base padding + (character count * average character width) @@ -698,7 +705,7 @@ function RemoteFunctions(config) { position: absolute; left: ${leftPos}px; top: ${topPos}px; - max-width: ${boxWidth}px; + max-width: fit-content; box-sizing: border-box; pointer-events: none; } From dca465f69afa869659c18c6ff96b4d1056d81b1f Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 23 Jul 2025 16:32:23 +0530 Subject: [PATCH 038/849] fix: info box getting out of bounds --- .../BrowserScripts/RemoteFunctions.js | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 1bac39ad52..00eb633d49 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -442,6 +442,7 @@ function RemoteFunctions(config) { // the box width and the positions where it should be placed const boxWidth = 82; + const boxHeight = 40; // rough estimate for options box const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; const scrollTop = window.pageYOffset || document.documentElement.scrollTop; @@ -456,8 +457,6 @@ function RemoteFunctions(config) { // default position (right aligned with element) let leftPos = elemBounds.right - boxWidth + scrollLeft; - - // this will be calculated based on whether we have overlap issue or not let topPos; if (isOverlap) { @@ -475,6 +474,25 @@ function RemoteFunctions(config) { : elemBounds.top - 30) + scrollTop; } + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + // right edge + if (leftPos + boxWidth > windowWidth) { + leftPos = Math.max(10, windowWidth - boxWidth - 10); + } + // left edge + if (leftPos < 0) { + leftPos = 10; + } + // bottom edge + if (topPos + boxHeight > windowHeight) { + topPos = Math.max(10, windowHeight - boxHeight - 10); + } + // top edge + if (topPos < 0) { + topPos = 10; + } + // the icons that is displayed in the box const ICONS = { arrowUp: ` @@ -656,6 +674,7 @@ function RemoteFunctions(config) { // Calculate estimated width based on character count // Formula: base padding + (character count * average character width) const boxWidth = basePadding + (charCount * avgCharWidth); + const boxHeight = 40 + (id ? 16 : 0) + (classes.length > 0 ? 16 : 0); // rough estimate // we need to check for overlap if this is from a click if (this.isFromClick) { @@ -692,6 +711,25 @@ function RemoteFunctions(config) { } } + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + // right edge + if (leftPos + boxWidth > windowWidth) { + leftPos = Math.max(10, windowWidth - boxWidth - 10); + } + // left edge + if (leftPos < 0) { + leftPos = 10; + } + // bottom edge + if (topPos + boxHeight > windowHeight) { + topPos = Math.max(10, windowHeight - boxHeight - 10); + } + // top edge + if (topPos < 0) { + topPos = 10; + } + const styles = ` .box { background-color: #4285F4; From 80bbb324429032eed0965a6e764ce61bf54a8b43 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 23 Jul 2025 17:30:54 +0530 Subject: [PATCH 039/849] fix: remove redundant positioning checks --- .../BrowserScripts/RemoteFunctions.js | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 00eb633d49..90c20dbae3 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -474,25 +474,6 @@ function RemoteFunctions(config) { : elemBounds.top - 30) + scrollTop; } - const windowWidth = window.innerWidth; - const windowHeight = window.innerHeight; - // right edge - if (leftPos + boxWidth > windowWidth) { - leftPos = Math.max(10, windowWidth - boxWidth - 10); - } - // left edge - if (leftPos < 0) { - leftPos = 10; - } - // bottom edge - if (topPos + boxHeight > windowHeight) { - topPos = Math.max(10, windowHeight - boxHeight - 10); - } - // top edge - if (topPos < 0) { - topPos = 10; - } - // the icons that is displayed in the box const ICONS = { arrowUp: ` @@ -711,25 +692,6 @@ function RemoteFunctions(config) { } } - const windowWidth = window.innerWidth; - const windowHeight = window.innerHeight; - // right edge - if (leftPos + boxWidth > windowWidth) { - leftPos = Math.max(10, windowWidth - boxWidth - 10); - } - // left edge - if (leftPos < 0) { - leftPos = 10; - } - // bottom edge - if (topPos + boxHeight > windowHeight) { - topPos = Math.max(10, windowHeight - boxHeight - 10); - } - // top edge - if (topPos < 0) { - topPos = 10; - } - const styles = ` .box { background-color: #4285F4; From 2e7ea12536656d2c2352338d62f41d015f8b51ac Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 23 Jul 2025 22:33:27 +0530 Subject: [PATCH 040/849] feat: enable undo in live preview to undo live preview edit operations --- .../BrowserScripts/LiveDevProtocolRemote.js | 9 +++++++++ src/LiveDevelopment/LivePreviewEdit.js | 14 +++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js index 551ae1ac04..54460e7d8e 100644 --- a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js +++ b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js @@ -439,5 +439,14 @@ } } window.document.addEventListener("click", onDocumentClick); + window.document.addEventListener("keydown", function (e) { + // for undo. refer to LivePreviewEdit.js file 'handleLivePreviewEditOperation' function + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") { + MessageBroker.send({ + livePreviewEditEnabled: true, + undoLivePreviewOperation: true + }); + } + }); }(this)); diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 4594117ba8..0c843ec26a 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -142,7 +142,8 @@ define(function (require, exports, module) { /** * This is the main function that is exported. - * it will be called by LiveDevProtocol when it receives a message from RemoteFunctions.js using MessageBroker + * it will be called by LiveDevProtocol when it receives a message from RemoteFunctions.js + * or LiveDevProtocolRemote.js (for undo) using MessageBroker * Refer to: `handleOptionClick` function in the RemoteFunctions.js and `_receive` function in LiveDevProtocol.js * * @param {Object} message - this is the object that is passed by RemoteFunctions.js using MessageBroker @@ -151,11 +152,22 @@ define(function (require, exports, module) { livePreviewEditEnabled: true, tagId: tagId, delete || duplicate || livePreviewTextEdit: true + undoLivePreviewOperation: true (this property is available only for undo operation) } * these are the main properties that are passed through the message */ function handleLivePreviewEditOperation(message) { if (!message.element || !message.tagId) { + // check for undo + if(message.undoLivePreviewOperation) { + const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); + if(!currLiveDoc || !currLiveDoc.editor) { + return; + } + + const editor = currLiveDoc.editor; + editor.undo(); + } return; } From f9df628cceb27b9cc2cce7631a54faf189fa8b3e Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 24 Jul 2025 11:40:36 +0530 Subject: [PATCH 041/849] feat: drag drop live preview implementation --- .../BrowserScripts/RemoteFunctions.js | 171 ++++++++++++++++-- src/LiveDevelopment/LivePreviewEdit.js | 68 ++++++- 2 files changed, 218 insertions(+), 21 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 90c20dbae3..0161be8c09 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -323,6 +323,157 @@ function RemoteFunctions(config) { delete element._originalDragOpacity; } + // CSS class name for drop markers + let DROP_MARKER_CLASSNAME = "__brackets-drop-marker"; + + /** + * This function creates a marker to indicate a valid drop position + * @param {DOMElement} element - The element where the drop is possible + */ + function _createDropMarker(element) { + // clean any existing marker from that element + _removeDropMarkerFromElement(element); + + // create the marker element + let marker = window.document.createElement("div"); + marker.className = DROP_MARKER_CLASSNAME; + + // position the marker at the top of the element + let rect = element.getBoundingClientRect(); + let scrollTop = window.pageYOffset || document.documentElement.scrollTop; + let scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + + // marker styling + marker.style.position = "absolute"; + marker.style.top = (rect.top + scrollTop - 5) + "px"; + marker.style.left = (rect.left + scrollLeft) + "px"; + marker.style.width = rect.width + "px"; + marker.style.height = "2px"; + marker.style.backgroundColor = "#4285F4"; + marker.style.zIndex = "2147483646"; + + element._dropMarker = marker; // we need this in the _removeDropMarkerFromElement function + window.document.body.appendChild(marker); + } + + /** + * This function removes a drop marker from a specific element + * @param {DOMElement} element - The element to remove the marker from + */ + function _removeDropMarkerFromElement(element) { + if (element._dropMarker && element._dropMarker.parentNode) { + element._dropMarker.parentNode.removeChild(element._dropMarker); + delete element._dropMarker; + } + } + + /** + * this function is to clear all the drop markers from the document + */ + function _clearDropMarkers() { + let markers = window.document.querySelectorAll("." + DROP_MARKER_CLASSNAME); + for (let i = 0; i < markers.length; i++) { + if (markers[i].parentNode) { + markers[i].parentNode.removeChild(markers[i]); + } + } + + // Also clear any element references + let elements = window.document.querySelectorAll("[data-brackets-id]"); + for (let j = 0; j < elements.length; j++) { + delete elements[j]._dropMarker; + } + } + + /** + * Handle dragover events on the document + * Shows drop markers on valid drop targets + * @param {Event} event - The dragover event + */ + function onDragOver(event) { + // we set this on dragStart + if (!window._currentDraggedElement) { + return; + } + + event.preventDefault(); + + // get the element under the cursor + let target = document.elementFromPoint(event.clientX, event.clientY); + if (!target || target === window._currentDraggedElement) { + return; + } + + // get the closest element with a data-brackets-id + while (target && !target.hasAttribute("data-brackets-id")) { + target = target.parentElement; + } + + // skip if no valid target found or if it's the dragged element + if (!target || target === window._currentDraggedElement) { + return; + } + + // Skip BODY and HTML tags + if (target.tagName === "BODY" || target.tagName === "HTML") { + return; + } + + // before creating a drop marker, make sure that we clear all the drop markers + _clearDropMarkers(); + _createDropMarker(target); + } + + /** + * Handle drop events on the document + * Processes the drop of a dragged element onto a valid target + * @param {Event} event - The drop event + */ + function onDrop(event) { + if (!window._currentDraggedElement) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + // get the element under the cursor + let target = document.elementFromPoint(event.clientX, event.clientY); + + // get the closest element with a data-brackets-id + while (target && !target.hasAttribute("data-brackets-id")) { + target = target.parentElement; + } + + // skip if no valid target found or if it's the dragged element + if (!target || target === window._currentDraggedElement) { + return; + } + + // Skip BODY and HTML tags + if (target.tagName === "BODY" || target.tagName === "HTML") { + return; + } + + // IDs of the source and target elements + const sourceId = window._currentDraggedElement.getAttribute("data-brackets-id"); + const targetId = target.getAttribute("data-brackets-id"); + + // send message to the editor + window._Brackets_MessageBroker.send({ + livePreviewEditEnabled: true, + sourceElement: window._currentDraggedElement, + targetElement: target, + sourceId: Number(sourceId), + targetId: Number(targetId), + move: true + }); + + _clearDropMarkers(); + _dragEndChores(window._currentDraggedElement); + delete window._currentDraggedElement; + } + /** * This function is to calculate the width of the info box based on the number of chars in the box * @param {String} tagName - the element's tag name @@ -406,26 +557,16 @@ function RemoteFunctions(config) { event.stopPropagation(); event.dataTransfer.setData("text/plain", this.element.getAttribute("data-brackets-id")); _dragStartChores(this.element); - console.log("pluto- dragstart: ", this.element.getAttribute("data-brackets-id")); - }); - - this.element.addEventListener("dragover", (event) => { - event.preventDefault(); - event.stopPropagation(); - console.log("pluto- dragover"); + _clearDropMarkers(); + window._currentDraggedElement = this.element; }); this.element.addEventListener("dragend", (event) => { event.preventDefault(); event.stopPropagation(); _dragEndChores(this.element); - console.log("pluto- dragend"); - }); - - this.element.addEventListener("drop", (event) => { - event.preventDefault(); - event.stopPropagation(); - console.log("pluto- drop"); + _clearDropMarkers(); + delete window._currentDraggedElement; }); }, @@ -1778,6 +1919,8 @@ function RemoteFunctions(config) { window.document.addEventListener("mouseover", onElementHover); window.document.addEventListener("mouseout", onElementHoverOut); window.document.addEventListener("click", onClick); + window.document.addEventListener("dragover", onDragOver); + window.document.addEventListener("drop", onDrop); if (experimental) { window.document.addEventListener("keydown", onKeyDown); diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 0c843ec26a..fc6fd78723 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -8,8 +8,8 @@ define(function (require, exports, module) { * @return {Object} - Object with openTag and closeTag properties */ function _findContentBoundaries(html) { - const openTagEnd = html.indexOf('>') + 1; - const closeTagStart = html.lastIndexOf('<'); + const openTagEnd = html.indexOf(">") + 1; + const closeTagStart = html.lastIndexOf("<"); if (openTagEnd > 0 && closeTagStart > openTagEnd) { return { @@ -38,7 +38,7 @@ define(function (require, exports, module) { */ function _editTextInSource(message) { const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); - if(!currLiveDoc || !currLiveDoc.editor || !message.tagId) { + if (!currLiveDoc || !currLiveDoc.editor || !message.tagId) { return; } @@ -73,7 +73,7 @@ define(function (require, exports, module) { function _duplicateElementInSourceByTagId(tagId) { // this is to get the currently live document that is being served in the live preview const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); - if(!currLiveDoc) { + if (!currLiveDoc) { return; } @@ -121,7 +121,7 @@ define(function (require, exports, module) { function _deleteElementInSourceByTagId(tagId) { // this is to get the currently live document that is being served in the live preview const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); - if(!currLiveDoc) { + if (!currLiveDoc) { return; } @@ -140,6 +140,50 @@ define(function (require, exports, module) { editor.replaceRange("", range.from, range.to); } + /** + * This function is responsible for moving an element from one position to another in the source code + * it is called when there is drag-drop in the live preview + * @param {Number} sourceId - the data-brackets-id of the element being moved + * @param {Number} targetId - the data-brackets-id of the target element where to move + */ + function _moveElementInSource(sourceId, targetId) { + // this is to get the currently live document that is being served in the live preview + const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); + if (!currLiveDoc) { + return; + } + + const editor = currLiveDoc.editor; + if (!editor || !sourceId || !targetId) { + return; + } + + // position of source and target elements in the editor + const sourceRange = HTMLInstrumentation.getPositionFromTagId(editor, sourceId); + + if (!sourceRange) { + return; + } + + const sourceText = editor.getTextBetween(sourceRange.from, sourceRange.to); + + // creating a batch operation so that undo in live preview works fine + editor.document.batchOperation(function () { + // first, we need to remove the source code from its initial position + editor.replaceRange("", sourceRange.from, sourceRange.to); + + // get the target range, this is where we want to insert the text + const targetRange = HTMLInstrumentation.getPositionFromTagId(editor, targetId); + if(!targetRange) { + return; + } + const targetText = editor.getTextBetween(targetRange.from, targetRange.to); + + // sourceText + targetText is done so that new source text can maintain the indentation + editor.replaceRange(sourceText + targetText, targetRange.from, targetRange.to); + }); + } + /** * This is the main function that is exported. * it will be called by LiveDevProtocol when it receives a message from RemoteFunctions.js @@ -153,15 +197,25 @@ define(function (require, exports, module) { tagId: tagId, delete || duplicate || livePreviewTextEdit: true undoLivePreviewOperation: true (this property is available only for undo operation) + + sourceId: sourceId, (these are for move (drag & drop)) + targetId: targetId, + move: true } * these are the main properties that are passed through the message */ function handleLivePreviewEditOperation(message) { + // handle move(drag & drop) + if (message.move && message.sourceId && message.targetId) { + _moveElementInSource(message.sourceId, message.targetId); + return; + } + if (!message.element || !message.tagId) { // check for undo - if(message.undoLivePreviewOperation) { + if (message.undoLivePreviewOperation) { const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); - if(!currLiveDoc || !currLiveDoc.editor) { + if (!currLiveDoc || !currLiveDoc.editor) { return; } From bc07af8cc2bca54240a8ec0606f128b57dc54c3b Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 24 Jul 2025 11:57:03 +0530 Subject: [PATCH 042/849] fix: indent of target element after drag & drop operation --- src/LiveDevelopment/LivePreviewEdit.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index fc6fd78723..771049157c 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -178,9 +178,15 @@ define(function (require, exports, module) { return; } const targetText = editor.getTextBetween(targetRange.from, targetRange.to); - - // sourceText + targetText is done so that new source text can maintain the indentation - editor.replaceRange(sourceText + targetText, targetRange.from, targetRange.to); + const targetIndent = editor.getTextBetween({ line: targetRange.from.line, ch: 0 }, targetRange.from); + + // to check if there is only indentation and no text before it + if (targetIndent.trim() === "") { + const finalText = sourceText + '\n' + targetIndent + targetText; + editor.replaceRange(finalText, targetRange.from, targetRange.to); + } else { + editor.replaceRange(sourceText + targetText, targetRange.from, targetRange.to); + } }); } From 06f2944913dc450bcd73fb4106fe977c3e20fc75 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 24 Jul 2025 12:31:25 +0530 Subject: [PATCH 043/849] feat: allow dropping before as well as after the element --- .../BrowserScripts/RemoteFunctions.js | 26 ++++++++++-- src/LiveDevelopment/LivePreviewEdit.js | 40 +++++++++++++------ 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 0161be8c09..955dd43abd 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -329,8 +329,9 @@ function RemoteFunctions(config) { /** * This function creates a marker to indicate a valid drop position * @param {DOMElement} element - The element where the drop is possible + * @param {Boolean} showAtBottom - Whether to show the marker at the bottom of the element */ - function _createDropMarker(element) { + function _createDropMarker(element, showAtBottom) { // clean any existing marker from that element _removeDropMarkerFromElement(element); @@ -338,20 +339,26 @@ function RemoteFunctions(config) { let marker = window.document.createElement("div"); marker.className = DROP_MARKER_CLASSNAME; - // position the marker at the top of the element + // position the marker at the top or bottom of the element let rect = element.getBoundingClientRect(); let scrollTop = window.pageYOffset || document.documentElement.scrollTop; let scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; // marker styling marker.style.position = "absolute"; - marker.style.top = (rect.top + scrollTop - 5) + "px"; marker.style.left = (rect.left + scrollLeft) + "px"; marker.style.width = rect.width + "px"; marker.style.height = "2px"; marker.style.backgroundColor = "#4285F4"; marker.style.zIndex = "2147483646"; + // position the marker at the top or at the bottom of the element + if (showAtBottom) { + marker.style.top = (rect.bottom + scrollTop + 3) + "px"; + } else { + marker.style.top = (rect.top + scrollTop - 5) + "px"; + } + element._dropMarker = marker; // we need this in the _removeDropMarkerFromElement function window.document.body.appendChild(marker); } @@ -419,9 +426,14 @@ function RemoteFunctions(config) { return; } + // check if the cursor is in the top half or bottom half of the target element + const rect = target.getBoundingClientRect(); + const middleY = rect.top + (rect.height / 2); + const showAtBottom = event.clientY > middleY; + // before creating a drop marker, make sure that we clear all the drop markers _clearDropMarkers(); - _createDropMarker(target); + _createDropMarker(target, showAtBottom); } /** @@ -455,6 +467,11 @@ function RemoteFunctions(config) { return; } + // check if the cursor is in the top half or bottom half of the target element + const rect = target.getBoundingClientRect(); + const middleY = rect.top + (rect.height / 2); + const insertAfter = event.clientY > middleY; + // IDs of the source and target elements const sourceId = window._currentDraggedElement.getAttribute("data-brackets-id"); const targetId = target.getAttribute("data-brackets-id"); @@ -466,6 +483,7 @@ function RemoteFunctions(config) { targetElement: target, sourceId: Number(sourceId), targetId: Number(targetId), + insertAfter: insertAfter, move: true }); diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 771049157c..fdd0a9d586 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -145,8 +145,9 @@ define(function (require, exports, module) { * it is called when there is drag-drop in the live preview * @param {Number} sourceId - the data-brackets-id of the element being moved * @param {Number} targetId - the data-brackets-id of the target element where to move + * @param {Boolean} insertAfter - whether to insert the source element after the target element */ - function _moveElementInSource(sourceId, targetId) { + function _moveElementInSource(sourceId, targetId, insertAfter) { // this is to get the currently live document that is being served in the live preview const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); if (!currLiveDoc) { @@ -160,32 +161,44 @@ define(function (require, exports, module) { // position of source and target elements in the editor const sourceRange = HTMLInstrumentation.getPositionFromTagId(editor, sourceId); + const targetRange = HTMLInstrumentation.getPositionFromTagId(editor, targetId); - if (!sourceRange) { + if (!sourceRange || !targetRange) { return; } const sourceText = editor.getTextBetween(sourceRange.from, sourceRange.to); + const targetIndent = editor.getTextBetween({ line: targetRange.from.line, ch: 0 }, targetRange.from); // creating a batch operation so that undo in live preview works fine editor.document.batchOperation(function () { // first, we need to remove the source code from its initial position editor.replaceRange("", sourceRange.from, sourceRange.to); - // get the target range, this is where we want to insert the text - const targetRange = HTMLInstrumentation.getPositionFromTagId(editor, targetId); - if(!targetRange) { + // recalculate the target range, as the source text is not removed + const updatedTargetRange = HTMLInstrumentation.getPositionFromTagId(editor, targetId); + if (!updatedTargetRange) { return; } - const targetText = editor.getTextBetween(targetRange.from, targetRange.to); - const targetIndent = editor.getTextBetween({ line: targetRange.from.line, ch: 0 }, targetRange.from); - // to check if there is only indentation and no text before it - if (targetIndent.trim() === "") { - const finalText = sourceText + '\n' + targetIndent + targetText; - editor.replaceRange(finalText, targetRange.from, targetRange.to); + if (insertAfter) { + const insertPos = { + line: updatedTargetRange.to.line, + ch: updatedTargetRange.to.ch + }; + + editor.replaceRange("\n" + targetIndent + sourceText, insertPos); } else { - editor.replaceRange(sourceText + targetText, targetRange.from, targetRange.to); + // insert before + const targetText = editor.getTextBetween(updatedTargetRange.from, updatedTargetRange.to); + + // to check if there is only indentation and no text before it + if (targetIndent.trim() === "") { + const finalText = sourceText + '\n' + targetIndent + targetText; + editor.replaceRange(finalText, updatedTargetRange.from, updatedTargetRange.to); + } else { + editor.replaceRange(sourceText + targetText, updatedTargetRange.from, updatedTargetRange.to); + } } }); } @@ -206,6 +219,7 @@ define(function (require, exports, module) { sourceId: sourceId, (these are for move (drag & drop)) targetId: targetId, + insertAfter: boolean, (whether to insert after the target element) move: true } * these are the main properties that are passed through the message @@ -213,7 +227,7 @@ define(function (require, exports, module) { function handleLivePreviewEditOperation(message) { // handle move(drag & drop) if (message.move && message.sourceId && message.targetId) { - _moveElementInSource(message.sourceId, message.targetId); + _moveElementInSource(message.sourceId, message.targetId, message.insertAfter); return; } From e832ac26d05c49da565a22a0335c6aad9eacfb85 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 25 Jul 2025 12:02:42 +0530 Subject: [PATCH 044/849] fix: internal live preview editing styles leak on users source code --- .../BrowserScripts/RemoteFunctions.js | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 955dd43abd..caa08a824f 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1773,7 +1773,21 @@ function RemoteFunctions(config) { len = elem.attributes.length; for (i = 0; i < len; i++) { node = elem.attributes.item(i); + + // skip internal attributes that shouldn't be serialized + if (node.name === "draggable" && node.value === "true") { + continue; + } value = (node.name === "data-brackets-id") ? parseInt(node.value, 10) : node.value; + // Clean internal style properties + if (node.name === "style") { + const cleanedStyle = _cleanInternalStyles(value); + + if (cleanedStyle.trim() === '') { + continue; // Skip empty style attribute + } + value = cleanedStyle; + } json.attributes[node.name] = value; } @@ -1834,7 +1848,7 @@ function RemoteFunctions(config) { element.setAttribute("contenteditable", "true"); element.focus(); - element._originalContent = element.innerHTML; + element._originalContent = cleanupElementProperties(element); // Add event listeners for editing function onBlur() { @@ -1864,6 +1878,30 @@ function RemoteFunctions(config) { }; } + // Helper function to clean internal style properties + function _cleanInternalStyles(styleValue) { + if (typeof styleValue !== "string") { + return styleValue; + } + + let cleanedStyle = styleValue; + + // remove internal background color + cleanedStyle = cleanedStyle.replace(/background-color:\s*rgba\(0,\s*162,\s*255,\s*0\.2\)\s*;?\s*/gi, ""); + + // remove internal outline + cleanedStyle = cleanedStyle.replace(/outline:\s*rgb\(66,\s*133,\s*244\)\s+solid\s+1px\s*;?\s*/gi, ""); + cleanedStyle = cleanedStyle.replace(/outline:\s*1px\s+solid\s+#4285F4\s*;?\s*/gi, ""); + + // clean up any extra spaces or semicolons + cleanedStyle = cleanedStyle + .replace(/;\s*;/g, ";") + .replace(/^\s*;\s*/, "") + .replace(/\s*;\s*$/, ""); + + return cleanedStyle; + } + // this function is to remove the internal properties from elements before getting the innerHTML // then add all the properties back to the elements // internal properties such as 'data-brackets-id', 'data-ld-highlight' etc @@ -1880,9 +1918,21 @@ function RemoteFunctions(config) { el.removeAttribute('data-ld-highlight'); } - // Remove empty style attribute - if (el.hasAttribute('style') && el.getAttribute('style').trim() === '') { - el.removeAttribute('style'); + // remove the draggable attribute added internally + if (el.hasAttribute('draggable')) { + el.removeAttribute('draggable'); + } + + // remove internal style properties + if (el.hasAttribute('style')) { + const style = el.getAttribute('style'); + const cleanedStyle = _cleanInternalStyles(style); + + if (cleanedStyle.trim() === '') { + el.removeAttribute('style'); + } else { + el.setAttribute('style', cleanedStyle); + } } }); From d91dc27bc321274bb7974a4ac2d9b5c8fdc81f91 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 25 Jul 2025 12:28:37 +0530 Subject: [PATCH 045/849] fix: remove extra line after drag-drop operation --- src/LiveDevelopment/LivePreviewEdit.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index fdd0a9d586..6ee8631e2b 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -175,6 +175,13 @@ define(function (require, exports, module) { // first, we need to remove the source code from its initial position editor.replaceRange("", sourceRange.from, sourceRange.to); + // since we remove content from the source, we want to clear the extra line + if(sourceRange.from.line !== 0) { + const prevLineText = editor.getLine(sourceRange.from.line - 1); + const chPrevLine = prevLineText ? prevLineText.length : 0; + editor.replaceRange("", {line: sourceRange.from.line - 1, ch: chPrevLine}, sourceRange.from); + } + // recalculate the target range, as the source text is not removed const updatedTargetRange = HTMLInstrumentation.getPositionFromTagId(editor, targetId); if (!updatedTargetRange) { From 1d7f00601a8da2b20749d435a965cac498da5b74 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 25 Jul 2025 14:22:37 +0530 Subject: [PATCH 046/849] feat: remove boxes when escape key is pressed or user clicks on a empty area --- .../BrowserScripts/RemoteFunctions.js | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index caa08a824f..69a776a0cf 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1377,6 +1377,12 @@ function RemoteFunctions(config) { event.target._originalOutline = event.target.style.outline; event.target.style.outline = "1px solid #4285F4"; previouslyClickedElement = event.target; + } else if ( // when user clicks on the HTML or the BODY tag, we want to remove the boxes + isFlagActive && + _nodeMoreOptionsBox && + (event.target.tagName === "HTML" || event.target.tagName === "BODY") + ) { + dismissMoreOptionsBox(); } } @@ -1394,6 +1400,9 @@ function RemoteFunctions(config) { } function onKeyDown(event) { + if ((event.key === "Escape" || event.key === "Esc")) { + dismissMoreOptionsBox(); + } if (!_setup && _validEvent(event)) { window.document.addEventListener("keyup", onKeyUp); window.document.addEventListener("mouseover", onMouseOver); @@ -1838,6 +1847,32 @@ function RemoteFunctions(config) { return JSON.stringify(config); } + /** + * This function is responsible to remove the more options box + * we do this either when user presses the Esc key or clicks on the HTML or Body tags + */ + function dismissMoreOptionsBox() { + if (_nodeMoreOptionsBox) { + _nodeMoreOptionsBox.remove(); + _nodeMoreOptionsBox = null; + } + + if (_nodeInfoBox) { + _nodeInfoBox.remove(); + _nodeInfoBox = null; + } + + if (previouslyClickedElement) { + if (previouslyClickedElement._originalOutline !== undefined) { + previouslyClickedElement.style.outline = previouslyClickedElement._originalOutline; + } else { + previouslyClickedElement.style.outline = ""; + } + delete previouslyClickedElement._originalOutline; + previouslyClickedElement = null; + } + } + // Function to handle direct editing of elements in the live preview function startEditing(element) { if (!element) { @@ -1989,10 +2024,7 @@ function RemoteFunctions(config) { window.document.addEventListener("click", onClick); window.document.addEventListener("dragover", onDragOver); window.document.addEventListener("drop", onDrop); - - if (experimental) { - window.document.addEventListener("keydown", onKeyDown); - } + window.document.addEventListener("keydown", onKeyDown); return { "DOMEditHandler" : DOMEditHandler, From 898ace075d7b7b9b4128c7e1a0a6ca6970ad80c8 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 25 Jul 2025 20:20:00 +0530 Subject: [PATCH 047/849] fix: delete empty lines when deleting an element from the live preview --- src/LiveDevelopment/LivePreviewEdit.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 6ee8631e2b..d6f1ebca79 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -138,6 +138,13 @@ define(function (require, exports, module) { } editor.replaceRange("", range.from, range.to); + + // since we remove content from the source, we want to clear the extra line + if(range.from.line !== 0) { + const prevLineText = editor.getLine(range.from.line - 1); + const chPrevLine = prevLineText ? prevLineText.length : 0; + editor.replaceRange("", {line: range.from.line - 1, ch: chPrevLine}, range.from); + } } /** From e78ec69a7c12bd62bed59102010321d4964d93b7 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 25 Jul 2025 20:40:45 +0530 Subject: [PATCH 048/849] fix: error when trying to remove node info box --- 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 69a776a0cf..787b66c97d 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -732,7 +732,7 @@ function RemoteFunctions(config) { }, remove: function() { - if (this.body && this.body.parentNode) { + if (this.body && this.body.parentNode && this.body.parentNode === window.document.body) { window.document.body.removeChild(this.body); this.body = null; } @@ -896,7 +896,7 @@ function RemoteFunctions(config) { }, remove: function() { - if (this.body && this.body.parentNode) { + if (this.body && this.body.parentNode && this.body.parentNode === window.document.body) { window.document.body.removeChild(this.body); this.body = null; } From 85d08deb322e1309cc7c60ba7490c68b93d8a76f Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 25 Jul 2025 21:59:54 +0530 Subject: [PATCH 049/849] fix: live preview not handling escape keypress when highlight pref is on --- .../BrowserScripts/RemoteFunctions.js | 20 +++++++++++++++++- src/LiveDevelopment/LiveDevMultiBrowser.js | 21 +++++++++++++++++++ src/LiveDevelopment/main.js | 19 ++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 787b66c97d..ed3f157ffa 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1847,19 +1847,32 @@ function RemoteFunctions(config) { return JSON.stringify(config); } + /** + * This function checks if there are any live preview boxes currently visible + * @return {boolean} true if any boxes are visible, false otherwise + */ + function hasVisibleLivePreviewBoxes() { + return _nodeMoreOptionsBox !== null || _nodeInfoBox !== null || previouslyClickedElement !== null; + } + /** * This function is responsible to remove the more options box * we do this either when user presses the Esc key or clicks on the HTML or Body tags + * @return {boolean} true if any boxes were dismissed, false otherwise */ function dismissMoreOptionsBox() { + let dismissed = false; + if (_nodeMoreOptionsBox) { _nodeMoreOptionsBox.remove(); _nodeMoreOptionsBox = null; + dismissed = true; } if (_nodeInfoBox) { _nodeInfoBox.remove(); _nodeInfoBox = null; + dismissed = true; } if (previouslyClickedElement) { @@ -1870,7 +1883,10 @@ function RemoteFunctions(config) { } delete previouslyClickedElement._originalOutline; previouslyClickedElement = null; + dismissed = true; } + + return dismissed; } // Function to handle direct editing of elements in the live preview @@ -2037,6 +2053,8 @@ function RemoteFunctions(config) { "getSimpleDOM" : getSimpleDOM, "updateConfig" : updateConfig, "startEditing" : startEditing, - "finishEditing" : finishEditing + "finishEditing" : finishEditing, + "dismissMoreOptionsBox" : dismissMoreOptionsBox, + "hasVisibleLivePreviewBoxes" : hasVisibleLivePreviewBoxes }; } diff --git a/src/LiveDevelopment/LiveDevMultiBrowser.js b/src/LiveDevelopment/LiveDevMultiBrowser.js index baeba8cbe3..0f2d54836f 100644 --- a/src/LiveDevelopment/LiveDevMultiBrowser.js +++ b/src/LiveDevelopment/LiveDevMultiBrowser.js @@ -699,6 +699,25 @@ define(function (require, exports, module) { } } + /** + * Check if live preview boxes are currently visible + */ + function hasVisibleLivePreviewBoxes() { + if (_protocol) { + return _protocol.evaluate("_LD.hasVisibleLivePreviewBoxes()"); + } + return false; + } + + /** + * Dismiss live preview more options box and info box + */ + function dismissLivePreviewBoxes() { + if (_protocol) { + _protocol.evaluate("_LD.dismissMoreOptionsBox()"); + } + } + /** * Originally unload and reload agents. It doesn't apply for this new implementation. * @return {jQuery.Promise} Already resolved promise. @@ -765,6 +784,8 @@ define(function (require, exports, module) { exports.showHighlight = showHighlight; exports.hideHighlight = hideHighlight; exports.redrawHighlight = redrawHighlight; + exports.hasVisibleLivePreviewBoxes = hasVisibleLivePreviewBoxes; + exports.dismissLivePreviewBoxes = dismissLivePreviewBoxes; exports.init = init; exports.isActive = isActive; exports.setLivePreviewPinned= setLivePreviewPinned; diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 6d7cb962c9..5a02c9b755 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -42,7 +42,8 @@ define(function main(require, exports, module) { Strings = require("strings"), ExtensionUtils = require("utils/ExtensionUtils"), StringUtils = require("utils/StringUtils"), - EventDispatcher = require("utils/EventDispatcher"); + EventDispatcher = require("utils/EventDispatcher"), + WorkspaceManager = require("view/WorkspaceManager"); const EVENT_LIVE_HIGHLIGHT_PREF_CHANGED = "liveHighlightPrefChange"; @@ -237,6 +238,19 @@ define(function main(require, exports, module) { } } + /** + * this function handles escape key for live preview to hide boxes if they are visible + * @param {Event} event + */ + function _handleLivePreviewEscapeKey(event) { + // we only handle the escape keypress for live preview when its active + if (MultiBrowserLiveDev.status === MultiBrowserLiveDev.STATUS_ACTIVE) { + MultiBrowserLiveDev.dismissLivePreviewBoxes(); + } + // returning false to let the editor also handle the escape key + return false; + } + /** Initialize LiveDevelopment */ AppInit.appReady(function () { params.parse(); @@ -291,6 +305,9 @@ define(function main(require, exports, module) { exports.trigger(exports.EVENT_LIVE_PREVIEW_RELOAD, clientDetails); }); + // allow live preview to handle escape key event + // Escape is mainly to hide boxes if they are visible + WorkspaceManager.addEscapeKeyEventHandler("livePreview", _handleLivePreviewEscapeKey); }); // init prefs From 6823f6a6aaffe5710cef0f9e548a8aa62b96ca94 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 26 Jul 2025 12:02:29 +0530 Subject: [PATCH 050/849] feat: enable redo when ctrl + y is pressed in live preview --- .../BrowserScripts/LiveDevProtocolRemote.js | 8 +++++ src/LiveDevelopment/LivePreviewEdit.js | 29 ++++++++++++++----- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js index 54460e7d8e..a959721cd4 100644 --- a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js +++ b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js @@ -447,6 +447,14 @@ undoLivePreviewOperation: true }); } + + // for redo + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "y") { + MessageBroker.send({ + livePreviewEditEnabled: true, + redoLivePreviewOperation: true + }); + } }); }(this)); diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index d6f1ebca79..3075131809 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -217,6 +217,25 @@ define(function (require, exports, module) { }); } + /** + * This function is to handle the undo redo operation in the live preview + * @param {String} undoOrRedo - "undo" when to undo, and "redo" for redo + */ + function handleUndoRedoOperation(undoOrRedo) { + const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); + if (!currLiveDoc || !currLiveDoc.editor) { + return; + } + + const editor = currLiveDoc.editor; + + if (undoOrRedo === "undo") { + editor.undo(); + } else if (undoOrRedo === "redo") { + editor.redo(); + } + } + /** * This is the main function that is exported. * it will be called by LiveDevProtocol when it receives a message from RemoteFunctions.js @@ -247,14 +266,8 @@ define(function (require, exports, module) { if (!message.element || !message.tagId) { // check for undo - if (message.undoLivePreviewOperation) { - const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); - if (!currLiveDoc || !currLiveDoc.editor) { - return; - } - - const editor = currLiveDoc.editor; - editor.undo(); + if (message.undoLivePreviewOperation || message.redoLivePreviewOperation) { + message.undoLivePreviewOperation ? handleUndoRedoOperation("undo") : handleUndoRedoOperation("redo"); } return; } From 9b470978a09a4f2abc2938a8b46ed2e1e606264e Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 26 Jul 2025 13:22:16 +0530 Subject: [PATCH 051/849] fix: prevent links and buttons to move to another webpage when edit mode is enabled --- 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 ed3f157ffa..f5f5b9b56e 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1350,6 +1350,7 @@ function RemoteFunctions(config) { event.target.tagName !== "BODY" && event.target.tagName !== "HTML" ) { + event.preventDefault(); event.stopPropagation(); if (_nodeMoreOptionsBox) { _nodeMoreOptionsBox.remove(); From 0eea0512937ec1f583d22606130220c0779e5f8d Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 26 Jul 2025 13:40:56 +0530 Subject: [PATCH 052/849] feat: add pen icon to edit text in the more options box --- .../BrowserScripts/RemoteFunctions.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index f5f5b9b56e..d14de10dac 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -301,6 +301,8 @@ function RemoteFunctions(config) { function handleOptionClick(e, action, element) { if (action === "select-parent") { _handleSelectParentOptionClick(e, element); + } else if (action === "edit-text") { + startEditing(element); } else if (action === "duplicate") { _handleDuplicateOptionClick(e, element); } else if (action === "delete") { @@ -545,8 +547,8 @@ function RemoteFunctions(config) { // calc estimate width based on the char count const infoBoxWidth = basePadding + (charCount * avgCharWidth); - // more options box is 82px - const moreOptionsBoxWidth = 82; + // more options box is 106px + const moreOptionsBoxWidth = 106; // check if there's enough space for both boxes // 20px buffer for spacing between boxes @@ -600,8 +602,7 @@ function RemoteFunctions(config) { let elemBounds = this.element.getBoundingClientRect(); // the box width and the positions where it should be placed - const boxWidth = 82; - const boxHeight = 40; // rough estimate for options box + const boxWidth = 106; const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; const scrollTop = window.pageYOffset || document.documentElement.scrollTop; @@ -641,6 +642,12 @@ function RemoteFunctions(config) { `, + edit: ` + + + + `, + copy: ` ${ICONS.arrowUp} + + ${ICONS.edit} + ${ICONS.copy} From 8662ebc1e5dfe9894af30cc2be15c282a883b629 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 26 Jul 2025 15:36:56 +0530 Subject: [PATCH 053/849] fix: more options box doesn't appear because click event was removed --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index d14de10dac..877fdf127c 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1403,7 +1403,6 @@ function RemoteFunctions(config) { window.document.removeEventListener("mouseover", onMouseOver); window.document.removeEventListener("mouseout", onMouseOut); window.document.removeEventListener("mousemove", onMouseMove); - window.document.removeEventListener("click", onClick); _localHighlight.clear(); _localHighlight = undefined; _setup = false; From 1ecee7198b4f61e6b8996322cabce61918d2af9a Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 26 Jul 2025 15:46:32 +0530 Subject: [PATCH 054/849] fix: do delete and duplicate operation in batch so that undo and redo works as expected --- src/LiveDevelopment/LivePreviewEdit.js | 52 ++++++++++++++------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 3075131809..7a9fc517df 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -94,24 +94,26 @@ define(function (require, exports, module) { // this is the indentation on the line const indent = editor.getTextBetween({ line: range.from.line, ch: 0 }, range.from); - // make sure there is only indentation and no text before it - if (indent.trim() === "") { - // this is the position where we need to insert - // we're giving the char as 0 because since we insert a new line using '\n' - // that's why writing any char value will not work, as the line is empty - // and codemirror doesn't allow to insert at a column (ch) greater than the length of the line - // So, the logic is to just append the indent before the text at this insertPos - const insertPos = { - line: range.from.line + (range.to.line - range.from.line + 1), - ch: 0 - }; + editor.document.batchOperation(function () { + // make sure there is only indentation and no text before it + if (indent.trim() === "") { + // this is the position where we need to insert + // we're giving the char as 0 because since we insert a new line using '\n' + // that's why writing any char value will not work, as the line is empty + // and codemirror doesn't allow to insert at a column (ch) greater than the length of the line + // So, the logic is to just append the indent before the text at this insertPos + const insertPos = { + line: range.from.line + (range.to.line - range.from.line + 1), + ch: 0 + }; - editor.replaceRange("\n", range.to); - editor.replaceRange(indent + text, insertPos); - } else { - // if there is some text, we just add the duplicated text right next to it - editor.replaceRange(text, range.from); - } + editor.replaceRange("\n", range.to); + editor.replaceRange(indent + text, insertPos); + } else { + // if there is some text, we just add the duplicated text right next to it + editor.replaceRange(text, range.from); + } + }); } /** @@ -137,14 +139,16 @@ define(function (require, exports, module) { return; } - editor.replaceRange("", range.from, range.to); + editor.document.batchOperation(function () { + editor.replaceRange("", range.from, range.to); - // since we remove content from the source, we want to clear the extra line - if(range.from.line !== 0) { - const prevLineText = editor.getLine(range.from.line - 1); - const chPrevLine = prevLineText ? prevLineText.length : 0; - editor.replaceRange("", {line: range.from.line - 1, ch: chPrevLine}, range.from); - } + // since we remove content from the source, we want to clear the extra line + if(range.from.line !== 0) { + const prevLineText = editor.getLine(range.from.line - 1); + const chPrevLine = prevLineText ? prevLineText.length : 0; + editor.replaceRange("", {line: range.from.line - 1, ch: chPrevLine}, range.from); + } + }); } /** From f8d2e96623223d6d1ccce1c74a14a1ea84a4b99c Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 26 Jul 2025 19:13:31 +0530 Subject: [PATCH 055/849] feat: hide edit option when element is non-editable --- .../BrowserScripts/RemoteFunctions.js | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 877fdf127c..822170f895 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -559,6 +559,59 @@ function RemoteFunctions(config) { return true; } + /** + * this function is to check if an element should show the edit text option + * it is needed because edit text option doesn't make sense with many elements like images, videos, hr tag etc + * @param {Element} element - DOM element to check + * @returns {boolean} - true if we should show the edit text option otherwise false + */ + function _shouldShowEditTextOption(element) { + if (!element || !element.tagName) { + return false; + } + + const tagName = element.tagName.toLowerCase(); + + // these are self-closing tags and don't allow any text content + const voidElements = [ + "img", + "br", + "hr", + "input", + "meta", + "link", + "area", + "base", + "col", + "embed", + "source", + "track", + "wbr" + ]; + + // these elements are non-editable as they have their own mechanisms + const nonEditableElements = [ + "script", + "style", + "noscript", + "canvas", + "svg", + "video", + "audio", + "iframe", + "object", + "button", + "select", + "textarea" + ]; + + if (voidElements.includes(tagName) || nonEditableElements.includes(tagName)) { + return false; + } + + return true; + } + /** * This is for the advanced DOM options that appears when a DOM element is clicked * advanced options like: 'select parent', 'duplicate', 'delete' @@ -601,8 +654,11 @@ function RemoteFunctions(config) { // the element that was clicked let elemBounds = this.element.getBoundingClientRect(); + // check if edit text option should be shown to determine box width + const showEditTextOption = _shouldShowEditTextOption(this.element); + // the box width and the positions where it should be placed - const boxWidth = 106; + const boxWidth = showEditTextOption ? 106 : 82; const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; const scrollTop = window.pageYOffset || document.documentElement.scrollTop; @@ -666,11 +722,15 @@ function RemoteFunctions(config) { let content = `
${ICONS.arrowUp} - - + `; + + if (showEditTextOption) { // to check if the element is editable + content += ` ${ICONS.edit} - - + `; + } + + content += ` ${ICONS.copy} From a0077460c5e6a911d1c14b5edcb747e34a1b7259 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 26 Jul 2025 19:34:00 +0530 Subject: [PATCH 056/849] feat: hide select-parent option when element doesn't supports it --- .../BrowserScripts/RemoteFunctions.js | 60 +++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 822170f895..ae81b622b4 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -612,6 +612,30 @@ function RemoteFunctions(config) { return true; } + /** + * this function is to check if an element should show the 'select-parent' option + * because we don't want to show the select parent option when the parent is directly the body/html tag + * or the parent doesn't have the 'data-brackets-id' + * @param {Element} element - DOM element to check + * @returns {boolean} - true if we should show the select parent option otherwise false + */ + function _shouldShowSelectParentOption(element) { + if (!element || !element.parentElement) { + return false; + } + + const parentElement = element.parentElement; + + if (parentElement.tagName === "HTML" || parentElement.tagName === "BODY") { + return false; + } + if (!parentElement.hasAttribute("data-brackets-id")) { + return false; + } + + return true; + } + /** * This is for the advanced DOM options that appears when a DOM element is clicked * advanced options like: 'select parent', 'duplicate', 'delete' @@ -654,11 +678,29 @@ function RemoteFunctions(config) { // the element that was clicked let elemBounds = this.element.getBoundingClientRect(); - // check if edit text option should be shown to determine box width + // check which options should be shown to determine box width const showEditTextOption = _shouldShowEditTextOption(this.element); + const showSelectParentOption = _shouldShowSelectParentOption(this.element); - // the box width and the positions where it should be placed - const boxWidth = showEditTextOption ? 106 : 82; + // calculate box width based on visible options + // NOTE: duplicate and delete buttons are always shown + let optionCount = 2; + if (showSelectParentOption) { + optionCount++; + } + if (showEditTextOption) { + optionCount++; + } + + // box width we need to decide based on the no. of options + let boxWidth; + if (optionCount === 2) { + boxWidth = 52; + } else if (optionCount === 3) { + boxWidth = 82; + } else { + boxWidth = 106; + } const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; const scrollTop = window.pageYOffset || document.documentElement.scrollTop; @@ -719,17 +761,23 @@ function RemoteFunctions(config) { ` }; - let content = `
- + let content = `
`; + + // Only include select parent option if element supports it + if (showSelectParentOption) { + content += ` ${ICONS.arrowUp} `; + } - if (showEditTextOption) { // to check if the element is editable + // Only include edit text option if element supports it + if (showEditTextOption) { content += ` ${ICONS.edit} `; } + // Always include duplicate and delete options content += ` ${ICONS.copy} From 19755fb7fbb38be1ef080c44fad223582a56050c Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 26 Jul 2025 19:42:28 +0530 Subject: [PATCH 057/849] fix: unable to edit text inside elements with no text content --- src/LiveDevelopment/LivePreviewEdit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 7a9fc517df..e99f4b7476 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -11,7 +11,7 @@ define(function (require, exports, module) { const openTagEnd = html.indexOf(">") + 1; const closeTagStart = html.lastIndexOf("<"); - if (openTagEnd > 0 && closeTagStart > openTagEnd) { + if (openTagEnd > 0 && closeTagStart >= openTagEnd) { return { openTag: html.substring(0, openTagEnd), closeTag: html.substring(closeTagStart) From c1f519157ab9c907b8e240e09c1242d253553b17 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 26 Jul 2025 20:20:27 +0530 Subject: [PATCH 058/849] fix: more options box gets out of viewport with very long elements --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index ae81b622b4..72849cdc3a 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -713,8 +713,15 @@ function RemoteFunctions(config) { const isOverlap = checkOverlap(elemBounds.width, tagName, id, classes); - // default position (right aligned with element) - let leftPos = elemBounds.right - boxWidth + scrollLeft; + const viewportWidth = window.innerWidth; + const idealLeftPos = elemBounds.right - boxWidth + scrollLeft; + const maxLeftPos = viewportWidth - boxWidth - 10 + scrollLeft; + // 10px is just the padding, because we want some space + const minLeftPos = 10 + scrollLeft; + + // we'll use the position that keeps the box within viewport bounds + let leftPos = Math.min(idealLeftPos, maxLeftPos); + leftPos = Math.max(leftPos, minLeftPos); let topPos; if (isOverlap) { From cb92fa1dddfd5e8f6b8f19a0d78ed7c7ebdb8f52 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 26 Jul 2025 21:24:48 +0530 Subject: [PATCH 059/849] fix: button's onclick was overriding more options box --- .../BrowserScripts/RemoteFunctions.js | 75 +++++++------------ 1 file changed, 25 insertions(+), 50 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 72849cdc3a..b75becbec0 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1028,42 +1028,6 @@ function RemoteFunctions(config) { } }; - function Editor(element) { - this.onBlur = this.onBlur.bind(this); - this.onKeyPress = this.onKeyPress.bind(this); - - this.element = element; - this.element.setAttribute("contenteditable", "true"); - this.element.focus(); - this.element.addEventListener("blur", this.onBlur); - this.element.addEventListener("keypress", this.onKeyPress); - - this.revertText = this.element.innerHTML; - - _trigger(this.element, "edit", 1); - } - - Editor.prototype = { - onBlur: function (event) { - this.element.removeAttribute("contenteditable"); - this.element.removeEventListener("blur", this.onBlur); - this.element.removeEventListener("keypress", this.onKeyPress); - _trigger(this.element, "edit", 0, true); - }, - - onKeyPress: function (event) { - switch (event.which) { - case 13: // return - this.element.blur(); - break; - case 27: // esc - this.element.innerHTML = this.revertText; - this.element.blur(); - break; - } - } - }; - function Highlight(color, trigger) { this.color = color; this.trigger = !!trigger; @@ -1364,19 +1328,7 @@ function RemoteFunctions(config) { } }; - var _currentEditor; - function _toggleEditor(element) { - _currentEditor = new Editor(element); - } - var _currentMenu; - function _toggleMenu(element) { - if (_currentMenu) { - _currentMenu.remove(); - } - _currentMenu = new Menu(element); - } - var _localHighlight; var _remoteHighlight; var _hoverHighlight; @@ -1477,6 +1429,8 @@ function RemoteFunctions(config) { ) { event.preventDefault(); event.stopPropagation(); + event.stopImmediatePropagation(); + if (_nodeMoreOptionsBox) { _nodeMoreOptionsBox.remove(); _nodeMoreOptionsBox = null; @@ -1512,6 +1466,26 @@ function RemoteFunctions(config) { } } + /** + * this function handles the double click event + * @param {Event} event + */ + function onDoubleClick(event) { + if ( + isFlagActive && + event.target.hasAttribute("data-brackets-id") && + event.target.tagName !== "BODY" && + event.target.tagName !== "HTML" + ) { + // because we only want to allow double click text editing where we show the edit option + if (_shouldShowEditTextOption(event.target)) { + event.preventDefault(); + event.stopPropagation(); + startEditing(event.target); + } + } + } + function onKeyUp(event) { if (_setup && !_validEvent(event)) { window.document.removeEventListener("keyup", onKeyUp); @@ -1533,7 +1507,7 @@ function RemoteFunctions(config) { window.document.addEventListener("mouseover", onMouseOver); window.document.addEventListener("mouseout", onMouseOut); window.document.addEventListener("mousemove", onMouseMove); - window.document.addEventListener("click", onClick); + window.document.addEventListener("click", onClick, true); _localHighlight = new Highlight("#ecc", true); _setup = true; } @@ -2162,7 +2136,8 @@ function RemoteFunctions(config) { // Add event listeners for hover window.document.addEventListener("mouseover", onElementHover); window.document.addEventListener("mouseout", onElementHoverOut); - window.document.addEventListener("click", onClick); + window.document.addEventListener("click", onClick, true); + window.document.addEventListener("dblclick", onDoubleClick); window.document.addEventListener("dragover", onDragOver); window.document.addEventListener("drop", onDrop); window.document.addEventListener("keydown", onKeyDown); From a38b2f9031de258a709dbc8ed79ee9ff90ea53b9 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 27 Jul 2025 00:47:22 +0530 Subject: [PATCH 060/849] fix: revert code as it broke the existing tests --- 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 b75becbec0..7b7606f21f 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1507,7 +1507,7 @@ function RemoteFunctions(config) { window.document.addEventListener("mouseover", onMouseOver); window.document.addEventListener("mouseout", onMouseOut); window.document.addEventListener("mousemove", onMouseMove); - window.document.addEventListener("click", onClick, true); + window.document.addEventListener("click", onClick); _localHighlight = new Highlight("#ecc", true); _setup = true; } @@ -2136,7 +2136,7 @@ function RemoteFunctions(config) { // Add event listeners for hover window.document.addEventListener("mouseover", onElementHover); window.document.addEventListener("mouseout", onElementHoverOut); - window.document.addEventListener("click", onClick, true); + window.document.addEventListener("click", onClick); window.document.addEventListener("dblclick", onDoubleClick); window.document.addEventListener("dragover", onDragOver); window.document.addEventListener("drop", onDrop); From 7f4c6cdcd2f83a1132c5fd8ff08339f4659ce485 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 27 Jul 2025 13:40:46 +0530 Subject: [PATCH 061/849] fix: more option box reappearing after operation performed --- 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 7b7606f21f..6574921e6b 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -860,6 +860,7 @@ function RemoteFunctions(config) { if (this.body && this.body.parentNode && this.body.parentNode === window.document.body) { window.document.body.removeChild(this.body); this.body = null; + _nodeMoreOptionsBox = null; } } }; From 218b316dc4a323b42aa86a665dc849980b5dbfb6 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 27 Jul 2025 17:11:40 +0530 Subject: [PATCH 062/849] fix: info box not visible in viewport when element is at the very bottom --- .../BrowserScripts/RemoteFunctions.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 6574921e6b..763af945b9 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -948,6 +948,7 @@ function RemoteFunctions(config) { if (isOverlap) { const windowWidth = window.innerWidth; + const viewportHeight = window.innerHeight; // Estimate the height of the info box based on its content // base height for tag name + padding @@ -963,9 +964,21 @@ function RemoteFunctions(config) { estimatedHeight += 15; } - // align with the bottom of the info box + // check if element is near bottom of viewport + const elementBottomFromViewportTop = elemBounds.bottom; + const availableSpaceBelow = viewportHeight - elementBottomFromViewportTop; + + // align with the bottom of the info box (original behavior) topPos = (elemBounds.top + elemBounds.height - estimatedHeight) + scrollTop; + // If element is near bottom and there's not enough space below, + // push the info box up a bit to prevent scrollbar + if (availableSpaceBelow < estimatedHeight + 10) { + // Push it up by the amount it would overflow + const pushUpAmount = estimatedHeight - availableSpaceBelow; + topPos -= pushUpAmount; + } + // decide whether position at left or right based on available space // check if there's enough space on the left side if (elemBounds.left > boxWidth + 10) { From ab82a76c4a18f4a8da0eb0b777d6c1fd62c974e4 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 27 Jul 2025 18:12:28 +0530 Subject: [PATCH 063/849] fix: more options box not getting hidden after drag & drop operation --- 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 763af945b9..5ed1e00d6a 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -491,6 +491,7 @@ function RemoteFunctions(config) { _clearDropMarkers(); _dragEndChores(window._currentDraggedElement); + dismissMoreOptionsBox(); delete window._currentDraggedElement; } From 72e5d7220ff9ba95c0c44919f123b6b994c59b36 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 27 Jul 2025 18:53:29 +0530 Subject: [PATCH 064/849] fix: make sure info box is within live preview bounds --- .../BrowserScripts/RemoteFunctions.js | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 5ed1e00d6a..c183f39099 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -991,6 +991,18 @@ function RemoteFunctions(config) { } } + // to make sure that the info box stays under the viewport width + const viewportWidth = window.innerWidth; + const margin = 10; + + // horizontal boundary checking + if (leftPos + boxWidth + margin > viewportWidth + scrollLeft) { + leftPos = viewportWidth + scrollLeft - boxWidth - margin; + } + if (leftPos < scrollLeft + margin) { + leftPos = scrollLeft + margin; + } + const styles = ` .box { background-color: #4285F4; @@ -1583,6 +1595,20 @@ function RemoteFunctions(config) { _clickHighlight.selector = rule; } + // recreate UI boxes (info box and more options box) + function redrawUIBoxes() { + if (_nodeMoreOptionsBox) { + const element = _nodeMoreOptionsBox.element; + _nodeMoreOptionsBox.remove(); + _nodeMoreOptionsBox = new NodeMoreOptionsBox(element); + + if (_nodeInfoBox) { + _nodeInfoBox.remove(); + _nodeInfoBox = new NodeInfoBox(element, true); // true means it came from a click + } + } + } + // redraw active highlights function redrawHighlights() { if (_remoteHighlight) { @@ -1594,19 +1620,15 @@ function RemoteFunctions(config) { if (_hoverHighlight) { _hoverHighlight.redraw(); } - if (_nodeMoreOptionsBox) { - const element = _nodeMoreOptionsBox.element; - _nodeMoreOptionsBox.remove(); - _nodeMoreOptionsBox = new NodeMoreOptionsBox(element); + } - if (_nodeInfoBox) { - _nodeInfoBox.remove(); - _nodeInfoBox = new NodeInfoBox(element, true); // true means it came from a click - } - } + // just a wrapper function when we need to redraw highlights as well as UI boxes + function redrawEverything() { + redrawHighlights(); + redrawUIBoxes(); } - window.addEventListener("resize", redrawHighlights); + window.addEventListener("resize", redrawEverything); // Add a capture-phase scroll listener to update highlights when // any element scrolls. @@ -1875,7 +1897,7 @@ function RemoteFunctions(config) { this.rememberedNodes = {}; // update highlight after applying diffs - redrawHighlights(); + redrawEverything(); }; function applyDOMEdits(edits) { @@ -2164,6 +2186,7 @@ function RemoteFunctions(config) { "highlight" : highlight, "highlightRule" : highlightRule, "redrawHighlights" : redrawHighlights, + "redrawEverything" : redrawEverything, "applyDOMEdits" : applyDOMEdits, "getSimpleDOM" : getSimpleDOM, "updateConfig" : updateConfig, From 44699c5939f3568affe1fd9d8ee6124473f0975e Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 27 Jul 2025 19:01:49 +0530 Subject: [PATCH 065/849] fix: remove boxes when user scrolls the live preview --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index c183f39099..d235afba4b 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1642,6 +1642,8 @@ function RemoteFunctions(config) { window.setTimeout(redrawHighlights, 0); } } + + dismissMoreOptionsBox(); } window.addEventListener("scroll", _scrollHandler, true); From 518d693c315926e100b869a1740cc34ce5075427 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 27 Jul 2025 19:04:29 +0530 Subject: [PATCH 066/849] fix: remove more options box when user starts editing --- 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 d235afba4b..944f91eec4 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2036,6 +2036,7 @@ function RemoteFunctions(config) { // Make the element editable element.setAttribute("contenteditable", "true"); element.focus(); + dismissMoreOptionsBox(); element._originalContent = cleanupElementProperties(element); From 29f5f2d3aa5d54dd4cc6891bc73140fceeb167e5 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 27 Jul 2025 19:56:07 +0530 Subject: [PATCH 067/849] fix: drag & drop logic breaks when target is after source --- src/LiveDevelopment/LivePreviewEdit.js | 122 +++++++++++++++++++------ 1 file changed, 96 insertions(+), 26 deletions(-) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index e99f4b7476..83e59fc694 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -181,41 +181,111 @@ define(function (require, exports, module) { const sourceText = editor.getTextBetween(sourceRange.from, sourceRange.to); const targetIndent = editor.getTextBetween({ line: targetRange.from.line, ch: 0 }, targetRange.from); - // creating a batch operation so that undo in live preview works fine - editor.document.batchOperation(function () { - // first, we need to remove the source code from its initial position - editor.replaceRange("", sourceRange.from, sourceRange.to); + // Check if source is before target to determine order of operations + // check if the source is before target or after the target + // we need this because + // If source is before target → we need to insert first, then remove + // If target is before source → remove first, then insert + const sourceBeforeTarget = + sourceRange.from.line < targetRange.from.line || + (sourceRange.from.line === targetRange.from.line && sourceRange.from.ch < targetRange.from.ch); + + // this function is to clean up the empty lines after an element is removed + function cleanupAfterRemoval(range) { + const lineToCheck = range.from.line; + + // check if the line where element was removed is now empty + if (lineToCheck < editor.lineCount()) { + const currentLineText = editor.getLine(lineToCheck); + if (currentLineText && currentLineText.trim() === "") { + // remove the empty line + const lineStart = { line: lineToCheck, ch: 0 }; + const lineEnd = { line: lineToCheck + 1, ch: 0 }; + editor.replaceRange("", lineStart, lineEnd); + } + } - // since we remove content from the source, we want to clear the extra line - if(sourceRange.from.line !== 0) { - const prevLineText = editor.getLine(sourceRange.from.line - 1); - const chPrevLine = prevLineText ? prevLineText.length : 0; - editor.replaceRange("", {line: sourceRange.from.line - 1, ch: chPrevLine}, sourceRange.from); + // also we need to check the previous line if it became empty + if (lineToCheck > 0) { + const prevLineText = editor.getLine(lineToCheck - 1); + if (prevLineText && prevLineText.trim() === "") { + const lineStart = { line: lineToCheck - 1, ch: 0 }; + const lineEnd = { line: lineToCheck, ch: 0 }; + editor.replaceRange("", lineStart, lineEnd); + } } + } + + // this function is to make sure that we insert elements with proper indentation + function insertElementWithIndentation(insertPos, insertAfterMode, useTargetIndent) { + const indent = useTargetIndent ? targetIndent : targetIndent; + + if (insertAfterMode) { + // Insert after the target element + editor.replaceRange("\n" + indent + sourceText, insertPos); + } else { + // Insert before the target element + const insertLine = insertPos.line; + const lineStart = { line: insertLine, ch: 0 }; + + // Get current line content to preserve any existing indentation structure + const currentLine = editor.getLine(insertLine); - // recalculate the target range, as the source text is not removed - const updatedTargetRange = HTMLInstrumentation.getPositionFromTagId(editor, targetId); - if (!updatedTargetRange) { - return; + if (currentLine && currentLine.trim() === "") { + // the line is empty, replace it entirely + editor.replaceRange(indent + sourceText, lineStart, { line: insertLine, ch: currentLine.length }); + } else { + // the line has content, insert before it + editor.replaceRange(indent + sourceText + "\n", lineStart); + } } + } - if (insertAfter) { - const insertPos = { - line: updatedTargetRange.to.line, - ch: updatedTargetRange.to.ch - }; + // creating a batch operation so that undo in live preview works fine + editor.document.batchOperation(function () { + if (sourceBeforeTarget) { + // this handles the case when source is before target: insert first, then remove + if (insertAfter) { + const insertPos = { + line: targetRange.to.line, + ch: targetRange.to.ch + }; + insertElementWithIndentation(insertPos, true, true); + } else { + // insert before target + insertElementWithIndentation(targetRange.from, false, true); + } - editor.replaceRange("\n" + targetIndent + sourceText, insertPos); + // Now remove the source element (NOTE: the positions have shifted) + const updatedSourceRange = HTMLInstrumentation.getPositionFromTagId(editor, sourceId); + if (updatedSourceRange) { + editor.replaceRange("", updatedSourceRange.from, updatedSourceRange.to); + cleanupAfterRemoval(updatedSourceRange); + } } else { - // insert before - const targetText = editor.getTextBetween(updatedTargetRange.from, updatedTargetRange.to); + // This handles the case when target is before source: remove first, then insert + // Store source range before removal + const originalSourceRange = { ...sourceRange }; + + // Remove the source element first + editor.replaceRange("", sourceRange.from, sourceRange.to); + cleanupAfterRemoval(originalSourceRange); + + // Recalculate target range after source removal as the positions have shifted + const updatedTargetRange = HTMLInstrumentation.getPositionFromTagId(editor, targetId); + if (!updatedTargetRange) { + return; + } - // to check if there is only indentation and no text before it - if (targetIndent.trim() === "") { - const finalText = sourceText + '\n' + targetIndent + targetText; - editor.replaceRange(finalText, updatedTargetRange.from, updatedTargetRange.to); + if (insertAfter) { + const insertPos = { + line: updatedTargetRange.to.line, + ch: updatedTargetRange.to.ch + }; + insertElementWithIndentation(insertPos, true, true); } else { - editor.replaceRange(sourceText + targetText, updatedTargetRange.from, updatedTargetRange.to); + // Insert before target + insertElementWithIndentation(updatedTargetRange.from, false, true); } } }); From 4de8d9a05b80e4e7bfbe18fa06d2e6f86dc40c24 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 27 Jul 2025 20:43:04 +0530 Subject: [PATCH 068/849] fix: move cursor to end of the text content when editing a text --- .../BrowserScripts/RemoteFunctions.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 944f91eec4..340327b9f3 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2027,6 +2027,19 @@ function RemoteFunctions(config) { return dismissed; } + /** + * This function is responsible to move the cursor to the end of the text content when we start editing + * @param {DOMElement} element + */ + function moveCursorToEnd(element) { + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(element); + range.collapse(false); + selection.removeAllRanges(); + selection.addRange(range); + } + // Function to handle direct editing of elements in the live preview function startEditing(element) { if (!element) { @@ -2036,6 +2049,8 @@ function RemoteFunctions(config) { // Make the element editable element.setAttribute("contenteditable", "true"); element.focus(); + + moveCursorToEnd(element); dismissMoreOptionsBox(); element._originalContent = cleanupElementProperties(element); @@ -2137,6 +2152,7 @@ function RemoteFunctions(config) { // Remove contenteditable attribute element.removeAttribute("contenteditable"); + dismissMoreOptionsBox(); // Remove event listeners if (element._editListeners) { From 52d90c95eaa6a6ad8f3b15b5c3202b97bcd468ef Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 27 Jul 2025 21:29:28 +0530 Subject: [PATCH 069/849] fix: unable to select text because start editing was called again and again --- .../BrowserScripts/RemoteFunctions.js | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 340327b9f3..76e5b16fbb 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -893,7 +893,7 @@ function RemoteFunctions(config) { // this value decides where we need to show the box in the UI // we are creating this here, because if the element has IDs and Classes then we need to increase the value // so that the box doesn't obscure the element - let pushBoxUp = 28; // px value + let pushBoxUp = 32; // px value // get the ID and classes for that element, as we need to display it in the box const id = this.element.id; @@ -905,7 +905,7 @@ function RemoteFunctions(config) { // Add ID if present if (id) { content += "
#" + id + "
"; - pushBoxUp += 16; + pushBoxUp += 20; } // Add classes (limit to 3 with dropdown indicator) @@ -918,7 +918,7 @@ function RemoteFunctions(config) { content += "+" + (classes.length - 3) + " more"; } content += "
"; - pushBoxUp += 16; + pushBoxUp += 20; } let leftPos = elemBounds.left + scrollLeft; @@ -941,7 +941,6 @@ function RemoteFunctions(config) { // Calculate estimated width based on character count // Formula: base padding + (character count * average character width) const boxWidth = basePadding + (charCount * avgCharWidth); - const boxHeight = 40 + (id ? 16 : 0) + (classes.length > 0 ? 16 : 0); // rough estimate // we need to check for overlap if this is from a click if (this.isFromClick) { @@ -953,16 +952,16 @@ function RemoteFunctions(config) { // Estimate the height of the info box based on its content // base height for tag name + padding - let estimatedHeight = 20; + let estimatedHeight = 32; // height adjustment if ID is present if (id) { - estimatedHeight += 15; + estimatedHeight += 20; } // height adjustment if classes are present if (classes.length > 0) { - estimatedHeight += 15; + estimatedHeight += 20; } // check if element is near bottom of viewport @@ -2031,8 +2030,7 @@ function RemoteFunctions(config) { * This function is responsible to move the cursor to the end of the text content when we start editing * @param {DOMElement} element */ - function moveCursorToEnd(element) { - const selection = window.getSelection(); + function moveCursorToEnd(selection, element) { const range = document.createRange(); range.selectNodeContents(element); range.collapse(false); @@ -2050,7 +2048,12 @@ function RemoteFunctions(config) { element.setAttribute("contenteditable", "true"); element.focus(); - moveCursorToEnd(element); + // Move cursor to end if no existing selection + const selection = window.getSelection(); + if (selection.rangeCount === 0 || selection.isCollapsed) { + moveCursorToEnd(selection, element); + } + dismissMoreOptionsBox(); element._originalContent = cleanupElementProperties(element); From bb9c0ccb161ba27bc3bf71a60a66b72707bb175b Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 28 Jul 2025 00:20:53 +0530 Subject: [PATCH 070/849] fix: text content becomes undefined when user presses escape --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 76e5b16fbb..8b229273ca 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2066,13 +2066,12 @@ function RemoteFunctions(config) { function onKeyDown(event) { if (event.key === "Escape") { // Cancel editing - element.innerHTML = element._originalContent; - finishEditing(element); event.preventDefault(); + finishEditing(element); } else if (event.key === "Enter" && !event.shiftKey) { // Finish editing on Enter (unless Shift is held) - finishEditing(element); event.preventDefault(); + finishEditing(element); } } From 4f37db15d72105123653dc5e1b064e081722191b Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 30 Jul 2025 23:27:38 +0530 Subject: [PATCH 071/849] fix: duplication and deletion gives syntax error when code is not beautified --- src/LiveDevelopment/LivePreviewEdit.js | 49 ++++++++++++++++---------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 83e59fc694..c0f26ffedc 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -1,6 +1,7 @@ define(function (require, exports, module) { const HTMLInstrumentation = require("LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation"); const LiveDevMultiBrowser = require("LiveDevelopment/LiveDevMultiBrowser"); + const CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"); /** * this is a helper function to find the content boundaries in HTML @@ -82,36 +83,42 @@ define(function (require, exports, module) { return; } - // this will give us the start pos and end pos of the DOM element in the source code - // can be referenced using range.from and range.to - const range = HTMLInstrumentation.getPositionFromTagId(editor, tagId); - if (!range) { + // get the start range from the getPositionFromTagId function + // and we get the end range from the findMatchingTag function + // NOTE: we cannot get the end range from getPositionFromTagId + // because on non-beautified code getPositionFromTagId may not provide correct end position + const startRange = HTMLInstrumentation.getPositionFromTagId(editor, tagId); + const endRange = CodeMirror.findMatchingTag(editor._codeMirror, editor.getCursorPos()); + + if (!startRange || !endRange) { return; } + const startPos = startRange.from; + const endPos = endRange.close.to; // this is the actual source code for the element that we need to duplicate - const text = editor.getTextBetween(range.from, range.to); + const text = editor.getTextBetween(startPos, endPos); // this is the indentation on the line - const indent = editor.getTextBetween({ line: range.from.line, ch: 0 }, range.from); + const indent = editor.getTextBetween({ line: startPos.line, ch: 0 }, startPos); editor.document.batchOperation(function () { // make sure there is only indentation and no text before it if (indent.trim() === "") { // this is the position where we need to insert // we're giving the char as 0 because since we insert a new line using '\n' - // that's why writing any char value will not work, as the line is empty + // that's why writing any char value will not work, as the line is emptys // and codemirror doesn't allow to insert at a column (ch) greater than the length of the line // So, the logic is to just append the indent before the text at this insertPos const insertPos = { - line: range.from.line + (range.to.line - range.from.line + 1), + line: startPos.line + (endPos.line - startPos.line + 1), ch: 0 }; - editor.replaceRange("\n", range.to); + editor.replaceRange("\n", endPos); editor.replaceRange(indent + text, insertPos); } else { // if there is some text, we just add the duplicated text right next to it - editor.replaceRange(text, range.from); + editor.replaceRange(text, startPos); } }); } @@ -132,21 +139,27 @@ define(function (require, exports, module) { return; } - // this will give us the start pos and end pos of the DOM element in the source code - // can be referenced using range.from and range.to - const range = HTMLInstrumentation.getPositionFromTagId(editor, tagId); - if (!range) { + // get the start range from the getPositionFromTagId function + // and we get the end range from the findMatchingTag function + // NOTE: we cannot get the end range from getPositionFromTagId + // because on non-beautified code getPositionFromTagId may not provide correct end position + const startRange = HTMLInstrumentation.getPositionFromTagId(editor, tagId); + const endRange = CodeMirror.findMatchingTag(editor._codeMirror, editor.getCursorPos()); + + if (!startRange || !endRange) { return; } + const startPos = startRange.from; + const endPos = endRange.close.to; editor.document.batchOperation(function () { - editor.replaceRange("", range.from, range.to); + editor.replaceRange("", startPos, endPos); // since we remove content from the source, we want to clear the extra line - if(range.from.line !== 0) { - const prevLineText = editor.getLine(range.from.line - 1); + 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: range.from.line - 1, ch: chPrevLine}, range.from); + editor.replaceRange("", {line: startPos.line - 1, ch: chPrevLine}, startPos); } }); } From c24875a5d6cfd87f24e377ff132a30c6474ec5e6 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 30 Jul 2025 23:45:50 +0530 Subject: [PATCH 072/849] fix: use start range from instead of the current cursor position to find the matching tag --- src/LiveDevelopment/LivePreviewEdit.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index c0f26ffedc..e3179636e0 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -88,11 +88,15 @@ define(function (require, exports, module) { // NOTE: we cannot get the end range from getPositionFromTagId // because on non-beautified code getPositionFromTagId may not provide correct end position const startRange = HTMLInstrumentation.getPositionFromTagId(editor, tagId); - const endRange = CodeMirror.findMatchingTag(editor._codeMirror, editor.getCursorPos()); + if(!startRange) { + return; + } - if (!startRange || !endRange) { + const endRange = CodeMirror.findMatchingTag(editor._codeMirror, startRange.from); + if (!endRange) { return; } + const startPos = startRange.from; const endPos = endRange.close.to; @@ -144,11 +148,15 @@ define(function (require, exports, module) { // NOTE: we cannot get the end range from getPositionFromTagId // because on non-beautified code getPositionFromTagId may not provide correct end position const startRange = HTMLInstrumentation.getPositionFromTagId(editor, tagId); - const endRange = CodeMirror.findMatchingTag(editor._codeMirror, editor.getCursorPos()); + if(!startRange) { + return; + } - if (!startRange || !endRange) { + const endRange = CodeMirror.findMatchingTag(editor._codeMirror, startRange.from); + if (!endRange) { return; } + const startPos = startRange.from; const endPos = endRange.close.to; From 018a5868d9c1127492ea4a4d70096b9ae5d065f7 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 30 Jul 2025 23:52:07 +0530 Subject: [PATCH 073/849] fix: use findMatchingTag to get the end position as it is more robust --- src/LiveDevelopment/LivePreviewEdit.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index e3179636e0..09ef5efe27 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -44,12 +44,24 @@ define(function (require, exports, module) { } const editor = currLiveDoc.editor; - const range = HTMLInstrumentation.getPositionFromTagId(editor, message.tagId); - if (!range) { + // get the start range from the getPositionFromTagId function + // and we get the end range from the findMatchingTag function + // NOTE: we cannot get the end range from getPositionFromTagId + // because on non-beautified code getPositionFromTagId may not provide correct end position + const startRange = HTMLInstrumentation.getPositionFromTagId(editor, message.tagId); + if(!startRange) { + return; + } + + const endRange = CodeMirror.findMatchingTag(editor._codeMirror, startRange.from); + if (!endRange) { return; } - const text = editor.getTextBetween(range.from, range.to); + const startPos = startRange.from; + const endPos = endRange.close.to; + + const text = editor.getTextBetween(startPos, endPos); let splittedText; // we need to find the content boundaries to find exactly where the content starts and where it ends @@ -61,7 +73,7 @@ define(function (require, exports, module) { // if the text split was done successfully, apply the edit if (splittedText && splittedText.length === 2) { const finalText = splittedText[0] + message.newContent + splittedText[1]; - editor.replaceRange(finalText, range.from, range.to); + editor.replaceRange(finalText, startPos, endPos); } else { console.error("Live preview text edit operation failed."); } From 4fc035ec7579d493366487772d532b6200bba6f2 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 31 Jul 2025 00:17:31 +0530 Subject: [PATCH 074/849] fix: fix architecture of move function by using the getMatchingTag function to get the end pos --- src/LiveDevelopment/LivePreviewEdit.js | 191 ++++++++++++++++--------- 1 file changed, 125 insertions(+), 66 deletions(-) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 09ef5efe27..6fc3f5a962 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -184,6 +184,67 @@ define(function (require, exports, module) { }); } + /** + * this function is to clean up the empty lines after an element is removed + * @param {Object} editor - the editor instance + * @param {Object} range - the range where element was removed + */ + function _cleanupAfterRemoval(editor, range) { + const lineToCheck = range.from.line; + + // check if the line where element was removed is now empty + if (lineToCheck < editor.lineCount()) { + const currentLineText = editor.getLine(lineToCheck); + if (currentLineText && currentLineText.trim() === "") { + // remove the empty line + const lineStart = { line: lineToCheck, ch: 0 }; + const lineEnd = { line: lineToCheck + 1, ch: 0 }; + editor.replaceRange("", lineStart, lineEnd); + } + } + + // also we need to check the previous line if it became empty + if (lineToCheck > 0) { + const prevLineText = editor.getLine(lineToCheck - 1); + if (prevLineText && prevLineText.trim() === "") { + const lineStart = { line: lineToCheck - 1, ch: 0 }; + const lineEnd = { line: lineToCheck, ch: 0 }; + editor.replaceRange("", lineStart, lineEnd); + } + } + } + + /** + * this function is to make sure that we insert elements with proper indentation + * + * @param {Object} editor - the editor instance + * @param {Object} insertPos - position where to insert + * @param {Boolean} insertAfterMode - whether to insert after the position + * @param {String} targetIndent - the indentation to use + * @param {String} sourceText - the text to insert + */ + function _insertElementWithIndentation(editor, insertPos, insertAfterMode, targetIndent, sourceText) { + if (insertAfterMode) { + // Insert after the target element + editor.replaceRange("\n" + targetIndent + sourceText, insertPos); + } else { + // Insert before the target element + const insertLine = insertPos.line; + const lineStart = { line: insertLine, ch: 0 }; + + // Get current line content to preserve any existing indentation structure + const currentLine = editor.getLine(insertLine); + + if (currentLine && currentLine.trim() === "") { + // the line is empty, replace it entirely + editor.replaceRange(targetIndent + sourceText, lineStart, { line: insertLine, ch: currentLine.length }); + } else { + // the line has content, insert before it + editor.replaceRange(targetIndent + sourceText + "\n", lineStart); + } + } + } + /** * This function is responsible for moving an element from one position to another in the source code * it is called when there is drag-drop in the live preview @@ -203,14 +264,40 @@ define(function (require, exports, module) { return; } - // position of source and target elements in the editor - const sourceRange = HTMLInstrumentation.getPositionFromTagId(editor, sourceId); - const targetRange = HTMLInstrumentation.getPositionFromTagId(editor, targetId); + // get the start range from the getPositionFromTagId function + // and we get the end range from the findMatchingTag function + // NOTE: we cannot get the end range from getPositionFromTagId + // because on non-beautified code getPositionFromTagId may not provide correct end position + const sourceStartRange = HTMLInstrumentation.getPositionFromTagId(editor, sourceId); + if(!sourceStartRange) { + return; + } + + const sourceEndRange = CodeMirror.findMatchingTag(editor._codeMirror, sourceStartRange.from); + if (!sourceEndRange) { + return; + } + + const targetStartRange = HTMLInstrumentation.getPositionFromTagId(editor, targetId); + if(!targetStartRange) { + return; + } - if (!sourceRange || !targetRange) { + const targetEndRange = CodeMirror.findMatchingTag(editor._codeMirror, targetStartRange.from); + if (!targetEndRange) { return; } + const sourceRange = { + from: sourceStartRange.from, + to: sourceEndRange.close.to + }; + + const targetRange = { + from: targetStartRange.from, + to: targetEndRange.close.to + }; + const sourceText = editor.getTextBetween(sourceRange.from, sourceRange.to); const targetIndent = editor.getTextBetween({ line: targetRange.from.line, ch: 0 }, targetRange.from); @@ -223,57 +310,6 @@ define(function (require, exports, module) { sourceRange.from.line < targetRange.from.line || (sourceRange.from.line === targetRange.from.line && sourceRange.from.ch < targetRange.from.ch); - // this function is to clean up the empty lines after an element is removed - function cleanupAfterRemoval(range) { - const lineToCheck = range.from.line; - - // check if the line where element was removed is now empty - if (lineToCheck < editor.lineCount()) { - const currentLineText = editor.getLine(lineToCheck); - if (currentLineText && currentLineText.trim() === "") { - // remove the empty line - const lineStart = { line: lineToCheck, ch: 0 }; - const lineEnd = { line: lineToCheck + 1, ch: 0 }; - editor.replaceRange("", lineStart, lineEnd); - } - } - - // also we need to check the previous line if it became empty - if (lineToCheck > 0) { - const prevLineText = editor.getLine(lineToCheck - 1); - if (prevLineText && prevLineText.trim() === "") { - const lineStart = { line: lineToCheck - 1, ch: 0 }; - const lineEnd = { line: lineToCheck, ch: 0 }; - editor.replaceRange("", lineStart, lineEnd); - } - } - } - - // this function is to make sure that we insert elements with proper indentation - function insertElementWithIndentation(insertPos, insertAfterMode, useTargetIndent) { - const indent = useTargetIndent ? targetIndent : targetIndent; - - if (insertAfterMode) { - // Insert after the target element - editor.replaceRange("\n" + indent + sourceText, insertPos); - } else { - // Insert before the target element - const insertLine = insertPos.line; - const lineStart = { line: insertLine, ch: 0 }; - - // Get current line content to preserve any existing indentation structure - const currentLine = editor.getLine(insertLine); - - if (currentLine && currentLine.trim() === "") { - // the line is empty, replace it entirely - editor.replaceRange(indent + sourceText, lineStart, { line: insertLine, ch: currentLine.length }); - } else { - // the line has content, insert before it - editor.replaceRange(indent + sourceText + "\n", lineStart); - } - } - } - // creating a batch operation so that undo in live preview works fine editor.document.batchOperation(function () { if (sourceBeforeTarget) { @@ -283,17 +319,27 @@ define(function (require, exports, module) { line: targetRange.to.line, ch: targetRange.to.ch }; - insertElementWithIndentation(insertPos, true, true); + _insertElementWithIndentation(editor, insertPos, true, targetIndent, sourceText); } else { // insert before target - insertElementWithIndentation(targetRange.from, false, true); + _insertElementWithIndentation(editor, targetRange.from, false, targetIndent, sourceText); } // Now remove the source element (NOTE: the positions have shifted) - const updatedSourceRange = HTMLInstrumentation.getPositionFromTagId(editor, sourceId); - if (updatedSourceRange) { - editor.replaceRange("", updatedSourceRange.from, updatedSourceRange.to); - cleanupAfterRemoval(updatedSourceRange); + const updatedSourceStartRange = HTMLInstrumentation.getPositionFromTagId(editor, sourceId); + if (updatedSourceStartRange) { + const updatedSourceEndRange = CodeMirror.findMatchingTag( + editor._codeMirror, updatedSourceStartRange.from + ); + + if (updatedSourceEndRange) { + const updatedSourceRange = { + from: updatedSourceStartRange.from, + to: updatedSourceEndRange.close.to + }; + editor.replaceRange("", updatedSourceRange.from, updatedSourceRange.to); + _cleanupAfterRemoval(editor, updatedSourceRange); + } } } else { // This handles the case when target is before source: remove first, then insert @@ -302,23 +348,36 @@ define(function (require, exports, module) { // Remove the source element first editor.replaceRange("", sourceRange.from, sourceRange.to); - cleanupAfterRemoval(originalSourceRange); + _cleanupAfterRemoval(editor, originalSourceRange); // Recalculate target range after source removal as the positions have shifted - const updatedTargetRange = HTMLInstrumentation.getPositionFromTagId(editor, targetId); - if (!updatedTargetRange) { + const updatedTargetStartRange = HTMLInstrumentation.getPositionFromTagId(editor, targetId); + if (!updatedTargetStartRange) { return; } + const updatedTargetEndRange = CodeMirror.findMatchingTag( + editor._codeMirror, updatedTargetStartRange.from + ); + + if (!updatedTargetEndRange) { + return; + } + + const updatedTargetRange = { + from: updatedTargetStartRange.from, + to: updatedTargetEndRange.close.to + }; + if (insertAfter) { const insertPos = { line: updatedTargetRange.to.line, ch: updatedTargetRange.to.ch }; - insertElementWithIndentation(insertPos, true, true); + _insertElementWithIndentation(editor, insertPos, true, targetIndent, sourceText); } else { // Insert before target - insertElementWithIndentation(updatedTargetRange.from, false, true); + _insertElementWithIndentation(editor, updatedTargetRange.from, false, targetIndent, sourceText); } } }); From ae230eae50f8fcc93038fc81574b6c023fab9298 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 1 Aug 2025 00:18:41 +0530 Subject: [PATCH 075/849] fix: browser removing incorrect css syntax from code which is unintended behaviour --- .../BrowserScripts/RemoteFunctions.js | 135 +------------- src/LiveDevelopment/LivePreviewEdit.js | 167 ++++++++++++++---- 2 files changed, 138 insertions(+), 164 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 8b229273ca..79e2ba865f 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1905,57 +1905,6 @@ function RemoteFunctions(config) { _editHandler.apply(edits); } - /** - * - * @param {Element} elem - */ - function _domElementToJSON(elem) { - var json = { tag: elem.tagName.toLowerCase(), attributes: {}, children: [] }, - i, - len, - node, - value; - - len = elem.attributes.length; - for (i = 0; i < len; i++) { - node = elem.attributes.item(i); - - // skip internal attributes that shouldn't be serialized - if (node.name === "draggable" && node.value === "true") { - continue; - } - value = (node.name === "data-brackets-id") ? parseInt(node.value, 10) : node.value; - // Clean internal style properties - if (node.name === "style") { - const cleanedStyle = _cleanInternalStyles(value); - - if (cleanedStyle.trim() === '') { - continue; // Skip empty style attribute - } - value = cleanedStyle; - } - json.attributes[node.name] = value; - } - - len = elem.childNodes.length; - for (i = 0; i < len; i++) { - node = elem.childNodes.item(i); - - // ignores comment nodes and visuals generated by live preview - if (node.nodeType === Node.ELEMENT_NODE && node.className !== HIGHLIGHT_CLASSNAME) { - json.children.push(_domElementToJSON(node)); - } else if (node.nodeType === Node.TEXT_NODE) { - json.children.push({ content: node.nodeValue }); - } - } - - return json; - } - - function getSimpleDOM() { - return JSON.stringify(_domElementToJSON(window.document.documentElement)); - } - function updateConfig(newConfig) { var oldConfig = config; config = JSON.parse(newConfig); @@ -2056,9 +2005,6 @@ function RemoteFunctions(config) { dismissMoreOptionsBox(); - element._originalContent = cleanupElementProperties(element); - - // Add event listeners for editing function onBlur() { finishEditing(element); } @@ -2067,7 +2013,7 @@ function RemoteFunctions(config) { if (event.key === "Escape") { // Cancel editing event.preventDefault(); - finishEditing(element); + finishEditing(element, false); // false means that the edit operation was cancelled } else if (event.key === "Enter" && !event.shiftKey) { // Finish editing on Enter (unless Shift is held) event.preventDefault(); @@ -2085,69 +2031,9 @@ function RemoteFunctions(config) { }; } - // Helper function to clean internal style properties - function _cleanInternalStyles(styleValue) { - if (typeof styleValue !== "string") { - return styleValue; - } - - let cleanedStyle = styleValue; - - // remove internal background color - cleanedStyle = cleanedStyle.replace(/background-color:\s*rgba\(0,\s*162,\s*255,\s*0\.2\)\s*;?\s*/gi, ""); - - // remove internal outline - cleanedStyle = cleanedStyle.replace(/outline:\s*rgb\(66,\s*133,\s*244\)\s+solid\s+1px\s*;?\s*/gi, ""); - cleanedStyle = cleanedStyle.replace(/outline:\s*1px\s+solid\s+#4285F4\s*;?\s*/gi, ""); - - // clean up any extra spaces or semicolons - cleanedStyle = cleanedStyle - .replace(/;\s*;/g, ";") - .replace(/^\s*;\s*/, "") - .replace(/\s*;\s*$/, ""); - - return cleanedStyle; - } - - // this function is to remove the internal properties from elements before getting the innerHTML - // then add all the properties back to the elements - // internal properties such as 'data-brackets-id', 'data-ld-highlight' etc - function cleanupElementProperties(element) { - const clone = element.cloneNode(true); - const allElements = [clone, ...clone.querySelectorAll('*')]; - - allElements.forEach(el => { - // Remove Phoenix internal attributes - if (el.hasAttribute('data-brackets-id')) { - el.removeAttribute('data-brackets-id'); - } - if (el.hasAttribute('data-ld-highlight')) { - el.removeAttribute('data-ld-highlight'); - } - - // remove the draggable attribute added internally - if (el.hasAttribute('draggable')) { - el.removeAttribute('draggable'); - } - - // remove internal style properties - if (el.hasAttribute('style')) { - const style = el.getAttribute('style'); - const cleanedStyle = _cleanInternalStyles(style); - - if (cleanedStyle.trim() === '') { - el.removeAttribute('style'); - } else { - el.setAttribute('style', cleanedStyle); - } - } - }); - - return clone.innerHTML; - } - // Function to finish editing and apply changes - function finishEditing(element) { + // isEditSuccessful: this is a boolean value, defaults to true. false only when the edit operation is cancelled + function finishEditing(element, isEditSuccessful = true) { if (!element || !element.hasAttribute("contenteditable")) { return; } @@ -2163,23 +2049,17 @@ function RemoteFunctions(config) { delete element._editListeners; } - // Get the new content after cleaning up unwanted properties - const newContent = cleanupElementProperties(element); - - // If content has changed, send the edit to the editor - if (newContent !== element._originalContent && element.hasAttribute("data-brackets-id")) { + if (element.hasAttribute("data-brackets-id")) { const tagId = element.getAttribute("data-brackets-id"); window._Brackets_MessageBroker.send({ livePreviewEditEnabled: true, + livePreviewTextEdit: true, element: element, - newContent: newContent, + newContent: element.outerHTML, tagId: Number(tagId), - livePreviewTextEdit: true + isEditSuccessful: isEditSuccessful }); } - - // Clean up - delete element._originalContent; } // init @@ -2209,7 +2089,6 @@ function RemoteFunctions(config) { "redrawHighlights" : redrawHighlights, "redrawEverything" : redrawEverything, "applyDOMEdits" : applyDOMEdits, - "getSimpleDOM" : getSimpleDOM, "updateConfig" : updateConfig, "startEditing" : startEditing, "finishEditing" : finishEditing, diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 6fc3f5a962..fa2846d453 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -4,39 +4,131 @@ define(function (require, exports, module) { const CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"); /** - * this is a helper function to find the content boundaries in HTML - * @param {string} html - The HTML string to parse - * @return {Object} - Object with openTag and closeTag properties + * This function is to sync text content changes between the original source code + * and the live preview DOM after a text edit operation + * + * @param {String} oldContent - the original source code from the editor + * @param {String} newContent - the DOM element's outerHTML after editing in live preview + * @returns {String} - the updated content that should replace the original code in the editor + * + * NOTE: This function is a bit complex to read, read this jsdoc to understand the flow: + * + * First, we parse both the old and new content using DOMParser to get proper HTML DOM structures + * Then we compare each element and text node between the old and new content + * + * the main goal is that we ONLY want to update text content, and not element nodes or their attributes + * because if we allow element/attribute changes, the browser might try to fix the HTML + * to make it syntactically correct or to make it efficient, which would mess up the user's original code + * We don't want that - we need to respect how the user wrote their code + * For example: if user wrote
+ * The browser sees this is invalid CSS and would remove the color attribute entirely + * We want to keep that invalid code as it is because it's what the user wanted to do + * + * Here's how the comparison works: + * - if both nodes are text: update the old text with the new text + * - if both nodes are elements: we recursively check their children (for nested content) + * - if old is text, new is element: replace text with element (like when user adds
) + * - if old is element, new is text: replace element with text (like when user removes
) + * note: when adding new elements (like
tags), we only copy the tag name and content, + * never the attributes, to avoid internal Phoenix properties leaking into user's code */ - function _findContentBoundaries(html) { - const openTagEnd = html.indexOf(">") + 1; - const closeTagStart = html.lastIndexOf("<"); - - if (openTagEnd > 0 && closeTagStart >= openTagEnd) { - return { - openTag: html.substring(0, openTagEnd), - closeTag: html.substring(closeTagStart) - }; + function _syncTextContentChanges(oldContent, newContent) { + const parser = new DOMParser(); + const oldDoc = parser.parseFromString(oldContent, "text/html"); + const newDoc = parser.parseFromString(newContent, "text/html"); + + // as DOM parser will add the complete html structure with the HTML tags and all, + // so we just need to get the main content + const oldRoot = oldDoc.body; + const newRoot = newDoc.body; + + // here oldNode and newNode are full HTML elements which are direct children of the body tag + function syncText(oldNode, newNode) { + if (!oldNode || !newNode) { + return; + } + + // if both the oldNode and newNode has text, replace the old node's text content with the new one + if (oldNode.nodeType === Node.TEXT_NODE && newNode.nodeType === Node.TEXT_NODE) { + oldNode.nodeValue = newNode.nodeValue; + + } else if ( + // if both have element node, then we recursively get their child elements + // this is so that we can get & update the text content in deeply nested DOM + oldNode.nodeType === Node.ELEMENT_NODE && + newNode.nodeType === Node.ELEMENT_NODE + ) { + + const oldChildren = oldNode.childNodes; + const newChildren = newNode.childNodes; + + const minLength = Math.min(oldChildren.length, newChildren.length); + + for (let i = 0; i < minLength; i++) { + syncText(oldChildren[i], newChildren[i]); + } + + // append if there are any new nodes, this is mainly when
tags needs to be inserted + // as user pressed shift + enter to create empty lines in the new content + for (let i = minLength; i < newChildren.length; i++) { + const newChild = newChildren[i]; + let cleanChild; + + if (newChild.nodeType === Node.ELEMENT_NODE) { + // only the element name and not its attributes + // this is to prevent internal properties like data-brackets-id, etc to appear in users code + cleanChild = document.createElement(newChild.tagName); + cleanChild.innerHTML = newChild.innerHTML; + } else { + // for text nodes, comment nodes, etc. clone normally + cleanChild = newChild.cloneNode(true); + } + + oldNode.appendChild(cleanChild); + } + + // remove extra old nodes (maybe extra
's were removed) + for (let i = oldChildren.length - 1; i >= newChildren.length; i--) { + oldNode.removeChild(oldChildren[i]); + } + + } else if (oldNode.nodeType === Node.TEXT_NODE && newNode.nodeType === Node.ELEMENT_NODE) { + // when old has text node and new has element node + // this generally happens when we remove the complete content which results in empty
tag + // for ex:
hello
, here if we remove the 'hello' from live preview then result will be + //

+ const replacement = document.createElement(newNode.tagName); + replacement.innerHTML = newNode.innerHTML; + oldNode.parentNode.replaceChild(replacement, oldNode); + } else if (oldNode.nodeType === Node.ELEMENT_NODE && newNode.nodeType === Node.TEXT_NODE) { + // this is opposite of previous one when earlier it was just
or some tag + // and now we add text content in that + const replacement = document.createTextNode(newNode.nodeValue); + oldNode.parentNode.replaceChild(replacement, oldNode); + } + } + + const oldEls = oldRoot.children; + const newEls = newRoot.children; + + for (let i = 0; i < Math.min(oldEls.length, newEls.length); i++) { + syncText(oldEls[i], newEls[i]); } - return null; + return oldRoot.innerHTML; } /** * this function handles the text edit in the source code when user updates the text in the live preview + * * @param {Object} message - the message object - * { - * livePreviewEditEnabled: true, - element: the DOM element that was modified, - oldContent: the text that was present before the edit, - newContent: the new text, - tagId: data-brackets-id of the DOM element, - livePreviewTextEdit: true - } - * - * The logic is: get the text in the editor using the tagId. split that text using the old content - * join the text back and add the new content in between - */ + * - livePreviewEditEnabled: true + * - livePreviewTextEdit: true + * - element: element + * - newContent: element.outerHTML (the edited content from live preview) + * - tagId: Number (data-brackets-id of the edited element) + * - isEditSuccessful: boolean (false when user pressed Escape to cancel, otherwise true always) + */ function _editTextInSource(message) { const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); if (!currLiveDoc || !currLiveDoc.editor || !message.tagId) { @@ -44,6 +136,7 @@ define(function (require, exports, module) { } const editor = currLiveDoc.editor; + // get the start range from the getPositionFromTagId function // and we get the end range from the findMatchingTag function // NOTE: we cannot get the end range from getPositionFromTagId @@ -62,20 +155,22 @@ define(function (require, exports, module) { const endPos = endRange.close.to; const text = editor.getTextBetween(startPos, endPos); - let splittedText; - // we need to find the content boundaries to find exactly where the content starts and where it ends - const boundaries = _findContentBoundaries(text); - if (boundaries) { - splittedText = [boundaries.openTag, boundaries.closeTag]; - } + // if the edit was cancelled (mainly by pressing Escape key) + // we just replace the same text with itself + // this is a quick trick because as the code is changed for that element in the file, + // the live preview for that element gets refreshed and the changes are discarded in the live preview + if(!message.isEditSuccessful) { + editor.replaceRange(text, startPos, endPos); + } else { - // if the text split was done successfully, apply the edit - if (splittedText && splittedText.length === 2) { - const finalText = splittedText[0] + message.newContent + splittedText[1]; + // if the edit operation was successful, we call a helper function that + // is responsible to provide the actual content that needs to be written in the editor + // + // text: the actual current source code in the editor + // message.newContent: the new content in the live preview after the edit operation + const finalText = _syncTextContentChanges(text, message.newContent); editor.replaceRange(finalText, startPos, endPos); - } else { - console.error("Live preview text edit operation failed."); } } From 38080946cea2648d611e69816fa271f195a1c3b7 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 1 Aug 2025 12:55:13 +0530 Subject: [PATCH 076/849] refactor: remove dead code --- .../BrowserScripts/RemoteFunctions.js | 124 +----------------- 1 file changed, 1 insertion(+), 123 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 79e2ba865f..0bb9bf2d25 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -38,12 +38,6 @@ 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; - var experimental; - if (!config) { - experimental = false; - } else { - experimental = config.experimental; - } var req, timeout; var animateHighlight = function (time) { if(req) { @@ -76,19 +70,6 @@ function RemoteFunctions(config) { } } - // determine the color for a type - function _typeColor(type, highlight) { - switch (type) { - case "html": - return highlight ? "#eec" : "#ffe"; - case "css": - return highlight ? "#cee" : "#eff"; - case "js": - return highlight ? "#ccf" : "#eef"; - default: - return highlight ? "#ddd" : "#eee"; - } - } // compute the screen offset of an element function _screenOffset(element) { @@ -138,87 +119,6 @@ function RemoteFunctions(config) { return element.offsetTop + (element.offsetParent ? getDocumentOffsetTop(element.offsetParent) : 0); } - // construct the info menu - function Menu(element) { - this.element = element; - _trigger(this.element, "showgoto", 1, true); - window.setTimeout(window.remoteShowGoto); - this.remove = this.remove.bind(this); - } - - Menu.prototype = { - onClick: function (url, event) { - event.preventDefault(); - _trigger(this.element, "goto", url, true); - this.remove(); - }, - - createBody: function () { - if (this.body) { - return; - } - - // compute the position on screen - var offset = _screenOffset(this.element), - x = offset.left, - y = offset.top + this.element.offsetHeight; - - // create the container - this.body = window.document.createElement("div"); - this.body.style.setProperty("z-index", 2147483647); - this.body.style.setProperty("position", "absolute"); - this.body.style.setProperty("left", x + "px"); - this.body.style.setProperty("top", y + "px"); - this.body.style.setProperty("font-size", "11pt"); - - // draw the background - this.body.style.setProperty("background", "#fff"); - this.body.style.setProperty("border", "1px solid #888"); - this.body.style.setProperty("-webkit-box-shadow", "2px 2px 6px 0px #ccc"); - this.body.style.setProperty("border-radius", "6px"); - this.body.style.setProperty("padding", "6px"); - }, - - addItem: function (target) { - var item = window.document.createElement("div"); - item.style.setProperty("padding", "2px 6px"); - if (this.body.childNodes.length > 0) { - item.style.setProperty("border-top", "1px solid #ccc"); - } - item.style.setProperty("cursor", "pointer"); - item.style.setProperty("background", _typeColor(target.type)); - item.innerHTML = target.name; - item.addEventListener("click", this.onClick.bind(this, target.url)); - - if (target.file) { - var file = window.document.createElement("i"); - file.style.setProperty("float", "right"); - file.style.setProperty("margin-left", "12px"); - file.innerHTML = " " + target.file; - item.appendChild(file); - } - this.body.appendChild(item); - }, - - show: function () { - if (!this.body) { - this.createBody(); - } - if (!this.body.parentNode) { - window.document.body.appendChild(this.body); - } - window.document.addEventListener("click", this.remove); - }, - - remove: function () { - if (this.body && this.body.parentNode) { - window.document.body.removeChild(this.body); - } - window.document.removeEventListener("click", this.remove); - } - - }; - /** * This function gets called when the delete button is clicked * it sends a message to the editor using postMessage to delete the element from the source code @@ -1354,9 +1254,7 @@ function RemoteFunctions(config) { } }; - var _currentMenu; var _localHighlight; - var _remoteHighlight; var _hoverHighlight; var _clickHighlight; var _nodeInfoBox; @@ -1541,25 +1439,9 @@ function RemoteFunctions(config) { /** Public Commands **********************************************************/ - // show goto - function showGoto(targets) { - if (!_currentMenu) { - return; - } - _currentMenu.createBody(); - var i; - for (i in targets) { - _currentMenu.addItem(targets[i]); - } - _currentMenu.show(); - } // remove active highlights function hideHighlight() { - if (_remoteHighlight) { - _remoteHighlight.clear(); - _remoteHighlight = null; - } if (_clickHighlight) { _clickHighlight.clear(); _clickHighlight = null; @@ -1610,9 +1492,6 @@ function RemoteFunctions(config) { // redraw active highlights function redrawHighlights() { - if (_remoteHighlight) { - _remoteHighlight.redraw(); - } if (_clickHighlight) { _clickHighlight.redraw(); } @@ -1637,7 +1516,7 @@ function RemoteFunctions(config) { if (e.target === window.document) { redrawHighlights(); } else { - if (_remoteHighlight || _localHighlight || _clickHighlight || _hoverHighlight) { + if (_localHighlight || _clickHighlight || _hoverHighlight) { window.setTimeout(redrawHighlights, 0); } } @@ -2082,7 +1961,6 @@ function RemoteFunctions(config) { return { "DOMEditHandler" : DOMEditHandler, - "showGoto" : showGoto, "hideHighlight" : hideHighlight, "highlight" : highlight, "highlightRule" : highlightRule, From 36a0fdd7c8bde395f4b629bad6d93ddd7ce17bd6 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 1 Aug 2025 13:35:33 +0530 Subject: [PATCH 077/849] fix: operations doesn't work for empty tags as we were relying on the end tag --- src/LiveDevelopment/LivePreviewEdit.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index fa2846d453..236566da81 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -152,7 +152,8 @@ define(function (require, exports, module) { } const startPos = startRange.from; - const endPos = endRange.close.to; + // for empty tags endRange.close might not exist, for ex: img tag + const endPos = endRange.close ? endRange.close.to : endRange.open.to; const text = editor.getTextBetween(startPos, endPos); @@ -205,7 +206,8 @@ define(function (require, exports, module) { } const startPos = startRange.from; - const endPos = endRange.close.to; + // for empty tags endRange.close might not exist, for ex: img tag + const endPos = endRange.close ? endRange.close.to : endRange.open.to; // this is the actual source code for the element that we need to duplicate const text = editor.getTextBetween(startPos, endPos); @@ -265,7 +267,8 @@ define(function (require, exports, module) { } const startPos = startRange.from; - const endPos = endRange.close.to; + // for empty tags endRange.close might not exist, for ex: img tag + const endPos = endRange.close ? endRange.close.to : endRange.open.to; editor.document.batchOperation(function () { editor.replaceRange("", startPos, endPos); @@ -385,12 +388,12 @@ define(function (require, exports, module) { const sourceRange = { from: sourceStartRange.from, - to: sourceEndRange.close.to + to: sourceEndRange.close ? sourceEndRange.close.to : sourceEndRange.open.to }; const targetRange = { from: targetStartRange.from, - to: targetEndRange.close.to + to: targetEndRange.close ? targetEndRange.close.to : targetEndRange.open.to }; const sourceText = editor.getTextBetween(sourceRange.from, sourceRange.to); @@ -430,7 +433,9 @@ define(function (require, exports, module) { if (updatedSourceEndRange) { const updatedSourceRange = { from: updatedSourceStartRange.from, - to: updatedSourceEndRange.close.to + to: updatedSourceEndRange.close + ? updatedSourceEndRange.close.to + : updatedSourceEndRange.open.to }; editor.replaceRange("", updatedSourceRange.from, updatedSourceRange.to); _cleanupAfterRemoval(editor, updatedSourceRange); @@ -461,7 +466,7 @@ define(function (require, exports, module) { const updatedTargetRange = { from: updatedTargetStartRange.from, - to: updatedTargetEndRange.close.to + to: updatedTargetEndRange.close ? updatedTargetEndRange.close.to : updatedTargetEndRange.open.to }; if (insertAfter) { From 943f651bc5e3e5f50d23c10ecf8471a348764a93 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 1 Aug 2025 19:29:48 +0530 Subject: [PATCH 078/849] refactor: allow buttons to be editable --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 0bb9bf2d25..583b15d299 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -501,7 +501,6 @@ function RemoteFunctions(config) { "audio", "iframe", "object", - "button", "select", "textarea" ]; From e67fd2d4565026870789d5d5a29b28c9824bfcb6 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 2 Aug 2025 12:32:42 +0530 Subject: [PATCH 079/849] fix: text formatting not working when trying to edit text in live preview --- src/LiveDevelopment/LivePreviewEdit.js | 130 +++++++++++-------------- 1 file changed, 56 insertions(+), 74 deletions(-) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 236566da81..100bd2fb6c 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -4,112 +4,94 @@ define(function (require, exports, module) { const CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"); /** - * This function is to sync text content changes between the original source code - * and the live preview DOM after a text edit operation + * This function syncs text content changes between the original source code + * and the live preview DOM after a text edit in the browser * + * @private * @param {String} oldContent - the original source code from the editor - * @param {String} newContent - the DOM element's outerHTML after editing in live preview - * @returns {String} - the updated content that should replace the original code in the editor + * @param {String} newContent - the outerHTML after editing in live preview + * @returns {String} - the updated content that should replace the original editor code * - * NOTE: This function is a bit complex to read, read this jsdoc to understand the flow: + * NOTE: We don’t touch tag names or attributes — + * we only care about text changes or things like newlines,
, or formatting like , , etc. * - * First, we parse both the old and new content using DOMParser to get proper HTML DOM structures - * Then we compare each element and text node between the old and new content + * Here's the basic idea: + * - Parse both old and new HTML strings into DOM trees + * - Then walk both DOMs side by side and sync changes * - * the main goal is that we ONLY want to update text content, and not element nodes or their attributes - * because if we allow element/attribute changes, the browser might try to fix the HTML - * to make it syntactically correct or to make it efficient, which would mess up the user's original code - * We don't want that - we need to respect how the user wrote their code - * For example: if user wrote
- * The browser sees this is invalid CSS and would remove the color attribute entirely - * We want to keep that invalid code as it is because it's what the user wanted to do + * What we handle: + * - if both are text nodes → update the text if changed + * - if both are elements with same tag → go deeper and sync their children + * - if one is text and one is an element → replace (like when user adds/removes
or adds bold/italic) + * - if a node got added or removed → do that in the old DOM * - * Here's how the comparison works: - * - if both nodes are text: update the old text with the new text - * - if both nodes are elements: we recursively check their children (for nested content) - * - if old is text, new is element: replace text with element (like when user adds
) - * - if old is element, new is text: replace element with text (like when user removes
) - * note: when adding new elements (like
tags), we only copy the tag name and content, - * never the attributes, to avoid internal Phoenix properties leaking into user's code + * We don’t recreate or touch existing elements unless absolutely needed, + * so all original user-written attributes and tag structure stay exactly the same. + * + * This avoids the browser trying to “fix” broken HTML (which we don’t want) */ function _syncTextContentChanges(oldContent, newContent) { const parser = new DOMParser(); const oldDoc = parser.parseFromString(oldContent, "text/html"); const newDoc = parser.parseFromString(newContent, "text/html"); - // as DOM parser will add the complete html structure with the HTML tags and all, - // so we just need to get the main content const oldRoot = oldDoc.body; const newRoot = newDoc.body; - // here oldNode and newNode are full HTML elements which are direct children of the body tag function syncText(oldNode, newNode) { if (!oldNode || !newNode) { return; } - // if both the oldNode and newNode has text, replace the old node's text content with the new one + // when both are text nodes, we just need to replace the old text with the new one if (oldNode.nodeType === Node.TEXT_NODE && newNode.nodeType === Node.TEXT_NODE) { - oldNode.nodeValue = newNode.nodeValue; - - } else if ( - // if both have element node, then we recursively get their child elements - // this is so that we can get & update the text content in deeply nested DOM - oldNode.nodeType === Node.ELEMENT_NODE && - newNode.nodeType === Node.ELEMENT_NODE - ) { + if (oldNode.nodeValue !== newNode.nodeValue) { + oldNode.nodeValue = newNode.nodeValue; + } + return; + } - const oldChildren = oldNode.childNodes; - const newChildren = newNode.childNodes; + // when both are elements + if (oldNode.nodeType === Node.ELEMENT_NODE && newNode.nodeType === Node.ELEMENT_NODE) { + const oldChildren = Array.from(oldNode.childNodes); + const newChildren = Array.from(newNode.childNodes); - const minLength = Math.min(oldChildren.length, newChildren.length); + const maxLen = Math.max(oldChildren.length, newChildren.length); - for (let i = 0; i < minLength; i++) { - syncText(oldChildren[i], newChildren[i]); - } - - // append if there are any new nodes, this is mainly when
tags needs to be inserted - // as user pressed shift + enter to create empty lines in the new content - for (let i = minLength; i < newChildren.length; i++) { + for (let i = 0; i < maxLen; i++) { + const oldChild = oldChildren[i]; const newChild = newChildren[i]; - let cleanChild; - if (newChild.nodeType === Node.ELEMENT_NODE) { - // only the element name and not its attributes - // this is to prevent internal properties like data-brackets-id, etc to appear in users code - cleanChild = document.createElement(newChild.tagName); - cleanChild.innerHTML = newChild.innerHTML; + if (!oldChild && newChild) { + // if new child added → clone and insert + oldNode.appendChild(newChild.cloneNode(true)); + } else if (oldChild && !newChild) { + // if child removed → delete + oldNode.removeChild(oldChild); + } else if ( + oldChild.nodeType === newChild.nodeType && + oldChild.nodeType === Node.ELEMENT_NODE && + oldChild.tagName === newChild.tagName + ) { + // same element tag → sync recursively + syncText(oldChild, newChild); + } else if ( + oldChild.nodeType === Node.TEXT_NODE && + newChild.nodeType === Node.TEXT_NODE + ) { + if (oldChild.nodeValue !== newChild.nodeValue) { + oldChild.nodeValue = newChild.nodeValue; + } } else { - // for text nodes, comment nodes, etc. clone normally - cleanChild = newChild.cloneNode(true); + // different node types or tags → replace + oldNode.replaceChild(newChild.cloneNode(true), oldChild); } - - oldNode.appendChild(cleanChild); } - - // remove extra old nodes (maybe extra
's were removed) - for (let i = oldChildren.length - 1; i >= newChildren.length; i--) { - oldNode.removeChild(oldChildren[i]); - } - - } else if (oldNode.nodeType === Node.TEXT_NODE && newNode.nodeType === Node.ELEMENT_NODE) { - // when old has text node and new has element node - // this generally happens when we remove the complete content which results in empty
tag - // for ex:
hello
, here if we remove the 'hello' from live preview then result will be - //

- const replacement = document.createElement(newNode.tagName); - replacement.innerHTML = newNode.innerHTML; - oldNode.parentNode.replaceChild(replacement, oldNode); - } else if (oldNode.nodeType === Node.ELEMENT_NODE && newNode.nodeType === Node.TEXT_NODE) { - // this is opposite of previous one when earlier it was just
or some tag - // and now we add text content in that - const replacement = document.createTextNode(newNode.nodeValue); - oldNode.parentNode.replaceChild(replacement, oldNode); } } - const oldEls = oldRoot.children; - const newEls = newRoot.children; + const oldEls = Array.from(oldRoot.children); + const newEls = Array.from(newRoot.children); for (let i = 0; i < Math.min(oldEls.length, newEls.length); i++) { syncText(oldEls[i], newEls[i]); From cd61d01de8207a97af3f59f2e5c343c744e20715 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 2 Aug 2025 14:14:52 +0530 Subject: [PATCH 080/849] fix: body tag gets removed from source code when trying to edit a body tag directly --- 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 583b15d299..bcd9f71ec6 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1867,7 +1867,10 @@ function RemoteFunctions(config) { // Function to handle direct editing of elements in the live preview function startEditing(element) { - if (!element) { + if (!element + || element.tagName === "BODY" + || element.tagName === "HTML" + || !element.hasAttribute("data-brackets-id")) { return; } From d099ba2063c96dd85990493f17dc379ac987d5ab Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 2 Aug 2025 15:13:26 +0530 Subject: [PATCH 081/849] refactor: calculate the position of the boxes --- .../BrowserScripts/RemoteFunctions.js | 190 +++++------------- 1 file changed, 49 insertions(+), 141 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index bcd9f71ec6..7399d1e892 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -64,10 +64,9 @@ function RemoteFunctions(config) { if (window.navigator.platform.substr(0, 3) === "Mac") { // Mac return event.metaKey; - } else { - // Windows - return event.ctrlKey; } + // Windows + return event.ctrlKey; } @@ -567,6 +566,20 @@ function RemoteFunctions(config) { }); }, + // note: this box width is the width of the more options box + // we need this as the value is not consistent, it depends on the number of options we show in the box + _getBoxPosition: function(boxWidth) { + const elemBounds = this.element.getBoundingClientRect(); + + const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + + let topPos = elemBounds.top - 30 + scrollTop; + let leftPos = elemBounds.right - boxWidth + scrollLeft; + + return {topPos: topPos, leftPos: leftPos}; + }, + _style: function() { this.body = window.document.createElement("div"); @@ -575,9 +588,6 @@ function RemoteFunctions(config) { // {mode: "closed"} means that users will not be able to access the shadow DOM const shadow = this.body.attachShadow({ mode: "closed" }); - // the element that was clicked - let elemBounds = this.element.getBoundingClientRect(); - // check which options should be shown to determine box width const showEditTextOption = _shouldShowEditTextOption(this.element); const showSelectParentOption = _shouldShowSelectParentOption(this.element); @@ -601,43 +611,6 @@ function RemoteFunctions(config) { } else { boxWidth = 106; } - const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; - const scrollTop = window.pageYOffset || document.documentElement.scrollTop; - - // get the ID and classes for the element - // we need this to check for overlap issue between the info box and this box - // because when we have classes and ids then the info box tends to stretch in width - const id = this.element.id; - const classes = this.element.className ? this.element.className.split(/\s+/).filter(Boolean) : []; - const tagName = this.element.tagName.toLowerCase(); - - const isOverlap = checkOverlap(elemBounds.width, tagName, id, classes); - - const viewportWidth = window.innerWidth; - const idealLeftPos = elemBounds.right - boxWidth + scrollLeft; - const maxLeftPos = viewportWidth - boxWidth - 10 + scrollLeft; - // 10px is just the padding, because we want some space - const minLeftPos = 10 + scrollLeft; - - // we'll use the position that keeps the box within viewport bounds - let leftPos = Math.min(idealLeftPos, maxLeftPos); - leftPos = Math.max(leftPos, minLeftPos); - let topPos; - - if (isOverlap) { - if (elemBounds.top > 40) { // check if there's enough space at the top - // place at the top - topPos = elemBounds.top - 30 + scrollTop; - } else { - // at the bottom - topPos = elemBounds.top + elemBounds.height + 5 + scrollTop; - } - } else { - // no overlap, so it comes just above the element - topPos = (elemBounds.top - 30 < 0 - ? elemBounds.top + elemBounds.height + 5 - : elemBounds.top - 30) + scrollTop; - } // the icons that is displayed in the box const ICONS = { @@ -693,6 +666,8 @@ function RemoteFunctions(config) {
`; + const boxPos = this._getBoxPosition(boxWidth); + const styles = ` .box { background-color: #4285F4; @@ -704,8 +679,8 @@ function RemoteFunctions(config) { font-family: Arial, sans-serif; z-index: 2147483647; position: absolute; - left: ${leftPos}px; - top: ${topPos}px; + left: ${boxPos.leftPos}px; + top: ${boxPos.topPos}px; width: ${boxWidth}px; box-sizing: border-box; } @@ -774,6 +749,32 @@ function RemoteFunctions(config) { } NodeInfoBox.prototype = { + _calcHeight: function() { + const element = this.element; + + let baseHeight = 26.75; + if(element.id) { + baseHeight += 17.25; + } + if(element.className.length !== 0) { + baseHeight += 17.25; + } + + return baseHeight; + }, + + _getBoxPosition: function() { + const elemBounds = this.element.getBoundingClientRect(); + + const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + + let topPos = elemBounds.top - this._calcHeight() + scrollTop; + let leftPos = elemBounds.left + scrollLeft; + + return {topPos: topPos, leftPos: leftPos}; + }, + _style: function() { this.body = window.document.createElement("div"); @@ -782,18 +783,6 @@ function RemoteFunctions(config) { // {mode: "closed"} means that users will not be able to access the shadow DOM const shadow = this.body.attachShadow({ mode: "closed" }); - // the element that was clicked - let elemBounds = this.element.getBoundingClientRect(); - - // the positions where it should be placed - const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; - const scrollTop = window.pageYOffset || document.documentElement.scrollTop; - - // this value decides where we need to show the box in the UI - // we are creating this here, because if the element has IDs and Classes then we need to increase the value - // so that the box doesn't obscure the element - let pushBoxUp = 32; // px value - // get the ID and classes for that element, as we need to display it in the box const id = this.element.id; const classes = this.element.className ? this.element.className.split(/\s+/).filter(Boolean) : []; @@ -804,7 +793,6 @@ function RemoteFunctions(config) { // Add ID if present if (id) { content += "
#" + id + "
"; - pushBoxUp += 20; } // Add classes (limit to 3 with dropdown indicator) @@ -817,89 +805,9 @@ function RemoteFunctions(config) { content += "+" + (classes.length - 3) + " more"; } content += "
"; - pushBoxUp += 20; } - let leftPos = elemBounds.left + scrollLeft; - let topPos = (elemBounds.top - pushBoxUp < 0 - ? elemBounds.top + elemBounds.height + 5 - : elemBounds.top - pushBoxUp) + scrollTop; - - let avgCharWidth = 6; - const basePadding = 16; - - // Get the tag name - const tagName = this.element.tagName.toLowerCase(); - - // Count characters in tag name, id, and classes - let charCount = _calculateInfoBoxCharCount(tagName, id, classes); - if(charCount <= 10) { - avgCharWidth = 7.5; - } - - // Calculate estimated width based on character count - // Formula: base padding + (character count * average character width) - const boxWidth = basePadding + (charCount * avgCharWidth); - - // we need to check for overlap if this is from a click - if (this.isFromClick) { - const isOverlap = checkOverlap(elemBounds.width, tagName, id, classes); - - if (isOverlap) { - const windowWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - // Estimate the height of the info box based on its content - // base height for tag name + padding - let estimatedHeight = 32; - - // height adjustment if ID is present - if (id) { - estimatedHeight += 20; - } - - // height adjustment if classes are present - if (classes.length > 0) { - estimatedHeight += 20; - } - - // check if element is near bottom of viewport - const elementBottomFromViewportTop = elemBounds.bottom; - const availableSpaceBelow = viewportHeight - elementBottomFromViewportTop; - - // align with the bottom of the info box (original behavior) - topPos = (elemBounds.top + elemBounds.height - estimatedHeight) + scrollTop; - - // If element is near bottom and there's not enough space below, - // push the info box up a bit to prevent scrollbar - if (availableSpaceBelow < estimatedHeight + 10) { - // Push it up by the amount it would overflow - const pushUpAmount = estimatedHeight - availableSpaceBelow; - topPos -= pushUpAmount; - } - - // decide whether position at left or right based on available space - // check if there's enough space on the left side - if (elemBounds.left > boxWidth + 10) { - leftPos = elemBounds.left - boxWidth - 10 + scrollLeft; - } else if (windowWidth - elemBounds.right > boxWidth + 10) { - // position on the right - leftPos = elemBounds.right + 10 + scrollLeft; - } - } - } - - // to make sure that the info box stays under the viewport width - const viewportWidth = window.innerWidth; - const margin = 10; - - // horizontal boundary checking - if (leftPos + boxWidth + margin > viewportWidth + scrollLeft) { - leftPos = viewportWidth + scrollLeft - boxWidth - margin; - } - if (leftPos < scrollLeft + margin) { - leftPos = scrollLeft + margin; - } + const boxPos = this._getBoxPosition(); const styles = ` .box { @@ -912,8 +820,8 @@ function RemoteFunctions(config) { font-family: Arial, sans-serif; z-index: 2147483647; position: absolute; - left: ${leftPos}px; - top: ${topPos}px; + left: ${boxPos.leftPos}px; + top: ${boxPos.topPos}px; max-width: fit-content; box-sizing: border-box; pointer-events: none; From 964d62f87f9b8bfe8d885f8daf42fb0612f3acf1 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 2 Aug 2025 20:12:04 +0530 Subject: [PATCH 082/849] fix: move boxes to down when no space at top --- .../BrowserScripts/RemoteFunctions.js | 77 +++---------------- 1 file changed, 11 insertions(+), 66 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 7399d1e892..1615368ce5 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -394,71 +394,6 @@ function RemoteFunctions(config) { delete window._currentDraggedElement; } - /** - * This function is to calculate the width of the info box based on the number of chars in the box - * @param {String} tagName - the element's tag name - * @param {String} id - the element's id - * @param {Array} classes - the array of class names - * @returns {Number} - the total char count - */ - function _calculateInfoBoxCharCount(tagName, id, classes) { - // char count for tag name - let tagNameCharCount = tagName.length; - let idNameCharCount = 0; - let classNameCharCount = 0; - // char count for id - if (id) { - idNameCharCount = id.length + 1; // +1 for # - } - - // char count for classes - if (classes.length > 0) { - for (let i = 0; i < Math.min(classes.length, 3); i++) { - classNameCharCount += classes[i].length + 1; // +1 for . - } - - if (classes.length > 3) { - // "+ X more" for more than 3 classes - const moreText = `+${classes.length - 3} more`; - classNameCharCount += moreText.length; - } - } - return Math.max(tagNameCharCount, idNameCharCount, classNameCharCount); - } - - /** - * This function checks whether there is overlap between the info and the more options box - * @param {Number} elemWidth - the width of the DOM element - * @param {String} tagName - the element's tag name - * @param {String} id - the element's id - * @param {Array} classes - the array of class names - * @returns {Number} - the total char count - */ - function checkOverlap(elemWidth, tagName, id, classes) { - let avgCharWidth = 6; - const basePadding = 16; - - // char count for tag name, id, and classes - let charCount = _calculateInfoBoxCharCount(tagName, id, classes); - if(charCount <= 10) { - avgCharWidth = 7.5; - } - - // calc estimate width based on the char count - const infoBoxWidth = basePadding + (charCount * avgCharWidth); - - // more options box is 106px - const moreOptionsBoxWidth = 106; - - // check if there's enough space for both boxes - // 20px buffer for spacing between boxes - if (elemWidth > (infoBoxWidth + moreOptionsBoxWidth + 20)) { - return false; // No overlap - } - - return true; - } - /** * this function is to check if an element should show the edit text option * it is needed because edit text option doesn't make sense with many elements like images, videos, hr tag etc @@ -577,6 +512,10 @@ function RemoteFunctions(config) { let topPos = elemBounds.top - 30 + scrollTop; let leftPos = elemBounds.right - boxWidth + scrollLeft; + if (elemBounds.top - 30 < 0) { + topPos = elemBounds.top + elemBounds.height + 5 + scrollTop; + } + return {topPos: topPos, leftPos: leftPos}; }, @@ -769,9 +708,15 @@ function RemoteFunctions(config) { const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; const scrollTop = window.pageYOffset || document.documentElement.scrollTop; - let topPos = elemBounds.top - this._calcHeight() + scrollTop; + const boxHeight = this._calcHeight(); + + let topPos = elemBounds.top - boxHeight + scrollTop; let leftPos = elemBounds.left + scrollLeft; + if (elemBounds.top - boxHeight < 0) { + topPos = elemBounds.top + elemBounds.height + 5 + scrollTop; + } + return {topPos: topPos, leftPos: leftPos}; }, From 1b1ade56334343ae7a54818f2c4fb3ae7b504166 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 3 Aug 2025 13:23:03 +0530 Subject: [PATCH 083/849] feat: update background color when a option button is hovered --- .../BrowserScripts/RemoteFunctions.js | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 1615368ce5..a249ec24b8 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -544,11 +544,11 @@ function RemoteFunctions(config) { // box width we need to decide based on the no. of options let boxWidth; if (optionCount === 2) { - boxWidth = 52; + boxWidth = 48; } else if (optionCount === 3) { - boxWidth = 82; + boxWidth = 72; } else { - boxWidth = 106; + boxWidth = 96; } // the icons that is displayed in the box @@ -612,7 +612,6 @@ function RemoteFunctions(config) { background-color: #4285F4; color: white; border-radius: 3px; - padding: 5px 8px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); font-size: 12px; font-family: Arial, sans-serif; @@ -626,14 +625,27 @@ function RemoteFunctions(config) { .node-options { display: flex; - gap: 8px; align-items: center; } .node-options span { + padding: 4px 3.9px; cursor: pointer; display: flex; align-items: center; + border-radius: 0; + } + + .node-options span:first-child { + border-radius: 3px 0 0 3px; + } + + .node-options span:last-child { + border-radius: 0 3px 3px 0; + } + + .node-options span:hover { + background-color: rgba(255, 255, 255, 0.15); } .node-options span > svg { From b3bc4d11079db890231112d374c8ad19675893cb Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 3 Aug 2025 14:49:09 +0530 Subject: [PATCH 084/849] fix: positioning of the info box --- .../BrowserScripts/RemoteFunctions.js | 72 +++++++++---------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index a249ec24b8..eac56c0b51 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -505,15 +505,13 @@ function RemoteFunctions(config) { // we need this as the value is not consistent, it depends on the number of options we show in the box _getBoxPosition: function(boxWidth) { const elemBounds = this.element.getBoundingClientRect(); + const offset = _screenOffset(this.element); - const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; - const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + let topPos = offset.top - 30; + let leftPos = offset.left + elemBounds.width - boxWidth; - let topPos = elemBounds.top - 30 + scrollTop; - let leftPos = elemBounds.right - boxWidth + scrollLeft; - - if (elemBounds.top - 30 < 0) { - topPos = elemBounds.top + elemBounds.height + 5 + scrollTop; + if (offset.top - 30 < 0) { + topPos = offset.top + elemBounds.height + 5; } return {topPos: topPos, leftPos: leftPos}; @@ -700,33 +698,15 @@ function RemoteFunctions(config) { } NodeInfoBox.prototype = { - _calcHeight: function() { - const element = this.element; - - let baseHeight = 26.75; - if(element.id) { - baseHeight += 17.25; - } - if(element.className.length !== 0) { - baseHeight += 17.25; - } - - return baseHeight; - }, - - _getBoxPosition: function() { + _getBoxPosition: function(boxHeight) { const elemBounds = this.element.getBoundingClientRect(); + const offset = _screenOffset(this.element); - const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; - const scrollTop = window.pageYOffset || document.documentElement.scrollTop; - - const boxHeight = this._calcHeight(); - - let topPos = elemBounds.top - boxHeight + scrollTop; - let leftPos = elemBounds.left + scrollLeft; + let topPos = offset.top - boxHeight - 6; // 6 for just some little space to breathes + let leftPos = offset.left; - if (elemBounds.top - boxHeight < 0) { - topPos = elemBounds.top + elemBounds.height + 5 + scrollTop; + if (offset.top - boxHeight < 0) { + topPos = offset.top + elemBounds.height + 6; } return {topPos: topPos, leftPos: leftPos}; @@ -737,8 +717,8 @@ function RemoteFunctions(config) { // this is shadow DOM. // we need it because if we add the box directly to the DOM then users style might override it. - // {mode: "closed"} means that users will not be able to access the shadow DOM - const shadow = this.body.attachShadow({ mode: "closed" }); + // {mode: "open"} allows us to access the shadow DOM to get actual height/position of the boxes + const shadow = this.body.attachShadow({ mode: "open" }); // get the ID and classes for that element, as we need to display it in the box const id = this.element.id; @@ -764,10 +744,14 @@ function RemoteFunctions(config) { content += "
"; } - const boxPos = this._getBoxPosition(); + // initially, we place our info box -1000px to the top but at the right left pos. this is done so that + // we can take the text-wrapping inside the info box in account when calculating the height + // after calculating the height of the box, we place it at the exact position above the element + const offset = _screenOffset(this.element); + const leftPos = offset.left; const styles = ` - .box { + .phoenix-node-info-box { background-color: #4285F4; color: white; border-radius: 3px; @@ -777,9 +761,9 @@ function RemoteFunctions(config) { font-family: Arial, sans-serif; z-index: 2147483647; position: absolute; - left: ${boxPos.leftPos}px; - top: ${boxPos.topPos}px; - max-width: fit-content; + left: ${leftPos}px; + top: -1000px; + max-width: 300px; box-sizing: border-box; pointer-events: none; } @@ -799,7 +783,7 @@ function RemoteFunctions(config) { `; // add everything to the shadow box - shadow.innerHTML = `
${content}
`; + shadow.innerHTML = `
${content}
`; this._shadow = shadow; }, @@ -808,6 +792,16 @@ function RemoteFunctions(config) { this._style(); // style the box window.document.body.appendChild(this.body); + + // get the actual rendered height of the box and then we reposition it to the actual place + const boxElement = this._shadow.querySelector('.phoenix-node-info-box'); + if (boxElement) { + const nodeInfoBoxHeight = boxElement.getBoundingClientRect().height; + const pos = this._getBoxPosition(nodeInfoBoxHeight); + + boxElement.style.left = pos.leftPos + 'px'; + boxElement.style.top = pos.topPos + 'px'; + } }, remove: function() { From 31a9f7bb75f44edef2608c13b6887778b698eb69 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 3 Aug 2025 16:01:11 +0530 Subject: [PATCH 085/849] fix: node more options box positioning --- .../BrowserScripts/RemoteFunctions.js | 55 +++++++------------ 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index eac56c0b51..c7d9a557fc 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -501,17 +501,15 @@ function RemoteFunctions(config) { }); }, - // note: this box width is the width of the more options box - // we need this as the value is not consistent, it depends on the number of options we show in the box - _getBoxPosition: function(boxWidth) { + _getBoxPosition: function(boxWidth, boxHeight) { const elemBounds = this.element.getBoundingClientRect(); const offset = _screenOffset(this.element); - let topPos = offset.top - 30; + let topPos = offset.top - boxHeight - 6; // 6 for just some little space to breathe let leftPos = offset.left + elemBounds.width - boxWidth; - if (offset.top - 30 < 0) { - topPos = offset.top + elemBounds.height + 5; + if (offset.top - boxHeight < 0) { + topPos = offset.top + elemBounds.height + 6; } return {topPos: topPos, leftPos: leftPos}; @@ -522,33 +520,13 @@ function RemoteFunctions(config) { // this is shadow DOM. // we need it because if we add the box directly to the DOM then users style might override it. - // {mode: "closed"} means that users will not be able to access the shadow DOM - const shadow = this.body.attachShadow({ mode: "closed" }); + // {mode: "open"} allows us to access the shadow DOM to get actual height/position of the boxes + const shadow = this.body.attachShadow({ mode: "open" }); // check which options should be shown to determine box width const showEditTextOption = _shouldShowEditTextOption(this.element); const showSelectParentOption = _shouldShowSelectParentOption(this.element); - // calculate box width based on visible options - // NOTE: duplicate and delete buttons are always shown - let optionCount = 2; - if (showSelectParentOption) { - optionCount++; - } - if (showEditTextOption) { - optionCount++; - } - - // box width we need to decide based on the no. of options - let boxWidth; - if (optionCount === 2) { - boxWidth = 48; - } else if (optionCount === 3) { - boxWidth = 72; - } else { - boxWidth = 96; - } - // the icons that is displayed in the box const ICONS = { arrowUp: ` @@ -603,10 +581,8 @@ function RemoteFunctions(config) {
`; - const boxPos = this._getBoxPosition(boxWidth); - const styles = ` - .box { + .phoenix-more-options-box { background-color: #4285F4; color: white; border-radius: 3px; @@ -615,9 +591,8 @@ function RemoteFunctions(config) { font-family: Arial, sans-serif; z-index: 2147483647; position: absolute; - left: ${boxPos.leftPos}px; - top: ${boxPos.topPos}px; - width: ${boxWidth}px; + left: -1000px; + top: -1000px; box-sizing: border-box; } @@ -654,7 +629,7 @@ function RemoteFunctions(config) { `; // add everything to the shadow box - shadow.innerHTML = `
${content}
`; + shadow.innerHTML = `
${content}
`; this._shadow = shadow; }, @@ -664,6 +639,16 @@ function RemoteFunctions(config) { window.document.body.appendChild(this.body); + // get the actual rendered dimensions of the box and then we reposition it to the actual place + const boxElement = this._shadow.querySelector('.phoenix-more-options-box'); + if (boxElement) { + const boxRect = boxElement.getBoundingClientRect(); + const pos = this._getBoxPosition(boxRect.width, boxRect.height); + + boxElement.style.left = pos.leftPos + 'px'; + boxElement.style.top = pos.topPos + 'px'; + } + // add click handler to all the buttons const spans = this._shadow.querySelectorAll('.node-options span'); spans.forEach(span => { From 7e4508d0f20a350c752bd6177f0718a036b5ac92 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 3 Aug 2025 17:31:59 +0530 Subject: [PATCH 086/849] fix: boxes overlapping each other --- .../BrowserScripts/RemoteFunctions.js | 68 ++++++++++++++++--- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index c7d9a557fc..5682643833 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -683,15 +683,51 @@ function RemoteFunctions(config) { } NodeInfoBox.prototype = { - _getBoxPosition: function(boxHeight) { + _checkOverlap: function(nodeInfoBoxPos, nodeInfoBoxDimensions) { + if (_nodeMoreOptionsBox && _nodeMoreOptionsBox._shadow) { + const moreOptionsBoxElement = _nodeMoreOptionsBox._shadow.querySelector('.phoenix-more-options-box'); + if (moreOptionsBoxElement) { + const moreOptionsBoxRect = moreOptionsBoxElement.getBoundingClientRect(); + + const infoBox = { + left: nodeInfoBoxPos.leftPos, + top: nodeInfoBoxPos.topPos, + right: nodeInfoBoxPos.leftPos + nodeInfoBoxDimensions.width, + bottom: nodeInfoBoxPos.topPos + nodeInfoBoxDimensions.height + }; + + const moreOptionsBox = { + left: moreOptionsBoxRect.left, + top: moreOptionsBoxRect.top, + right: moreOptionsBoxRect.right, + bottom: moreOptionsBoxRect.bottom + }; + + return !(infoBox.right < moreOptionsBox.left || + moreOptionsBox.right < infoBox.left || + infoBox.bottom < moreOptionsBox.top || + moreOptionsBox.bottom < infoBox.top); + } + } + return false; + }, + + _getBoxPosition: function(boxHeight, overlap = false) { const elemBounds = this.element.getBoundingClientRect(); const offset = _screenOffset(this.element); + let topPos = 0; + let leftPos = 0; - let topPos = offset.top - boxHeight - 6; // 6 for just some little space to breathes - let leftPos = offset.left; + if (overlap) { + topPos = offset.top + 2; + leftPos = offset.left + elemBounds.width + 6; + } else { + topPos = offset.top - boxHeight - 6; // 6 for just some little space to breathes + leftPos = offset.left; - if (offset.top - boxHeight < 0) { - topPos = offset.top + elemBounds.height + 6; + if (offset.top - boxHeight < 0) { + topPos = offset.top + elemBounds.height + 6; + } } return {topPos: topPos, leftPos: leftPos}; @@ -781,11 +817,23 @@ function RemoteFunctions(config) { // get the actual rendered height of the box and then we reposition it to the actual place const boxElement = this._shadow.querySelector('.phoenix-node-info-box'); if (boxElement) { - const nodeInfoBoxHeight = boxElement.getBoundingClientRect().height; - const pos = this._getBoxPosition(nodeInfoBoxHeight); - - boxElement.style.left = pos.leftPos + 'px'; - boxElement.style.top = pos.topPos + 'px'; + const nodeInfoBoxDimensions = { + height: boxElement.getBoundingClientRect().height, + width: boxElement.getBoundingClientRect().width + }; + const nodeInfoBoxPos = this._getBoxPosition(nodeInfoBoxDimensions.height); + + boxElement.style.left = nodeInfoBoxPos.leftPos + 'px'; + boxElement.style.top = nodeInfoBoxPos.topPos + 'px'; + + if(this.isFromClick) { + const isBoxOverlapping = this._checkOverlap(nodeInfoBoxPos, nodeInfoBoxDimensions); + if(isBoxOverlapping) { + const newPos = this._getBoxPosition(nodeInfoBoxDimensions.height, true); + boxElement.style.left = newPos.leftPos + 'px'; + boxElement.style.top = newPos.topPos + 'px'; + } + } } }, From 896028586414ea62c80c1e586fcb9f7a12b2bb77 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 3 Aug 2025 18:00:57 +0530 Subject: [PATCH 087/849] fix: use screenOffset to calculate overflow --- .../BrowserScripts/RemoteFunctions.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 5682643833..cdc519128b 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -687,6 +687,7 @@ function RemoteFunctions(config) { if (_nodeMoreOptionsBox && _nodeMoreOptionsBox._shadow) { const moreOptionsBoxElement = _nodeMoreOptionsBox._shadow.querySelector('.phoenix-more-options-box'); if (moreOptionsBoxElement) { + const moreOptionsBoxOffset = _screenOffset(moreOptionsBoxElement); const moreOptionsBoxRect = moreOptionsBoxElement.getBoundingClientRect(); const infoBox = { @@ -697,16 +698,18 @@ function RemoteFunctions(config) { }; const moreOptionsBox = { - left: moreOptionsBoxRect.left, - top: moreOptionsBoxRect.top, - right: moreOptionsBoxRect.right, - bottom: moreOptionsBoxRect.bottom + left: moreOptionsBoxOffset.left, + top: moreOptionsBoxOffset.top, + right: moreOptionsBoxOffset.left + moreOptionsBoxRect.width, + bottom: moreOptionsBoxOffset.top + moreOptionsBoxRect.height }; - return !(infoBox.right < moreOptionsBox.left || + const isOverlapping = !(infoBox.right < moreOptionsBox.left || moreOptionsBox.right < infoBox.left || infoBox.bottom < moreOptionsBox.top || moreOptionsBox.bottom < infoBox.top); + + return isOverlapping; } } return false; From 6bfc561277afc9cffab5278c1433f589def08d36 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 3 Aug 2025 18:25:59 +0530 Subject: [PATCH 088/849] fix: phoenix internal attributes leaking into users source code --- src/LiveDevelopment/LivePreviewEdit.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 100bd2fb6c..380c97efc2 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -38,6 +38,17 @@ define(function (require, exports, module) { const oldRoot = oldDoc.body; const newRoot = newDoc.body; + // this function is to remove the phoenix internal attributes from leaking into the user's source code + function cleanClonedElement(clonedElement) { + if (clonedElement.nodeType === Node.ELEMENT_NODE) { + clonedElement.removeAttribute("data-brackets-id"); + + const children = clonedElement.querySelectorAll("[data-brackets-id]"); + children.forEach(child => child.removeAttribute("data-brackets-id")); + } + return clonedElement; + } + function syncText(oldNode, newNode) { if (!oldNode || !newNode) { return; @@ -64,7 +75,8 @@ define(function (require, exports, module) { if (!oldChild && newChild) { // if new child added → clone and insert - oldNode.appendChild(newChild.cloneNode(true)); + const cloned = newChild.cloneNode(true); + oldNode.appendChild(cleanClonedElement(cloned)); } else if (oldChild && !newChild) { // if child removed → delete oldNode.removeChild(oldChild); @@ -84,7 +96,8 @@ define(function (require, exports, module) { } } else { // different node types or tags → replace - oldNode.replaceChild(newChild.cloneNode(true), oldChild); + const cloned = newChild.cloneNode(true); + oldNode.replaceChild(cleanClonedElement(cloned), oldChild); } } } From 7c965eeb38ebb23b1ab3da95f8c95819d4a3e5b1 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 3 Aug 2025 18:50:21 +0530 Subject: [PATCH 089/849] fix: boxes gets out of viewport --- .../BrowserScripts/RemoteFunctions.js | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index cdc519128b..a982be7322 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -508,10 +508,16 @@ function RemoteFunctions(config) { let topPos = offset.top - boxHeight - 6; // 6 for just some little space to breathe let leftPos = offset.left + elemBounds.width - boxWidth; + // Check if the box would go off the top of the viewport if (offset.top - boxHeight < 0) { topPos = offset.top + elemBounds.height + 6; } + // Check if the box would go off the left of the viewport + if (leftPos < 0) { + leftPos = offset.left; + } + return {topPos: topPos, leftPos: leftPos}; }, @@ -715,7 +721,7 @@ function RemoteFunctions(config) { return false; }, - _getBoxPosition: function(boxHeight, overlap = false) { + _getBoxPosition: function(boxDimensions, overlap = false) { const elemBounds = this.element.getBoundingClientRect(); const offset = _screenOffset(this.element); let topPos = 0; @@ -724,13 +730,23 @@ function RemoteFunctions(config) { if (overlap) { topPos = offset.top + 2; leftPos = offset.left + elemBounds.width + 6; + + // Check if overlap position would go off the right of the viewport + if (leftPos + boxDimensions.width > window.innerWidth) { + leftPos = offset.left - boxDimensions.width - 6; + } } else { - topPos = offset.top - boxHeight - 6; // 6 for just some little space to breathes + topPos = offset.top - boxDimensions.height - 6; // 6 for just some little space to breathe leftPos = offset.left; - if (offset.top - boxHeight < 0) { + if (offset.top - boxDimensions.height < 0) { topPos = offset.top + elemBounds.height + 6; } + + // Check if the box would go off the right of the viewport + if (leftPos + boxDimensions.width > window.innerWidth) { + leftPos = window.innerWidth - boxDimensions.width - 10; + } } return {topPos: topPos, leftPos: leftPos}; @@ -824,7 +840,7 @@ function RemoteFunctions(config) { height: boxElement.getBoundingClientRect().height, width: boxElement.getBoundingClientRect().width }; - const nodeInfoBoxPos = this._getBoxPosition(nodeInfoBoxDimensions.height); + const nodeInfoBoxPos = this._getBoxPosition(nodeInfoBoxDimensions, false); boxElement.style.left = nodeInfoBoxPos.leftPos + 'px'; boxElement.style.top = nodeInfoBoxPos.topPos + 'px'; @@ -832,7 +848,7 @@ function RemoteFunctions(config) { if(this.isFromClick) { const isBoxOverlapping = this._checkOverlap(nodeInfoBoxPos, nodeInfoBoxDimensions); if(isBoxOverlapping) { - const newPos = this._getBoxPosition(nodeInfoBoxDimensions.height, true); + const newPos = this._getBoxPosition(nodeInfoBoxDimensions, true); boxElement.style.left = newPos.leftPos + 'px'; boxElement.style.top = newPos.topPos + 'px'; } From a1501b11c92f159b0258a32a3381675dfe88f22f Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 3 Aug 2025 23:46:28 +0530 Subject: [PATCH 090/849] feat: disable all the live preview advance features when its disabled --- .../BrowserScripts/RemoteFunctions.js | 81 +++++++++++-------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index a982be7322..d27bf3458d 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -30,9 +30,8 @@ */ function RemoteFunctions(config) { - // this is responsible to make the advanced DOM features active or inactive - // TODO: give this var a better name - let isFlagActive = true; + // this is responsible to make the advanced live preview features active or inactive + let isLPEditFeaturesActive = false; // this will store the element that was clicked previously (before the new click) // we need this so that we can remove click styling from the previous element when a new element is clicked @@ -641,6 +640,11 @@ function RemoteFunctions(config) { create: function() { this.remove(); // remove existing box if already present + + if(!isLPEditFeaturesActive) { + return; + } + this._style(); // style the box window.document.body.appendChild(this.body); @@ -829,6 +833,11 @@ function RemoteFunctions(config) { create: function() { this.remove(); // remove existing box if already present + + if(!isLPEditFeaturesActive) { + return; + } + this._style(); // style the box window.document.body.appendChild(this.body); @@ -1196,7 +1205,7 @@ function RemoteFunctions(config) { } function onElementHover(event) { - if (_hoverHighlight) { + if (_hoverHighlight && isLPEditFeaturesActive) { _hoverHighlight.clear(); // Skip highlighting for HTML and BODY tags and for DOM elements which doesn't have 'data-brackets-id' @@ -1227,24 +1236,24 @@ function RemoteFunctions(config) { } function onElementHoverOut(event) { - if (_hoverHighlight) { + if (_hoverHighlight && isLPEditFeaturesActive) { _hoverHighlight.clear(); - } - // Restore original background color - if (event && event.target && event.target.nodeType === Node.ELEMENT_NODE && event.target.hasAttribute("data-brackets-id")) { - if (event.target._originalBackgroundColor !== undefined) { - event.target.style.backgroundColor = event.target._originalBackgroundColor; - } else { - event.target.style.backgroundColor = ""; + // Restore original background color + if (event && event.target && event.target.nodeType === Node.ELEMENT_NODE && event.target.hasAttribute("data-brackets-id")) { + if (event.target._originalBackgroundColor !== undefined) { + event.target.style.backgroundColor = event.target._originalBackgroundColor; + } else { + event.target.style.backgroundColor = ""; + } + delete event.target._originalBackgroundColor; } - delete event.target._originalBackgroundColor; - } - // Remove info box when mouse leaves the element - if (_nodeInfoBox) { - _nodeInfoBox.remove(); - _nodeInfoBox = null; + // Remove info box when mouse leaves the element + if (_nodeInfoBox) { + _nodeInfoBox.remove(); + _nodeInfoBox = null; + } } } @@ -1256,7 +1265,7 @@ function RemoteFunctions(config) { function onClick(event) { // make sure that the feature is enabled and also the clicked element has the attribute 'data-brackets-id' if ( - isFlagActive && + isLPEditFeaturesActive && event.target.hasAttribute("data-brackets-id") && event.target.tagName !== "BODY" && event.target.tagName !== "HTML" @@ -1292,7 +1301,6 @@ function RemoteFunctions(config) { event.target.style.outline = "1px solid #4285F4"; previouslyClickedElement = event.target; } else if ( // when user clicks on the HTML or the BODY tag, we want to remove the boxes - isFlagActive && _nodeMoreOptionsBox && (event.target.tagName === "HTML" || event.target.tagName === "BODY") ) { @@ -1306,7 +1314,7 @@ function RemoteFunctions(config) { */ function onDoubleClick(event) { if ( - isFlagActive && + isLPEditFeaturesActive && event.target.hasAttribute("data-brackets-id") && event.target.tagName !== "BODY" && event.target.tagName !== "HTML" @@ -1778,7 +1786,8 @@ function RemoteFunctions(config) { // Function to handle direct editing of elements in the live preview function startEditing(element) { - if (!element + if (!isLPEditFeaturesActive + || !element || element.tagName === "BODY" || element.tagName === "HTML" || !element.hasAttribute("data-brackets-id")) { @@ -1826,7 +1835,7 @@ function RemoteFunctions(config) { // Function to finish editing and apply changes // isEditSuccessful: this is a boolean value, defaults to true. false only when the edit operation is cancelled function finishEditing(element, isEditSuccessful = true) { - if (!element || !element.hasAttribute("contenteditable")) { + if (!isLPEditFeaturesActive || !element || !element.hasAttribute("contenteditable")) { return; } @@ -1857,20 +1866,22 @@ function RemoteFunctions(config) { // init _editHandler = new DOMEditHandler(window.document); - // Initialize hover highlight with Chrome-like colors - _hoverHighlight = new Highlight("#c8f9c5", true); // Green similar to Chrome's padding color + if (isLPEditFeaturesActive) { + // Initialize hover highlight with Chrome-like colors + _hoverHighlight = new Highlight("#c8f9c5", true); // Green similar to Chrome's padding color + + // Initialize click highlight with animation + _clickHighlight = new Highlight("#cfc", true); // Light green for click highlight - // Initialize click highlight with animation - _clickHighlight = new Highlight("#cfc", true); // Light green for click highlight + 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("keydown", onKeyDown); + } - // Add event listeners for hover - 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("keydown", onKeyDown); return { "DOMEditHandler" : DOMEditHandler, From 2c0130a461bd28c27e1dcbe3d209fdeb21ab32e8 Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 4 Aug 2025 18:33:58 +0530 Subject: [PATCH 091/849] feat: add a setting to show elements highlights on hover or click --- .../BrowserScripts/RemoteFunctions.js | 21 ++++++++----------- src/LiveDevelopment/main.js | 14 ++++++++++++- .../LivePreviewSettings.js | 15 +++++++++++-- .../livePreviewSettings.html | 9 ++++++++ 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index d27bf3458d..9c4199019a 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -30,9 +30,6 @@ */ function RemoteFunctions(config) { - // this is responsible to make the advanced live preview features active or inactive - let isLPEditFeaturesActive = false; - // this will store the element that was clicked previously (before the new click) // we need this so that we can remove click styling from the previous element when a new element is clicked let previouslyClickedElement = null; @@ -641,7 +638,7 @@ function RemoteFunctions(config) { create: function() { this.remove(); // remove existing box if already present - if(!isLPEditFeaturesActive) { + if(!config.isLPEditFeaturesActive) { return; } @@ -834,7 +831,7 @@ function RemoteFunctions(config) { create: function() { this.remove(); // remove existing box if already present - if(!isLPEditFeaturesActive) { + if(!config.isLPEditFeaturesActive) { return; } @@ -1205,7 +1202,7 @@ function RemoteFunctions(config) { } function onElementHover(event) { - if (_hoverHighlight && isLPEditFeaturesActive) { + if (_hoverHighlight && config.isLPEditFeaturesActive) { _hoverHighlight.clear(); // Skip highlighting for HTML and BODY tags and for DOM elements which doesn't have 'data-brackets-id' @@ -1236,7 +1233,7 @@ function RemoteFunctions(config) { } function onElementHoverOut(event) { - if (_hoverHighlight && isLPEditFeaturesActive) { + if (_hoverHighlight && config.isLPEditFeaturesActive) { _hoverHighlight.clear(); // Restore original background color @@ -1265,7 +1262,7 @@ function RemoteFunctions(config) { function onClick(event) { // make sure that the feature is enabled and also the clicked element has the attribute 'data-brackets-id' if ( - isLPEditFeaturesActive && + config.isLPEditFeaturesActive && event.target.hasAttribute("data-brackets-id") && event.target.tagName !== "BODY" && event.target.tagName !== "HTML" @@ -1314,7 +1311,7 @@ function RemoteFunctions(config) { */ function onDoubleClick(event) { if ( - isLPEditFeaturesActive && + config.isLPEditFeaturesActive && event.target.hasAttribute("data-brackets-id") && event.target.tagName !== "BODY" && event.target.tagName !== "HTML" @@ -1786,7 +1783,7 @@ function RemoteFunctions(config) { // Function to handle direct editing of elements in the live preview function startEditing(element) { - if (!isLPEditFeaturesActive + if (!config.isLPEditFeaturesActive || !element || element.tagName === "BODY" || element.tagName === "HTML" @@ -1835,7 +1832,7 @@ function RemoteFunctions(config) { // Function to finish editing and apply changes // isEditSuccessful: this is a boolean value, defaults to true. false only when the edit operation is cancelled function finishEditing(element, isEditSuccessful = true) { - if (!isLPEditFeaturesActive || !element || !element.hasAttribute("contenteditable")) { + if (!config.isLPEditFeaturesActive || !element || !element.hasAttribute("contenteditable")) { return; } @@ -1866,7 +1863,7 @@ function RemoteFunctions(config) { // init _editHandler = new DOMEditHandler(window.document); - if (isLPEditFeaturesActive) { + if (config.isLPEditFeaturesActive) { // Initialize hover highlight with Chrome-like colors _hoverHighlight = new Highlight("#c8f9c5", true); // Green similar to Chrome's padding color diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 5a02c9b755..f38598a130 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -45,8 +45,15 @@ define(function main(require, exports, module) { EventDispatcher = require("utils/EventDispatcher"), WorkspaceManager = require("view/WorkspaceManager"); + + // this is responsible to make the advanced live preview features active or inactive + let isLPEditFeaturesActive = true; + const EVENT_LIVE_HIGHLIGHT_PREF_CHANGED = "liveHighlightPrefChange"; + const PREFERENCE_PROJECT_ELEMENT_HIGHLIGHT = "livePreviewElementHighlights"; + const elemHighlightsPrefValue = PreferencesManager.get(PREFERENCE_PROJECT_ELEMENT_HIGHLIGHT); + var params = new UrlParams(); var config = { experimental: false, // enable experimental features @@ -58,7 +65,9 @@ define(function main(require, exports, module) { marginColor: {r: 246, g: 178, b: 107, a: 0.66}, paddingColor: {r: 147, g: 196, b: 125, a: 0.66}, showInfo: true - } + }, + isLPEditFeaturesActive: isLPEditFeaturesActive, + elemHighlights: elemHighlightsPrefValue }; // Status labels/styles are ordered: error, not connected, progress1, progress2, connected. var _status, @@ -330,6 +339,9 @@ define(function main(require, exports, module) { EventDispatcher.makeEventDispatcher(exports); + exports.isLPEditFeaturesActive = isLPEditFeaturesActive; + exports.PREFERENCE_PROJECT_ELEMENT_HIGHLIGHT = PREFERENCE_PROJECT_ELEMENT_HIGHLIGHT; + // public events exports.EVENT_OPEN_PREVIEW_URL = MultiBrowserLiveDev.EVENT_OPEN_PREVIEW_URL; exports.EVENT_CONNECTION_CLOSE = MultiBrowserLiveDev.EVENT_CONNECTION_CLOSE; diff --git a/src/extensionsIntegrated/Phoenix-live-preview/LivePreviewSettings.js b/src/extensionsIntegrated/Phoenix-live-preview/LivePreviewSettings.js index d301b340d6..c2489179be 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/LivePreviewSettings.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/LivePreviewSettings.js @@ -41,6 +41,7 @@ define(function (require, exports, module) { const livePreviewSettings = require("text!./livePreviewSettings.html"), + LiveDevelopmentMain = require("LiveDevelopment/main"), Dialogs = require("widgets/Dialogs"), ProjectManager = require("project/ProjectManager"), Strings = require("strings"), @@ -87,7 +88,10 @@ define(function (require, exports, module) { description: Strings.LIVE_DEV_SETTINGS_FRAMEWORK_PREFERENCES, values: Object.keys(SUPPORTED_FRAMEWORKS) }); - + PreferencesManager.definePreference(LiveDevelopmentMain.PREFERENCE_PROJECT_ELEMENT_HIGHLIGHT, "string", "hover", { + description: "show live preview element highlights on 'hover' or 'click'. Defaults to 'hover'" + }); + async function detectFramework($frameworkSelect, $hotReloadChk) { for(let framework of Object.keys(SUPPORTED_FRAMEWORKS)){ const configFile = SUPPORTED_FRAMEWORKS[framework].configFile, @@ -130,7 +134,8 @@ define(function (require, exports, module) { $hotReloadChk = $template.find("#hotReloadChk"), $hotReloadLabel = $template.find("#hotReloadLabel"), $frameworkLabel = $template.find("#frameworkLabel"), - $frameworkSelect = $template.find("#frameworkSelect"); + $frameworkSelect = $template.find("#frameworkSelect"), + $elementHighlights = $template.find("#elementHighlightWrapper"); $enableCustomServerChk.prop('checked', PreferencesManager.get(PREFERENCE_PROJECT_SERVER_ENABLED)); $showLivePreviewAtStartup.prop('checked', PreferencesManager.get(PREFERENCE_SHOW_LIVE_PREVIEW_PANEL)); $hotReloadChk.prop('checked', !!PreferencesManager.get(PREFERENCE_PROJECT_SERVER_HOT_RELOAD_SUPPORTED)); @@ -163,6 +168,12 @@ define(function (require, exports, module) { $frameworkSelect.addClass("forced-hidden"); $frameworkLabel.addClass("forced-hidden"); } + + if(LiveDevelopmentMain.isLPEditFeaturesActive) { + $elementHighlights.removeClass("forced-hidden"); + } else { + $elementHighlights.addClass("forced-hidden"); + } } $livePreviewServerURL.on("input", refreshValues); diff --git a/src/extensionsIntegrated/Phoenix-live-preview/livePreviewSettings.html b/src/extensionsIntegrated/Phoenix-live-preview/livePreviewSettings.html index ad3cd9aa89..8b10603ac7 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/livePreviewSettings.html +++ b/src/extensionsIntegrated/Phoenix-live-preview/livePreviewSettings.html @@ -25,6 +25,15 @@

{{Strings.LIVE_DEV_SETTINGS_TITLE}}

+ +
+
+ + +
From 840b701955dea1983f37499b81153e7d74d94634 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 5 Aug 2025 11:09:10 +0530 Subject: [PATCH 093/849] fix: element highlights setting not getting applied immediately directly --- .../BrowserScripts/RemoteFunctions.js | 34 +++++++++++-------- src/LiveDevelopment/LiveDevMultiBrowser.js | 20 +++++++++++ src/LiveDevelopment/main.js | 8 +++-- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index bc12d719e3..cac7189338 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1944,22 +1944,25 @@ function RemoteFunctions(config) { // init _editHandler = new DOMEditHandler(window.document); - if (config.isLPEditFeaturesActive) { - // Initialize hover highlight with Chrome-like colors - _hoverHighlight = new Highlight("#c8f9c5", true); // Green similar to Chrome's padding color - - // Initialize click highlight with animation - _clickHighlight = new Highlight("#cfc", true); // Light green for click highlight - - 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("keydown", onKeyDown); + function registerHandlers() { + if (config.isLPEditFeaturesActive) { + // Initialize hover highlight with Chrome-like colors + _hoverHighlight = new Highlight("#c8f9c5", true); // Green similar to Chrome's padding color + + // Initialize click highlight with animation + _clickHighlight = new Highlight("#cfc", true); // Light green for click highlight + + 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("keydown", onKeyDown); + } } + registerHandlers(); return { "DOMEditHandler" : DOMEditHandler, @@ -1973,6 +1976,7 @@ function RemoteFunctions(config) { "startEditing" : startEditing, "finishEditing" : finishEditing, "dismissMoreOptionsBox" : dismissMoreOptionsBox, - "hasVisibleLivePreviewBoxes" : hasVisibleLivePreviewBoxes + "hasVisibleLivePreviewBoxes" : hasVisibleLivePreviewBoxes, + "registerHandlers" : registerHandlers }; } diff --git a/src/LiveDevelopment/LiveDevMultiBrowser.js b/src/LiveDevelopment/LiveDevMultiBrowser.js index 0f2d54836f..976acda42b 100644 --- a/src/LiveDevelopment/LiveDevMultiBrowser.js +++ b/src/LiveDevelopment/LiveDevMultiBrowser.js @@ -718,6 +718,24 @@ define(function (require, exports, module) { } } + /** + * Register event handlers in the remote browser for live preview functionality + */ + function registerHandlers() { + if (_protocol) { + _protocol.evaluate("_LD.registerHandlers()"); + } + } + + /** + * Update configuration in the remote browser + */ + function updateConfig(configJSON) { + if (_protocol) { + _protocol.evaluate("_LD.updateConfig('" + configJSON + "')"); + } + } + /** * Originally unload and reload agents. It doesn't apply for this new implementation. * @return {jQuery.Promise} Already resolved promise. @@ -786,6 +804,8 @@ define(function (require, exports, module) { exports.redrawHighlight = redrawHighlight; exports.hasVisibleLivePreviewBoxes = hasVisibleLivePreviewBoxes; exports.dismissLivePreviewBoxes = dismissLivePreviewBoxes; + exports.registerHandlers = registerHandlers; + exports.updateConfig = updateConfig; exports.init = init; exports.isActive = isActive; exports.setLivePreviewPinned= setLivePreviewPinned; diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 347a3c436d..e35896cc4f 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -296,7 +296,7 @@ define(function main(require, exports, module) { .on("change", function () { config.remoteHighlight = prefs.get(PREF_REMOTEHIGHLIGHT); if (MultiBrowserLiveDev && MultiBrowserLiveDev.status >= MultiBrowserLiveDev.STATUS_ACTIVE) { - MultiBrowserLiveDev.agents.remote.call("updateConfig",JSON.stringify(config)); + MultiBrowserLiveDev.updateConfig(JSON.stringify(config)); } }); @@ -304,6 +304,10 @@ define(function main(require, exports, module) { function updateElementHighlightConfig() { const prefValue = PreferencesManager.get(PREFERENCE_PROJECT_ELEMENT_HIGHLIGHT); config.elemHighlights = prefValue || "hover"; + if (MultiBrowserLiveDev && MultiBrowserLiveDev.status >= MultiBrowserLiveDev.STATUS_ACTIVE) { + MultiBrowserLiveDev.updateConfig(JSON.stringify(config)); + MultiBrowserLiveDev.registerHandlers(); + } } PreferencesManager.on("change", PREFERENCE_PROJECT_ELEMENT_HIGHLIGHT, function() { @@ -335,7 +339,7 @@ define(function main(require, exports, module) { config.highlight = PreferencesManager.getViewState("livedevHighlight"); _updateHighlightCheckmark(); if (MultiBrowserLiveDev && MultiBrowserLiveDev.status >= MultiBrowserLiveDev.STATUS_ACTIVE) { - MultiBrowserLiveDev.agents.remote.call("updateConfig",JSON.stringify(config)); + MultiBrowserLiveDev.updateConfig(JSON.stringify(config)); } }); From 9c8859fc87a051ae8ee9c256e4c17dd5b567802b Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 5 Aug 2025 14:05:22 +0530 Subject: [PATCH 094/849] feat: add strings for elements highlight feature --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 8 ++++---- src/LiveDevelopment/main.js | 11 ++++++++++- .../Phoenix-live-preview/LivePreviewSettings.js | 2 +- .../Phoenix-live-preview/livePreviewSettings.html | 7 +++---- src/nls/root/strings.js | 8 ++++++++ 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index cac7189338..423b5cb517 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -562,23 +562,23 @@ function RemoteFunctions(config) { // Only include select parent option if element supports it if (showSelectParentOption) { - content += ` + content += ` ${ICONS.arrowUp} `; } // Only include edit text option if element supports it if (showEditTextOption) { - content += ` + content += ` ${ICONS.edit} `; } // Always include duplicate and delete options - content += ` + content += ` ${ICONS.copy} - + ${ICONS.trash} `; diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index e35896cc4f..4d2c095850 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -66,7 +66,16 @@ define(function main(require, exports, module) { showInfo: true }, isLPEditFeaturesActive: isLPEditFeaturesActive, - elemHighlights: "hover" // default value, this will get updated when the extension loads + elemHighlights: "hover", // default value, this will get updated when the extension loads + // this strings are used in RemoteFunctions.js + // we need to pass this through config as remoteFunctions runs in browser context and cannot + // directly reference Strings file + strings: { + selectParent: Strings.LIVE_DEV_MORE_OPTIONS_SELECT_PARENT, + editText: Strings.LIVE_DEV_MORE_OPTIONS_EDIT_TEXT, + duplicate: Strings.LIVE_DEV_MORE_OPTIONS_DUPLICATE, + delete: Strings.LIVE_DEV_MORE_OPTIONS_DELETE + } }; // Status labels/styles are ordered: error, not connected, progress1, progress2, connected. var _status, diff --git a/src/extensionsIntegrated/Phoenix-live-preview/LivePreviewSettings.js b/src/extensionsIntegrated/Phoenix-live-preview/LivePreviewSettings.js index 37784f0206..d5807c40de 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/LivePreviewSettings.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/LivePreviewSettings.js @@ -89,7 +89,7 @@ define(function (require, exports, module) { values: Object.keys(SUPPORTED_FRAMEWORKS) }); PreferencesManager.definePreference(LiveDevelopmentMain.PREFERENCE_PROJECT_ELEMENT_HIGHLIGHT, "string", "hover", { - description: "show live preview element highlights on 'hover' or 'click'. Defaults to 'hover'" + description: Strings.LIVE_DEV_SETTINGS_ELEMENT_HIGHLIGHT_PREFERENCE }); async function detectFramework($frameworkSelect, $hotReloadChk) { diff --git a/src/extensionsIntegrated/Phoenix-live-preview/livePreviewSettings.html b/src/extensionsIntegrated/Phoenix-live-preview/livePreviewSettings.html index d9d9af726a..d76c79ef83 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/livePreviewSettings.html +++ b/src/extensionsIntegrated/Phoenix-live-preview/livePreviewSettings.html @@ -27,11 +27,10 @@

{{Strings.LIVE_DEV_SETTINGS_TITLE}}

-
- +
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 88c738c16d..ae92f3b4ce 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -179,6 +179,14 @@ define({ "LIVE_DEV_SETTINGS_FRAMEWORK": "Server Framework", "LIVE_DEV_SETTINGS_FRAMEWORK_CUSTOM": "Custom", "LIVE_DEV_SETTINGS_FRAMEWORK_PREFERENCES": "Server Framework, currently supports only docusaurus", + "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_MORE_OPTIONS_SELECT_PARENT": "Select Parent", + "LIVE_DEV_MORE_OPTIONS_EDIT_TEXT": "Edit Text", + "LIVE_DEV_MORE_OPTIONS_DUPLICATE": "Duplicate", + "LIVE_DEV_MORE_OPTIONS_DELETE": "Delete", "LIVE_PREVIEW_CUSTOM_SERVER_BANNER": "Getting preview from your custom server {0}", "LIVE_DEV_DETACHED_REPLACED_WITH_DEVTOOLS": "Live Preview was canceled because the browser's developer tools were opened", From bfd3c16381f2b610d0239ef0faa29bbab5188e13 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 5 Aug 2025 14:14:41 +0530 Subject: [PATCH 095/849] feat: improve styles for element highlight setting --- .../Phoenix-live-preview/livePreviewSettings.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extensionsIntegrated/Phoenix-live-preview/livePreviewSettings.html b/src/extensionsIntegrated/Phoenix-live-preview/livePreviewSettings.html index d76c79ef83..71024d9a44 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/livePreviewSettings.html +++ b/src/extensionsIntegrated/Phoenix-live-preview/livePreviewSettings.html @@ -26,8 +26,8 @@

{{Strings.LIVE_DEV_SETTINGS_TITLE}}

-
- +
+ + +
+
+ `; + + shadow.innerHTML = `${content}`; + this._shadow = shadow; + }, + + create: function() { + this._style(); + window.document.body.appendChild(this.body); + + // Get the actual rendered dimensions of the box and position it + const boxElement = this._shadow.querySelector('.phoenix-ai-prompt-box'); + if (boxElement) { + const boxRect = boxElement.getBoundingClientRect(); + const pos = this._getBoxPosition(boxRect.width, boxRect.height); + + boxElement.style.left = pos.leftPos + 'px'; + boxElement.style.top = pos.topPos + 'px'; + } + + // Focus on the textarea + const textarea = this._shadow.querySelector('.phoenix-ai-prompt-textarea'); + if (textarea) { // small timer to make sure that the text area element is fetched + setTimeout(() => textarea.focus(), 50); + } + + this._attachEventHandlers(); + + // Prevent clicks inside the AI box from bubbling up and closing it + this.body.addEventListener('click', (event) => { + event.stopPropagation(); + }); + }, + + _attachEventHandlers: function() { + const textarea = this._shadow.querySelector('.phoenix-ai-prompt-textarea'); + const sendButton = this._shadow.querySelector('.phoenix-ai-prompt-send-button'); + + // Handle textarea input to enable/disable send button + if (textarea && sendButton) { + textarea.addEventListener('input', (event) => { + const hasText = event.target.value.trim().length > 0; + sendButton.disabled = !hasText; + }); + + // enter key + textarea.addEventListener('keydown', (event) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + if (textarea.value.trim()) { + this._handleSend(textarea.value.trim()); + } + } else if (event.key === 'Escape') { + event.preventDefault(); + this.remove(); + } + }); + } + + // send button click + if (sendButton) { + sendButton.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + if (textarea && textarea.value.trim()) { + this._handleSend(textarea.value.trim()); + } + }); + } + }, + + _handleSend: function(prompt) { + // TODO: need to implement the logic for backend handling here + console.log('AI Prompt:', prompt, 'for element:', this.element); + this.remove(); + }, + + remove: function() { + if (this._handleKeydown) { + document.removeEventListener('keydown', this._handleKeydown); + this._handleKeydown = null; + } + + if (this._handleResize) { + window.removeEventListener('resize', this._handleResize); + this._handleResize = null; + } + + if (this.body && this.body.parentNode && this.body.parentNode === window.document.body) { + window.document.body.removeChild(this.body); + this.body = null; + _aiPromptBox = null; + } + } + }; + function Highlight(color, trigger) { this.color = color; this.trigger = !!trigger; @@ -1829,6 +2091,7 @@ function RemoteFunctions(config) { var _clickHighlight; var _nodeInfoBox; var _nodeMoreOptionsBox; + var _aiPromptBox; var _setup = false; @@ -1904,9 +2167,7 @@ function RemoteFunctions(config) { _hoverHighlight.add(event.target, false, false); // false means no auto-scroll // Create info box for the hovered element - if (_nodeInfoBox) { - _nodeInfoBox.remove(); - } + dismissNodeInfoBox(); _nodeInfoBox = new NodeInfoBox(event.target); } } @@ -1933,10 +2194,7 @@ function RemoteFunctions(config) { } // Remove info box when mouse leaves the element - if (_nodeInfoBox) { - _nodeInfoBox.remove(); - _nodeInfoBox = null; - } + dismissNodeInfoBox(); } } @@ -1980,20 +2238,13 @@ function RemoteFunctions(config) { if (isElementVisible(element)) { _nodeMoreOptionsBox = new NodeMoreOptionsBox(element); - // show the info box when a DOM element is selected - if (_nodeInfoBox) { - _nodeInfoBox.remove(); - } + // show the info box when a DOM element is selected, but first remove any existing info box + dismissNodeInfoBox(); _nodeInfoBox = new NodeInfoBox(element); } else { // Element is hidden, so don't show UI boxes but still apply visual styling _nodeMoreOptionsBox = null; - - // Remove any existing info box since the element is not visible - if (_nodeInfoBox) { - _nodeInfoBox.remove(); - _nodeInfoBox = null; - } + dismissNodeInfoBox(); } element._originalOutline = element.style.outline; @@ -2019,6 +2270,8 @@ function RemoteFunctions(config) { * @param {Event} event */ function onClick(event) { + dismissAIPromptBox(); + // make sure that the feature is enabled and also the clicked element has the attribute 'data-brackets-id' if ( config.isLPEditFeaturesActive && @@ -2036,7 +2289,7 @@ function RemoteFunctions(config) { _nodeMoreOptionsBox && (event.target.tagName === "HTML" || event.target.tagName === "BODY" || _isInsideHeadTag(event.target)) ) { - dismissMoreOptionsBox(); + dismissUIAndCleanupState(); } } @@ -2075,7 +2328,7 @@ function RemoteFunctions(config) { function onKeyDown(event) { if ((event.key === "Escape" || event.key === "Esc")) { - dismissMoreOptionsBox(); + dismissUIAndCleanupState(); } if (!_setup && _validEvent(event)) { window.document.addEventListener("keyup", onKeyUp); @@ -2088,9 +2341,6 @@ function RemoteFunctions(config) { } } - /** Public Commands **********************************************************/ - - // remove active highlights function hideHighlight() { if (_clickHighlight) { @@ -2144,7 +2394,7 @@ function RemoteFunctions(config) { // if no valid element present we dismiss the boxes if (!foundValidElement) { - dismissMoreOptionsBox(); + dismissUIAndCleanupState(); } } @@ -2156,10 +2406,16 @@ function RemoteFunctions(config) { _nodeMoreOptionsBox = new NodeMoreOptionsBox(element); if (_nodeInfoBox) { - _nodeInfoBox.remove(); + dismissNodeInfoBox(); _nodeInfoBox = new NodeInfoBox(element); } } + + if (_aiPromptBox) { + const element = _aiPromptBox.element; + _aiPromptBox.remove(); + _aiPromptBox = new AIPromptBox(element); + } } // redraw active highlights @@ -2202,7 +2458,7 @@ function RemoteFunctions(config) { // 4 is just for pixelated differences if (Math.abs(calcNewDifference - prevDifference) > 4) { - dismissMoreOptionsBox(); + dismissUIAndCleanupState(); } } } @@ -2226,7 +2482,7 @@ function RemoteFunctions(config) { const prevDifference = _nodeInfoBox._possDifference; if (Math.abs(calcNewDifference - prevDifference) > 4) { - dismissMoreOptionsBox(); + dismissUIAndCleanupState(); } } } @@ -2525,14 +2781,8 @@ function RemoteFunctions(config) { window.document.removeEventListener("mouseout", onElementHoverOut); // Remove info box and more options box if highlight is disabled - if (_nodeInfoBox) { - _nodeInfoBox.remove(); - _nodeInfoBox = null; - } - if (_nodeMoreOptionsBox) { - _nodeMoreOptionsBox.remove(); - _nodeMoreOptionsBox = null; - } + dismissNodeInfoBox(); + dismissNodeMoreOptionsBox(); } // Handle element highlight mode changes for instant switching @@ -2560,8 +2810,7 @@ function RemoteFunctions(config) { // Remove info box when switching modes to avoid confusion if (_nodeInfoBox && !_nodeMoreOptionsBox) { - _nodeInfoBox.remove(); - _nodeInfoBox = null; + dismissNodeInfoBox(); } // Re-setup event listeners based on new mode to ensure proper behavior @@ -2585,25 +2834,61 @@ function RemoteFunctions(config) { } /** - * This function is responsible to remove the more options box - * we do this either when user presses the Esc key or clicks on the HTML or Body tags - * @return {boolean} true if any boxes were dismissed, false otherwise + * Helper function to dismiss NodeMoreOptionsBox if it exists + * @return {boolean} true if box was dismissed, false if it didn't exist */ - function dismissMoreOptionsBox() { - let dismissed = false; - + function dismissNodeMoreOptionsBox() { if (_nodeMoreOptionsBox) { _nodeMoreOptionsBox.remove(); _nodeMoreOptionsBox = null; - dismissed = true; + return true; } + return false; + } + /** + * Helper function to dismiss NodeInfoBox if it exists + * @return {boolean} true if box was dismissed, false if it didn't exist + */ + function dismissNodeInfoBox() { if (_nodeInfoBox) { _nodeInfoBox.remove(); _nodeInfoBox = null; - dismissed = true; + return true; } + return false; + } + /** + * Helper function to dismiss AIPromptBox if it exists + * @return {boolean} true if box was dismissed, false if it didn't exist + */ + function dismissAIPromptBox() { + if (_aiPromptBox) { + _aiPromptBox.remove(); + _aiPromptBox = null; + return true; + } + return false; + } + + /** + * Helper function to dismiss all UI boxes at once + * @return {boolean} true if any boxes were dismissed, false otherwise + */ + function dismissAllUIBoxes() { + let dismissed = false; + dismissed = dismissNodeMoreOptionsBox() || dismissed; + dismissed = dismissAIPromptBox() || dismissed; + dismissed = dismissNodeInfoBox() || dismissed; + return dismissed; + } + + /** + * Helper function to cleanup previously clicked element highlighting and state + * @return {boolean} true if cleanup was performed, false if no element to cleanup + */ + function cleanupPreviousElementState() { if (previouslyClickedElement) { if (previouslyClickedElement._originalOutline !== undefined) { previouslyClickedElement.style.outline = previouslyClickedElement._originalOutline; @@ -2622,12 +2907,29 @@ function RemoteFunctions(config) { } previouslyClickedElement = null; - dismissed = true; + return true; } + return false; + } + + /** + * This function dismisses all UI elements and cleans up application state + * Called when user presses Esc key, clicks on HTML/Body tags, or other dismissal events + * @return {boolean} true if any cleanup was performed, false otherwise + */ + function dismissUIAndCleanupState() { + let dismissed = false; + + // Dismiss all UI boxes + dismissed = dismissAllUIBoxes() || dismissed; + + // Cleanup previously clicked element state and highlighting + dismissed = cleanupPreviousElementState() || dismissed; return dismissed; } + /** * This function is responsible to move the cursor to the end of the text content when we start editing * @param {DOMElement} element @@ -2661,7 +2963,7 @@ function RemoteFunctions(config) { moveCursorToEnd(selection, element); } - dismissMoreOptionsBox(); + dismissUIAndCleanupState(); function onBlur() { finishEditing(element); @@ -2698,7 +3000,7 @@ function RemoteFunctions(config) { // Remove contenteditable attribute element.removeAttribute("contenteditable"); - dismissMoreOptionsBox(); + dismissUIAndCleanupState(); // Remove event listeners if (element._editListeners) { @@ -2755,7 +3057,6 @@ function RemoteFunctions(config) { "updateConfig" : updateConfig, "startEditing" : startEditing, "finishEditing" : finishEditing, - "dismissMoreOptionsBox" : dismissMoreOptionsBox, "hasVisibleLivePreviewBoxes" : hasVisibleLivePreviewBoxes, "registerHandlers" : registerHandlers }; From efd41fcd177db9fd8acadd7cb13de1b01e2b0a92 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 15 Aug 2025 14:34:06 +0530 Subject: [PATCH 124/849] feat: add live preview mode settings to preferences --- .../BrowserScripts/RemoteFunctions.js | 3 +- src/LiveDevelopment/LiveDevMultiBrowser.js | 4 +- .../Phoenix-live-preview/main.js | 73 +++++++++++++++++-- 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 0845aeed80..aea1a05209 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2830,7 +2830,7 @@ function RemoteFunctions(config) { * @return {boolean} true if any boxes are visible, false otherwise */ function hasVisibleLivePreviewBoxes() { - return _nodeMoreOptionsBox !== null || _nodeInfoBox !== null || previouslyClickedElement !== null; + return _nodeMoreOptionsBox !== null || _nodeInfoBox !== null || _aiPromptBox !== null || previouslyClickedElement !== null; } /** @@ -3058,6 +3058,7 @@ function RemoteFunctions(config) { "startEditing" : startEditing, "finishEditing" : finishEditing, "hasVisibleLivePreviewBoxes" : hasVisibleLivePreviewBoxes, + "dismissUIAndCleanupState" : dismissUIAndCleanupState, "registerHandlers" : registerHandlers }; } diff --git a/src/LiveDevelopment/LiveDevMultiBrowser.js b/src/LiveDevelopment/LiveDevMultiBrowser.js index 976acda42b..3b2381dc09 100644 --- a/src/LiveDevelopment/LiveDevMultiBrowser.js +++ b/src/LiveDevelopment/LiveDevMultiBrowser.js @@ -710,11 +710,11 @@ define(function (require, exports, module) { } /** - * Dismiss live preview more options box and info box + * Dismiss live preview boxes like info box, options box, AI box */ function dismissLivePreviewBoxes() { if (_protocol) { - _protocol.evaluate("_LD.dismissMoreOptionsBox()"); + _protocol.evaluate("_LD.dismissUIAndCleanupState()"); } } diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js index 052ad7ed27..22f86dae9b 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/main.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js @@ -86,6 +86,15 @@ define(function (require, exports, module) { const PREVIEW_TRUSTED_PROJECT_KEY = "preview_trusted"; const PREVIEW_PROJECT_README_KEY = "preview_readme"; + // live preview mode pref + const PREFERENCE_LIVE_PREVIEW_MODE = "livePreviewMode"; + const DEFAULT_LIVE_PREVIEW_MODE = "preview"; // preview, inspect or edit + // define the live preview mode preference + PreferencesManager.definePreference(PREFERENCE_LIVE_PREVIEW_MODE, "string", DEFAULT_LIVE_PREVIEW_MODE, { + description: "Default live preview mode on startup (preview, inspect, edit)", + values: ["preview", "inspect", "edit"] + }); + const LIVE_PREVIEW_PANEL_ID = "live-preview-panel"; const LIVE_PREVIEW_IFRAME_ID = "panel-live-preview-frame"; const LIVE_PREVIEW_IFRAME_HTML = ` @@ -179,6 +188,40 @@ define(function (require, exports, module) { } } + /** + * update the mode button text in the live preview toolbar UI based on the current mode + * @param {String} mode - The current mode ("preview", "inspect", or "edit") + */ + function _updateModeButton(mode) { + if ($modeBtn) { + if (mode === "inspect") { + $modeBtn[0].textContent = "Inspect Mode"; + } else if (mode === "edit") { + $modeBtn[0].textContent = "Edit Mode"; + } else { + $modeBtn[0].textContent = "Preview Mode"; + } + } + } + + /** + * init live preview mode from saved preferences + */ + function _initializeMode() { + const savedMode = PreferencesManager.get(PREFERENCE_LIVE_PREVIEW_MODE) || "preview"; + + // apply the saved + if (savedMode === "inspect") { + _LPInspectMode(); + } else if (savedMode === "edit") { + _LPEditMode(); + } else { + _LPPreviewMode(); + } + + _updateModeButton(savedMode); + } + function _showModeSelectionDropdown(event) { const items = ["Preview Mode", "Inspect Mode", "Edit Mode"]; @@ -204,20 +247,19 @@ define(function (require, exports, module) { // handle the option selection dropdown.on("select", function (e, item, index) { + // here we just set the preference + // as the preferences listener will automatically handle the required changes if (index === 0) { - _LPPreviewMode(); + PreferencesManager.set(PREFERENCE_LIVE_PREVIEW_MODE, "preview"); } else if (index === 1) { - _LPInspectMode(); + PreferencesManager.set(PREFERENCE_LIVE_PREVIEW_MODE, "inspect"); } else if (index === 2) { - _LPEditMode(); + PreferencesManager.set(PREFERENCE_LIVE_PREVIEW_MODE, "edit"); } // need to dismiss the previous highlighting and stuff LiveDevelopment.hideHighlight(); LiveDevelopment.dismissLivePreviewBoxes(); - if($modeBtn) { - $modeBtn[0].textContent = item; - } }); // Remove the button after the dropdown is hidden @@ -897,6 +939,25 @@ define(function (require, exports, module) { fileMenu.addMenuItem(Commands.FILE_LIVE_FILE_PREVIEW_SETTINGS, "", Menus.AFTER, Commands.FILE_LIVE_FILE_PREVIEW); fileMenu.addMenuDivider(Menus.BEFORE, Commands.FILE_LIVE_FILE_PREVIEW); + + // init live preview mode from saved preferences + _initializeMode(); + // listen for pref changes + PreferencesManager.on("change", PREFERENCE_LIVE_PREVIEW_MODE, function () { + // Get the current preference value directly + const newMode = PreferencesManager.get(PREFERENCE_LIVE_PREVIEW_MODE); + + if (newMode === "inspect") { + _LPInspectMode(); + } else if (newMode === "edit") { + _LPEditMode(); + } else { + _LPPreviewMode(); + } + + _updateModeButton(newMode); + }); + LiveDevelopment.openLivePreview(); LiveDevelopment.on(LiveDevelopment.EVENT_OPEN_PREVIEW_URL, _openLivePreviewURL); LiveDevelopment.on(LiveDevelopment.EVENT_LIVE_PREVIEW_RELOAD, ()=>{ From 014044fa2df6ed44babe7a484dacaecd950d0626 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 15 Aug 2025 23:26:55 +0530 Subject: [PATCH 125/849] feat: move all the strings to strings.js file --- .../BrowserScripts/RemoteFunctions.js | 9 +++-- src/LiveDevelopment/main.js | 4 ++- .../Phoenix-live-preview/main.js | 36 ++++++++++--------- src/nls/root/strings.js | 6 ++++ 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index aea1a05209..87545a9113 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1180,7 +1180,7 @@ function RemoteFunctions(config) { let content = `
`; // not sure if we need to hide/show the AI icon, right now showing always - content += ` + content += ` ${ICONS.ai} `; @@ -1681,7 +1681,7 @@ function RemoteFunctions(config) {
- -
- - -