diff --git a/docs/API-Reference/view/PanelView.md b/docs/API-Reference/view/PanelView.md
index cd9055fc0..3a00fb116 100644
--- a/docs/API-Reference/view/PanelView.md
+++ b/docs/API-Reference/view/PanelView.md
@@ -52,7 +52,8 @@ Determines if the panel is visible
### panel.registerCanBeShownHandler(canShowHandlerFn) ⇒ boolean
-Registers a call back function that will be called just before panel is shown. The handler should return true
if the panel can be shown, else return false and the panel will not be shown.
+Registers a call back function that will be called just before panel is shown. The handler should return true
+if the panel can be shown, else return false and the panel will not be shown.
**Kind**: instance method of [Panel](#Panel)
**Returns**: boolean - true if visible, false if not
@@ -70,7 +71,8 @@ Returns true if th panel can be shown, else false.
### panel.registerOnCloseRequestedHandler(handler)
-Registers an async handler that is called before the panel is closed via user interaction (e.g. clicking the
tab close button). The handler should return `true` to allow the close, or `false` to prevent it.
+Registers an async handler that is called before the panel is closed via user interaction (e.g. clicking the
+tab close button). The handler should return `true` to allow the close, or `false` to prevent it.
**Kind**: instance method of [Panel](#Panel)
@@ -81,7 +83,9 @@ Registers an async handler that is called before the panel is closed via user in
### panel.requestClose() ⇒ Promise.<boolean>
-Requests the panel to hide, invoking the registered onCloseRequested handler first (if any).
If the handler returns false, the panel stays open. If it returns true or no handler is
registered, `hide()` is called.
+Requests the panel to hide, invoking the registered onCloseRequested handler first (if any).
+If the handler returns false, the panel stays open. If it returns true or no handler is
+registered, `hide()` is called.
**Kind**: instance method of [Panel](#Panel)
**Returns**: Promise.<boolean> - Resolves to true if the panel was hidden, false if prevented.
@@ -100,7 +104,8 @@ Hides the panel
### panel.focus() ⇒ boolean
-Attempts to focus the panel. Override this in panels that support focus
(e.g. terminal). The default implementation returns false.
+Attempts to focus the panel. Override this in panels that support focus
+(e.g. terminal). The default implementation returns false.
**Kind**: instance method of [Panel](#Panel)
**Returns**: boolean - true if the panel accepted focus, false otherwise
@@ -129,7 +134,8 @@ Updates the display title shown in the tab bar for this panel.
### panel.destroy()
-Destroys the panel, removing it from the tab bar, internal maps, and the DOM.
After calling this, the Panel instance should not be reused.
+Destroys the panel, removing it from the tab bar, internal maps, and the DOM.
+After calling this, the Panel instance should not be reused.
**Kind**: instance method of [Panel](#Panel)
@@ -197,6 +203,18 @@ The editor holder element, passed from WorkspaceManager
## \_recomputeLayout : function
recomputeLayout callback from WorkspaceManager
+**Kind**: global variable
+
+
+## \_defaultPanelId : string \| null
+The default/quick-access panel ID
+
+**Kind**: global variable
+
+
+## \_$addBtn : jQueryObject
+The "+" button inside the tab overflow area
+
**Kind**: global variable
@@ -231,13 +249,17 @@ type for bottom panel
## MAXIMIZE\_THRESHOLD : number
-Pixel threshold for detecting near-maximize state during resize.
If the editor holder height is within this many pixels of zero, the
panel is treated as maximized. Keeps the maximize icon responsive
during drag without being overly sensitive.
+Pixel threshold for detecting near-maximize state during resize.
+If the editor holder height is within this many pixels of zero, the
+panel is treated as maximized. Keeps the maximize icon responsive
+during drag without being overly sensitive.
**Kind**: global constant
## MIN\_PANEL\_HEIGHT : number
-Minimum panel height (matches Resizer minSize) used as a floor
when computing a sensible restore height.
+Minimum panel height (matches Resizer minSize) used as a floor
+when computing a sensible restore height.
**Kind**: global constant
@@ -245,11 +267,18 @@ Minimum panel height (matches Resizer minSize) used as a floor
when computing a
## PREF\_BOTTOM\_PANEL\_MAXIMIZED
Preference key for persisting the maximize state across reloads.
+**Kind**: global constant
+
+
+## PREF\_BOTTOM\_PANEL\_MAXIMIZED
+Preference key for persisting the maximize state across reloads.
+
**Kind**: global constant
## init($container, $tabBar, $tabsOverflow, $editorHolder, recomputeLayoutFn, defaultPanelId)
-Initializes the PanelView module with references to the bottom panel container DOM elements.
Called by WorkspaceManager during htmlReady.
+Initializes the PanelView module with references to the bottom panel container DOM elements.
+Called by WorkspaceManager during htmlReady.
**Kind**: global function
@@ -265,19 +294,26 @@ Initializes the PanelView module with references to the bottom panel container D
## exitMaximizeOnResize()
-Exit maximize state without resizing (for external callers like drag-resize).
Clears internal maximize state and resets the button icon.
+Exit maximize state without resizing (for external callers like drag-resize).
+Clears internal maximize state and resets the button icon.
**Kind**: global function
## enterMaximizeOnResize()
-Enter maximize state during a drag-resize that reaches the maximum
height. No pre-maximize height is stored because the user arrived
here via continuous dragging; a sensible default will be computed if
they later click the Restore button.
+Enter maximize state during a drag-resize that reaches the maximum
+height. No pre-maximize height is stored because the user arrived
+here via continuous dragging; a sensible default will be computed if
+they later click the Restore button.
**Kind**: global function
## restoreIfMaximized()
-Restore the container's CSS height to the pre-maximize value and clear maximize state.
Must be called BEFORE Resizer.hide() so the Resizer reads the correct height.
If not maximized, this is a no-op.
When the saved height is near-max or unknown, a sensible default is used.
+Restore the container's CSS height to the pre-maximize value and clear maximize state.
+Must be called BEFORE Resizer.hide() so the Resizer reads the correct height.
+If not maximized, this is a no-op.
+When the saved height is near-max or unknown, a sensible default is used.
**Kind**: global function
@@ -308,7 +344,8 @@ Returns the currently active (visible) bottom panel, or null if none.
## showNextPanel() ⇒ boolean
-Cycle to the next open bottom panel tab. If the container is hidden
or no panels are open, does nothing and returns false.
+Cycle to the next open bottom panel tab. If the container is hidden
+or no panels are open, does nothing and returns false.
**Kind**: global function
**Returns**: boolean - true if a panel switch occurred
diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js
index cf054caa3..bc4c450d8 100644
--- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js
+++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js
@@ -41,19 +41,12 @@ function RemoteFunctions(config = {}) {
// Expose the currently selected element globally for external access
window.__current_ph_lp_selected = null;
- var req, timeout;
- function animateHighlight(time) {
- if(req) {
- window.cancelAnimationFrame(req);
- window.clearTimeout(timeout);
- }
- req = window.requestAnimationFrame(redrawHighlights);
-
- timeout = setTimeout(function () {
- window.cancelAnimationFrame(req);
- req = null;
- }, time * 1000);
- }
+ const COLORS = {
+ highlightPadding: "rgba(147, 196, 125, 0.55)",
+ highlightMargin: "rgba(246, 178, 107, 0.66)",
+ outlineEditable: "#4285F4",
+ outlineNonEditable: "#3C3F41"
+ };
// the following fucntions can be in the handler and live preview will call those functions when the below
// events happen
@@ -61,6 +54,7 @@ function RemoteFunctions(config = {}) {
"dismiss", // when handler gets this event, it should dismiss all ui it renders in the live preview
"createToolBox",
"createInfoBox",
+ "createHoverBox",
"createMoreOptionsDropdown",
// render an icon or html when the selected element toolbox appears in edit mode.
"renderToolBoxItem",
@@ -181,7 +175,8 @@ function RemoteFunctions(config = {}) {
handleElementClick: handleElementClick,
cleanupPreviousElementState: cleanupPreviousElementState,
disableHoverListeners: disableHoverListeners,
- enableHoverListeners: enableHoverListeners
+ enableHoverListeners: enableHoverListeners,
+ redrawHighlights: redrawHighlights
};
/**
@@ -265,310 +260,141 @@ function RemoteFunctions(config = {}) {
return element.offsetTop + (element.offsetParent ? getDocumentOffsetTop(element.offsetParent) : 0);
}
- function Highlight(color, trigger) {
- this.color = color;
+ function Highlight(trigger) {
this.trigger = !!trigger;
this.elements = [];
this.selector = "";
+ this._divs = [];
}
Highlight.prototype = {
- _elementExists: function (element) {
- var i;
- for (i in this.elements) {
- if (this.elements[i] === element) {
- return true;
- }
- }
- return false;
- },
- _makeHighlightDiv: function (element, doAnimation) {
- const remoteHighlight = {
- animateStartValue: {
- "background-color": "rgba(0, 162, 255, 0.5)",
- "opacity": 0
- },
- animateEndValue: {
- "background-color": "rgba(0, 162, 255, 0)",
- "opacity": 0.6
- },
- paddingStyling: {
- "background-color": "rgba(200, 249, 197, 0.7)"
- },
- marginStyling: {
- "background-color": "rgba(249, 204, 157, 0.7)"
- },
- borderColor: "rgba(200, 249, 197, 0.85)",
- showPaddingMargin: true
- };
- var elementBounds = element.getBoundingClientRect(),
- highlightDiv = window.document.createElement("div"),
- elementStyling = window.getComputedStyle(element),
- transitionDuration = parseFloat(elementStyling.getPropertyValue('transition-duration')),
- animationDuration = parseFloat(elementStyling.getPropertyValue('animation-duration'));
-
- highlightDiv.trackingElement = element; // save which node are we highlighting
-
- if (doAnimation) {
- if (transitionDuration) {
- animateHighlight(transitionDuration);
- }
-
- if (animationDuration) {
- animateHighlight(animationDuration);
- }
- }
-
- // Don't highlight elements with 0 width & height
- 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);
- outerWidth = innerWidth + parseFloat(realElBorder.right) +
- parseFloat(realElBorder.left),
- 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["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);
- elStyling["height"] = outerHeight + margin["top"] + margin["bottom"] + "px";
- elStyling["top"] = "-" + (margin["top"] + parseFloat(realElBorder.top)) + "px";
- } else {
- elStyling["height"] = elementStyling.getPropertyValue("margin-" + side);
- elStyling["width"] = outerWidth + "px";
- elStyling["left"] = "-" + realElBorder.left;
- }
-
- elStyling[side] = "-" + (margin[side] + parseFloat(realElBorder[side])) + "px";
- elStyling["position"] = "absolute";
-
- return elStyling;
- };
-
- var setVisibility = function (el) {
- if (
- !remoteHighlight.showPaddingMargin ||
- parseInt(el.height, 10) <= 0 ||
- parseInt(el.width, 10) <= 0
- ) {
- el.display = 'none';
- } else {
- el.display = 'block';
- }
- };
-
- var paddingVisualisations = [
- drawPaddingRect("top"),
- drawPaddingRect("right"),
- drawPaddingRect("bottom"),
- drawPaddingRect("left")
- ];
-
- var marginVisualisations = [
- drawMarginRect("top"),
- drawMarginRect("right"),
- drawMarginRect("bottom"),
- drawMarginRect("left")
- ];
-
- var setupVisualisations = function (arr, visualConfig) {
- 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"),
- styles = Object.assign({}, visualConfig, arr[i]);
-
- _setStyleValues(styles, el.style);
-
- highlightDiv.appendChild(el);
- }
- };
-
- setupVisualisations(
- marginVisualisations,
- remoteHighlight.marginStyling
- );
- setupVisualisations(
- paddingVisualisations,
- remoteHighlight.paddingStyling
- );
-
- highlightDiv.className = GLOBALS.HIGHLIGHT_CLASSNAME;
-
- var offset = LivePreviewView.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
- // default project at improper places. Some other cases might fail as the above code said they
- // introduces that removed computation for fixing some box-model regression. If you are here to fix a
- // related bug, check history of this changes in git.
-
- var stylesToSet = {
- "left": offset.left + "px",
- "top": offset.top + "px",
- "width": elementBounds.width + "px",
- "height": elementBounds.height + "px",
- "z-index": 2147483645,
- "margin": 0,
- "padding": 0,
- "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'),
- "border-bottom": elementStyling.getPropertyValue('border-bottom'),
- "border-color": remoteHighlight.borderColor
- };
-
- var mergedStyles = Object.assign({}, stylesToSet, remoteHighlight.stylesToSet);
-
- var animateStartValues = remoteHighlight.animateStartValue;
-
- var animateEndValues = remoteHighlight.animateEndValue;
-
- var transitionValues = {
- "transition-property": "opacity, background-color, transform",
- "transition-duration": "300ms, 2.3s"
- };
-
- function _setStyleValues(styleValues, obj) {
- var prop;
-
- for (prop in styleValues) {
- obj.setProperty(prop, styleValues[prop]);
- }
- }
-
- _setStyleValues(mergedStyles, highlightDiv.style);
- _setStyleValues(
- doAnimation ? animateStartValues : animateEndValues,
- highlightDiv.style
- );
-
-
- if (doAnimation) {
- _setStyleValues(transitionValues, highlightDiv.style);
-
- window.setTimeout(function () {
- _setStyleValues(animateEndValues, highlightDiv.style);
- }, 20);
- }
-
- window.document.body.appendChild(highlightDiv);
- },
-
- add: function (element, doAnimation) {
- if (this._elementExists(element) || element === window.document) {
+ add: function (element) {
+ if (this.elements.includes(element) || element === window.document) {
return;
}
if (this.trigger) {
_trigger(element, "highlight", 1);
}
-
this.elements.push(element);
- this._makeHighlightDiv(element, doAnimation);
+ this._createOverlay(element);
},
clear: function () {
- var i, highlights = window.document.querySelectorAll("." + GLOBALS.HIGHLIGHT_CLASSNAME),
- body = window.document.body;
-
- for (i = 0; i < highlights.length; i++) {
- body.removeChild(highlights[i]);
- }
-
- for (i = 0; i < this.elements.length; i++) {
- if (this.trigger) {
- _trigger(this.elements[i], "highlight", 0);
+ this._divs.forEach(function (div) {
+ if (div.parentNode) {
+ div.parentNode.removeChild(div);
}
- clearElementHoverHighlight(this.elements[i]);
- }
+ });
+ this._divs = [];
+ if (this.trigger) {
+ this.elements.forEach(function (el) {
+ _trigger(el, "highlight", 0);
+ });
+ }
this.elements = [];
},
redraw: function () {
- var i, highlighted;
+ const elements = this.selector
+ ? Array.from(window.document.querySelectorAll(this.selector))
+ : this.elements.slice();
+ this.clear();
+ elements.forEach(function (el) { this.add(el); }, this);
+ },
- // When redrawing a selector-based highlight, run a new selector
- // query to ensure we have the latest set of elements to highlight.
- if (this.selector) {
- highlighted = window.document.querySelectorAll(this.selector);
+ _createOverlay: function (element) {
+ const bounds = element.getBoundingClientRect();
+ if (bounds.width === 0 && bounds.height === 0) { return; }
+
+ const cs = window.getComputedStyle(element);
+ const div = window.document.createElement("div");
+ div.className = GLOBALS.HIGHLIGHT_CLASSNAME;
+ div.trackingElement = element;
+
+ // Parse box model values
+ const bt = parseFloat(cs.borderTopWidth) || 0,
+ br = parseFloat(cs.borderRightWidth) || 0,
+ bb = parseFloat(cs.borderBottomWidth) || 0,
+ bl = parseFloat(cs.borderLeftWidth) || 0;
+ const pt = parseFloat(cs.paddingTop) || 0,
+ pr = parseFloat(cs.paddingRight) || 0,
+ pb = parseFloat(cs.paddingBottom) || 0,
+ pl = parseFloat(cs.paddingLeft) || 0;
+ const mt = parseFloat(cs.marginTop) || 0,
+ mr = parseFloat(cs.marginRight) || 0,
+ mb = parseFloat(cs.marginBottom) || 0,
+ ml = parseFloat(cs.marginLeft) || 0;
+
+ const isBorderBox = cs.boxSizing === "border-box";
+ const w = parseFloat(cs.width) || 0;
+ const h = parseFloat(cs.height) || 0;
+
+ // Dimensions inside border
+ let innerW, innerH;
+ if (isBorderBox) {
+ innerW = w - bl - br;
+ innerH = h - bt - bb;
} else {
- highlighted = this.elements.slice(0);
+ innerW = w + pl + pr;
+ innerH = h + pt + pb;
}
-
- this.clear();
- for (i = 0; i < highlighted.length; i++) {
- this.add(highlighted[i], false);
+ const contentH = innerH - pt - pb;
+ const outerW = innerW + bl + br;
+ const outerH = innerH + bt + bb;
+
+ // Position the overlay to match the element
+ const offset = LivePreviewView.screenOffset(element);
+ const divStyle = div.style;
+ divStyle.position = "absolute";
+ divStyle.left = offset.left + "px";
+ divStyle.top = offset.top + "px";
+ divStyle.width = bounds.width + "px";
+ divStyle.height = bounds.height + "px";
+ divStyle.zIndex = 2147483645;
+ divStyle.margin = "0";
+ divStyle.padding = "0";
+ divStyle.pointerEvents = "none";
+ divStyle.boxSizing = cs.boxSizing;
+ divStyle.borderTopWidth = bt + "px";
+ divStyle.borderRightWidth = br + "px";
+ divStyle.borderBottomWidth = bb + "px";
+ divStyle.borderLeftWidth = bl + "px";
+ divStyle.borderStyle = "solid";
+ divStyle.borderColor = "transparent";
+
+ // Helper to create a colored rect
+ function makeRect(styles, color) {
+ if (parseFloat(styles.width) <= 0 || parseFloat(styles.height) <= 0) { return; }
+ const r = window.document.createElement("div");
+ r.style.position = "absolute";
+ r.style.backgroundColor = color;
+ r.style.transform = "none";
+ for (const prop in styles) {
+ r.style[prop] = styles[prop];
+ }
+ div.appendChild(r);
}
+
+ // Padding rects (top/bottom full width, left/right content height)
+ const padColor = COLORS.highlightPadding;
+ makeRect({ top: "0", left: "0", width: innerW + "px", height: pt + "px" }, padColor);
+ makeRect({ bottom: "0", left: "0", width: innerW + "px", height: pb + "px" }, padColor);
+ makeRect({ top: pt + "px", left: "0", width: pl + "px", height: contentH + "px" }, padColor);
+ makeRect({ top: pt + "px", right: "0", width: pr + "px", height: contentH + "px" }, padColor);
+
+ // Margin rects (top/bottom element width, left/right full height)
+ const margColor = COLORS.highlightMargin;
+ const mTop = -(mt + bt) + "px";
+ const mBot = -(mb + bb) + "px";
+ const fullH = (outerH + mt + mb) + "px";
+ makeRect({ top: mTop, left: -bl + "px", width: outerW + "px", height: mt + "px" }, margColor);
+ makeRect({ bottom: mBot, left: -bl + "px", width: outerW + "px", height: mb + "px" }, margColor);
+ makeRect({ top: mTop, left: -(ml + bl) + "px", width: ml + "px", height: fullH }, margColor);
+ makeRect({ top: mTop, right: -(mr + br) + "px", width: mr + "px", height: fullH }, margColor);
+
+ window.document.body.appendChild(div);
+ this._divs.push(div);
}
};
@@ -608,25 +434,35 @@ function RemoteFunctions(config = {}) {
// if _hoverHighlight is uninitialized, initialize it
if (!_hoverHighlight && shouldShowHighlightOnHover()) {
- _hoverHighlight = new Highlight("#c8f9c5", true);
+ _hoverHighlight = new Highlight(true);
}
// this is to check the user's settings, if they want to show the elements highlights on hover or click
if (_hoverHighlight && shouldShowHighlightOnHover()) {
+ _hoverHighlight.elements.forEach(clearElementHoverHighlight);
_hoverHighlight.clear();
- // Store original outline to restore on hover out, then apply a blue border
- element._originalHoverOutline = element.style.outline;
- const outlineColor = element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR) ? "#4285F4" : "#3C3F41";
- element.style.outline = `1px solid ${outlineColor}`;
-
- _hoverHighlight.add(element, false);
+ // Skip hover outline and overlay for the currently click-selected element.
+ // It already has its own outline and overlay from the click/selection flow.
+ // Adding hover state on top would corrupt _originalHoverOutline (it would capture
+ // the click outline instead of the true original) and stack duplicate overlays.
+ if (element !== previouslySelectedElement) {
+ // Store original outline to restore on hover out, then apply a border
+ element._originalHoverOutline = element.style.outline;
+ const isEditable = element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
+ const outlineColor = isEditable ? COLORS.outlineEditable : COLORS.outlineNonEditable;
+ element.style.outline = `1px solid ${outlineColor}`;
+
+ _hoverHighlight.add(element);
+ }
- // create the info box for the hovered element
- const infoBoxHandler = LivePreviewView.getToolHandler("InfoBox");
- if (infoBoxHandler) {
- infoBoxHandler.dismiss();
- infoBoxHandler.createInfoBox(element);
+ // Show minimal hover tooltip (tag + dimensions)
+ const hoverBoxHandler = LivePreviewView.getToolHandler("HoverBox");
+ if (hoverBoxHandler) {
+ hoverBoxHandler.dismiss();
+ if (element !== previouslySelectedElement) {
+ hoverBoxHandler.createHoverBox(element);
+ }
}
}
}
@@ -636,15 +472,17 @@ function RemoteFunctions(config = {}) {
if (SHARED_STATE.isAutoScrolling) { return; }
const element = event.target;
- if(LivePreviewView.isElementEditable(element) && element.nodeType === Node.ELEMENT_NODE) {
+ // Use isElementInspectable (not isElementEditable) so that JS-rendered
+ // elements also get their hover highlight and hover box properly dismissed.
+ if(LivePreviewView.isElementInspectable(element) && element.nodeType === Node.ELEMENT_NODE) {
// this is to check the user's settings, if they want to show the elements highlights on hover or click
if (_hoverHighlight && shouldShowHighlightOnHover()) {
_hoverHighlight.clear();
clearElementHoverHighlight(element);
- // dismiss the info box
- const infoBoxHandler = LivePreviewView.getToolHandler("InfoBox");
- if (infoBoxHandler) {
- infoBoxHandler.dismiss();
+ // dismiss the hover box
+ const hoverBoxHandler = LivePreviewView.getToolHandler("HoverBox");
+ if (hoverBoxHandler) {
+ hoverBoxHandler.dismiss();
}
}
}
@@ -674,7 +512,7 @@ function RemoteFunctions(config = {}) {
// this should also be there when users are in highlight mode
scrollElementToViewPort(element);
- if(!LivePreviewView.isElementInspectable(element)) {
+ if(!LivePreviewView.isElementInspectable(element, true)) {
return false;
}
@@ -698,14 +536,15 @@ function RemoteFunctions(config = {}) {
}
element._originalOutline = element.style.outline;
- const outlineColor = element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR) ? "#4285F4" : "#3C3F41";
+ const isEditable = element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
+ const outlineColor = isEditable ? COLORS.outlineEditable : COLORS.outlineNonEditable;
element.style.outline = `1px solid ${outlineColor}`;
if (!_clickHighlight) {
- _clickHighlight = new Highlight("#cfc");
+ _clickHighlight = new Highlight();
}
_clickHighlight.clear();
- _clickHighlight.add(element, true);
+ _clickHighlight.add(element);
previouslySelectedElement = element;
window.__current_ph_lp_selected = element;
@@ -812,10 +651,13 @@ function RemoteFunctions(config = {}) {
clearCssSelectorHighlight();
// Create new temporary highlight for all matching elements
- _cssSelectorHighlight = new Highlight("#cfc");
- for (var i = 0; i < nodes.length; i++) {
- if (LivePreviewView.isElementInspectable(nodes[i], true) && nodes[i].nodeType === Node.ELEMENT_NODE) {
- _cssSelectorHighlight.add(nodes[i], true);
+ // Skip the selected element since it already has a click highlight
+ _cssSelectorHighlight = new Highlight();
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i] !== previouslySelectedElement &&
+ LivePreviewView.isElementInspectable(nodes[i], true) &&
+ nodes[i].nodeType === Node.ELEMENT_NODE) {
+ _cssSelectorHighlight.add(nodes[i]);
}
}
_cssSelectorHighlight.selector = rule;
@@ -831,6 +673,7 @@ function RemoteFunctions(config = {}) {
_clickHighlight = null;
}
if (_hoverHighlight) {
+ _hoverHighlight.elements.forEach(clearElementHoverHighlight);
_hoverHighlight.clear();
_hoverHighlight = null;
}
@@ -840,13 +683,13 @@ function RemoteFunctions(config = {}) {
// highlight an element
function highlight(element, clear) {
if (!_clickHighlight) {
- _clickHighlight = new Highlight("#cfc");
+ _clickHighlight = new Highlight();
}
if (clear) {
_clickHighlight.clear();
}
if (LivePreviewView.isElementInspectable(element, true) && element.nodeType === Node.ELEMENT_NODE) {
- _clickHighlight.add(element, true);
+ _clickHighlight.add(element);
}
}
@@ -934,23 +777,24 @@ function RemoteFunctions(config = {}) {
// recreate UI boxes so that they are placed properly
function redrawUIBoxes() {
- if (SHARED_STATE._toolBox) {
- const element = SHARED_STATE._toolBox.element;
- const toolBoxHandler = LivePreviewView.getToolHandler("ToolBox");
- if (toolBoxHandler) {
- toolBoxHandler.dismiss();
- toolBoxHandler.createToolBox(element);
- }
- }
-
- if (SHARED_STATE._infoBox) {
- const element = SHARED_STATE._infoBox.element;
- const infoBoxHandler = LivePreviewView.getToolHandler("InfoBox");
- if (infoBoxHandler) {
- infoBoxHandler.dismiss();
- infoBoxHandler.createInfoBox(element);
- }
- }
+ // commented out for unified box redesign
+ // if (SHARED_STATE._toolBox) {
+ // const element = SHARED_STATE._toolBox.element;
+ // const toolBoxHandler = LivePreviewView.getToolHandler("ToolBox");
+ // if (toolBoxHandler) {
+ // toolBoxHandler.dismiss();
+ // toolBoxHandler.createToolBox(element);
+ // }
+ // }
+
+ // if (SHARED_STATE._infoBox) {
+ // const element = SHARED_STATE._infoBox.element;
+ // const infoBoxHandler = LivePreviewView.getToolHandler("InfoBox");
+ // if (infoBoxHandler) {
+ // infoBoxHandler.dismiss();
+ // infoBoxHandler.createInfoBox(element);
+ // }
+ // }
}
// redraw active highlights
@@ -1263,7 +1107,8 @@ function RemoteFunctions(config = {}) {
} else {
// Suppression is active - re-apply outline since attrChange may have wiped it
if (previouslySelectedElement && previouslySelectedElement.isConnected) {
- const outlineColor = previouslySelectedElement.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR) ? "#4285F4" : "#3C3F41";
+ const isEditable = previouslySelectedElement.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
+ const outlineColor = isEditable ? COLORS.outlineEditable : COLORS.outlineNonEditable;
previouslySelectedElement.style.outline = `1px solid ${outlineColor}`;
}
}
@@ -1301,7 +1146,25 @@ function RemoteFunctions(config = {}) {
_handleConfigurationChange();
}
+ // Preserve the currently selected element across re-registration
+ // so that toggling options (e.g. show measurements, show spacing handles)
+ // doesn't clear the element highlighting.
+ const selectedBeforeReregister = previouslySelectedElement;
registerHandlers();
+ if (!isModeChanged && !highlightModeChanged && selectedBeforeReregister
+ && config.mode === 'edit') {
+ // Restore the click highlight for the previously selected element
+ if (!_clickHighlight) {
+ _clickHighlight = new Highlight(true);
+ }
+ _clickHighlight.add(selectedBeforeReregister);
+ previouslySelectedElement = selectedBeforeReregister;
+ window.__current_ph_lp_selected = selectedBeforeReregister;
+ // Restore the outline
+ const isEditable = selectedBeforeReregister.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
+ const outlineColor = isEditable ? COLORS.outlineEditable : COLORS.outlineNonEditable;
+ selectedBeforeReregister.style.outline = `1px solid ${outlineColor}`;
+ }
return JSON.stringify(config);
}
@@ -1318,6 +1181,10 @@ function RemoteFunctions(config = {}) {
*/
function cleanupPreviousElementState() {
if (previouslySelectedElement) {
+ // Safety net: clear any stale hover outline tracking before hideHighlight runs.
+ // This prevents clearElementHoverHighlight from re-applying a captured click outline
+ // in edge cases where _originalHoverOutline was set on the selected element.
+ delete previouslySelectedElement._originalHoverOutline;
if (previouslySelectedElement._originalOutline !== undefined) {
previouslySelectedElement.style.outline = previouslySelectedElement._originalOutline;
} else {
@@ -1380,11 +1247,8 @@ function RemoteFunctions(config = {}) {
});
if (config.mode === 'edit') {
- // 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
+ _hoverHighlight = new Highlight(true);
+ _clickHighlight = new Highlight(true);
// register the event handlers
enableHoverListeners();
@@ -1449,7 +1313,16 @@ function RemoteFunctions(config = {}) {
"dismissUIAndCleanupState": dismissUIAndCleanupState,
"escapeKeyPressInEditor": _handleEscapeKeyPress,
"getMode": function() { return config.mode; },
- "suppressDOMEditDismissal": suppressDOMEditDismissal
+ "suppressDOMEditDismissal": suppressDOMEditDismissal,
+ "setHotCornerHidden": function(hidden) {
+ if (SHARED_STATE._hotCorner && SHARED_STATE._hotCorner.hotCorner) {
+ if (hidden) {
+ SHARED_STATE._hotCorner.hotCorner.classList.add('hc-hidden');
+ } else {
+ SHARED_STATE._hotCorner.hotCorner.classList.remove('hc-hidden');
+ }
+ }
+ }
};
// the below code comment is replaced by added scripts for extensibility
diff --git a/src/LiveDevelopment/LivePreviewConstants.js b/src/LiveDevelopment/LivePreviewConstants.js
index ecfc9a95d..928b326b6 100644
--- a/src/LiveDevelopment/LivePreviewConstants.js
+++ b/src/LiveDevelopment/LivePreviewConstants.js
@@ -41,4 +41,5 @@ define(function main(require, exports, module) {
exports.HIGHLIGHT_CLICK = "click";
exports.PREFERENCE_SHOW_RULER_LINES = "livePreviewShowMeasurements";
+ exports.PREFERENCE_SHOW_SPACING_HANDLES = "livePreviewShowSpacingHandles";
});
diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js
index 60d4caebf..87a3a5c91 100644
--- a/src/LiveDevelopment/main.js
+++ b/src/LiveDevelopment/main.js
@@ -70,6 +70,7 @@ define(function main(require, exports, module) {
mode: LIVE_HIGHLIGHT_MODE, // will be updated when we fetch entitlements
elemHighlights: CONSTANTS.HIGHLIGHT_HOVER, // default value, this will get updated when the extension loads
showRulerLines: false, // default value, this will get updated when the extension loads
+ showSpacingHandles: true, // default value, this will get updated when the extension loads
isPaidUser: false, // will be updated when we fetch entitlements
isLoggedIn: false, // will be updated when we fetch entitlements
hasLiveEditCapability: false // handled inside _liveEditCapabilityChanged function
@@ -324,6 +325,13 @@ define(function main(require, exports, module) {
MultiBrowserLiveDev.updateConfig(config);
}
+ function updateSpacingHandlesConfig() {
+ const prefValue = PreferencesManager.get(CONSTANTS.PREFERENCE_SHOW_SPACING_HANDLES);
+ const config = MultiBrowserLiveDev.getConfig();
+ config.showSpacingHandles = prefValue !== false;
+ MultiBrowserLiveDev.updateConfig(config);
+ }
+
EventDispatcher.makeEventDispatcher(exports);
// private api
@@ -347,6 +355,7 @@ define(function main(require, exports, module) {
exports.setLivePreviewTransportBridge = setLivePreviewTransportBridge;
exports.updateElementHighlightConfig = updateElementHighlightConfig;
exports.updateRulerLinesConfig = updateRulerLinesConfig;
+ exports.updateSpacingHandlesConfig = updateSpacingHandlesConfig;
exports.getConnectionIds = MultiBrowserLiveDev.getConnectionIds;
exports.getLivePreviewDetails = MultiBrowserLiveDev.getLivePreviewDetails;
exports.hideHighlight = MultiBrowserLiveDev.hideHighlight;
diff --git a/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css b/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css
index 6e3536afd..0bda851c2 100644
--- a/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css
+++ b/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css
@@ -80,13 +80,9 @@
}
#live-preview-plugin-toolbar:hover .lp-settings-icon {
- display: flex;
- align-items: center;
- color: #a0a0a0;
opacity: 1;
visibility: visible;
transition: unset;
- padding-left: 7.5px;
}
.live-preview-settings input.error, .live-preview-settings input:focus.error{
@@ -98,25 +94,37 @@
.lp-settings-icon {
opacity: 0;
color: #a0a0a0;
- display: flex;
- align-items: center;
visibility: hidden;
transition: opacity 1s, visibility 0s linear 1s; /* Fade-out effect */
- padding-left: 7.5px;
+ width: 30px;
+ height: 22px;
+ padding: 1px 6px;
+ flex-shrink: 0;
+ margin-top: 3.5px;
}
.lp-device-size-icon {
- color: #a0a0a0;
+ min-width: fit-content;
display: flex;
align-items: center;
- padding-left: 7.5px;
- margin-right: 7.5px;
+ margin: 3.5px 4px 0 3px;
+ cursor: pointer;
+ background: #3C3F41;
+ box-shadow: none;
+ border: 1px solid #3C3F41;
+ box-sizing: border-box;
+ color: #a0a0a0;
+ padding: 0 0.35em;
+}
+
+.lp-device-size-icon:hover {
+ border: 1px solid rgba(0, 0, 0, 0.24) !important;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12) !important;
}
#deviceSizeBtn.btn-dropdown::after {
position: static;
- margin-top: 2px;
- margin-left: 3px;
+ margin-left: 5px;
}
.device-size-item-icon {
@@ -154,7 +162,7 @@
min-width: fit-content;
display: flex;
align-items: center;
- margin: 3px 4px 0 3px;
+ margin: 3.5px 4px 0 3px;
max-width: 80%;
text-overflow: ellipsis;
overflow: hidden;
diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js
index 6b63e3080..c33c959ce 100644
--- a/src/extensionsIntegrated/Phoenix-live-preview/main.js
+++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js
@@ -110,6 +110,12 @@ define(function (require, exports, module) {
description: Strings.LIVE_DEV_SETTINGS_SHOW_RULER_LINES_PREFERENCE
});
+ // live preview spacing handles preference (show/hide spacing handles on element selection)
+ const PREFERENCE_SHOW_SPACING_HANDLES = CONSTANTS.PREFERENCE_SHOW_SPACING_HANDLES;
+ PreferencesManager.definePreference(PREFERENCE_SHOW_SPACING_HANDLES, "boolean", true, {
+ description: Strings.LIVE_DEV_SETTINGS_SHOW_SPACING_HANDLES_PREFERENCE
+ });
+
const LIVE_PREVIEW_PANEL_ID = "live-preview-panel";
const LIVE_PREVIEW_IFRAME_ID = "panel-live-preview-frame";
const LIVE_PREVIEW_IFRAME_HTML = `
@@ -338,6 +344,7 @@ define(function (require, exports, module) {
items.push("---");
items.push(Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON);
items.push(Strings.LIVE_PREVIEW_SHOW_RULER_LINES);
+ items.push(Strings.LIVE_PREVIEW_SHOW_SPACING_HANDLES);
}
const currentMode = LiveDevelopment.getCurrentMode();
@@ -372,6 +379,12 @@ define(function (require, exports, module) {
return `✓ ${Strings.LIVE_PREVIEW_SHOW_RULER_LINES}`;
}
return `${'\u00A0'.repeat(4)}${Strings.LIVE_PREVIEW_SHOW_RULER_LINES}`;
+ } else if (item === Strings.LIVE_PREVIEW_SHOW_SPACING_HANDLES) {
+ const isEnabled = PreferencesManager.get(PREFERENCE_SHOW_SPACING_HANDLES);
+ if(isEnabled) {
+ return `✓ ${Strings.LIVE_PREVIEW_SHOW_SPACING_HANDLES}`;
+ }
+ return `${'\u00A0'.repeat(4)}${Strings.LIVE_PREVIEW_SHOW_SPACING_HANDLES}`;
}
return item;
});
@@ -429,6 +442,15 @@ define(function (require, exports, module) {
const currentValue = PreferencesManager.get(PREFERENCE_SHOW_RULER_LINES);
PreferencesManager.set(PREFERENCE_SHOW_RULER_LINES, !currentValue);
return; // Don't dismiss highlights for this option
+ } else if (item === Strings.LIVE_PREVIEW_SHOW_SPACING_HANDLES) {
+ // Don't allow spacing handles toggle if edit features are not active
+ if (!isEditFeaturesActive) {
+ return;
+ }
+ // Toggle spacing handles on/off
+ const currentValue = PreferencesManager.get(PREFERENCE_SHOW_SPACING_HANDLES);
+ PreferencesManager.set(PREFERENCE_SHOW_SPACING_HANDLES, !currentValue);
+ return; // Don't dismiss highlights for this option
}
});
@@ -1227,10 +1249,14 @@ define(function (require, exports, module) {
PreferencesManager.on("change", PREFERENCE_SHOW_RULER_LINES, function() {
LiveDevelopment.updateRulerLinesConfig();
});
+ PreferencesManager.on("change", PREFERENCE_SHOW_SPACING_HANDLES, function() {
+ LiveDevelopment.updateSpacingHandlesConfig();
+ });
- // Initialize element highlight and ruler lines config on startup
+ // Initialize element highlight, ruler lines, and spacing handles config on startup
LiveDevelopment.updateElementHighlightConfig();
LiveDevelopment.updateRulerLinesConfig();
+ LiveDevelopment.updateSpacingHandlesConfig();
LiveDevelopment.openLivePreview();
LiveDevelopment.on(LiveDevelopment.EVENT_OPEN_PREVIEW_URL, _openLivePreviewURL);
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js
index fa76f2918..356cf4d10 100644
--- a/src/nls/root/strings.js
+++ b/src/nls/root/strings.js
@@ -196,6 +196,14 @@ define({
"LIVE_DEV_MORE_OPTIONS_CUT": "Cut",
"LIVE_DEV_MORE_OPTIONS_COPY": "Copy",
"LIVE_DEV_MORE_OPTIONS_PASTE": "Paste",
+ "LIVE_DEV_INSERT_ELEMENT": "Insert Element",
+ "LIVE_DEV_INSERT_BEFORE": "Before",
+ "LIVE_DEV_INSERT_AFTER": "After",
+ "LIVE_DEV_INSERT_INSIDE": "Inside",
+ "LIVE_DEV_INSERT_WRAP": "Wrap",
+ "LIVE_DEV_INSERT_SEARCH_PLACEHOLDER": "Search elements\u2026",
+ "LIVE_DEV_INSERT_COMMON": "Common",
+ "LIVE_DEV_INSERT_NO_RESULTS": "No matching elements",
"LIVE_DEV_IMAGE_GALLERY_USE_IMAGE": "Download image",
"LIVE_DEV_IMAGE_GALLERY_SELECT_DOWNLOAD_FOLDER": "Choose image download folder",
"LIVE_DEV_IMAGE_GALLERY_SEARCH_PLACEHOLDER": "Search images\u2026",
@@ -232,6 +240,21 @@ define({
"LIVE_DEV_STYLES_PANEL_NO_STYLES": "No styles found",
"LIVE_DEV_STYLES_PANEL_PROPERTY_PLACEHOLDER": "property",
"LIVE_DEV_STYLES_PANEL_VALUE_PLACEHOLDER": "value",
+ "LIVE_DEV_STYLES_TAB_ADVANCED": "Advanced Styles",
+ "LIVE_DEV_STYLES_TAB_COMPUTED": "Computed",
+ "LIVE_DEV_STYLES_FILTER_ALL": "All",
+ "LIVE_DEV_STYLES_FILTER_LAYOUT": "Layout",
+ "LIVE_DEV_STYLES_FILTER_TYPOGRAPHY": "Typography",
+ "LIVE_DEV_STYLES_FILTER_COLOR": "Color",
+ "LIVE_DEV_STYLES_FILTER_EFFECTS": "Effects",
+ "LIVE_DEV_STYLES_FILTER_BOX_MODEL": "Box Model",
+ "LIVE_DEV_STYLES_COMPUTED_SEARCH": "Filter properties\u2026",
+ "LIVE_DEV_STYLES_COMPUTED_NO_RESULTS": "No results found",
+ "LIVE_DEV_STYLES_COMPUTED_USER_AGENT": "User Agent",
+ "LIVE_DEV_FORMAT_BOLD": "Bold",
+ "LIVE_DEV_FORMAT_ITALIC": "Italic",
+ "LIVE_DEV_FORMAT_UNDERLINE": "Underline",
+ "LIVE_DEV_FORMAT_STRIKETHROUGH": "Strikethrough",
"LIVE_DEV_TOAST_NOT_EDITABLE": "Element not editable - generated by script",
"LIVE_DEV_COPY_TOAST_MESSAGE": "Element copied. Use 'Paste' to add it after the selected element",
"LIVE_DEV_TOAST_FIXED_ELEMENT_DISMISSED": "Element doesn't scroll with page - edit boxes hidden",
@@ -255,6 +278,8 @@ define({
"LIVE_PREVIEW_MODE_EDIT": "Edit Mode",
"LIVE_PREVIEW_EDIT_HIGHLIGHT_ON": "Inspect Element on Hover",
"LIVE_PREVIEW_SHOW_RULER_LINES": "Show Measurements",
+ "LIVE_PREVIEW_SHOW_SPACING_HANDLES": "Show Spacing Handles",
+ "LIVE_DEV_SETTINGS_SHOW_SPACING_HANDLES_PREFERENCE": "Show spacing handles when elements are selected in live preview edit mode. Defaults to 'true'",
"LIVE_PREVIEW_MODE_PREFERENCE": "'{0}' shows only the webpage, '{1}' connects the webpage to your code - click on elements to jump to their code and vice versa, '{2}' provides highlighting along with advanced element manipulation",
"LIVE_PREVIEW_CONFIGURE_MODES": "Configure Live Preview Modes",