Skip to content

Commit 1865d3e

Browse files
committed
fix: editing text inside live preview leaks phoenix internal attributes
1 parent 49ce40c commit 1865d3e

File tree

3 files changed

+47
-63
lines changed

3 files changed

+47
-63
lines changed

src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@
395395
var element = event.target;
396396
if (element && element.hasAttribute('data-brackets-id')) {
397397
// Check if it's a double-click for direct editing
398-
if (event.detail === 2 && element.textContent && !['INPUT', 'TEXTAREA', 'SELECT'].includes(element.tagName)) {
398+
if (event.detail === 2 && !['INPUT', 'TEXTAREA', 'SELECT'].includes(element.tagName)) {
399399
// Double-click detected, enable direct editing
400400
// Make the element editable
401401
if (window._LD && window._LD.DOMEditHandler) {

src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

Lines changed: 14 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1518,9 +1518,7 @@ function RemoteFunctions(config) {
15181518
element.setAttribute("contenteditable", "true");
15191519
element.focus();
15201520

1521-
// Save the original content for potential cancellation
15221521
element._originalContent = element.innerHTML;
1523-
element._originalTextContent = element.textContent;
15241522

15251523
// Add event listeners for editing
15261524
function onBlur() {
@@ -1550,51 +1548,29 @@ function RemoteFunctions(config) {
15501548
};
15511549
}
15521550

1553-
// this function is to remove the properties from elements before getting the innerHTML
1551+
// this function is to remove the internal properties from elements before getting the innerHTML
15541552
// then add all the properties back to the elements
1553+
// internal properties such as 'data-brackets-id', 'data-ld-highlight' etc
15551554
function cleanupElementProperties(element) {
1556-
// a temporary container to hold a clean copy
1557-
const tempContainer = document.createElement('div');
1558-
tempContainer.innerHTML = element.innerHTML;
1559-
1560-
// get all elements in the temporary container
1561-
const allElements = tempContainer.querySelectorAll('*');
1562-
1563-
// Store original attributes for later restoration
1564-
const originalAttributes = new Map();
1565-
const elementsInOriginal = element.querySelectorAll('*');
1566-
1567-
// Save original attributes
1568-
elementsInOriginal.forEach((el, index) => {
1569-
const attrs = {};
1570-
for (let i = 0; i < el.attributes.length; i++) {
1571-
const attr = el.attributes[i];
1572-
attrs[attr.name] = attr.value;
1573-
}
1574-
originalAttributes.set(index, attrs);
1575-
});
1555+
const clone = element.cloneNode(true);
1556+
const allElements = [clone, ...clone.querySelectorAll('*')];
15761557

1577-
// Remove all attributes from elements in the temp container
15781558
allElements.forEach(el => {
1579-
while (el.attributes.length > 0) {
1580-
el.removeAttribute(el.attributes[0].name);
1559+
// Remove Phoenix internal attributes
1560+
if (el.hasAttribute('data-brackets-id')) {
1561+
el.removeAttribute('data-brackets-id');
1562+
}
1563+
if (el.hasAttribute('data-ld-highlight')) {
1564+
el.removeAttribute('data-ld-highlight');
15811565
}
1582-
});
1583-
1584-
// Get the clean HTML content
1585-
const cleanContent = tempContainer.innerHTML;
15861566

1587-
// Restore original attributes to the actual elements
1588-
elementsInOriginal.forEach((el, index) => {
1589-
if (originalAttributes.has(index)) {
1590-
const attrs = originalAttributes.get(index);
1591-
for (const [name, value] of Object.entries(attrs)) {
1592-
el.setAttribute(name, value);
1593-
}
1567+
// Remove empty style attribute
1568+
if (el.hasAttribute('style') && el.getAttribute('style').trim() === '') {
1569+
el.removeAttribute('style');
15941570
}
15951571
});
15961572

1597-
return cleanContent;
1573+
return clone.innerHTML;
15981574
}
15991575

16001576
// Function to finish editing and apply changes
@@ -1622,8 +1598,6 @@ function RemoteFunctions(config) {
16221598
window._Brackets_MessageBroker.send({
16231599
livePreviewEditEnabled: true,
16241600
element: element,
1625-
oldContent: element._originalContent,
1626-
oldTextContent: element._originalTextContent,
16271601
newContent: newContent,
16281602
tagId: Number(tagId),
16291603
livePreviewTextEdit: true
@@ -1632,7 +1606,6 @@ function RemoteFunctions(config) {
16321606

16331607
// Clean up
16341608
delete element._originalContent;
1635-
delete element._originalTextContent;
16361609
}
16371610

16381611
// init

src/LiveDevelopment/livePreviewEdit.js

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@ define(function (require, exports, module) {
22
const HTMLInstrumentation = require("LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation");
33
const LiveDevMultiBrowser = require("LiveDevelopment/LiveDevMultiBrowser");
44

5+
/**
6+
* this is a helper function to find the content boundaries in HTML
7+
* @param {string} html - The HTML string to parse
8+
* @return {Object} - Object with openTag and closeTag properties
9+
*/
10+
function _findContentBoundaries(html) {
11+
const openTagEnd = html.indexOf('>') + 1;
12+
const closeTagStart = html.lastIndexOf('<');
13+
14+
if (openTagEnd > 0 && closeTagStart > openTagEnd) {
15+
return {
16+
openTag: html.substring(0, openTagEnd),
17+
closeTag: html.substring(closeTagStart)
18+
};
19+
}
20+
21+
return null;
22+
}
23+
524
/**
625
* this function handles the text edit in the source code when user updates the text in the live preview
726
* @param {Object} message - the message object
@@ -18,40 +37,32 @@ define(function (require, exports, module) {
1837
* join the text back and add the new content in between
1938
*/
2039
function _editTextInSource(message) {
21-
// this is to get the currently live document that is being served in the live preview
2240
const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc();
23-
if(!currLiveDoc) {
41+
if(!currLiveDoc || !currLiveDoc.editor || !message.tagId) {
2442
return;
2543
}
2644

2745
const editor = currLiveDoc.editor;
28-
if (!editor || !message.tagId) {
29-
return;
30-
}
31-
32-
// this will give us the start pos and end pos of the DOM element in the source code
33-
// can be referenced using range.from and range.to
3446
const range = HTMLInstrumentation.getPositionFromTagId(editor, message.tagId);
3547
if (!range) {
3648
return;
3749
}
3850

39-
// this is the actual source code for the element that we need to duplicate
4051
const text = editor.getTextBetween(range.from, range.to);
41-
// remove the <b>, <i> and <u> tags from the text,
42-
// this is done because we split the text using the textContent and not the innerHTML
43-
// and textContent doesn't have all these tags
44-
const cleanedText = text.replace(/<\/?(b|i|u)>/gi, "");
45-
46-
// split the text as we want to remove the old content from the source code
47-
// for ex: if we have <h1>hello</h1> then splitting from hello will give us [<h1>, </h1>]
48-
const splittedText = cleanedText.split(message.oldTextContent);
49-
50-
// make sure that the split was successful
51-
if (splittedText.length === 2) {
52-
// so now we just merge the whole thing back replacing the old text content with the new one
52+
let splittedText;
53+
54+
// we need to find the content boundaries to find exactly where the content starts and where it ends
55+
const boundaries = _findContentBoundaries(text);
56+
if (boundaries) {
57+
splittedText = [boundaries.openTag, boundaries.closeTag];
58+
}
59+
60+
// if the text split was done successfully, apply the edit
61+
if (splittedText && splittedText.length === 2) {
5362
const finalText = splittedText[0] + message.newContent + splittedText[1];
5463
editor.replaceRange(finalText, range.from, range.to);
64+
} else {
65+
console.error("Live preview text edit operation failed.");
5566
}
5667
}
5768

0 commit comments

Comments
 (0)