Skip to content

[Feat]: Word-Level Navigation for Text-Heavy HTML in Live Preview #2244

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
6 changes: 6 additions & 0 deletions docs/API-Reference/command/Commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ Reloads live preview
## FILE\_LIVE\_HIGHLIGHT
Toggles live highlight

**Kind**: global variable
<a name="FILE_LIVE_WORD_NAVIGATION"></a>

## FILE\_LIVE\_WORD\_NAVIGATION
Toggles word-level navigation in live preview

**Kind**: global variable
<a name="FILE_PROJECT_SETTINGS"></a>

Expand Down
4 changes: 2 additions & 2 deletions src-node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

154 changes: 152 additions & 2 deletions src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,12 +382,146 @@
}


/**
* Gets the word at the clicked position along with additional information
* @param {Element} element - The element that was clicked
* @param {MouseEvent} event - The click event
* @return {Object|null} - Object containing the word and additional info, or null if not found
*/
function getClickedWord(element, event) {

// Try to find the clicked position within the element
const range = document.caretRangeFromPoint(event.clientX, event.clientY);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This api doesn't work in Firefox, so maybe we can fallback to caretPositionFromPoint in Firefox(caretPositionFromPoint doesnt work in safari), or disable the feature in Firefox.

if (!range) {
return null;
}

const textNode = range.startContainer;
const offset = range.startOffset;

// Check if we have a text node
if (textNode.nodeType !== Node.TEXT_NODE) {

// If the element itself contains text, try to extract a word from it
if (element.textContent && element.textContent.trim()) {
const text = element.textContent.trim();

// Simple word extraction - get the first word
const match = text.match(/\b(\w+)\b/);
if (match) {
const word = match[1];

// Since we're just getting the first word, it's the first occurrence
return {
word: word,
occurrenceIndex: 0,
context: text.substring(0, Math.min(40, text.length))
};
}
}

return null;
}

const nodeText = textNode.textContent;

// Function to extract a word and its occurrence index
function extractWordAndOccurrence(text, wordStart, wordEnd) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what will happen if we don't do word search? is the text not not always the correct target on click?

const word = text.substring(wordStart, wordEnd);

// Calculate which occurrence of this word it is
const textBeforeWord = text.substring(0, wordStart);
const regex = new RegExp("\\b" + word + "\\b", "g");
let occurrenceIndex = 0;
let match;

while ((match = regex.exec(textBeforeWord)) !== null) {
occurrenceIndex++;
}


// Get context around the word (up to 20 chars before and after)
const contextStart = Math.max(0, wordStart - 20);
const contextEnd = Math.min(text.length, wordEnd + 20);
const context = text.substring(contextStart, contextEnd);

return {
word: word,
occurrenceIndex: occurrenceIndex,
context: context
};
}

// If we're at a space or the text is empty, try to find a nearby word
if (nodeText.length === 0 || (offset < nodeText.length && /\s/.test(nodeText[offset]))) {

// Look for the nearest word
let leftPos = offset - 1;
let rightPos = offset;

// Check to the left
while (leftPos >= 0 && /\s/.test(nodeText[leftPos])) {
leftPos--;
}

// Check to the right
while (rightPos < nodeText.length && /\s/.test(nodeText[rightPos])) {
rightPos++;
}

// If we found a non-space character to the left, extract that word
if (leftPos >= 0) {
let wordStart = leftPos;
while (wordStart > 0 && /\w/.test(nodeText[wordStart - 1])) {
wordStart--;
}

return extractWordAndOccurrence(nodeText, wordStart, leftPos + 1);
}

// If we found a non-space character to the right, extract that word
if (rightPos < nodeText.length) {
let wordEnd = rightPos;
while (wordEnd < nodeText.length && /\w/.test(nodeText[wordEnd])) {
wordEnd++;
}

return extractWordAndOccurrence(nodeText, rightPos, wordEnd);
}

return null;
}

// Find word boundaries
let startPos = offset;
let endPos = offset;

// Move start position to the beginning of the word
while (startPos > 0 && /\w/.test(nodeText[startPos - 1])) {
startPos--;
}

// Move end position to the end of the word
while (endPos < nodeText.length && /\w/.test(nodeText[endPos])) {
endPos++;
}


// Extract the word and its occurrence index
if (endPos > startPos) {
return extractWordAndOccurrence(nodeText, startPos, endPos);
}

return null;
}

/**
* Sends the message containing tagID which is being clicked
* to the editor in order to change the cursor position to
* the HTML tag corresponding to the clicked element.
*/
function onDocumentClick(event) {

// Get the user's current selection
const selection = window.getSelection();

Expand All @@ -399,16 +533,32 @@
return;
}
var element = event.target;

if (element && element.hasAttribute('data-brackets-id')) {
MessageBroker.send({

// Get the clicked word and its information
const clickedWordInfo = getClickedWord(element, event);

// Prepare the message with the clicked word information
const message = {
"tagId": element.getAttribute('data-brackets-id'),
"nodeID": element.id,
"nodeClassList": element.classList,
"nodeName": element.nodeName,
"allSelectors": _getAllInheritedSelectorsInOrder(element),
"contentEditable": element.contentEditable === 'true',
"clicked": true
});
};

// Add word information if available
if (clickedWordInfo) {
message.clickedWord = clickedWordInfo.word;
message.wordContext = clickedWordInfo.context;
message.wordOccurrenceIndex = clickedWordInfo.occurrenceIndex;
}

MessageBroker.send(message);
} else {
}
}
window.document.addEventListener("click", onDocumentClick);
Expand Down
Loading
Loading