Skip to content
Open
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
adcddde
feat: use styles bar color picker in layers panel
devvaannsh Sep 14, 2026
95018e3
fix: polish layers color picker styles
devvaannsh Sep 14, 2026
af56ef2
fix: enlarge and polish layers insert popup
devvaannsh Sep 14, 2026
1c35fe5
fix: replace native position dropdown in layers insert popup
devvaannsh Sep 14, 2026
87fffd6
fix: show caret outline across layers row tools
devvaannsh Sep 14, 2026
b1a5f57
feat: add styles bar popped out hint string
devvaannsh Sep 15, 2026
bdfa043
fix: drop docked live preview connection as soon as its iframe is rem…
devvaannsh Sep 15, 2026
bc60e3f
feat: open live preview in browser from layers panel
devvaannsh Sep 15, 2026
7b19c3d
refactor: redesign layers panel empty states
devvaannsh Sep 15, 2026
ed497af
feat: list html files in layers panel when no page is previewed
devvaannsh Sep 15, 2026
b3bf6eb
feat: add loading state strings and styles for layers panel
devvaannsh Sep 15, 2026
c6881db
feat: add strings for insert element names and parameterized layers l…
devvaannsh Sep 15, 2026
bf968e4
fix: apply live preview edit opt-out classes only to pointer actions
devvaannsh Sep 15, 2026
e4e0bfc
refactor: remove tag name tooltip setting from layers panel
devvaannsh Sep 15, 2026
40c54db
feat: add strings and styles for overridden declaration info in layer…
devvaannsh Sep 15, 2026
8823350
feat: replace layers cut and copy notice with row badge strings and s…
devvaannsh Sep 15, 2026
f9d1386
fix: flag layers panel page selections so the editor keeps focus and …
devvaannsh Sep 15, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,8 @@
window.parent.postMessage({
handlerName: "ph-liveServer",
eventName: 'whoAmIframePhoenix',
href: location.href
href: location.href,
clientID: clientID
}, "*");
}
}(this));
40 changes: 22 additions & 18 deletions src/LiveDevelopment/BrowserScripts/RemoteFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,8 @@
* inspectable elements are those which doesn't have GLOBALS.DATA_BRACKETS_ID_ATTR ('data-brackets-id'),
* this normally happens when content is DOM content is inserted by some scripting language
*
* Elements opted out via `phcode-no-lp-edit` (cascades to descendants) or
* `phcode-no-lp-edit-this` (this element only) are also non-inspectable so
* every downstream tool inherits the opt-out automatically.
* The `phcode-no-lp-edit` opt-out is not part of this check: it only keeps the
* pointer off an element, see isPointerEditOptedOut.
*
* @param {DOMElement} element
* @param {boolean} [onlyHighlight=false] - If true, bypasses the mode check
Expand All @@ -144,8 +143,7 @@
element.tagName.toLowerCase() !== "html" && // shouldn't be the HTML tag
// this attribute is used by phoenix internal elements
!element.closest(`[${GLOBALS.PHCODE_INTERNAL_ATTR}]`) &&
!_isInsideHeadTag(element) && // shouldn't be inside the head tag like meta tags and all
!_isEditOptedOut(element)) {
!_isInsideHeadTag(element)) { // shouldn't be inside the head tag like meta tags and all
return true;
}
return false;
Expand All @@ -157,21 +155,24 @@
return !!(element && element.tagName && element.tagName.toLowerCase() === "body");
}

// a named selection lifts the `phcode-no-lp-edit` opt-out and the body block, both pointer-only guards
// a named selection lifts the body block, a pointer-only guard
function _isNamedSelection(element) {
return !!element && element === _namedSelection;
}

