Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e867e41
feat: layers panel button styles and strings
devvaannsh Aug 27, 2026
4265db4
feat: add strings for element section in layers panel
devvaannsh Aug 28, 2026
16d3e89
feat: add strings for details section in layers panel
devvaannsh Aug 28, 2026
ebabc13
feat: move layers panel into a sidebar tab
devvaannsh Aug 29, 2026
a6251b4
feat: simplify layers element rows
devvaannsh Aug 29, 2026
8740f7f
feat: row actions and drag reorder in layers panel
devvaannsh Aug 29, 2026
9230e88
feat: replace layers details tabs with collapsible sections
devvaannsh Aug 29, 2026
b1b4607
feat: inline property editing in layers panel
devvaannsh Aug 29, 2026
8d833b7
fix: give long attribute names room in layers properties
devvaannsh Aug 29, 2026
ae35d39
fix: bound the layers elements section so it scrolls and resizes
devvaannsh Aug 29, 2026
219fce0
feat: keyboard navigation in the layers elements tree
devvaannsh Aug 29, 2026
0ee6426
fix: keep the layers row actions in view when the tree scrolls sideways
devvaannsh Aug 29, 2026
19c1fcb
fix: rename the layers mode gate strings to edit mode
devvaannsh Aug 29, 2026
4746e68
feat: split layers styles into its own resizable section
devvaannsh Aug 30, 2026
a333f24
fix: even out the layers selection highlight
devvaannsh Aug 30, 2026
3ffdc90
fix: keep focus where it was when live preview is asked to select
devvaannsh Aug 30, 2026
8748bc3
feat: mark the editor cursor's element in the layers tree
devvaannsh Aug 30, 2026
884be51
feat: flash the rows a layers edit touched
devvaannsh Aug 30, 2026
413c3bb
fix: let the editor select a live preview element without clicking it
devvaannsh Aug 30, 2026
b868475
fix: drop row hover while walking the layers tree by keyboard
devvaannsh Aug 30, 2026
902fcf2
fix: wrap and tidy the layers panel gate message
devvaannsh Sep 1, 2026
6d2c992
fix: drop the rotate for the layers move down icon
devvaannsh Sep 1, 2026
dda47dc
feat: style the layers drag handle and add its tooltip string
devvaannsh Sep 1, 2026
96ce467
feat: style the layers menu icons and lift the popup height cap
devvaannsh Sep 1, 2026
658e08a
build: update pro deps
devvaannsh Sep 1, 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
74 changes: 58 additions & 16 deletions src/LiveDevelopment/BrowserScripts/RemoteFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
// we need this so that we can remove click styling from the previous element when a new element is clicked
let previouslySelectedElement = null;
let _selectedFromEditor = false;
// the selected element the `phcode-no-lp-edit` opt-out is lifted for, see _isEditOptedOut
let _editOptOutOverride = null;
// Expose the currently selected element globally for external access
window.__current_ph_lp_selected = null;

Expand Down Expand Up @@ -121,6 +123,9 @@
* 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.
*
* @param {DOMElement} element
* @param {boolean} [onlyHighlight=false] - If true, bypasses the mode check
*/
function isElementInspectable(element, onlyHighlight = false) {
if(config.mode !== 'edit' && !onlyHighlight) {
Expand All @@ -133,13 +138,31 @@
// 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
!element.closest('.phcode-no-lp-edit') &&
!(element.classList && element.classList.contains('phcode-no-lp-edit-this'))) {
!_isEditOptedOut(element)) {
return true;
}
return false;
}

/**
* `phcode-no-lp-edit` cascades to descendants, `phcode-no-lp-edit-this` covers
* the one element.
*
* The opt-out exists so that a pointer landing on the page is read as the page's
* own business rather than as an edit, and a click still is: handleElementClick
* tests for it before anything else. But an element the editor named outright -
* a row picked in the layers panel - is being edited on purpose and there is
* nothing ambiguous to protect, so while such an element holds the selection
* every tool treats it like any other.
*/
function _isEditOptedOut(element) {
if (element === _editOptOutOverride) {
return false;
}
return !!(element.closest('.phcode-no-lp-edit') ||
(element.classList && element.classList.contains('phcode-no-lp-edit-this')));

Check warning on line 163 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=AaBT77XVpsdzSMnaKfX6&open=AaBT77XVpsdzSMnaKfX6&pullRequest=3153
}

