Skip to content

Commit d5cf4fd

Browse files
committed
feat: add edit link support for anchor tags
1 parent b34b701 commit d5cf4fd

File tree

4 files changed

+210
-0
lines changed

4 files changed

+210
-0
lines changed

src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,16 @@ function RemoteFunctions(config = {}) {
315315
});
316316
}
317317

318+
/**
319+
* This function gets called when the edit hyperlink button is clicked
320+
* @param {Event} event
321+
* @param {DOMElement} element - the HTML link element
322+
*/
323+
function _handleEditHyperlinkOptionClick(event, element) {
324+
dismissHyperlinkEditor();
325+
_hyperlinkEditor = new HyperlinkEditor(element);
326+
}
327+
318328
/**
319329
* This function gets called when the delete button is clicked
320330
* it sends a message to the editor using postMessage to delete the element from the source code
@@ -460,6 +470,8 @@ function RemoteFunctions(config = {}) {
460470
_handleSelectParentOptionClick(e, element);
461471
} else if (action === "edit-text") {
462472
startEditing(element);
473+
} else if (action === "edit-hyperlink") {
474+
_handleEditHyperlinkOptionClick(e, element);
463475
} else if (action === "duplicate") {
464476
_handleDuplicateOptionClick(e, element);
465477
} else if (action === "delete") {
@@ -1501,6 +1513,13 @@ function RemoteFunctions(config = {}) {
15011513
<svg viewBox="0 0 24 24" fill="currentColor">
15021514
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
15031515
</svg>
1516+
`,
1517+
1518+
link: `
1519+
<svg viewBox="0 0 16 16" fill="currentColor">
1520+
<path d="M6.354 5.5H4a3 3 0 0 0 0 6h3a3 3 0 0 0 2.83-4H9c-.086 0-.17.01-.25.031A2 2 0 0 1 7 10.5H4a2 2 0 1 1 0-4h1.535c.218-.376.495-.714.82-1z"/>
1521+
<path d="M9 5.5a3 3 0 0 0-2.83 4h1.098A2 2 0 0 1 9 6.5h3a2 2 0 1 1 0 4h-1.535a4.02 4.02 0 0 1-.82 1H12a3 3 0 1 0 0-6H9z"/>
1522+
</svg>
15041523
`
15051524
};
15061525

@@ -1598,6 +1617,13 @@ function RemoteFunctions(config = {}) {
15981617
</span>`;
15991618
}
16001619

1620+
// if its a link element, we show the edit hyperlink icon
1621+
if (this.element && this.element.tagName.toLowerCase() === 'a') {
1622+
content += `<span data-action="edit-hyperlink" title="${config.strings.editHyperlink}">
1623+
${ICONS.link}
1624+
</span>`;
1625+
}
1626+
16011627
// if its an image element, we show the image gallery icon
16021628
if (this.element && this.element.tagName.toLowerCase() === 'img') {
16031629
content += `<span data-action="image-gallery" title="${config.strings.imageGallery}">
@@ -1749,6 +1775,143 @@ function RemoteFunctions(config = {}) {
17491775
}
17501776
};
17511777

1778+
/**
1779+
* This shows a floating input box above the element which allows you to edit the link of the 'a' tag
1780+
*/
1781+
function HyperlinkEditor(element) {
1782+
this.element = element;
1783+
this.remove = this.remove.bind(this);
1784+
this.create();
1785+
}
1786+
1787+
HyperlinkEditor.prototype = {
1788+
create: function() {
1789+
const currentHref = this.element.getAttribute('href') || '';
1790+
1791+
// Create shadow DOM container
1792+
this.body = document.createElement('div');
1793+
this.body.setAttribute('data-phcode-internal-c15r5a9', '1');
1794+
document.body.appendChild(this.body);
1795+
1796+
const shadow = this.body.attachShadow({ mode: 'open' });
1797+
1798+
// Create input HTML + styles
1799+
const html = `
1800+
<style>
1801+
:host { all: initial !important; }
1802+
.hyperlink-input-box {
1803+
position: absolute;
1804+
background: white;
1805+
border: 1px solid #4285F4;
1806+
border-radius: 3px;
1807+
padding: 6px 8px;
1808+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
1809+
z-index: 2147483647;
1810+
min-width: 200px;
1811+
max-width: 400px;
1812+
box-sizing: border-box;
1813+
}
1814+
input {
1815+
width: 100%;
1816+
border: none;
1817+
outline: none;
1818+
font: 12px Monaco, Consolas, monospace;
1819+
color: #333;
1820+
background: transparent;
1821+
}
1822+
input::placeholder {
1823+
color: #999;
1824+
}
1825+
</style>
1826+
<div class="hyperlink-input-box">
1827+
<input type="text" value="${currentHref}" placeholder="https://example.com" spellcheck="false" />
1828+
</div>
1829+
`;
1830+
1831+
shadow.innerHTML = html;
1832+
this._shadow = shadow;
1833+
1834+
this._positionInput();
1835+
1836+
// setup the event listeners
1837+
const input = shadow.querySelector('input');
1838+
input.focus();
1839+
input.select();
1840+
1841+
input.addEventListener('keydown', (e) => this._handleKeydown(e));
1842+
input.addEventListener('blur', () => this._handleBlur());
1843+
},
1844+
1845+
_positionInput: function() {
1846+
const inputBoxElement = this._shadow.querySelector('.hyperlink-input-box');
1847+
if (!inputBoxElement) {
1848+
return;
1849+
}
1850+
1851+
const boxRect = inputBoxElement.getBoundingClientRect();
1852+
const elemBounds = this.element.getBoundingClientRect();
1853+
const offset = _screenOffset(this.element);
1854+
1855+
let topPos = offset.top - boxRect.height - 6;
1856+
let leftPos = offset.left + elemBounds.width - boxRect.width;
1857+
1858+
// If would go off top, position below
1859+
if (elemBounds.top - boxRect.height < 6) {
1860+
topPos = offset.top + elemBounds.height + 6;
1861+
}
1862+
1863+
// If would go off left, align left
1864+
if (leftPos < 0) {
1865+
leftPos = offset.left;
1866+
}
1867+
1868+
inputBoxElement.style.left = leftPos + 'px';
1869+
inputBoxElement.style.top = topPos + 'px';
1870+
},
1871+
1872+
_handleKeydown: function(event) {
1873+
if (event.key === 'Enter') {
1874+
event.preventDefault();
1875+
this._save();
1876+
} else if (event.key === 'Escape') {
1877+
event.preventDefault();
1878+
dismissHyperlinkEditor();
1879+
}
1880+
},
1881+
1882+
_handleBlur: function() {
1883+
setTimeout(() => this._save(), 100);
1884+
},
1885+
1886+
_save: function() {
1887+
const input = this._shadow.querySelector('input');
1888+
const newHref = input.value.trim();
1889+
const oldHref = this.element.getAttribute('href') || '';
1890+
1891+
if (newHref !== oldHref) {
1892+
this.element.setAttribute('href', newHref);
1893+
1894+
const tagId = this.element.getAttribute('data-brackets-id');
1895+
window._Brackets_MessageBroker.send({
1896+
livePreviewEditEnabled: true,
1897+
livePreviewHyperlinkEdit: true,
1898+
element: this.element,
1899+
tagId: Number(tagId),
1900+
newHref: newHref
1901+
});
1902+
}
1903+
1904+
dismissUIAndCleanupState();
1905+
},
1906+
1907+
remove: function() {
1908+
if (this.body && this.body.parentNode) {
1909+
this.body.parentNode.removeChild(this.body);
1910+
this.body = null;
1911+
}
1912+
}
1913+
};
1914+
17521915
/**
17531916
* this is called when user clicks on the Show Ruler lines option in the more options dropdown
17541917
* @param {Event} event - click event
@@ -4191,6 +4354,7 @@ function RemoteFunctions(config = {}) {
41914354
var _moreOptionsDropdown;
41924355
var _aiPromptBox;
41934356
var _imageRibbonGallery;
4357+
var _hyperlinkEditor;
41944358
var _currentRulerLines;
41954359
var _setup = false;
41964360
var _hoverLockTimer = null;
@@ -5251,6 +5415,13 @@ function RemoteFunctions(config = {}) {
52515415
}
52525416
}
52535417

5418+
function dismissHyperlinkEditor() {
5419+
if (_hyperlinkEditor) {
5420+
_hyperlinkEditor.remove();
5421+
_hyperlinkEditor = null;
5422+
}
5423+
}
5424+
52545425
/**
52555426
* Helper function to dismiss all UI boxes at once
52565427
*/
@@ -5260,6 +5431,7 @@ function RemoteFunctions(config = {}) {
52605431
dismissAIPromptBox();
52615432
dismissNodeInfoBox();
52625433
dismissImageRibbonGallery();
5434+
dismissHyperlinkEditor();
52635435
dismissToastMessage();
52645436
}
52655437