/**
* Whether the page keeps pointer actions on this element to itself: hover,
* click, double click and drops in the live preview leave it alone, so its
* own widgets keep working. It is still an element like any other to every
* path that is not the pointer - the layers panel, the caret, the tools.
* `phcode-no-lp-edit` cascades to descendants, `phcode-no-lp-edit-this` covers
* the one element.
* @param {DOMElement} element
* @returns {boolean}
*/
function _isEditOptedOut(element) {
if (_isNamedSelection(element)) {
return false;
}
return !!(element.closest('.phcode-no-lp-edit') ||
(element.classList && element.classList.contains('phcode-no-lp-edit-this')));
function isPointerEditOptedOut(element) {

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move function 'isPointerEditOptedOut' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCmEfuLOH9wYT9BpgoJ&open=AaCmEfuLOH9wYT9BpgoJ&pullRequest=3198
return !!(element && element.closest && (element.closest('.phcode-no-lp-edit') ||
(element.classList && element.classList.contains('phcode-no-lp-edit-this'))));

Check warning on line 175 in src/LiveDevelopment/BrowserScripts/RemoteFunctions.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=AaCmEfuLOH9wYT9BpgoK&open=AaCmEfuLOH9wYT9BpgoK&pullRequest=3198
}

/**
Expand Down Expand Up @@ -250,6 +251,7 @@
getAllToolHandlers: getAllToolHandlers,
isElementEditable: isElementEditable,
isElementInspectable: isElementInspectable,
isPointerEditOptedOut: isPointerEditOptedOut,
isBodyElement: isBodyElement,
isSourceless: isSourceless,
getElementRef: getElementRef,
Expand Down Expand Up @@ -751,7 +753,7 @@
return;
}
if(isBodyElement(element) || !LivePreviewView.isElementInspectable(element) ||
element.nodeType !== Node.ELEMENT_NODE) {
isPointerEditOptedOut(element) || element.nodeType !== Node.ELEMENT_NODE) {
return;
}
_lastHoverTarget = element;
Expand Down Expand Up @@ -830,7 +832,7 @@
}

dismissUIAndCleanupState();
// set after the dismissal, which clears the previous selection's exemption
// set after the dismissal, which clears the previous selection's body exemption
_namedSelection = byName ? element : null;
// this should also be there when users are in highlight mode
scrollElementToViewPort(element);
Expand Down Expand Up @@ -1008,8 +1010,7 @@
return;
}
// Opted-out elements: silent no-op so the user's existing selection isn't dismissed.
// (isElementInspectable would also reject them, but that path runs dismissUIAndCleanupState.)
if(element && (element.closest('.phcode-no-lp-edit') || element.classList.contains('phcode-no-lp-edit-this'))) {
if(isPointerEditOptedOut(element)) {
return;
}
// a blank-space click lands on the body and deselects, even a body selected by name
Expand Down Expand Up @@ -1037,8 +1038,10 @@
* the same way without a pointer gesture ever touching the page.
*
* @param {HTMLElement} element
* @param {boolean=} requested - the editor side asked for this selection, so it keeps its
* focus and reads the report as the echo of its own pick
*/
function sendSelectionToEditor(element) {
function sendSelectionToEditor(element, requested) {
if (config.syncSourceAndPreview === false) {
return;
}
Expand All @@ -1052,7 +1055,8 @@
"nodeName": element.nodeName,
"allSelectors": window.getAllInheritedSelectorsInOrder(element),
"contentEditable": element.contentEditable === "true",
"clicked": true
"clicked": true,
"requested": !!requested
});
}

