Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 37 additions & 37 deletions docs/API-Reference/language/CSSUtils.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,43 @@ value of the specified property
url for import

**Kind**: global constant
<a name="_RE_PAREN_SEMI"></a>

## \_RE\_PAREN\_SEMI ⇒ [<code>Array.&lt;SelectorInfo&gt;</code>](#SelectorInfo)
Extracts all CSS selectors from the given text
Returns an array of SelectorInfo. Each SelectorInfo is an object with the following properties:
selector: the text of the selector (note: comma separated selector groups like
"h1, h2" are broken into separate selectors)
ruleStartLine: line in the text where the rule (including preceding comment) appears
ruleStartChar: column in the line where the rule (including preceding comment) starts
selectorStartLine: line in the text where the selector appears
selectorStartChar: column in the line where the selector starts
selectorEndLine: line where the selector ends
selectorEndChar: column where the selector ends
selectorGroupStartLine: line where the comma-separated selector group (e.g. .foo, .bar, .baz)
starts that this selector (e.g. .baz) is part of. Particularly relevant for
groups that are on multiple lines.
selectorGroupStartChar: column in line where the selector group starts.
selectorGroup: the entire selector group containing this selector, or undefined if there
is only one selector in the rule.
declListStartLine: line where the declaration list for the rule starts
declListStartChar: column in line where the declaration list for the rule starts
declListEndLine: line where the declaration list for the rule ends
declListEndChar: column in the line where the declaration list for the rule ends
level: the level of the current selector including any containing @media block in the
nesting level count. Use this property with caution since it is primarily for internal
parsing use. For example, two sibling selectors may have different levels if one
of them is nested inside an @media block and it should not be used for sibling info.
parentSelectors: all ancestor selectors separated with '/' if the current selector is a nested one

