@@ -42,6 +42,8 @@ function RemoteFunctions(config = {}) {
4242 // we need this so that we can remove click styling from the previous element when a new element is clicked
4343 let previouslySelectedElement = null ;
4444 let _selectedFromEditor = false ;
45+ // the selected element the `phcode-no-lp-edit` opt-out is lifted for, see _isEditOptedOut
46+ let _editOptOutOverride = null ;
4547 // Expose the currently selected element globally for external access
4648 window . __current_ph_lp_selected = null ;
4749
@@ -121,6 +123,9 @@ function RemoteFunctions(config = {}) {
121123 * Elements opted out via `phcode-no-lp-edit` (cascades to descendants) or
122124 * `phcode-no-lp-edit-this` (this element only) are also non-inspectable so
123125 * every downstream tool inherits the opt-out automatically.
126+ *
127+ * @param {DOMElement } element
128+ * @param {boolean } [onlyHighlight=false] - If true, bypasses the mode check
124129 */
125130 function isElementInspectable ( element , onlyHighlight = false ) {
126131 if ( config . mode !== 'edit' && ! onlyHighlight ) {
@@ -133,13 +138,31 @@ function RemoteFunctions(config = {}) {
133138 // this attribute is used by phoenix internal elements
134139 ! element . closest ( `[${ GLOBALS . PHCODE_INTERNAL_ATTR } ]` ) &&
135140 ! _isInsideHeadTag ( element ) && // shouldn't be inside the head tag like meta tags and all
136- ! element . closest ( '.phcode-no-lp-edit' ) &&
137- ! ( element . classList && element . classList . contains ( 'phcode-no-lp-edit-this' ) ) ) {
141+ ! _isEditOptedOut ( element ) ) {
138142 return true ;
139143 }
140144 return false ;
141145 }
142146
147+ /**
148+ * `phcode-no-lp-edit` cascades to descendants, `phcode-no-lp-edit-this` covers
149+ * the one element.
150+ *
151+ * The opt-out exists so that a pointer landing on the page is read as the page's
152+ * own business rather than as an edit, and a click still is: handleElementClick
153+ * tests for it before anything else. But an element the editor named outright -
154+ * a row picked in the layers panel - is being edited on purpose and there is
155+ * nothing ambiguous to protect, so while such an element holds the selection
156+ * every tool treats it like any other.
157+ */
158+ function _isEditOptedOut ( element ) {
159+ if ( element === _editOptOutOverride ) {
160+ return false ;
161+ }
162+ return ! ! ( element . closest ( '.phcode-no-lp-edit' ) ||
163+ ( element . classList && element . classList . contains ( 'phcode-no-lp-edit-this' ) ) ) ;
164+ }
165+
143166 /**
144167 * This is a checker function for editable elements, it makes sure that the element satisfies all the required check
145168 * - When onlyHighlight is false → config.mode must be 'edit'
@@ -185,6 +208,7 @@ function RemoteFunctions(config = {}) {
185208 isElementVisible : isElementVisible ,
186209 screenOffset : screenOffset ,
187210 selectElement : selectElement ,
211+ sendSelectionToEditor : sendSelectionToEditor ,
188212 brieflyDisableHoverListeners : brieflyDisableHoverListeners ,
189213 handleElementClick : handleElementClick ,
190214 cleanupPreviousElementState : cleanupPreviousElementState ,
@@ -668,8 +692,11 @@ function RemoteFunctions(config = {}) {
668692 * @param {boolean } [fromEditor] - If true, this is an editor-cursor-driven selection;
669693 * only lightweight highlights (outline, margin/padding overlay) are shown, not interactive
670694 * UI like control box, spacing handles, or measurements.
695+ * @param {boolean } [ignoreEditOptOut] - Edit this element even though it opted
696+ * out of live preview editing. For selections asked for by name from the
697+ * editor side; holds only while the element stays selected.
671698 */
672- function selectElement ( element , fromEditor ) {
699+ function selectElement ( element , fromEditor , ignoreEditOptOut ) {
673700 // When a cursor-based highlight re-selects the already-selected element,
674701 // just refresh the highlight overlay without dismissing existing UI panels
675702 // (control box, editor box, element-info). This prevents cursor activity
@@ -685,6 +712,8 @@ function RemoteFunctions(config = {}) {
685712 }
686713
687714 dismissUIAndCleanupState ( ) ;
715+ // set after the dismissal, which clears the previous selection's exemption
716+ _editOptOutOverride = ignoreEditOptOut ? element : null ;
688717 // this should also be there when users are in highlight mode
689718 scrollElementToViewPort ( element ) ;
690719
@@ -804,24 +833,36 @@ function RemoteFunctions(config = {}) {
804833 selection . removeAllRanges ( ) ;
805834 }
806835
807- // send cursor movement message to editor so cursor jumps to clicked element
808- if ( element . hasAttribute ( GLOBALS . DATA_BRACKETS_ID_ATTR ) &&
809- config . syncSourceAndPreview !== false ) {
810- MessageBroker . send ( {
811- "tagId" : element . getAttribute ( GLOBALS . DATA_BRACKETS_ID_ATTR ) ,
812- "nodeID" : element . id ,
813- "nodeClassList" : element . classList ,
814- "nodeName" : element . nodeName ,
815- "allSelectors" : window . getAllInheritedSelectorsInOrder ( element ) ,
816- "contentEditable" : element . contentEditable === "true" ,
817- "clicked" : true
818- } ) ;
819- }
836+ sendSelectionToEditor ( element ) ;
820837
821838 brieflyDisableHoverListeners ( ) ;
822839 selectElement ( element ) ;
823840 }
824841
842+ /**
843+ * Tells the editor which element is now selected, so the cursor jumps to it and
844+ * the css reverse highlight follows. Split out of the click handler because a
845+ * selection can also be asked for from the editor side, which must report itself
846+ * the same way without a pointer gesture ever touching the page.
847+ *
848+ * @param {HTMLElement } element
849+ */
850+ function sendSelectionToEditor ( element ) {
851+ if ( ! element . hasAttribute ( GLOBALS . DATA_BRACKETS_ID_ATTR ) ||
852+ config . syncSourceAndPreview === false ) {
853+ return ;
854+ }
855+ MessageBroker . send ( {
856+ "tagId" : element . getAttribute ( GLOBALS . DATA_BRACKETS_ID_ATTR ) ,
857+ "nodeID" : element . id ,
858+ "nodeClassList" : element . classList ,
859+ "nodeName" : element . nodeName ,
860+ "allSelectors" : window . getAllInheritedSelectorsInOrder ( element ) ,
861+ "contentEditable" : element . contentEditable === "true" ,
862+ "clicked" : true
863+ } ) ;
864+ }
865+
825866 // clear CSS selector highlights
826867 function clearCssSelectorHighlight ( ) {
827868 if ( _cssSelectorHighlightTimer ) {
@@ -1500,6 +1541,7 @@ function RemoteFunctions(config = {}) {
15001541 previouslySelectedElement = null ;
15011542 window . __current_ph_lp_selected = null ;
15021543 }
1544+ _editOptOutOverride = null ;
15031545
15041546 // Reset hover tracking so the same-element skip doesn't suppress
15051547 // re-highlighting after a full state cleanup (e.g. Escape, dismiss).
0 commit comments