Expand Down
31 changes: 9 additions & 22 deletions src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,21 +212,9 @@ define(function (require, exports, module) {
// A selection Phoenix asked the live preview to make - the layers panel picking
// an element - never took focus away from the editor side, so there is nothing
// to restore and pulling focus into the editor would take it off whatever asked
// for the selection. Time boxed so a selection that never reports back cannot
// leave the next real click in the preview without its focus.
const KEEP_FOCUS_WINDOW_MS = 1500;
let _keepFocusUntil = 0;

function keepFocusOnNextSelect() {
_keepFocusUntil = Date.now() + KEEP_FOCUS_WINDOW_MS;
}

function _shouldKeepFocus() {
return Date.now() < _keepFocusUntil;
}

function _focusEditorIfNeeded(editor, tagName, contentEditable) {
if (WorkspaceManager.isInDesignMode() || _shouldKeepFocus()) {
// for the selection. The page flags the report of such a selection as requested.
function _focusEditorIfNeeded(editor, tagName, contentEditable, keepFocus) {
if (WorkspaceManager.isInDesignMode() || keepFocus) {
return;
}
const focusShouldBeInLivePreview = ['INPUT', 'TEXTAREA'].includes(tagName) || contentEditable;
Expand Down Expand Up @@ -274,7 +262,7 @@ define(function (require, exports, module) {
}
}

function _tagSelectedInLivePreview(tagId, nodeName, contentEditable, allSelectors) {
function _tagSelectedInLivePreview(tagId, nodeName, contentEditable, allSelectors, keepFocus) {
const livePreviewMode = PreferencesManager.get(CONSTANTS.PREFERENCE_LIVE_PREVIEW_MODE);
if(livePreviewMode === CONSTANTS.LIVE_PREVIEW_MODE){
// hilights are enabled only in edit and highlight mode
Expand All @@ -287,7 +275,7 @@ define(function (require, exports, module) {
activeEditorPath = activeEditor ? activeEditor.document.file.fullPath : null,
activeFullEditorPath = activeFullEditor ? activeFullEditor.document.file.fullPath : null;
if(!liveDocPath){
if (activeEditor && !WorkspaceManager.isInDesignMode() && !_shouldKeepFocus()) {
if (activeEditor && !WorkspaceManager.isInDesignMode() && !keepFocus) {
activeEditor.focus(); // restore focus from live preview
}
return;
Expand All @@ -306,7 +294,7 @@ define(function (require, exports, module) {
const position = positionResult.from;
const masterEditor = fullHtmlEditor.document._masterEditor || fullHtmlEditor;
masterEditor.setCursorPos(position.line, position.ch, true);
_focusEditorIfNeeded(masterEditor, nodeName, contentEditable);
_focusEditorIfNeeded(masterEditor, nodeName, contentEditable, keepFocus);
}
}
if(liveDocPath === activeFullEditorPath) {
Expand All @@ -316,7 +304,7 @@ define(function (require, exports, module) {
// the active editor takes the priority in the workflow. If a css related file is active,
// then we dont need to open the html live doc. For less files, we dont check if its related as
// its not directly linked usually and needs a compile step. so we just do a fuzzy search.
_focusEditorIfNeeded(activeEditor, nodeName, contentEditable);
_focusEditorIfNeeded(activeEditor, nodeName, contentEditable, keepFocus);
_searchAndCursorIfCSS(activeEditor, allSelectors, nodeName);
// in this case, see if we need to do any css reverse highlight magic here
} else if(!allOpenFileCount){
Expand Down Expand Up @@ -402,12 +390,12 @@ define(function (require, exports, module) {
const liveDoc = LiveDevMultiBrowser.getCurrentLiveDoc();
editMode && liveDoc && liveDoc.disableHighlightOnCursorActivity(true);
try {
_tagSelectedInLivePreview(msg.tagId, msg.nodeName, msg.contentEditable, msg.allSelectors);
_tagSelectedInLivePreview(msg.tagId, msg.nodeName, msg.contentEditable, msg.allSelectors,
!!msg.requested);
exports.trigger(EVENT_LIVE_PREVIEW_CLICKED, msg);
} catch (e) {
console.error("error in tag selection", e);
}
_keepFocusUntil = 0;
editMode && liveDoc && liveDoc.disableHighlightOnCursorActivity(false);
// the caret did not move for a script-added element, re-highlighting would drop its selection
liveDoc && !msg.sourceless && liveDoc.updateHighlight();
Expand Down Expand Up @@ -792,7 +780,6 @@ define(function (require, exports, module) {
exports.setLivePreviewMessageHandler = setLivePreviewMessageHandler;
exports.setCustomRemoteFunctionProvider = setCustomRemoteFunctionProvider;
// lp communication functions
exports.keepFocusOnNextSelect = keepFocusOnNextSelect;
exports.registerPhoenixFn = registerPhoenixFn;
exports.triggerLPFn = triggerLPFn;
exports.LIVE_DEV_REMOTE_SCRIPTS_FILE_NAME = LIVE_DEV_REMOTE_SCRIPTS_FILE_NAME;
Expand Down
53 changes: 48 additions & 5 deletions src/extensionsIntegrated/Phoenix-live-preview/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,12 @@
}
}

StaticServer.on(EVENT_EMBEDDED_IFRAME_WHO_AM_I, function () {
StaticServer.on(EVENT_EMBEDDED_IFRAME_WHO_AM_I, function (_ev, event) {
if($iframe && $iframe[0]) {
const iframeDom = $iframe[0];
if (event && event.source === iframeDom.contentWindow && event.data.clientID) {
_dockedClientID = event.data.clientID;
}
iframeDom.contentWindow.postMessage({
type: "WHO_AM_I_RESPONSE",
isTauri: Phoenix.isNativeApp,
Expand Down Expand Up @@ -616,6 +619,7 @@
currentPreviewFile = '',
_loadGeneration = 0,
_isMdviewrActive = false,
_dockedClientID = null,
$mdviewrIframe = null; // persistent md iframe, survives HTML preview switches

/**
Expand All @@ -631,6 +635,17 @@
exports.trigger(EVENT_PREVIEWED_FILE_CHANGE, fullPath);
}

// A removed iframe never fires beforeunload, so its connection would linger until the tab heartbeat expires.
function _dropDockedConnection() {
if (!_dockedClientID) {
return;
}
const clientID = _dockedClientID;
_dockedClientID = null;
StaticServer.livePreviewTabs.delete(clientID);
StaticServer.trigger('BROWSER_CLOSE', { data: { message: {clientID}}});
}

function _blankIframe() {
// we have to remove the dom node altog as at time chrome fails to clear workers if we just change
// src. so we delete the node itself to eb thorough.
Expand All @@ -656,6 +671,7 @@
} else {
let newIframe = $(LIVE_PREVIEW_IFRAME_HTML);
newIframe.insertAfter($iframe);
_dropDockedConnection();
$iframe.remove();
$iframe = newIframe;
}
Expand Down Expand Up @@ -717,7 +733,7 @@
// In design mode the LP panel fills the editor area — hiding it would
// leave the user staring at a blank workspace. Keep the panel open and
// just open the popout alongside it.
const closePanelAfterPopout = !WorkspaceManager.isInDesignMode();
const closePanelAfterPopout = !WorkspaceManager.isInDesignMode() && panel.isVisible();
if(browserName && ALLOWED_BROWSERS_NAMES.includes(browserName)){
Metrics.countEvent(Metrics.EVENT_TYPE.LIVE_PREVIEW, "popout", browserName);
NodeUtils.openUrlInBrowser(openURL, browserName)
Expand Down Expand Up @@ -746,6 +762,33 @@
}
}

function _isPopoutSupported() {

Check warning on line 765 in src/extensionsIntegrated/Phoenix-live-preview/main.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move function '_isPopoutSupported' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaCmEflmOH9wYT9BpgoI&open=AaCmEflmOH9wYT9BpgoI&pullRequest=3198
return Phoenix.isNativeApp || Phoenix.browser.desktop.isChromeBased || Phoenix.browser.desktop.isFirefox;
}

/**
* Whether the previewed page can be opened in a browser: the platform allows it and a page has been previewed.
* @return {boolean}
*/
function canPopoutLivePreview() {
return _isPopoutSupported() && !!currentLivePreviewURL;
}

/**
* Opens the previewed page in the default browser, starting the live preview session if it is off.
* @return {boolean} false when there is no page to open
*/
function popoutLivePreview() {
if (!canPopoutLivePreview()) {
return false;
}
if (LiveDevelopment.isInactive()) {
LiveDevelopment.openLivePreview();
}
_popoutLivePreview();
return true;
}

function _setTitle(fileName, fullPath, currentLivePreviewURL) {
let message = Strings.LIVE_DEV_SELECT_FILE_TO_PREVIEW,
tooltip = message;
Expand Down Expand Up @@ -909,9 +952,7 @@
Metrics.countEvent(Metrics.EVENT_TYPE.LIVE_PREVIEW, "settingsBtn", "click");
});

const popoutSupported = Phoenix.isNativeApp
|| Phoenix.browser.desktop.isChromeBased || Phoenix.browser.desktop.isFirefox;
if(!popoutSupported){
if(!_isPopoutSupported()){
// live preview can be popped out currently in only chrome based browsers. The cross domain iframe
// that serves the live preview(phcode.live) is sandboxed to the tab in which phcode.dev resides.
// all iframes in the tab can communicate between each other, but when you popout another tab, it forms
Expand Down Expand Up @@ -1790,6 +1831,8 @@
exports.showInterstitial = showInterstitial;
exports.hideInterstitial = hideInterstitial;
exports.getPreviewedFilePath = getPreviewedFilePath;
exports.canPopoutLivePreview = canPopoutLivePreview;
exports.popoutLivePreview = popoutLivePreview;
});


Loading
Loading