Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
125 changes: 122 additions & 3 deletions src/LiveDevelopment/BrowserScripts/RemoteFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// to distinguish between phoenix internal vs user created elements
PHCODE_INTERNAL_ATTR: "data-phcode-internal-c15r5a9",
DATA_BRACKETS_ID_ATTR: "data-brackets-id", // data attribute used to track elements for live preview operations
LP_REF_ATTR: "data-phcode-lp-ref", // identity of a script-added element, stamped only when it is selected
HIGHLIGHT_CLASSNAME: "__brackets-ld-highlight" // CSS class name used for highlighting elements in live preview
};

Expand Down Expand Up @@ -41,6 +42,12 @@
// this will store the element that was clicked previously (before the new click)
// we need this so that we can remove click styling from the previous element when a new element is clicked
let previouslySelectedElement = null;
let _sourcelessObserver = null;
let _sourcelessCheckTimer = null;
let _sourcelessPath = null;
let _sourcelessTag = null;
let _sourcelessClass = null;
const SOURCELESS_RECOVER_DELAY_MS = 60;
let _selectedFromEditor = false;
// the selected element the `phcode-no-lp-edit` opt-out is lifted for, see _isEditOptedOut
let _editOptOutOverride = null;
Expand Down Expand Up @@ -176,6 +183,40 @@
return isElementInspectable(element, onlyHighlight) && element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
}

// no data-brackets-id means a script added the element, so there is no HTML source for it
function isSourceless(element) {
return !!element && !element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
}

let _lpRefCounter = 0;
const LP_REF_PREFIX = "j";
const RE_NUMERIC_ID = /^\d+$/;

function getElementRef(element) {
if (!element || element.nodeType !== Node.ELEMENT_NODE) {

Check warning on line 196 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=AaBypYMhrlDMFfw2UVVv&open=AaBypYMhrlDMFfw2UVVv&pullRequest=3177
return null;
}
const tagId = element.getAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
if (tagId) {
return tagId;
}
let ref = element.getAttribute(GLOBALS.LP_REF_ATTR);
if (!ref) {
ref = LP_REF_PREFIX + (++_lpRefCounter);
element.setAttribute(GLOBALS.LP_REF_ATTR, ref);
}
return ref;
}

function getElementByRef(ref) {
if (ref === null || ref === undefined || ref === "") {
return null;
}
const text = String(ref);
const attr = RE_NUMERIC_ID.test(text) ? GLOBALS.DATA_BRACKETS_ID_ATTR : GLOBALS.LP_REF_ATTR;
return window.document.querySelector("[" + attr + '="' + text + '"]');
}