**Kind**: global constant
**Returns**: [<code>Array.&lt;SelectorInfo&gt;</code>](#SelectorInfo) - Array with objects specifying selectors.

| Param | Type | Description |
| --- | --- | --- |
| text | <code>string</code> | CSS text to extract from |
| documentMode | <code>string</code> | language mode of the document that text belongs to, default to css if undefined. |

<a name="isCSSPreprocessorFile"></a>

## isCSSPreprocessorFile(filePath) ⇒ <code>boolean</code>
Expand Down Expand Up @@ -80,43 +117,6 @@ in info.
| info | [<code>SelectorInfo</code>](#SelectorInfo) | |
| [useGroup] | <code>boolean</code> | true to append selectorGroup instead of selector |

<a name="extractAllSelectors"></a>

## extractAllSelectors(text, documentMode) ⇒ [<code>Array.&lt;SelectorInfo&gt;</code>](#SelectorInfo)
Extracts all CSS selectors from the given text
Returns an array of SelectorInfo. Each SelectorInfo is an object with the following properties:
selector: the text of the selector (note: comma separated selector groups like
"h1, h2" are broken into separate selectors)
ruleStartLine: line in the text where the rule (including preceding comment) appears
ruleStartChar: column in the line where the rule (including preceding comment) starts
selectorStartLine: line in the text where the selector appears
selectorStartChar: column in the line where the selector starts
selectorEndLine: line where the selector ends
selectorEndChar: column where the selector ends
selectorGroupStartLine: line where the comma-separated selector group (e.g. .foo, .bar, .baz)
starts that this selector (e.g. .baz) is part of. Particularly relevant for
groups that are on multiple lines.
selectorGroupStartChar: column in line where the selector group starts.
selectorGroup: the entire selector group containing this selector, or undefined if there
is only one selector in the rule.
declListStartLine: line where the declaration list for the rule starts
declListStartChar: column in line where the declaration list for the rule starts
declListEndLine: line where the declaration list for the rule ends
declListEndChar: column in the line where the declaration list for the rule ends
level: the level of the current selector including any containing @media block in the
nesting level count. Use this property with caution since it is primarily for internal
parsing use. For example, two sibling selectors may have different levels if one
of them is nested inside an @media block and it should not be used for sibling info.
parentSelectors: all ancestor selectors separated with '/' if the current selector is a nested one

**Kind**: global function
**Returns**: [<code>Array.&lt;SelectorInfo&gt;</code>](#SelectorInfo) - Array with objects specifying selectors.

| Param | Type | Description |
| --- | --- | --- |
| text | <code>string</code> | CSS text to extract from |
| documentMode | <code>string</code> | language mode of the document that text belongs to, default to css if undefined. |

<a name="findMatchingRules"></a>

## findMatchingRules(selector, htmlDocument) ⇒ <code>$.Promise</code>
Expand Down
16 changes: 12 additions & 4 deletions src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,18 @@
* Evaluate an expresion and return its result.
*/
evaluate: function (msg) {
var result = eval(msg.params.expression);
MessageBroker.respond(msg, {
result: JSON.stringify(result) // TODO: in original protocol this is an object handle
});
// an unanswered request leaves the editor side waiting forever
try {
var result = eval(msg.params.expression);
MessageBroker.respond(msg, {
result: JSON.stringify(result) // TODO: in original protocol this is an object handle
});
} catch (e) {
console.error("[Brackets LiveDev] Runtime.evaluate failed", e);
MessageBroker.respond(msg, {
error: String(e && e.message || e)

Check warning on line 150 in src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A9BG_O3QC9-cNE2&open=AaCF9A9BG_O3QC9-cNE2&pullRequest=3188
});
}
}
};

Expand Down
152 changes: 115 additions & 37 deletions src/LiveDevelopment/BrowserScripts/RemoteFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,34 +450,78 @@
_overlayPool.push(overlay);
}

// Update an existing overlay's position, dimensions, and colors to match the target element.
// No DOM elements are created or destroyed — only style properties are updated.
function _updateOverlay(overlay, element) {
// Everything an overlay needs read off the page. Split from the painting
// below so a batch of overlays can read first and write after: interleaving
// the two forces a layout per element.
// What screenOffset() needs off the body, read once for a whole batch
// instead of once per element.
function _bodyOffsetContext() {
const body = window.document.body;
if (window.getComputedStyle(body).position === "static") {
return { isStatic: true, x: window.pageXOffset, y: window.pageYOffset };
}
const bodyBounds = body.getBoundingClientRect();
return { isStatic: false, x: bodyBounds.left, y: bodyBounds.top };
}

function _offsetFromBounds(bounds, bodyOffset) {

Check warning on line 467 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move function '_offsetFromBounds' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNE3&open=AaCF9A-lG_O3QC9-cNE3&pullRequest=3188
if (bodyOffset.isStatic) {
return { left: bounds.left + bodyOffset.x, top: bounds.top + bodyOffset.y };
}
return { left: bounds.left - bodyOffset.x, top: bounds.top - bodyOffset.y };
}

function _measureOverlay(element, bodyOffset) {
const bounds = element.getBoundingClientRect();
if (bounds.width === 0 && bounds.height === 0) {
return null;
}
const cs = window.getComputedStyle(element);
return {
bounds: bounds,
scroll: _offsetFromBounds(bounds, bodyOffset || _bodyOffsetContext()),
bt: parseFloat(cs.borderTopWidth) || 0,

Check warning on line 483 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNE4&open=AaCF9A-lG_O3QC9-cNE4&pullRequest=3188
br: parseFloat(cs.borderRightWidth) || 0,

Check warning on line 484 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNE5&open=AaCF9A-lG_O3QC9-cNE5&pullRequest=3188
bb: parseFloat(cs.borderBottomWidth) || 0,

Check warning on line 485 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNE6&open=AaCF9A-lG_O3QC9-cNE6&pullRequest=3188
bl: parseFloat(cs.borderLeftWidth) || 0,

Check warning on line 486 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNE7&open=AaCF9A-lG_O3QC9-cNE7&pullRequest=3188
pt: parseFloat(cs.paddingTop) || 0,

Check warning on line 487 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNE8&open=AaCF9A-lG_O3QC9-cNE8&pullRequest=3188
pr: parseFloat(cs.paddingRight) || 0,

Check warning on line 488 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNE9&open=AaCF9A-lG_O3QC9-cNE9&pullRequest=3188
pb: parseFloat(cs.paddingBottom) || 0,

Check warning on line 489 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNE-&open=AaCF9A-lG_O3QC9-cNE-&pullRequest=3188
pl: parseFloat(cs.paddingLeft) || 0,

Check warning on line 490 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNE_&open=AaCF9A-lG_O3QC9-cNE_&pullRequest=3188
mt: parseFloat(cs.marginTop) || 0,

Check warning on line 491 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNFA&open=AaCF9A-lG_O3QC9-cNFA&pullRequest=3188
mr: parseFloat(cs.marginRight) || 0,

Check warning on line 492 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNFB&open=AaCF9A-lG_O3QC9-cNFB&pullRequest=3188
mb: parseFloat(cs.marginBottom) || 0,

Check warning on line 493 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNFC&open=AaCF9A-lG_O3QC9-cNFC&pullRequest=3188
ml: parseFloat(cs.marginLeft) || 0

Check warning on line 494 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseFloat` over `parseFloat`.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNFD&open=AaCF9A-lG_O3QC9-cNFD&pullRequest=3188
};
}

function _measureAll(elements) {
const bodyOffset = _bodyOffsetContext();
const measured = [];
for (let i = 0; i < elements.length; i++) {
measured.push(_measureOverlay(elements[i], bodyOffset));
}

Check warning on line 503 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected a `for-of` loop instead of a `for` loop with this simple iteration.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNFE&open=AaCF9A-lG_O3QC9-cNFE&pullRequest=3188
return measured;
}

// Update an existing overlay's position, dimensions, and colors to match the target element.
// No DOM elements are created or destroyed — only style properties are updated.
function _paintOverlay(overlay, element, measured) {
if (!measured) {
overlay.classList.add('hidden');
return;
}

const cs = window.getComputedStyle(element);
const bounds = measured.bounds;

// Parse box model values (getComputedStyle always resolves to px)
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 bt = measured.bt, br = measured.br, bb = measured.bb, bl = measured.bl;
const pt = measured.pt, pr = measured.pr, pb = measured.pb, pl = measured.pl;
const mt = measured.mt, mr = measured.mr, mb = measured.mb, ml = measured.ml;

// Compute the 4 absolute boxes exactly like dev tools:
// getBoundingClientRect() always returns the border box regardless of box-sizing.
const scroll = LivePreviewView.screenOffset(element);
const scroll = measured.scroll;
const borderBox = {
left: scroll.left,
top: scroll.top,
Expand Down Expand Up @@ -551,6 +595,10 @@
outlineStyle.border = `1px solid ${outlineColor}`;
}

function _updateOverlay(overlay, element) {
_paintOverlay(overlay, element, _measureOverlay(element));
}

function Highlight(trigger) {
this.trigger = !!trigger;
this.elements = [];
Expand All @@ -573,6 +621,28 @@
_updateOverlay(overlay, element);
},

addAll: function (elements) {
const seen = new Set(this.elements);
const fresh = [];
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (element !== window.document && !seen.has(element)) {
seen.add(element);
fresh.push(element);
}
}

Check warning on line 633 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected a `for-of` loop instead of a `for` loop with this simple iteration.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNFF&open=AaCF9A-lG_O3QC9-cNFF&pullRequest=3188
const measured = _measureAll(fresh);
for (let i = 0; i < fresh.length; i++) {
if (this.trigger) {
_trigger(fresh[i], "highlight", 1);
}
this.elements.push(fresh[i]);
const overlay = _getOverlay();
this._overlays.push(overlay);
_paintOverlay(overlay, fresh[i], measured[i]);
}
},

clear: function () {
this._overlays.forEach(function (overlay) {
_releaseOverlay(overlay);
Expand Down Expand Up @@ -611,8 +681,9 @@
this.elements = elements;

// Update all overlays in place — no DOM creation or destruction
const measured = _measureAll(elements);
for (let i = 0; i < elements.length; i++) {
_updateOverlay(this._overlays[i], elements[i]);
_paintOverlay(this._overlays[i], elements[i], measured[i]);
}
}
};
Expand Down Expand Up @@ -672,11 +743,6 @@
if (SHARED_STATE.isAutoScrolling || SHARED_STATE._isDraggingSVG) {
return;
}
if (customReturns.selectorBox && customReturns.selectorBox.isOpen &&
customReturns.selectorBox.isOpen()) {
return;
}

const element = event.target;

if (element === _lastHoverTarget) {
Expand Down Expand Up @@ -742,7 +808,7 @@
* @param {Element} element - The DOM element to select
* @param {boolean} [fromEditor] - If true, this is an editor-cursor-driven selection;
* only lightweight highlights (outline, margin/padding overlay) are shown, not interactive
* UI like control box, spacing handles, or measurements.
* UI like the control box or the styles bar.
* @param {boolean} [byName] - Selected by name (a layers panel row), so the edit
* opt-out and the body block don't apply while it stays selected.
*/
Expand Down Expand Up @@ -1008,13 +1074,15 @@
// Highlight all matching elements except the selected one
// (it already has a click highlight)
_cssSelectorHighlight = new Highlight();
const wanted = [];
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]);
wanted.push(nodes[i]);
}
}
_cssSelectorHighlight.addAll(wanted);
_cssSelectorHighlight.selector = rule;
}

Expand Down Expand Up @@ -1044,6 +1112,20 @@
}
}

function highlightAll(elements) {
if (!_clickHighlight) {
_clickHighlight = new Highlight();
}
const wanted = [];
for (let i = 0; i < elements.length; i++) {
if (LivePreviewView.isElementInspectable(elements[i], true) &&
elements[i].nodeType === Node.ELEMENT_NODE) {
wanted.push(elements[i]);
}
}

Check warning on line 1125 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected a `for-of` loop instead of a `for` loop with this simple iteration.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCF9A-lG_O3QC9-cNFG&open=AaCF9A-lG_O3QC9-cNFG&pullRequest=3188
_clickHighlight.addAll(wanted);
}

/**
* Find the best element to select from a list of matched nodes
* Prefers: previously selected element > parent of selected > first valid element
Expand Down Expand Up @@ -1110,26 +1192,22 @@

const nodes = window.document.querySelectorAll(rule);

// Highlight all matching nodes. selectElement() will narrow _clickHighlight
// down to the chosen element below; createCssSelectorHighlight() then
// re-highlights the siblings in a separate overlay.
for (let i = 0; i < nodes.length; i++) {
highlight(nodes[i]);
}

if (_clickHighlight) {
_clickHighlight.selector = rule;
}

// Both edit and highlight modes go through the same selection path:
// selectElement() handles scroll-to-view and the prominent click-highlight,
// createCssSelectorHighlight() shows siblings dimly. fromEditor=true
// suppresses tool-handler invocation, so highlight mode gets the
// highlighting/scroll behavior without any UI boxes.
const { element, skipSelection } = findBestElementToSelect(nodes, rule);

if (!skipSelection) {
if (skipSelection) {
// A recent preview click owns the selection and its open tools.
// Keep the existing selector highlight without re-selecting it.
highlightAll(nodes);
_clickHighlight.selector = rule;
} else {
if (element) {

Check warning on line 1208 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'If' statement should not be the only statement in 'else' block

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCKt_51CXUCLYagPTZG&open=AaCKt_51CXUCLYagPTZG&pullRequest=3188
// Select first: drawing every match here would immediately be
// cleared by selectElement() and drawn again as siblings below.
selectElement(element, true);
} else {
// No valid element found, dismiss UI
Expand Down
Loading
Loading