/**
* This is a checker function for editable elements, it makes sure that the element satisfies all the required check
* - When onlyHighlight is false → config.mode must be 'edit'
Expand Down Expand Up @@ -185,6 +208,7 @@
isElementVisible: isElementVisible,
screenOffset: screenOffset,
selectElement: selectElement,
sendSelectionToEditor: sendSelectionToEditor,
brieflyDisableHoverListeners: brieflyDisableHoverListeners,
handleElementClick: handleElementClick,
cleanupPreviousElementState: cleanupPreviousElementState,
Expand Down Expand Up @@ -668,8 +692,11 @@
* @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.
* @param {boolean} [ignoreEditOptOut] - Edit this element even though it opted
* out of live preview editing. For selections asked for by name from the
* editor side; holds only while the element stays selected.
*/
function selectElement(element, fromEditor) {
function selectElement(element, fromEditor, ignoreEditOptOut) {
// When a cursor-based highlight re-selects the already-selected element,
// just refresh the highlight overlay without dismissing existing UI panels
// (control box, editor box, element-info). This prevents cursor activity
Expand All @@ -685,6 +712,8 @@
}

dismissUIAndCleanupState();
// set after the dismissal, which clears the previous selection's exemption
_editOptOutOverride = ignoreEditOptOut ? element : null;
// this should also be there when users are in highlight mode
scrollElementToViewPort(element);

Expand Down Expand Up @@ -804,24 +833,36 @@
selection.removeAllRanges();
}

// send cursor movement message to editor so cursor jumps to clicked element
if (element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR) &&
config.syncSourceAndPreview !== false) {
MessageBroker.send({
"tagId": element.getAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR),
"nodeID": element.id,
"nodeClassList": element.classList,
"nodeName": element.nodeName,
"allSelectors": window.getAllInheritedSelectorsInOrder(element),
"contentEditable": element.contentEditable === "true",
"clicked": true
});
}
sendSelectionToEditor(element);

brieflyDisableHoverListeners();
selectElement(element);
}

/**
* Tells the editor which element is now selected, so the cursor jumps to it and
* the css reverse highlight follows. Split out of the click handler because a
* selection can also be asked for from the editor side, which must report itself
* the same way without a pointer gesture ever touching the page.
*
* @param {HTMLElement} element
*/
function sendSelectionToEditor(element) {
if (!element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR) ||
config.syncSourceAndPreview === false) {
return;
}
MessageBroker.send({
"tagId": element.getAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR),
"nodeID": element.id,
"nodeClassList": element.classList,
"nodeName": element.nodeName,
"allSelectors": window.getAllInheritedSelectorsInOrder(element),
"contentEditable": element.contentEditable === "true",
"clicked": true
});
}

