diff --git a/docs/API-Reference/command/Commands.md b/docs/API-Reference/command/Commands.md index 6e7a0950a7..d2f6748070 100644 --- a/docs/API-Reference/command/Commands.md +++ b/docs/API-Reference/command/Commands.md @@ -164,6 +164,12 @@ Reloads live preview ## FILE\_LIVE\_HIGHLIGHT Toggles live highlight +**Kind**: global variable + + +## FILE\_LIVE\_WORD\_NAVIGATION +Toggles word-level navigation in live preview + **Kind**: global variable diff --git a/src-node/package-lock.json b/src-node/package-lock.json index 6177376876..6183efc95f 100644 --- a/src-node/package-lock.json +++ b/src-node/package-lock.json @@ -1,12 +1,12 @@ { "name": "@phcode/node-core", - "version": "4.1.0-0", + "version": "4.1.1-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@phcode/node-core", - "version": "4.1.0-0", + "version": "4.1.1-0", "license": "GNU-AGPL3.0", "dependencies": { "@phcode/fs": "^3.0.1", diff --git a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js index 0731a1f86e..ec555860ae 100644 --- a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js +++ b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js @@ -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); + 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) { + 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(); @@ -399,8 +533,14 @@ 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, @@ -408,7 +548,17 @@ "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); diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index f6b9c39108..75c2662bb1 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -147,31 +147,335 @@ define(function (require, exports, module) { } } - function _tagSelectedInLivePreview(tagId, nodeName, contentEditable, allSelectors) { + function _findWordInEditor(editor, word, nodeName, tagId, wordContext, wordOccurrenceIndex) { + + if (!editor || !word) { + + return false; + } + + const codeMirror = editor._codeMirror; + + // First try to find the word within the context of the clicked element + if (tagId) { + + // Get the position of the tag in the editor + const tagPosition = HTMLInstrumentation.getPositionFromTagId(editor, parseInt(tagId, 10)); + + if (tagPosition) { + // Find the opening and closing tags to determine the element's content boundaries + let openTagEndLine = tagPosition.line; + let openTagEndCh = tagPosition.ch; + let closeTagStartLine = -1; + let closeTagStartCh = -1; + + // Find the end of the opening tag (>) + let foundOpenTagEnd = false; + for (let line = openTagEndLine; line < codeMirror.lineCount() && !foundOpenTagEnd; line++) { + const lineText = codeMirror.getLine(line); + let startCh = (line === openTagEndLine) ? openTagEndCh : 0; + + const gtIndex = lineText.indexOf('>', startCh); + if (gtIndex !== -1) { + openTagEndLine = line; + openTagEndCh = gtIndex + 1; // Position after the > + foundOpenTagEnd = true; + } + } + + if (!foundOpenTagEnd) { + return false; + } + + // Check if this is a self-closing tag + const lineWithOpenTag = codeMirror.getLine(openTagEndLine); + const isSelfClosing = lineWithOpenTag.substring(0, openTagEndCh).includes('/>'); + + if (!isSelfClosing) { + // Find the closing tag + const closeTagRegex = new RegExp(`]*>`, 'i'); + let searchStartPos = {line: openTagEndLine, ch: openTagEndCh}; + + // Create a search cursor to find the closing tag + const closeTagCursor = codeMirror.getSearchCursor(closeTagRegex, searchStartPos); + if (closeTagCursor.findNext()) { + closeTagStartLine = closeTagCursor.from().line; + closeTagStartCh = closeTagCursor.from().ch; + } else { + // If we can't find the closing tag, we'll just search in a reasonable range after the opening tag + closeTagStartLine = Math.min(openTagEndLine + 10, codeMirror.lineCount() - 1); + closeTagStartCh = codeMirror.getLine(closeTagStartLine).length; + } + } else { + // For self-closing tags, there's no content to search + return false; + } + + // Prioritize finding the word by its occurrence index within the element + if (typeof wordOccurrenceIndex === 'number') { + + // Create a word regex that matches the exact word + const wordRegex = new RegExp("\\b" + word + "\\b", "g"); + + // Create a search cursor limited to the element's content + const wordCursor = codeMirror.getSearchCursor( + wordRegex, + {line: openTagEndLine, ch: openTagEndCh}, + {line: closeTagStartLine, ch: closeTagStartCh} + ); + + let currentOccurrence = 0; + let found = false; + + // Find the specific occurrence of the word + while (wordCursor.findNext()) { + if (currentOccurrence === wordOccurrenceIndex) { + const wordPos = wordCursor.from(); + editor.setCursorPos(wordPos.line, wordPos.ch, true); + found = true; + break; + } + currentOccurrence++; + } + + if (found) { + return true; + } + + } + + // If occurrence index search failed or no occurrence index available, try context as a fallback + if (wordContext) { + + // Escape special regex characters in the context and word + const escapedContext = wordContext.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + // Create a regex that matches the context + const contextRegex = new RegExp(escapedContext, "g"); + + // Create a search cursor limited to the element's content + const contextCursor = codeMirror.getSearchCursor( + contextRegex, + {line: openTagEndLine, ch: openTagEndCh}, + {line: closeTagStartLine, ch: closeTagStartCh} + ); + + if (contextCursor.findNext()) { + // Found the context within the element + const contextPos = contextCursor.from(); + const contextText = codeMirror.getRange( + contextCursor.from(), + contextCursor.to() + ); + + // Find the position of the word within the context + const wordIndex = wordContext.indexOf(word); + if (wordIndex !== -1) { + const wordPos = { + line: contextPos.line, + ch: contextPos.ch + wordIndex + }; + + editor.setCursorPos(wordPos.line, wordPos.ch, true); + return true; + } + } + } + + // If both occurrence index and context search failed, search for any occurrence of the word + + // Create a word regex that matches the exact word + const wordRegex = new RegExp("\\b" + word + "\\b", "g"); + + // Create a search cursor limited to the element's content + const wordCursor = codeMirror.getSearchCursor( + wordRegex, + {line: openTagEndLine, ch: openTagEndCh}, + {line: closeTagStartLine, ch: closeTagStartCh} + ); + + // If we have an occurrence index, find the specific occurrence of the word + if (typeof wordOccurrenceIndex === 'number') { + + let currentOccurrence = 0; + let found = false; + + // Find the specific occurrence of the word + while (wordCursor.findNext()) { + if (currentOccurrence === wordOccurrenceIndex) { + const wordPos = wordCursor.from(); + editor.setCursorPos(wordPos.line, wordPos.ch, true); + found = true; + break; + } + currentOccurrence++; + } + + if (found) { + return true; + } + + } + // If no occurrence index or the specific occurrence wasn't found, just find the first occurrence + else { + if (wordCursor.findNext()) { + const wordPos = wordCursor.from(); + editor.setCursorPos(wordPos.line, wordPos.ch, true); + return true; + } + } + + // If exact word search failed, try a more flexible search within the element + + const flexWordRegex = new RegExp(word, "g"); + const flexWordCursor = codeMirror.getSearchCursor( + flexWordRegex, + {line: openTagEndLine, ch: openTagEndCh}, + {line: closeTagStartLine, ch: closeTagStartCh} + ); + + if (flexWordCursor.findNext()) { + const flexWordPos = flexWordCursor.from(); + editor.setCursorPos(flexWordPos.line, flexWordPos.ch, true); + return true; + } + } + } + + // If we couldn't find the word in the tag's context, try a document-wide search + // Prioritize finding the word by its occurrence index in the entire document + if (typeof wordOccurrenceIndex === 'number') { + + // Create a word regex that matches the exact word + const wordRegex = new RegExp("\\b" + word + "\\b", "g"); + const wordCursor = codeMirror.getSearchCursor(wordRegex); + + let currentOccurrence = 0; + let found = false; + + // Find the specific occurrence of the word + while (wordCursor.findNext()) { + if (currentOccurrence === wordOccurrenceIndex) { + const wordPos = wordCursor.from(); + editor.setCursorPos(wordPos.line, wordPos.ch, true); + found = true; + break; + } + currentOccurrence++; + } + + if (found) { + return true; + } + + } + + // If occurrence index search failed or no occurrence index available, try context as a fallback + if (wordContext) { + + // Escape special regex characters in the context + const escapedContext = wordContext.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + // Create a regex that matches the context with the word + const contextRegex = new RegExp(escapedContext, "g"); + + const contextCursor = codeMirror.getSearchCursor(contextRegex); + let contextFound = contextCursor.findNext(); + + if (contextFound) { + // Find the position of the word within the context + const contextMatch = contextCursor.from(); + const wordInContextIndex = wordContext.indexOf(word); + + if (wordInContextIndex !== -1) { + // Calculate the exact position of the word within the found context + const line = contextMatch.line; + const ch = contextMatch.ch + wordInContextIndex; + + editor.setCursorPos(line, ch, true); + return true; + } + } + } + + // If both occurrence index and context search failed, fall back to a simple search + + // Try to find the first occurrence of the exact word + const wordRegex = new RegExp("\\b" + word + "\\b", "g"); + + const cursor = codeMirror.getSearchCursor(wordRegex); + let foundFirstOccurrence = cursor.findNext(); + + if (foundFirstOccurrence) { + const position = {line: cursor.from().line, ch: cursor.from().ch}; + editor.setCursorPos(position.line, position.ch, true); + return true; + } + + // If exact word not found, try a more flexible search + const flexibleRegex = new RegExp(word, "g"); + + const flexCursor = codeMirror.getSearchCursor(flexibleRegex); + let flexibleFound = flexCursor.findNext(); + + if (flexibleFound) { + const position = {line: flexCursor.from().line, ch: flexCursor.from().ch}; + editor.setCursorPos(position.line, position.ch, true); + return true; + } + + return false; + } + + function _tagSelectedInLivePreview(tagId, nodeName, contentEditable, allSelectors, clickedWord, wordContext, wordOccurrenceIndex) { + const highlightPref = PreferencesManager.getViewState("livedevHighlight"); - if(!highlightPref){ - // live preview highlight and reverse highlight feature is disabled + const wordNavPref = PreferencesManager.getViewState("livedevWordNavigation"); + + + if(!highlightPref && !wordNavPref){ + // Both live preview highlight and word navigation are disabled return; } const liveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(), activeEditor = EditorManager.getActiveEditor(), // this can be an inline editor activeFullEditor = EditorManager.getCurrentFullEditor(); + + const liveDocPath = liveDoc ? liveDoc.doc.file.fullPath : null, activeEditorPath = activeEditor ? activeEditor.document.file.fullPath : null, activeFullEditorPath = activeFullEditor ? activeFullEditor.document.file.fullPath : null; + if(!liveDocPath){ activeEditor && activeEditor.focus(); // restore focus from live preview return; } const allOpenFileCount = MainViewManager.getWorkingSetSize(MainViewManager.ALL_PANES); + function selectInHTMLEditor(fullHtmlEditor) { + + // If word navigation is enabled and we have a clicked word, try to find it + if (wordNavPref && clickedWord && fullHtmlEditor) { + const masterEditor = fullHtmlEditor.document._masterEditor || fullHtmlEditor; + + const wordFound = _findWordInEditor(masterEditor, clickedWord, nodeName, tagId, wordContext, wordOccurrenceIndex); + + if (wordFound) { + _focusEditorIfNeeded(masterEditor, nodeName, contentEditable); + return; + } + } + + // Fall back to tag-based navigation if word navigation fails or is disabled const position = HTMLInstrumentation.getPositionFromTagId(fullHtmlEditor, parseInt(tagId, 10)); + if(position && fullHtmlEditor) { const masterEditor = fullHtmlEditor.document._masterEditor || fullHtmlEditor; masterEditor.setCursorPos(position.line, position.ch, true); _focusEditorIfNeeded(masterEditor, nodeName, contentEditable); } } + if(liveDocPath === activeFullEditorPath) { // if the active pane is the html being live previewed, select that. selectInHTMLEditor(activeFullEditor); @@ -180,7 +484,18 @@ define(function (require, exports, module) { // then we dont need to open the html live doc. For less files, we dont check if its related as // its not directly linked usually and needs a compile step. so we just do a fuzzy search. _focusEditorIfNeeded(activeEditor, nodeName, contentEditable); - _searchAndCursorIfCSS(activeEditor, allSelectors, nodeName); + + // Try word-level navigation first if enabled + if (wordNavPref && clickedWord) { + const wordFound = _findWordInEditor(activeEditor, clickedWord, nodeName, tagId, wordContext, wordOccurrenceIndex); + if (!wordFound) { + // Fall back to CSS selector search if word not found + _searchAndCursorIfCSS(activeEditor, allSelectors, nodeName); + } + } else { + // Use traditional CSS selector search + _searchAndCursorIfCSS(activeEditor, allSelectors, nodeName); + } // in this case, see if we need to do any css reverse highlight magic here } else if(!allOpenFileCount){ // no open editor in any panes, then open the html file directly. @@ -204,9 +519,11 @@ define(function (require, exports, module) { * @param {string} msg The message that was sent, in JSON string format */ function _receive(clientId, msgStr) { + var msg = JSON.parse(msgStr), - event = msg.method || "event", deferred; + + if (msg.id) { deferred = _responseDeferreds[msg.id]; if (deferred) { @@ -218,12 +535,13 @@ define(function (require, exports, module) { } } } else if (msg.clicked && msg.tagId) { - _tagSelectedInLivePreview(msg.tagId, msg.nodeName, msg.contentEditable, msg.allSelectors); + _tagSelectedInLivePreview(msg.tagId, msg.nodeName, msg.contentEditable, msg.allSelectors, + msg.clickedWord, msg.wordContext, msg.wordOccurrenceIndex); exports.trigger(EVENT_LIVE_PREVIEW_CLICKED, msg); } else { // enrich received message with clientId msg.clientId = clientId; - exports.trigger(event, msg); + exports.trigger(msg.method || "event", msg); } } @@ -295,13 +613,13 @@ define(function (require, exports, module) { _transport = transport; _transport - .on("connect.livedev", function (event, msg) { + .on("connect.livedev", function (_, msg) { _connect(msg[0], msg[1]); }) - .on("message.livedev", function (event, msg) { + .on("message.livedev", function (_, msg) { _receive(msg[0], msg[1]); }) - .on("close.livedev", function (event, msg) { + .on("close.livedev", function (_, msg) { _close(msg[0]); }); _transport.start(); @@ -386,7 +704,8 @@ define(function (require, exports, module) { url: url, text: text } - } + }, + clients ); } @@ -422,7 +741,7 @@ define(function (require, exports, module) { { method: "Page.reload", params: { - ignoreCache: true + ignoreCache: ignoreCache || true } }, clients diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 7d85eeab5f..c63a8e1ed1 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -45,12 +45,14 @@ define(function main(require, exports, module) { EventDispatcher = require("utils/EventDispatcher"); const EVENT_LIVE_HIGHLIGHT_PREF_CHANGED = "liveHighlightPrefChange"; + const EVENT_WORD_NAVIGATION_PREF_CHANGED = "wordNavigationPrefChange"; var params = new UrlParams(); var config = { experimental: false, // enable experimental features debug: true, // enable debug output and helpers highlight: true, // enable highlighting? + wordNavigation: false, // enable word-level navigation? highlightConfig: { // the highlight configuration for the Inspector borderColor: {r: 255, g: 229, b: 153, a: 0.66}, contentColor: {r: 111, g: 168, b: 220, a: 0.55}, @@ -227,6 +229,17 @@ define(function main(require, exports, module) { PreferencesManager.setViewState("livedevHighlight", config.highlight); } + function _updateWordNavigationCheckmark() { + CommandManager.get(Commands.FILE_LIVE_WORD_NAVIGATION).setChecked(config.wordNavigation); + exports.trigger(EVENT_WORD_NAVIGATION_PREF_CHANGED, config.wordNavigation); + } + + function toggleWordNavigation() { + config.wordNavigation = !config.wordNavigation; + _updateWordNavigationCheckmark(); + PreferencesManager.setViewState("livedevWordNavigation", config.wordNavigation); + } + /** Setup window references to useful LiveDevelopment modules */ function _setupDebugHelpers() { window.report = function report(params) { window.params = params; console.info(params); }; @@ -302,13 +315,22 @@ define(function main(require, exports, module) { _updateHighlightCheckmark(); }); + PreferencesManager.stateManager.definePreference("livedevWordNavigation", "boolean", false) + .on("change", function () { + config.wordNavigation = PreferencesManager.getViewState("livedevWordNavigation"); + _updateWordNavigationCheckmark(); + }); + config.highlight = PreferencesManager.getViewState("livedevHighlight"); + config.wordNavigation = PreferencesManager.getViewState("livedevWordNavigation"); // init commands CommandManager.register(Strings.CMD_LIVE_HIGHLIGHT, Commands.FILE_LIVE_HIGHLIGHT, togglePreviewHighlight); + CommandManager.register(Strings.CMD_LIVE_WORD_NAVIGATION, Commands.FILE_LIVE_WORD_NAVIGATION, toggleWordNavigation); CommandManager.register(Strings.CMD_RELOAD_LIVE_PREVIEW, Commands.CMD_RELOAD_LIVE_PREVIEW, _handleReloadLivePreviewCommand); CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).setEnabled(false); + CommandManager.get(Commands.FILE_LIVE_WORD_NAVIGATION).setEnabled(false); EventDispatcher.makeEventDispatcher(exports); @@ -318,6 +340,7 @@ define(function main(require, exports, module) { exports.EVENT_LIVE_PREVIEW_CLICKED = MultiBrowserLiveDev.EVENT_LIVE_PREVIEW_CLICKED; exports.EVENT_LIVE_PREVIEW_RELOAD = MultiBrowserLiveDev.EVENT_LIVE_PREVIEW_RELOAD; exports.EVENT_LIVE_HIGHLIGHT_PREF_CHANGED = EVENT_LIVE_HIGHLIGHT_PREF_CHANGED; + exports.EVENT_WORD_NAVIGATION_PREF_CHANGED = EVENT_WORD_NAVIGATION_PREF_CHANGED; // Export public functions exports.openLivePreview = openLivePreview; @@ -327,6 +350,7 @@ define(function main(require, exports, module) { exports.setLivePreviewPinned = setLivePreviewPinned; exports.setLivePreviewTransportBridge = setLivePreviewTransportBridge; exports.togglePreviewHighlight = togglePreviewHighlight; + exports.toggleWordNavigation = toggleWordNavigation; exports.getConnectionIds = MultiBrowserLiveDev.getConnectionIds; exports.getLivePreviewDetails = MultiBrowserLiveDev.getLivePreviewDetails; }); diff --git a/src/base-config/keyboard.json b/src/base-config/keyboard.json index 8674ac0619..b7d08e13ea 100644 --- a/src/base-config/keyboard.json +++ b/src/base-config/keyboard.json @@ -31,6 +31,9 @@ "file.previewHighlight": [ "Ctrl-Shift-C" ], + "file.previewWordNavigation": [ + "Ctrl-Shift-W" + ], "file.quit": [ "Ctrl-Q" ], diff --git a/src/command/Commands.js b/src/command/Commands.js index 2314c47e32..fa659fab5d 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -106,6 +106,9 @@ define(function (require, exports, module) { /** Toggles live highlight */ exports.FILE_LIVE_HIGHLIGHT = "file.previewHighlight"; // LiveDevelopment/main.js _handlePreviewHighlightCommand() + /** Toggles word-level navigation in live preview */ + exports.FILE_LIVE_WORD_NAVIGATION = "file.previewWordNavigation"; // LiveDevelopment/main.js toggleWordNavigation() + /** Opens project settings */ exports.FILE_PROJECT_SETTINGS = "file.projectSettings"; // ProjectManager.js _projectSettings() diff --git a/src/extensionsIntegrated/Phoenix-live-preview/images/sprites.svg b/src/extensionsIntegrated/Phoenix-live-preview/images/sprites.svg index 7c2005fa24..92b179e728 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/images/sprites.svg +++ b/src/extensionsIntegrated/Phoenix-live-preview/images/sprites.svg @@ -33,4 +33,13 @@ + + + + + + + + + diff --git a/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css b/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css index 3d217f27a6..1456b8c90c 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css +++ b/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css @@ -100,3 +100,16 @@ .pointer-icon { background: url("./images/sprites.svg#pointer-icon") center no-repeat; } + +.text-icon { /* Locate Section-Level HTML Text */ + background-image: url("./images/sprites.svg#text-icon"); + background-repeat: no-repeat; + background-size: 35px 35px; + background-position: 15% 35%; +} + +.text-fill-icon { /* Locate Word-Level HTML Text */ + background: url("./images/sprites.svg#text-fill-icon") center no-repeat; + background-size: 35px 35px; + background-position: 15% 30%; +} diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js index ddf80c12aa..54417d3084 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/main.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js @@ -110,6 +110,7 @@ define(function (require, exports, module) { $panel, $pinUrlBtn, $highlightBtn, + $wordNavigationBtn, $livePreviewPopBtn, $reloadBtn, $chromeButton, @@ -143,6 +144,11 @@ define(function (require, exports, module) { return CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).getChecked(); } + function _isWordNavigationEnabled() { + const isEnabled = CommandManager.get(Commands.FILE_LIVE_WORD_NAVIGATION).getChecked(); + return isEnabled; + } + function _getTrustProjectPage() { const trustProjectMessage = StringUtils.format(Strings.TRUST_PROJECT, path.basename(ProjectManager.getProjectRoot().fullPath)); @@ -301,6 +307,21 @@ define(function (require, exports, module) { Metrics.countEvent(Metrics.EVENT_TYPE.LIVE_PREVIEW, "HighlightBtn", "click"); } + function _updateWordNavigationToggleStatus() { + let isWordNavigationEnabled = _isWordNavigationEnabled(); + + if(isWordNavigationEnabled){ + $wordNavigationBtn.removeClass('text-icon').addClass('text-fill-icon'); + } else { + $wordNavigationBtn.removeClass('text-fill-icon').addClass('text-icon'); + } + } + + function _toggleWordNavigation() { + LiveDevelopment.toggleWordNavigation(); + Metrics.countEvent(Metrics.EVENT_TYPE.LIVE_PREVIEW, "WordNavigationBtn", "click"); + } + const ALLOWED_BROWSERS_NAMES = [`chrome`, `firefox`, `safari`, `edge`, `browser`, `browserPrivate`]; function _popoutLivePreview(browserName) { // We cannot use $iframe.src here if panel is hidden @@ -373,6 +394,7 @@ define(function (require, exports, module) { livePreview: Strings.LIVE_DEV_STATUS_TIP_OUT_OF_SYNC, clickToReload: Strings.LIVE_DEV_CLICK_TO_RELOAD_PAGE, toggleLiveHighlight: Strings.LIVE_DEV_TOGGLE_LIVE_HIGHLIGHT, + toggleWordNavigation: Strings.LIVE_DEV_TOGGLE_WORD_NAVIGATION, livePreviewSettings: Strings.LIVE_DEV_SETTINGS, clickToPopout: Strings.LIVE_DEV_CLICK_POPOUT, openInChrome: Strings.LIVE_DEV_OPEN_CHROME, @@ -389,6 +411,7 @@ define(function (require, exports, module) { $iframe = $panel.find("#panel-live-preview-frame"); $pinUrlBtn = $panel.find("#pinURLButton"); $highlightBtn = $panel.find("#highlightLPButton"); + $wordNavigationBtn = $panel.find("#wordNavigationLPButton"); $reloadBtn = $panel.find("#reloadLivePreviewButton"); $livePreviewPopBtn = $panel.find("#livePreviewPopoutButton"); $chromeButton = $panel.find("#chromeButton"); @@ -457,8 +480,10 @@ define(function (require, exports, module) { WorkspaceManager.recomputeLayout(false); _updateLiveHighlightToggleStatus(); + _updateWordNavigationToggleStatus(); $pinUrlBtn.click(_togglePinUrl); $highlightBtn.click(_toggleLiveHighlights); + $wordNavigationBtn.click(_toggleWordNavigation); $livePreviewPopBtn.click(_popoutLivePreview); $reloadBtn.click(()=>{ _loadPreview(true, true); @@ -475,6 +500,16 @@ define(function (require, exports, module) { } // panel-live-preview-title let previewDetails = await StaticServer.getPreviewDetails(); + + // Show or hide word navigation button based on file type + // Only show for HTML files where word navigation makes sense + if (previewDetails && $wordNavigationBtn) { + if (previewDetails.isHTMLFile) { + $wordNavigationBtn.removeClass('forced-hidden'); + } else { + $wordNavigationBtn.addClass('forced-hidden'); + } + } if(urlPinned && !force) { return; } @@ -831,6 +866,7 @@ define(function (require, exports, module) { LiveDevelopment.openLivePreview(); LiveDevelopment.on(LiveDevelopment.EVENT_OPEN_PREVIEW_URL, _openLivePreviewURL); LiveDevelopment.on(LiveDevelopment.EVENT_LIVE_HIGHLIGHT_PREF_CHANGED, _updateLiveHighlightToggleStatus); + LiveDevelopment.on(LiveDevelopment.EVENT_WORD_NAVIGATION_PREF_CHANGED, _updateWordNavigationToggleStatus); LiveDevelopment.on(LiveDevelopment.EVENT_LIVE_PREVIEW_RELOAD, ()=>{ // Usually, this event is listened by live preview iframes/tabs and they initiate a location.reload. // But in firefox, the embedded iframe will throw a 404 when we try to reload from within the iframe as diff --git a/src/extensionsIntegrated/Phoenix-live-preview/panel.html b/src/extensionsIntegrated/Phoenix-live-preview/panel.html index 9ecf95e8fa..938b58e9a3 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/panel.html +++ b/src/extensionsIntegrated/Phoenix-live-preview/panel.html @@ -3,6 +3,7 @@
+
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 7b996f7da4..3cbe6b2951 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -152,6 +152,7 @@ define({ "LIVE_DEV_SELECT_FILE_TO_PREVIEW": "Select File To Live Preview", "LIVE_DEV_CLICK_TO_RELOAD_PAGE": "Reload Page", "LIVE_DEV_TOGGLE_LIVE_HIGHLIGHT": "Toggle Live Preview Highlights", + "LIVE_DEV_TOGGLE_WORD_NAVIGATION": "Toggle Word-Level Navigation in Live Preview", "LIVE_DEV_CLICK_POPOUT": "Popout Live Preview To New Window", "LIVE_DEV_OPEN_CHROME": "Open In Chrome\u2026", "LIVE_DEV_OPEN_SAFARI": "Open In Safari\u2026", @@ -570,6 +571,7 @@ define({ "CMD_TOGGLE_ACTIVE_LINE": "Highlight Active Line", "CMD_TOGGLE_WORD_WRAP": "Word Wrap", "CMD_LIVE_HIGHLIGHT": "Live Preview Highlight", + "CMD_LIVE_WORD_NAVIGATION": "Word-Level Navigation in Live Preview", "CMD_VIEW_TOGGLE_INSPECTION": "Lint Files on Save", "CMD_VIEW_TOGGLE_PROBLEMS": "Problems", "CMD_WORKINGSET_SORT_BY_ADDED": "Sort by Added",