Skip to content

Commit 1567bfe

Browse files
committed
refactor: framework for optional lp edit handlers and refactor HyperlinkEditor
1 parent f91aff4 commit 1567bfe

File tree

4 files changed

+244
-182
lines changed

4 files changed

+244
-182
lines changed

src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

Lines changed: 52 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ function RemoteFunctions(config = {}) {
6868
let _moreOptionsDropdown;
6969
let _aiPromptBox;
7070
let _imageRibbonGallery;
71-
let _hyperlinkEditor;
7271
let _currentRulerLines;
7372
let _hotCorner;
7473
let _setup = false;
@@ -106,16 +105,24 @@ function RemoteFunctions(config = {}) {
106105
}, time * 1000);
107106
}
108107

109-
const _moreOptionsHandlers = {};
108+
const _moreOptionsHandlers = new Map();
110109
function registerNodeMoreOptionsHandler(handlerName, handler) {
111110
if(_moreOptionsHandlers[handlerName]) {
112111
console.error(`lp: More options handler '${handlerName}' already registered. Ignoring new registration`);
113112
return;
114113
}
115-
_moreOptionsHandlers[handlerName] = handler;
114+
if(!handler || !handler.dismiss || !handler.renderMoreOptions){
115+
console.error(`lp: More options handler Ignoring registration: '${
116+
handlerName}' missing one of required function : 'dismiss', 'renderMoreOptions'`);
117+
return;
118+
}
119+
_moreOptionsHandlers.set(handlerName, handler);
116120
}
117121
function getNodeMoreOptionsHandler(handlerName) {
118-
return _moreOptionsHandlers[handlerName];
122+
return _moreOptionsHandlers.get(handlerName);
123+
}
124+
function getAllNodeMoreOptionsHandlers() {
125+
return _moreOptionsHandlers.values();
119126
}
120127

121128
/**
@@ -151,11 +158,36 @@ function RemoteFunctions(config = {}) {
151158
return isElementInspectable(element, onlyHighlight) && element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
152159
}
153160

161+
/**
162+
* this function calc the screen offset of an element
163+
*
164+
* @param {DOMElement} element
165+
* @returns {{left: number, top: number}}
166+
*/
167+
function screenOffset(element) {
168+
const elemBounds = element.getBoundingClientRect();
169+
const body = window.document.body;
170+
let offsetTop;
171+
let offsetLeft;
172+
173+
if (window.getComputedStyle(body).position === "static") {
174+
offsetLeft = elemBounds.left + window.pageXOffset;
175+
offsetTop = elemBounds.top + window.pageYOffset;
176+
} else {
177+
const bodyBounds = body.getBoundingClientRect();
178+
offsetLeft = elemBounds.left - bodyBounds.left;
179+
offsetTop = elemBounds.top - bodyBounds.top;
180+
}
181+
return { left: offsetLeft, top: offsetTop };
182+
}
183+
154184
const LivePreviewView = {
155185
registerNodeMoreOptionsHandler: registerNodeMoreOptionsHandler,
156186
getNodeMoreOptionsHandler: getNodeMoreOptionsHandler,
187+
getAllNodeMoreOptionsHandlers: getAllNodeMoreOptionsHandlers,
157188
isElementEditable: isElementEditable,
158-
isElementInspectable: isElementInspectable
189+
isElementInspectable: isElementInspectable,
190+
screenOffset: screenOffset
159191
};
160192

