@@ -26,7 +26,8 @@ function RemoteFunctions(config = {}) {
2626 const SHARED_STATE = {
2727 __description : "Use this to keep shared state for Live Preview Edit instead of window.*" ,
2828 _suppressDOMEditDismissal : false ,
29- _suppressDOMEditDismissalTimeout : null
29+ _suppressDOMEditDismissalTimeout : null ,
30+ _boxModelHighlightHidden : false
3031 } ;
3132
3233 let _hoverHighlight ;
@@ -72,6 +73,9 @@ function RemoteFunctions(config = {}) {
7273 // called when an item is selected from the more options dropdown
7374 "handleDropdownClick" ,
7475 "updateContent" , // in-place content refresh for control box etc. after drag
76+ // a DOM edit changed the selected element's attributes or rebuilt its node —
77+ // refresh shown UI in place; must never resurrect dismissed UI
78+ "onSelectedElementMutated" ,
7579 "reRegisterEventHandlers" ,
7680 "handleClick" , // handle click on an icon in the tool box.
7781 // when escape key is presses in the editor, we may need to dismiss the live edit boxes.
@@ -187,7 +191,10 @@ function RemoteFunctions(config = {}) {
187191 disableHoverListeners : disableHoverListeners ,
188192 enableHoverListeners : enableHoverListeners ,
189193 redrawHighlights : redrawHighlights ,
190- redrawEverything : redrawEverything
194+ redrawEverything : redrawEverything ,
195+ getTreePath : _getTreePath ,
196+ getElementByTreePath : _getElementByTreePath ,
197+ getSourceChildren : _instrumentedChildren
191198 } ;
192199
193200 /**
@@ -443,15 +450,17 @@ function RemoteFunctions(config = {}) {
443450 s . backgroundColor = color ;
444451 }
445452
446- // Padding region
447- const padColor = COLORS . highlightPadding ;
453+ // Padding region. Rects stay in place when hidden, only their fill goes away,
454+ // so nothing has to be rebuilt when they come back.
455+ const boxModelHidden = SHARED_STATE . _boxModelHighlightHidden ;
456+ const padColor = boxModelHidden ? "transparent" : COLORS . highlightPadding ;
448457 setRect ( refs . padTop , paddingBox . left , paddingBox . top , paddingBox . width , pt , padColor ) ;
449458 setRect ( refs . padBottom , paddingBox . left , contentBox . top + contentBox . height , paddingBox . width , pb , padColor ) ;
450459 setRect ( refs . padLeft , paddingBox . left , contentBox . top , pl , contentBox . height , padColor ) ;
451460 setRect ( refs . padRight , contentBox . left + contentBox . width , contentBox . top , pr , contentBox . height , padColor ) ;
452461
453462 // Margin region
454- const margColor = COLORS . highlightMargin ;
463+ const margColor = boxModelHidden ? "transparent" : COLORS . highlightMargin ;
455464 setRect ( refs . marTop , marginBox . left , marginBox . top , marginBox . width , mt , margColor ) ;
456465 setRect ( refs . marBottom , marginBox . left , borderBox . top + borderBox . height , marginBox . width , mb , margColor ) ;
457466 setRect ( refs . marLeft , marginBox . left , borderBox . top , ml , borderBox . height , margColor ) ;
@@ -595,14 +604,13 @@ function RemoteFunctions(config = {}) {
595604 }
596605
597606 const element = event . target ;
598- if ( ! LivePreviewView . isElementInspectable ( element ) || element . nodeType !== Node . ELEMENT_NODE ) {
599- return ;
600- }
601607
602- // Same element as last hover — nothing changed, skip entirely
603608 if ( element === _lastHoverTarget ) {
604609 return ;
605610 }
611+ if ( ! LivePreviewView . isElementInspectable ( element ) || element . nodeType !== Node . ELEMENT_NODE ) {
612+ return ;
613+ }
606614 _lastHoverTarget = element ;
607615
608616 // if _hoverHighlight is uninitialized, initialize it
@@ -615,21 +623,30 @@ function RemoteFunctions(config = {}) {
615623 }
616624 }
617625
618- function onElementHoverOut ( event ) {
619- // don't want highlighting and stuff when auto scrolling
620- if ( SHARED_STATE . isAutoScrolling ) { return ; }
626+ function _clearHoverState ( ) {
627+ if ( SHARED_STATE . isAutoScrolling ) {
628+ return ;
629+ }
630+ if ( _hoverHighlight && shouldShowHighlightOnHover ( ) ) {
631+ _lastHoverTarget = null ;
632+ _scheduleHoverUpdate ( ) ;
633+ }
634+ }
621635
636+ function onElementHoverOut ( event ) {
622637 const element = event . target ;
623638 // Use isElementInspectable (not isElementEditable) so that JS-rendered
624639 // elements also get their hover highlight and hover box properly dismissed.
625640 if ( LivePreviewView . isElementInspectable ( element ) && element . nodeType === Node . ELEMENT_NODE ) {
626- if ( _hoverHighlight && shouldShowHighlightOnHover ( ) ) {
627- _lastHoverTarget = null ;
628- _scheduleHoverUpdate ( ) ;
629- }
641+ _clearHoverState ( ) ;
630642 }
631643 }
632644
645+ // for popped out window: the in-panel iframe case is forwarded parent-side via _LD.clearHoverState().
646+ function onDocumentMouseLeave ( ) {
647+ _clearHoverState ( ) ;
648+ }
649+
633650 function scrollElementToViewPort ( element ) {
634651 if ( ! element ) {
635652 return ;
@@ -711,7 +728,9 @@ function RemoteFunctions(config = {}) {
711728
712729 function disableHoverListeners ( ) {
713730 window . document . removeEventListener ( "mouseover" , onElementHover ) ;
731+ window . document . removeEventListener ( "mousemove" , onElementHover ) ;
714732 window . document . removeEventListener ( "mouseout" , onElementHoverOut ) ;
733+ window . document . documentElement . removeEventListener ( "mouseleave" , onDocumentMouseLeave ) ;
715734 // Cancel any pending rAF hover update so stale callbacks don't fire
716735 if ( _pendingHoverRAF ) {
717736 cancelAnimationFrame ( _pendingHoverRAF ) ;
@@ -732,7 +751,9 @@ function RemoteFunctions(config = {}) {
732751 if ( config . mode === 'edit' && shouldShowHighlightOnHover ( ) ) {
733752 disableHoverListeners ( ) ;
734753 window . document . addEventListener ( "mouseover" , onElementHover ) ;
754+ window . document . addEventListener ( "mousemove" , onElementHover ) ;
735755 window . document . addEventListener ( "mouseout" , onElementHoverOut ) ;
756+ window . document . documentElement . addEventListener ( "mouseleave" , onDocumentMouseLeave ) ;
736757 }
737758 }
738759
@@ -1039,6 +1060,25 @@ function RemoteFunctions(config = {}) {
10391060 return results && results [ 0 ] ;
10401061 } ;
10411062
1063+ // True for elements Phoenix adds to the page itself, like the tool boxes and
1064+ // the highlight overlays. They are not part of the user's source file.
1065+ function _isPhoenixInternalNode ( node ) {
1066+ return ! ! node && node . nodeType === Node . ELEMENT_NODE &&
1067+ ( node . hasAttribute ( GLOBALS . PHCODE_INTERNAL_ATTR ) ||
1068+ node . className === GLOBALS . HIGHLIGHT_CLASSNAME ) ;
1069+ }
1070+
1071+ /** The first of the Phoenix elements sitting at the end of `parent`, else null. */
1072+ function _firstTrailingInternalNode ( parent ) {
1073+ let node = parent . lastChild ;
1074+ let first = null ;
1075+ while ( _isPhoenixInternalNode ( node ) ) {
1076+ first = node ;
1077+ node = node . previousSibling ;
1078+ }
1079+ return first ;
1080+ }
1081+
10421082 /**
10431083 * @private
10441084 * Insert a new child element
@@ -1053,7 +1093,12 @@ function RemoteFunctions(config = {}) {
10531093 if ( edit . firstChild ) {
10541094 before = targetElement . firstChild ;
10551095 } else if ( edit . lastChild ) {
1056- after = targetElement . lastChild ;
1096+ // Phoenix's tool boxes are the last children of <body>, so appending
1097+ // here would put the new element after them, in the wrong place.
1098+ before = _firstTrailingInternalNode ( targetElement ) ;
1099+ if ( ! before ) {
1100+ after = targetElement . lastChild ;
1101+ }
10571102 }
10581103
10591104 if ( before ) {
@@ -1189,6 +1234,7 @@ function RemoteFunctions(config = {}) {
11891234 targetElement ,
11901235 childElement ,
11911236 self = this ;
1237+ let selectedElementMutated = false ;
11921238
11931239 this . rememberedNodes = { } ;
11941240
@@ -1223,9 +1269,15 @@ function RemoteFunctions(config = {}) {
12231269 case "attrChange" :
12241270 case "attrAdd" :
12251271 targetElement . setAttribute ( edit . attribute , self . _parseEntities ( edit . value ) ) ;
1272+ if ( targetElement === previouslySelectedElement ) {
1273+ selectedElementMutated = true ;
1274+ }
12261275 break ;
12271276 case "attrDelete" :
12281277 targetElement . removeAttribute ( edit . attribute ) ;
1278+ if ( targetElement === previouslySelectedElement ) {
1279+ selectedElementMutated = true ;
1280+ }
12291281 break ;
12301282 case "elementDelete" :
12311283 if ( targetElement . remove ) {
@@ -1360,9 +1412,22 @@ function RemoteFunctions(config = {}) {
13601412 SHARED_STATE . _editorBox . element = freshElement ;
13611413 }
13621414 redrawEverything ( ) ;
1415+ selectedElementMutated = true ;
13631416 }
13641417 }
13651418 }
1419+
1420+ // neither path above refreshes selection-anchored UI CONTENT (the paths
1421+ // only reposition / re-point element refs), so content rendered at
1422+ // selection time — e.g. the control box's tag/#id/.classes line — would
1423+ // stay stale after the selected element's markup changed
1424+ if ( selectedElementMutated && previouslySelectedElement && previouslySelectedElement . isConnected ) {
1425+ getAllToolHandlers ( ) . forEach ( function ( handler ) {
1426+ if ( handler . onSelectedElementMutated ) {
1427+ handler . onSelectedElementMutated ( previouslySelectedElement ) ;
1428+ }
1429+ } ) ;
1430+ }
13661431 } ;
13671432
13681433 function applyDOMEdits ( edits ) {
@@ -1444,6 +1509,9 @@ function RemoteFunctions(config = {}) {
14441509 _pendingHoverRAF = null ;
14451510 }
14461511
1512+ // the selection is gone, so a popover can no longer turn the fills back on
1513+ SHARED_STATE . _boxModelHighlightHidden = false ;
1514+
14471515 // Highlight.clear() removes all overlay divs (outline + margin/padding rects)
14481516 hideHighlight ( ) ;
14491517
@@ -1457,27 +1525,38 @@ function RemoteFunctions(config = {}) {
14571525 }
14581526 }
14591527
1528+ // Only the children that came from the source file. Phoenix's own elements have
1529+ // no data-brackets-id, and counting them would shift the tree path indexes.
1530+ function _instrumentedChildren ( parent ) {
1531+ const result = [ ] ;
1532+ const children = ( parent && parent . children ) || [ ] ;
1533+ for ( let i = 0 ; i < children . length ; i ++ ) {
1534+ if ( children [ i ] . hasAttribute ( GLOBALS . DATA_BRACKETS_ID_ATTR ) ) {
1535+ result . push ( children [ i ] ) ;
1536+ }
1537+ }
1538+ return result ;
1539+ }
1540+
14601541 /**
14611542 * Compute the tree path of an element as an array of child indices
14621543 * from <html> down. Used to re-locate the element after re-instrumentation
14631544 * when data-brackets-id changes and text matching is ambiguous.
14641545 * E.g. [1, 0, 0, 1] means html > 2nd child > 1st child > 1st child > 2nd child.
1546+ * @return {?Array.<number> } null if the element did not come from the source file.
14651547 */
14661548 function _getTreePath ( element ) {
14671549 const path = [ ] ;
14681550 let el = element ;
14691551 while ( el && el . parentElement ) {
1470- const parent = el . parentElement ;
1471- const children = parent . children ;
1472- for ( let i = 0 ; i < children . length ; i ++ ) {
1473- if ( children [ i ] === el ) {
1474- path . unshift ( i ) ;
1475- break ;
1476- }
1552+ const index = _instrumentedChildren ( el . parentElement ) . indexOf ( el ) ;
1553+ if ( index === - 1 ) {
1554+ return null ;
14771555 }
1478- el = parent ;
1556+ path . unshift ( index ) ;
1557+ el = el . parentElement ;
14791558 }
1480- return path ;
1559+ return path . length ? path : null ;
14811560 }
14821561
14831562 /**
@@ -1486,10 +1565,11 @@ function RemoteFunctions(config = {}) {
14861565 function _getElementByTreePath ( path ) {
14871566 let el = document . documentElement ;
14881567 for ( let i = 0 ; i < path . length ; i ++ ) {
1489- if ( ! el || ! el . children || ! el . children [ path [ i ] ] ) {
1568+ const siblings = _instrumentedChildren ( el ) ;
1569+ if ( ! siblings [ path [ i ] ] ) {
14901570 return null ;
14911571 }
1492- el = el . children [ path [ i ] ] ;
1572+ el = siblings [ path [ i ] ] ;
14931573 }
14941574 return el ;
14951575 }
@@ -1570,12 +1650,12 @@ function RemoteFunctions(config = {}) {
15701650
15711651 function _handleEscapeKeyPress ( ) {
15721652 enableHoverListeners ( ) ; // so that if hover lock is there it will get cleared
1573- dismissUIAndCleanupState ( ) ;
15741653 getAllToolHandlers ( ) . forEach ( handler => {
15751654 if ( handler . handleEscapePress ) {
15761655 handler . handleEscapePress ( ) ;
15771656 }
15781657 } ) ;
1658+ dismissUIAndCleanupState ( ) ;
15791659 }
15801660
15811661 // Modifier shortcuts forwarded to the Phoenix KeyBindingManager. Clipboard
@@ -1692,6 +1772,21 @@ function RemoteFunctions(config = {}) {
16921772 }
16931773 }
16941774
1775+ /**
1776+ * Hide just the margin/padding fills of the selected element highlight, keeping
1777+ * the outline and the selection itself. Used while editing paint properties like
1778+ * background color, where the fills sit on top of what the user is changing.
1779+ * @param {Boolean } hidden
1780+ */
1781+ function setBoxModelHighlightHidden ( hidden ) {
1782+ hidden = ! ! hidden ;
1783+ if ( SHARED_STATE . _boxModelHighlightHidden === hidden ) {
1784+ return ;
1785+ }
1786+ SHARED_STATE . _boxModelHighlightHidden = hidden ;
1787+ redrawHighlights ( ) ;
1788+ }
1789+
16951790 let customReturns = { } ;
16961791 // only apis that needs to be called from phoenix js layer should be customReturns. APis that are shared within
16971792 // the remote function context only should not be in customReturns and should be in
@@ -1714,7 +1809,9 @@ function RemoteFunctions(config = {}) {
17141809 "getHighlightCount" : getHighlightCount ,
17151810 "getHighlightTrackingElement" : getHighlightTrackingElement ,
17161811 "getHighlightStyle" : getHighlightStyle ,
1717- "setHotCornerHidden" : setHotCornerHidden
1812+ "setHotCornerHidden" : setHotCornerHidden ,
1813+ "setBoxModelHighlightHidden" : setBoxModelHighlightHidden ,
1814+ "clearHoverState" : _clearHoverState
17181815 } ;
17191816
17201817 // the below code comment is replaced by added scripts for extensibility
0 commit comments