// clear CSS selector highlights
function clearCssSelectorHighlight() {
if (_cssSelectorHighlightTimer) {
Expand Down Expand Up @@ -1500,6 +1541,7 @@
previouslySelectedElement = null;
window.__current_ph_lp_selected = null;
}
_editOptOutOverride = null;

// Reset hover tracking so the same-element skip doesn't suppress
// re-highlighting after a full state cleanup (e.g. Escape, dismiss).
Expand Down
22 changes: 20 additions & 2 deletions src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,24 @@ define(function (require, exports, module) {
document.body.dispatchEvent(event);
}

// 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()) {
if (WorkspaceManager.isInDesignMode() || _shouldKeepFocus()) {
return;
}
const focusShouldBeInLivePreview = ['INPUT', 'TEXTAREA'].includes(tagName) || contentEditable;
Expand Down Expand Up @@ -271,7 +287,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()) {
if (activeEditor && !WorkspaceManager.isInDesignMode() && !_shouldKeepFocus()) {
activeEditor.focus(); // restore focus from live preview
}
return;
Expand Down Expand Up @@ -384,6 +400,7 @@ define(function (require, exports, module) {
} catch (e) {
console.error("error in tag selection", e);
}
_keepFocusUntil = 0;
editMode && liveDoc && liveDoc.disableHighlightOnCursorActivity(false);
liveDoc && liveDoc.updateHighlight();
} else {
Expand Down Expand Up @@ -767,6 +784,7 @@ 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
35 changes: 33 additions & 2 deletions src/extensionsIntegrated/Phoenix-live-preview/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ define(function (require, exports, module) {
FileViewController = require("project/FileViewController"),
DocumentManager = require("document/DocumentManager"),
ExtensionInterface = require("utils/ExtensionInterface"),
EventDispatcher = require("utils/EventDispatcher"),
CommandManager = require("command/CommandManager"),
Commands = require("command/Commands"),
Menus = require("command/Menus"),
Expand Down Expand Up @@ -135,6 +136,9 @@ define(function (require, exports, module) {
});

const LIVE_PREVIEW_PANEL_ID = "live-preview-panel";

/** Fired with the full path whenever the preview switches to a different file. */
const EVENT_PREVIEWED_FILE_CHANGE = "previewedFileChange";
const LIVE_PREVIEW_IFRAME_ID = "panel-live-preview-frame";
const MDVIEWR_IFRAME_ID = "panel-md-preview-frame";
const _sandboxAttr = Phoenix.isTestWindow ? "" :
Expand Down Expand Up @@ -614,6 +618,19 @@ define(function (require, exports, module) {
_isMdviewrActive = false,
$mdviewrIframe = null; // persistent md iframe, survives HTML preview switches

/**
* Records which file the preview is showing and tells listeners when that changes.
* @param {string} fullPath
* @private
*/
function _setPreviewedFile(fullPath) {
if (currentPreviewFile === fullPath) {
return;
}
currentPreviewFile = fullPath;
exports.trigger(EVENT_PREVIEWED_FILE_CHANGE, fullPath);
}

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 Down Expand Up @@ -956,7 +973,7 @@ define(function (require, exports, module) {
const mdFileURL = encodeURI(previewDetails.URL);
const baseURL = mdFileURL.substring(0, mdFileURL.lastIndexOf("/") + 1);

currentPreviewFile = previewDetails.fullPath;
_setPreviewedFile(previewDetails.fullPath);
if (!urlPinned) {
currentLivePreviewURL = mdFileURL;
}
Expand Down Expand Up @@ -1075,7 +1092,7 @@ define(function (require, exports, module) {
// preview breaks sporadically. to alleviate this, we create a new iframe every time.
if(!urlPinned) {
currentLivePreviewURL = newSrc;
currentPreviewFile = previewDetails.fullPath;
_setPreviewedFile(previewDetails.fullPath);
}
if(isReload && previewDetails.isHTMLFile){
LiveDevelopment.openLivePreview();
Expand Down Expand Up @@ -1741,10 +1758,24 @@ define(function (require, exports, module) {
_loadPreview(true);
}

/**
* Full path of the file currently loaded in the preview, which is not always the
* file in the editor: the url can be pinned, and a file with no preview of its own
* leaves the last previewed one up.
* @return {string} empty string when nothing has been previewed yet
*/
function getPreviewedFilePath() {
return currentPreviewFile;
}

EventDispatcher.makeEventDispatcher(exports);

// private API to be used inside phoenix codebase only
exports.LIVE_PREVIEW_PANEL_ID = LIVE_PREVIEW_PANEL_ID;
exports.EVENT_PREVIEWED_FILE_CHANGE = EVENT_PREVIEWED_FILE_CHANGE;
exports.showInterstitial = showInterstitial;
exports.hideInterstitial = hideInterstitial;
exports.getPreviewedFilePath = getPreviewedFilePath;
});


32 changes: 32 additions & 0 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@ define({
"LIVE_DEV_IMAGE_FOLDER_DIALOG_REMEMBER": "Don't ask again for this project",
"AVAILABLE_IN_PRO_TITLE": "Available in Phoenix Pro",
"DEVICE_SIZE_LIMIT_MESSAGE": "To switch between mobile, tablet, and desktop views, upgrade to Phoenix Pro.",
"LAYERS_PANEL_LIMIT_MESSAGE": "To see your page structure and select or edit any element in it, upgrade to Phoenix Pro.",
"DEVICE_SIZE_NOT_ENOUGH_SPACE": "This screen doesn’t fit in the current window size — try expanding the window",
"DEVICE_SIZE_CYCLE_NO_FIT": "Screen too small to switch to another device size — increase the window size",
"DEVICE_SIZE_CYCLE_TOOLTIP": "Switch between mobile, tablet, and desktop",
Expand Down Expand Up @@ -809,6 +810,37 @@ define({
"LIVE_PREVIEW_SHOW_STYLES_BAR": "Show Styles Bar",
"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",
"LIVE_PREVIEW_LAYERS_TAB": "Layers",
"LIVE_PREVIEW_LAYERS_NEEDS_PREVIEW": "The Layers panel needs a live preview.",
"LIVE_PREVIEW_LAYERS_OPEN_PREVIEW": "Open Live Preview",
"LIVE_PREVIEW_LAYERS_NEEDS_HTML": "Open an HTML file to see its layers.",
"LIVE_PREVIEW_LAYERS_NEEDS_EDIT_MODE": "The Layers panel needs edit mode.",
"LIVE_PREVIEW_LAYERS_ENABLE_EDIT_MODE": "Turn on Edit Mode",
"LIVE_PREVIEW_LAYERS_ELEMENTS": "Elements",
"LIVE_PREVIEW_LAYERS_SEARCH": "Search elements",
"LIVE_PREVIEW_LAYERS_NO_ELEMENTS": "No elements to show",
"LIVE_PREVIEW_LAYERS_NO_MATCHES": "No matching elements",
"LIVE_PREVIEW_LAYERS_TRUNCATED": "Showing the first {0} elements",
"LIVE_PREVIEW_LAYERS_COLLAPSE_ALL": "Collapse All",
"LIVE_PREVIEW_LAYERS_EXPAND_LEVEL": "Expand to Level",
"LIVE_PREVIEW_LAYERS_LEVEL": "Level {0}",
"LIVE_PREVIEW_LAYERS_LEVEL_ALL": "All Levels",
"LIVE_PREVIEW_LAYERS_SHOW_TEXT_NODES": "Show Text Nodes",
"LIVE_PREVIEW_LAYERS_HIDE_TEXT_NODES": "Hide Text Nodes",
"LIVE_PREVIEW_LAYERS_MOVE_UP": "Move Up",
"LIVE_PREVIEW_LAYERS_MOVE_DOWN": "Move Down",
"LIVE_PREVIEW_LAYERS_DRAG_TO_MOVE": "Drag to move",
"LIVE_PREVIEW_LAYERS_REVEAL_IN_CODE": "Reveal in Code",
"LIVE_PREVIEW_LAYERS_ADD_ATTRIBUTE": "Add Attribute",
"LIVE_PREVIEW_LAYERS_REMOVE_ATTRIBUTE": "Remove Attribute",
"LIVE_PREVIEW_LAYERS_PROPERTIES": "Properties",
"LIVE_PREVIEW_LAYERS_STYLES": "Styles",
"LIVE_PREVIEW_LAYERS_NO_SELECTION": "Select an element to see its details",
"LIVE_PREVIEW_LAYERS_NOT_IN_SOURCE": "This element is not in the source file",
"LIVE_PREVIEW_LAYERS_NO_STYLES": "No styles applied",
"LIVE_PREVIEW_LAYERS_OPEN_SOURCE": "Open in editor",
"LIVE_PREVIEW_LAYERS_RULE_INACTIVE": "Not active in the current state",
"LIVE_PREVIEW_LAYERS_EDIT_STYLE": "Edit in Styles Bar",

"LIVE_DEV_DETACHED_REPLACED_WITH_DEVTOOLS": "Live Preview was canceled because the browser's developer tools were opened",
"LIVE_DEV_DETACHED_TARGET_CLOSED": "Live Preview was canceled because the page was closed in the browser",
Expand Down
Loading
Loading