161193
/**
@@ -369,16 +401,6 @@ function RemoteFunctions(config = {}) {
369401
});
370402
}
371403

372-
/**
373-
* This function gets called when the edit hyperlink button is clicked
374-
* @param {Event} event
375-
* @param {DOMElement} element - the HTML link element
376-
*/
377-
function _handleEditHyperlinkOptionClick(event, element) {
378-
dismissHyperlinkEditor();
379-
_hyperlinkEditor = new HyperlinkEditor(element);
380-
}
381-
382404
/**
383405
* This function gets called when the delete button is clicked
384406
* it sends a message to the editor using postMessage to delete the element from the source code
@@ -524,8 +546,6 @@ function RemoteFunctions(config = {}) {
524546
_handleSelectParentOptionClick(e, element);
525547
} else if (action === "edit-text") {
526548
startEditing(element);
527-
} else if (action === "edit-hyperlink") {
528-
_handleEditHyperlinkOptionClick(e, element);
529549
} else if (action === "duplicate") {
530550
_handleDuplicateOptionClick(e, element);
531551
} else if (action === "delete") {
@@ -540,6 +560,9 @@ function RemoteFunctions(config = {}) {
540560
_handleAIOptionClick(e, element);
541561
} else if (action === "image-gallery") {
542562
_handleImageGalleryOptionClick(e, element);
563+
} else if(LivePreviewView.getNodeMoreOptionsHandler(action)) {
564+
const handler = LivePreviewView.getNodeMoreOptionsHandler(action);
565+
handler.handleClick(e, element);
543566
}
544567
}
545568

@@ -1851,30 +1874,6 @@ function RemoteFunctions(config = {}) {
18511874
return true;
18521875
}
18531876

1854-
1855-
/**
1856-
* this function calc the screen offset of an element
1857-
*
1858-
* @param {DOMElement} element
1859-
* @returns {{left: number, top: number}}
1860-
*/
1861-
function _screenOffset(element) {
1862-
const elemBounds = element.getBoundingClientRect();
1863-
const body = window.document.body;
1864-
let offsetTop;
1865-
let offsetLeft;
1866-
1867-
if (window.getComputedStyle(body).position === "static") {
1868-
offsetLeft = elemBounds.left + window.pageXOffset;
1869-
offsetTop = elemBounds.top + window.pageYOffset;
1870-
} else {
1871-
const bodyBounds = body.getBoundingClientRect();
1872-
offsetLeft = elemBounds.left - bodyBounds.left;
1873-
offsetTop = elemBounds.top - bodyBounds.top;
1874-
}
1875-
return { left: offsetLeft, top: offsetTop };
1876-
}
1877-
18781877
/**
18791878
* Check if two rectangles overlap
18801879
* @param {Object} rect1 - First rectangle {left, top, right, bottom}
@@ -1931,7 +1930,7 @@ function RemoteFunctions(config = {}) {
19311930
* @returns {Object} - {leftPos, topPos}
19321931
*/
19331932
function _getCoordinatesForPosition(position, element, boxDimensions, verticalOffset, horizontalOffset) {
1934-
const offsetBounds = _screenOffset(element);
1933+
const offsetBounds = LivePreviewView.screenOffset(element);
19351934
const elemBounds = element.getBoundingClientRect();
19361935

19371936
let leftPos, topPos;
@@ -2228,24 +2227,24 @@ function RemoteFunctions(config = {}) {
22282227

22292228
// Only include select parent option if element supports it
22302229
if (showSelectParentOption) {
2231-
content += `<span data-action="select-parent" title="${strings.selectParent}">
2230+
content += `<span data-action="select-parent" class="lp-opt-select-parent" title="${strings.selectParent}">
22322231
${icons.arrowUp}
22332232
</span>`;
22342233
}
22352234

22362235
// Only include edit text option if element supports it
22372236
if (showEditTextOption) {
2238-
content += `<span data-action="edit-text" title="${strings.editText}">
2237+
content += `<span data-action="edit-text" class="lp-opt-edit-text" title="${strings.editText}">
22392238
${icons.edit}
22402239
</span>`;
22412240
}
22422241

2243-
// if its a link element, we show the edit hyperlink icon
2244-
if (this.element && this.element.tagName.toLowerCase() === 'a') {
2245-
content += `<span data-action="edit-hyperlink" title="${strings.editHyperlink}">
2246-
${icons.link}
2247-
</span>`;
2248-
}
2242+
LivePreviewView.getAllNodeMoreOptionsHandlers().forEach(handler => {
2243+
const optionalContent = handler.renderMoreOptions(this.element);
2244+
if(optionalContent) {
2245+
content += optionalContent;
2246+
}
2247+
});
22492248

22502249
const selectedClass = imageGallerySelected ? 'class="selected"' : "";
22512250

@@ -2335,123 +2334,6 @@ function RemoteFunctions(config = {}) {
23352334
}
23362335
};
23372336

2338-
/**
2339-
* This shows a floating input box above the element which allows you to edit the link of the 'a' tag
2340-
*/
2341-
function HyperlinkEditor(element) {
2342-
this.element = element;
2343-
this.remove = this.remove.bind(this);
2344-
this.create();
2345-
}
2346-
2347-
HyperlinkEditor.prototype = {
2348-
create: function() {
2349-
const currentHref = this.element.getAttribute('href') || '';
2350-
2351-
// Create shadow DOM container
2352-
this.body = document.createElement('div');
2353-
this.body.setAttribute(GLOBALS.PHCODE_INTERNAL_ATTR, "true");
2354-
document.body.appendChild(this.body);
2355-
2356-
const shadow = this.body.attachShadow({ mode: 'open' });
2357-
2358-
// Create input HTML + styles
2359-
const html = `
2360-
<style>
2361-
${cssStyles.hyperlinkEditor}
2362-
</style>
2363-
<div class="hyperlink-input-box">
2364-
<div class="link-icon" title="${currentHref.trim() || strings.hyperlinkNoHref}">
2365-
${icons.link}
2366-
</div>
2367-
<input type="text" value="${currentHref.trim()}" placeholder="https://example.com" spellcheck="false" />
2368-
</div>
2369-
`;
2370-
2371-
shadow.innerHTML = html;
2372-
this._shadow = shadow;
2373-
2374-
this._positionInput();
2375-
2376-
// setup the event listeners
2377-
const input = shadow.querySelector('input');
2378-
input.focus();
2379-
input.select();
2380-
2381-
input.addEventListener('keydown', (e) => this._handleKeydown(e));
2382-
input.addEventListener('blur', () => this._handleBlur());
2383-
},
2384-
2385-
_positionInput: function() {
2386-
const inputBoxElement = this._shadow.querySelector('.hyperlink-input-box');
2387-
if (!inputBoxElement) {
2388-
return;
2389-
}
2390-
2391-
const boxRect = inputBoxElement.getBoundingClientRect();
2392-
const elemBounds = this.element.getBoundingClientRect();
2393-
const offset = _screenOffset(this.element);
2394-
2395-
let topPos = offset.top - boxRect.height - 6;
2396-
let leftPos = offset.left + elemBounds.width - boxRect.width;
2397-
2398-
// If would go off top, position below
2399-
if (elemBounds.top - boxRect.height < 6) {
2400-
topPos = offset.top + elemBounds.height + 6;
2401-
}
2402-
2403-
// If would go off left, align left
2404-
if (leftPos < 0) {
2405-
leftPos = offset.left;
2406-
}
2407-
2408-
inputBoxElement.style.left = leftPos + 'px';
2409-
inputBoxElement.style.top = topPos + 'px';
2410-
},
2411-
2412-
_handleKeydown: function(event) {
2413-
if (event.key === 'Enter') {
2414-
event.preventDefault();
2415-
this._save();
2416-
} else if (event.key === 'Escape') {
2417-
event.preventDefault();
2418-
dismissHyperlinkEditor();
2419-
}
2420-
},
2421-
2422-
_handleBlur: function() {
2423-
setTimeout(() => this._save(), 100);
2424-
},
2425-
2426-
_save: function() {
2427-
const input = this._shadow.querySelector('input');
2428-
const newHref = input.value.trim();
2429-
const oldHref = this.element.getAttribute('href') || '';
2430-
2431-
if (newHref !== oldHref) {
2432-
this.element.setAttribute('href', newHref);
2433-
2434-
const tagId = this.element.getAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
2435-
window._Brackets_MessageBroker.send({
2436-
livePreviewEditEnabled: true,
2437-
livePreviewHyperlinkEdit: true,
2438-
element: this.element,
2439-
tagId: Number(tagId),
2440-
newHref: newHref
2441-
});
2442-
}
2443-
2444-
this.remove();
2445-
},
2446-
2447-
remove: function() {
2448-
if (this.body && this.body.parentNode) {
2449-
this.body.parentNode.removeChild(this.body);
2450-
this.body = null;
2451-
}
2452-
}
2453-
};
2454-
24552337
/**
24562338
* this is called when user clicks on the Show Ruler lines option in the more options dropdown
24572339
* @param {Event} event - click event
@@ -2784,7 +2666,7 @@ function RemoteFunctions(config = {}) {
27842666
AIPromptBox.prototype = {
27852667
_getBoxPosition: function(boxWidth, boxHeight) {
27862668
const elemBounds = this.element.getBoundingClientRect();
2787-
const offset = _screenOffset(this.element);
2669+
const offset = LivePreviewView.screenOffset(this.element);
27882670

27892671
let topPos = offset.top - boxHeight - 6; // 6 for just some little space to breathe
27902672
let leftPos = offset.left + elemBounds.width - boxWidth;
@@ -4240,7 +4122,7 @@ function RemoteFunctions(config = {}) {
42404122

42414123
highlight.className = GLOBALS.HIGHLIGHT_CLASSNAME;
42424124

4243-
var offset = _screenOffset(element);
4125+
var offset = LivePreviewView.screenOffset(element);
42444126

42454127
// some code to find element left/top was removed here. This seems to be relevant to box model
42464128
// live highlights. firether reading: https://github.com/adobe/brackets/pull/13357/files
@@ -5616,13 +5498,6 @@ function RemoteFunctions(config = {}) {
56165498
}
56175499
}
56185500

5619-
function dismissHyperlinkEditor() {
5620-
if (_hyperlinkEditor) {
5621-
_hyperlinkEditor.remove();
5622-
_hyperlinkEditor = null;
5623-
}
5624-
}
5625-
56265501
/**
56275502
* Helper function to dismiss all UI boxes at once
56285503
*/
@@ -5632,8 +5507,8 @@ function RemoteFunctions(config = {}) {
56325507
dismissAIPromptBox();
56335508
dismissNodeInfoBox();
56345509
dismissImageRibbonGallery();
5635-
dismissHyperlinkEditor();
56365510
dismissToastMessage();
5511+
getAllNodeMoreOptionsHandlers().forEach(handler => handler.dismiss());
56375512
}
56385513

56395514
let _toastTimeout = null;

src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -360,13 +360,41 @@ define(function (require, exports, module) {
360360
.replace("// DONT_STRIP_MINIFY:REPLACE_WITH_ADDED_REMOTE_SCRIPTS", remoteScriptText);
361361
}
362362

363-
function addRemoteFunctionScript(scriptName, scriptText) {
364-
if(remoteFunctionsConstantsScripts.has(scriptName) || remoteFunctionsScripts.has(scriptName)){
365-
console.error(`Remote function script ${scriptName} already exists. Script wont be added.`);
363+
const JS_RESERVED_WORDS = new Set([
364+
"break","case","catch","class","const","continue","debugger","default",
365+
"delete","do","else","export","extends","finally","for","function",
366+
"if","import","in","instanceof","new","return","super","switch",
367+
"this","throw","try","typeof","var","void","while","with","yield",
368+
"enum","await","implements","interface","let","package","private",
369+
"protected","public","static"
370+
]);
371+
372+
function isValidFunctionName(name) {
373+
if (typeof name !== "string" || !name.length) {
374+
return false;
375+
}
376+
377+
// JS identifier syntax
378+
if (!/^[$A-Z_][0-9A-Z_$]*$/i.test(name)) {
379+
return false;
380+
}
381+
382+
// Reserved words
383+
return !JS_RESERVED_WORDS.has(name);
384+
}
385+
386+
387+
function addRemoteFunctionScript(scriptFunctionName, scriptText) {
388+
if(remoteFunctionsConstantsScripts.has(scriptFunctionName) || remoteFunctionsScripts.has(scriptFunctionName)){
389+
console.error(`Remote function script ${scriptFunctionName} already exists. Script wont be added.`);
390+
return false;
391+
}
392+
if(scriptFunctionName.length > 100 || !isValidFunctionName(scriptFunctionName)){
393+
console.error(`Script name ${scriptFunctionName} should be a valid function name.`);
366394
return false;
367395
}
368-
scriptText = `(()=>{${scriptText}})();`;
369-
remoteFunctionsScripts.set(scriptName, scriptText);
396+
scriptText = `(function ${scriptFunctionName}(){${scriptText}})();`;
397+
remoteFunctionsScripts.set(scriptFunctionName, scriptText);
370398
if(!RemoteFunctions.includes("// DONT_STRIP_MINIFY:REPLACE_WITH_ADDED_REMOTE_SCRIPTS")){
371399
throw new Error("RemoteFunctions script is missing the placeholder // REPLACE_WITH_ADDED_REMOTE_SCRIPTS");
372400
}

0 commit comments

Comments
 (0)