src/LiveDevelopment/LivePreviewEdit.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,40 @@ define(function (require, exports, module) {
788788
}
789789
}
790790

791+
/**
792+
* Updates the href attribute of an anchor tag in the src code
793+
* @param {number} tagId - The data-brackets-id of the link element
794+
* @param {string} newHrefValue - The new href value to set
795+
*/
796+
function _updateHyperlinkHref(tagId, newHrefValue) {
797+
const editor = _getEditorAndValidate(tagId);
798+
if (!editor) {
799+
return;
800+
}
801+
802+
const range = _getElementRange(editor, tagId);
803+
if (!range) {
804+
return;
805+
}
806+
807+
const { startPos, endPos } = range;
808+
const elementText = editor.getTextBetween(startPos, endPos);
809+
810+
// parse it using DOM parser so that we can update the href attribute
811+
const parser = new DOMParser();
812+
const doc = parser.parseFromString(elementText, "text/html");
813+
const linkElement = doc.querySelector('a');
814+
815+
if (linkElement) {
816+
linkElement.setAttribute('href', newHrefValue);
817+
const updatedElementText = linkElement.outerHTML;
818+
819+
editor.document.batchOperation(function () {
820+
editor.replaceRange(updatedElementText, startPos, endPos);
821+
});
822+
}
823+
}
824+
791825
function _sendDownloadStatusToBrowser(eventType, data) {
792826
const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc();
793827
if (currLiveDoc && currLiveDoc.protocol && currLiveDoc.protocol.evaluate) {
@@ -1504,6 +1538,8 @@ define(function (require, exports, module) {
15041538
_pasteElementFromClipboard(message.tagId);
15051539
} else if (message.livePreviewTextEdit) {
15061540
_editTextInSource(message);
1541+
} else if (message.livePreviewHyperlinkEdit) {
1542+
_updateHyperlinkHref(message.tagId, message.newHref);
15071543
} else if (message.AISend) {
15081544
_editWithAI(message);
15091545
}

src/LiveDevelopment/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ define(function main(require, exports, module) {
106106
strings: {
107107
selectParent: Strings.LIVE_DEV_MORE_OPTIONS_SELECT_PARENT,
108108
editText: Strings.LIVE_DEV_MORE_OPTIONS_EDIT_TEXT,
109+
editHyperlink: Strings.LIVE_DEV_MORE_OPTIONS_EDIT_HYPERLINK,
109110
duplicate: Strings.LIVE_DEV_MORE_OPTIONS_DUPLICATE,
110111
delete: Strings.LIVE_DEV_MORE_OPTIONS_DELETE,
111112
ai: Strings.LIVE_DEV_MORE_OPTIONS_AI,

src/nls/root/strings.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ define({
187187
"LIVE_DEV_SETTINGS_SHOW_RULER_LINES_PREFERENCE": "Show ruler lines when elements are selected in live preview. Defaults to 'false'",
188188
"LIVE_DEV_MORE_OPTIONS_SELECT_PARENT": "Select Parent",
189189
"LIVE_DEV_MORE_OPTIONS_EDIT_TEXT": "Edit Text",
190+
"LIVE_DEV_MORE_OPTIONS_EDIT_HYPERLINK": "Edit Hyperlink",
190191
"LIVE_DEV_MORE_OPTIONS_DUPLICATE": "Duplicate",
191192
"LIVE_DEV_MORE_OPTIONS_DELETE": "Delete",
192193
"LIVE_DEV_MORE_OPTIONS_AI": "Edit with AI",

0 commit comments

Comments
 (0)