/**
* this function calc the screen offset of an element
*
Expand Down Expand Up @@ -205,6 +246,9 @@
getAllToolHandlers: getAllToolHandlers,
isElementEditable: isElementEditable,
isElementInspectable: isElementInspectable,
isSourceless: isSourceless,
getElementRef: getElementRef,
getElementByRef: getElementByRef,
isElementVisible: isElementVisible,
screenOffset: screenOffset,
selectElement: selectElement,
Expand Down Expand Up @@ -754,6 +798,78 @@
previouslySelectedElement = element;
_selectedFromEditor = fromEditor || false;
window.__current_ph_lp_selected = element;
if (isSourceless(element)) {
_watchSourcelessSelection(element);
}
}

function _elementIndexPath(element) {
const path = [];
let el = element;
while (el && el !== window.document.body) {
const parent = el.parentElement;
if (!parent) {
return null;
}
path.unshift(Array.prototype.indexOf.call(parent.children, el));
el = parent;
}
return el === window.document.body ? path : null;
}

function _elementAtIndexPath(path) {
let el = window.document.body;
for (let i = 0; i < path.length && el; i++) {
el = el.children[path[i]];
}
return el || null;
}

// a re-render replaces a script-added node, so re-select the same tag in the same place or dismiss
function _watchSourcelessSelection(element) {
_unwatchSourcelessSelection();
_sourcelessPath = _elementIndexPath(element);
if (!_sourcelessPath) {
return;
}
_sourcelessTag = element.tagName;
_sourcelessClass = typeof element.className === "string" ? element.className : "";
_sourcelessObserver = new MutationObserver(function () {
if (_sourcelessCheckTimer || !previouslySelectedElement || previouslySelectedElement.isConnected) {
return;
}
_sourcelessCheckTimer = setTimeout(_recoverSourcelessSelection, SOURCELESS_RECOVER_DELAY_MS);
});
_sourcelessObserver.observe(window.document.body, { childList: true, subtree: true });
}

function _unwatchSourcelessSelection() {
if (_sourcelessObserver) {
_sourcelessObserver.disconnect();
_sourcelessObserver = null;
}
if (_sourcelessCheckTimer) {
clearTimeout(_sourcelessCheckTimer);
_sourcelessCheckTimer = null;
}
_sourcelessPath = null;
}

function _recoverSourcelessSelection() {
_sourcelessCheckTimer = null;
const old = previouslySelectedElement;
if (!old || old.isConnected || !_sourcelessPath) {
return;
}
const fresh = _elementAtIndexPath(_sourcelessPath);
const className = fresh && typeof fresh.className === "string" ? fresh.className : "";
if (fresh && fresh.tagName === _sourcelessTag && className === _sourcelessClass &&
isSourceless(fresh) && isElementInspectable(fresh, true) && isElementVisible(fresh)) {
const fromEditor = _selectedFromEditor;
selectElement(fresh, fromEditor);
} else {
dismissUIAndCleanupState();
}
}

function disableHoverListeners() {
Expand Down Expand Up @@ -849,12 +965,14 @@
* @param {HTMLElement} element
*/
function sendSelectionToEditor(element) {
if (!element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR) ||
config.syncSourceAndPreview === false) {
if (config.syncSourceAndPreview === false) {
return;
}
// sent without a tagId too, so a css file in the editor can still jump to the rule
const tagId = element.getAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
MessageBroker.send({
"tagId": element.getAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR),
"tagId": tagId || null,
"sourceless": !tagId,
"nodeID": element.id,
"nodeClassList": element.classList,
"nodeName": element.nodeName,
Expand Down Expand Up @@ -1542,6 +1660,7 @@
previouslySelectedElement = null;
window.__current_ph_lp_selected = null;
}
_unwatchSourcelessSelection();
_editOptOutOverride = null;

// Reset hover tracking so the same-element skip doesn't suppress
Expand Down
12 changes: 10 additions & 2 deletions src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,13 @@ define(function (require, exports, module) {
}
return;
}
// a script-added element has no place in the html, only a css file can show it
if (!tagId) {
if (activeEditor && (liveDoc.isRelated(activeEditorPath) || _isLessOrSCSS(activeEditor))) {
_searchAndCursorIfCSS(activeEditor, allSelectors, nodeName);
}
return;
}
const allOpenFileCount = MainViewManager.getWorkingSetSize(MainViewManager.ALL_PANES);
function selectInHTMLEditor(fullHtmlEditor) {
const positionResult = HTMLInstrumentation.getPositionFromTagId(fullHtmlEditor, parseInt(tagId, 10));
Expand Down Expand Up @@ -385,7 +392,7 @@ define(function (require, exports, module) {
}
} else if (msg.keyForward) {
_forwardKeyboardShortcutFromIframe(msg);
} else if (msg.clicked && msg.tagId) {
} else if (msg.clicked && (msg.tagId || msg.sourceless)) {
// While previewing an html file, and if css related file is active in the editor, then clicking on the
// live preview, here we set the cursor position in the css file. but this will also trigger a css
// highlight as the cursor changes which jumps the live preview selection.
Expand All @@ -402,7 +409,8 @@ define(function (require, exports, module) {
}
_keepFocusUntil = 0;
editMode && liveDoc && liveDoc.disableHighlightOnCursorActivity(false);
liveDoc && liveDoc.updateHighlight();
// the caret did not move for a script-added element, re-highlighting would drop its selection
liveDoc && !msg.sourceless && liveDoc.updateHighlight();
} else {
// enrich received message with clientId
msg.clientId = clientId;
Expand Down
9 changes: 8 additions & 1 deletion src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ define({
"LIVE_DEV_SETTINGS_SHOW_STARTER_BAR_PREFERENCE": "Show the starter bar when the live preview page is empty. Defaults to 'true'",
"LIVE_DEV_SETTINGS_STYLES_BAR_POSITION_PREFERENCE": "Which edge the live preview styles bar docks to, 'top' or 'bottom'. Defaults to 'bottom'",
"LIVE_DEV_TOOLBOX_SELECT_PARENT": "Select Parent",
"LIVE_DEV_SCRIPT_GENERATED_TOOLTIP": "Added by a script, so it is not in the HTML source. Its CSS rules can still be edited.",
"LIVE_DEV_STYLER_SCRIPT_ELEMENT_NOTICE": "Script-generated element: edits are saved to its CSS rules",
"LIVE_DEV_TOOLBOX_EDIT_TEXT": "Edit Text",
"LIVE_DEV_TOOLBOX_DOUBLE_CLICK_HINT": "Double-click",
"LIVE_DEV_TOOLBOX_EDIT_ELEMENT_PROPS": "Edit Element Properties",
Expand Down Expand Up @@ -828,9 +830,9 @@ define({
"LIVE_PREVIEW_LAYERS_SETTINGS": "Settings",
"LIVE_PREVIEW_LAYERS_SHOW_TEXT_NODES": "Show Text Nodes",
"LIVE_PREVIEW_LAYERS_HIGHLIGHT_ON_HOVER": "Highlight on Hover in Preview",
"LIVE_PREVIEW_LAYERS_SCROLL_ON_HOVER": "Scroll to Element on Hover",
"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_ADD_ATTRIBUTE": "Add Attribute",
"LIVE_PREVIEW_LAYERS_REMOVE_ATTRIBUTE": "Remove Attribute",
"LIVE_PREVIEW_LAYERS_PROPERTIES": "Properties",
Expand All @@ -841,6 +843,11 @@ define({
"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_PREVIEW_LAYERS_ADD_DECLARATION": "Add Declaration",
"LIVE_PREVIEW_LAYERS_REMOVE_DECLARATION": "Remove Declaration",
"LIVE_PREVIEW_LAYERS_EDIT_DECLARATION": "Edit Declaration",
"LIVE_PREVIEW_LAYERS_SHOW_SCRIPT_NODES": "Show Script-Generated Elements",
"LIVE_PREVIEW_LAYERS_SCRIPT_GENERATED": "Generated by script, not in the source file",

"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