Skip to content

Commit 27f7431

Browse files
committed
feat(live-preview): add word-level navigation for text-heavy HTML
Based on customer request to jump directly to individual words in Live Preview rather than just sections.
1 parent 3a2db30 commit 27f7431

File tree

12 files changed

+582
-16
lines changed

12 files changed

+582
-16
lines changed

docs/API-Reference/command/Commands.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ Reloads live preview
164164
## FILE\_LIVE\_HIGHLIGHT
165165
Toggles live highlight
166166

167+
**Kind**: global variable
168+
<a name="FILE_LIVE_WORD_NAVIGATION"></a>
169+
170+
## FILE\_LIVE\_WORD\_NAVIGATION
171+
Toggles word-level navigation in live preview
172+
167173
**Kind**: global variable
168174
<a name="FILE_PROJECT_SETTINGS"></a>
169175

src-node/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,12 +382,146 @@
382382
}
383383

384384

385+
/**
386+
* Gets the word at the clicked position along with additional information
387+
* @param {Element} element - The element that was clicked
388+
* @param {MouseEvent} event - The click event
389+
* @return {Object|null} - Object containing the word and additional info, or null if not found
390+
*/
391+
function getClickedWord(element, event) {
392+
393+
// Try to find the clicked position within the element
394+
const range = document.caretRangeFromPoint(event.clientX, event.clientY);
395+
if (!range) {
396+
return null;
397+
}
398+
399+
const textNode = range.startContainer;
400+
const offset = range.startOffset;
401+
402+
// Check if we have a text node
403+
if (textNode.nodeType !== Node.TEXT_NODE) {
404+
405+
// If the element itself contains text, try to extract a word from it
406+
if (element.textContent && element.textContent.trim()) {
407+
const text = element.textContent.trim();
408+
409+
// Simple word extraction - get the first word
410+
const match = text.match(/\b(\w+)\b/);
411+
if (match) {
412+
const word = match[1];
413+
414+
// Since we're just getting the first word, it's the first occurrence
415+
return {
416+
word: word,
417+
occurrenceIndex: 0,
418+
context: text.substring(0, Math.min(40, text.length))
419+
};
420+
}
421+
}
422+
423+
return null;
424+
}
425+
426+
const nodeText = textNode.textContent;
427+
428+
// Function to extract a word and its occurrence index
429+
function extractWordAndOccurrence(text, wordStart, wordEnd) {
430+
const word = text.substring(wordStart, wordEnd);
431+
432+
// Calculate which occurrence of this word it is
433+
const textBeforeWord = text.substring(0, wordStart);
434+
const regex = new RegExp("\\b" + word + "\\b", "g");
435+
let occurrenceIndex = 0;
436+
let match;
437+
438+
while ((match = regex.exec(textBeforeWord)) !== null) {
439+
occurrenceIndex++;
440+
}
441+
442+
443+
// Get context around the word (up to 20 chars before and after)
444+
const contextStart = Math.max(0, wordStart - 20);
445+
const contextEnd = Math.min(text.length, wordEnd + 20);
446+
const context = text.substring(contextStart, contextEnd);
447+
448+
return {
449+
word: word,
450+
occurrenceIndex: occurrenceIndex,
451+
context: context
452+
};
453+
}
454+
455+
// If we're at a space or the text is empty, try to find a nearby word
456+
if (nodeText.length === 0 || (offset < nodeText.length && /\s/.test(nodeText[offset]))) {
457+
458+
// Look for the nearest word
459+
let leftPos = offset - 1;
460+
let rightPos = offset;
461+
462+
// Check to the left
463+
while (leftPos >= 0 && /\s/.test(nodeText[leftPos])) {
464+
leftPos--;
465+
}
466+
467+
// Check to the right
468+
while (rightPos < nodeText.length && /\s/.test(nodeText[rightPos])) {
469+
rightPos++;
470+
}
471+
472+
// If we found a non-space character to the left, extract that word
473+
if (leftPos >= 0) {
474+
let wordStart = leftPos;
475+
while (wordStart > 0 && /\w/.test(nodeText[wordStart - 1])) {
476+
wordStart--;
477+
}
478+
479+
return extractWordAndOccurrence(nodeText, wordStart, leftPos + 1);
480+
}
481+
482+
// If we found a non-space character to the right, extract that word
483+
if (rightPos < nodeText.length) {
484+
let wordEnd = rightPos;
485+
while (wordEnd < nodeText.length && /\w/.test(nodeText[wordEnd])) {
486+
wordEnd++;
487+
}
488+
489+
return extractWordAndOccurrence(nodeText, rightPos, wordEnd);
490+
}
491+
492+
return null;
493+
}
494+
495+
// Find word boundaries
496+
let startPos = offset;
497+
let endPos = offset;
498+
499+
// Move start position to the beginning of the word
500+
while (startPos > 0 && /\w/.test(nodeText[startPos - 1])) {
501+
startPos--;
502+
}
503+
504+
// Move end position to the end of the word
505+
while (endPos < nodeText.length && /\w/.test(nodeText[endPos])) {
506+
endPos++;
507+
}
508+
509+
510+
// Extract the word and its occurrence index
511+
if (endPos > startPos) {
512+
return extractWordAndOccurrence(nodeText, startPos, endPos);
513+
}
514+
515+
return null;
516+
}
517+
385518
/**
386519
* Sends the message containing tagID which is being clicked
387520
* to the editor in order to change the cursor position to
388521
* the HTML tag corresponding to the clicked element.
389522
*/
390523
function onDocumentClick(event) {
524+
391525
// Get the user's current selection
392526
const selection = window.getSelection();
393527

@@ -399,16 +533,32 @@
399533
return;
400534
}
401535
var element = event.target;
536+
402537
if (element && element.hasAttribute('data-brackets-id')) {
403-
MessageBroker.send({
538+
539+
// Get the clicked word and its information
540+
const clickedWordInfo = getClickedWord(element, event);
541+
542+
// Prepare the message with the clicked word information
543+
const message = {
404544
"tagId": element.getAttribute('data-brackets-id'),
405545
"nodeID": element.id,
406546
"nodeClassList": element.classList,
407547
"nodeName": element.nodeName,
408548
"allSelectors": _getAllInheritedSelectorsInOrder(element),
409549
"contentEditable": element.contentEditable === 'true',
410550
"clicked": true
411-
});
551+
};
552+
553+
// Add word information if available
554+
if (clickedWordInfo) {
555+
message.clickedWord = clickedWordInfo.word;
556+
message.wordContext = clickedWordInfo.context;
557+
message.wordOccurrenceIndex = clickedWordInfo.occurrenceIndex;
558+
}
559+
560+
MessageBroker.send(message);
561+
} else {
412562
}
413563
}
414564
window.document.addEventListener("click", onDocumentClick);

0 commit comments

Comments
 (0)