Skip to content

Commit 2fc2a7c

Browse files
committed
deploy: 84352e3
1 parent e104396 commit 2fc2a7c

File tree

1,401 files changed

+429590
-48178
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,401 files changed

+429590
-48178
lines changed

LiveDevelopment/BrowserScripts/DocumentObserver.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,25 @@
4848
}, false);
4949
}
5050

51+
window.addEventListener('scroll', function () {
52+
// save scroll position
53+
sessionStorage.setItem("saved-scroll-" + location.href, JSON.stringify({
54+
scrollX: window.scrollX,
55+
scrollY: window.scrollY
56+
}));
57+
});
58+
function scrollToLastPosition() {
59+
let saved = JSON.parse(sessionStorage.getItem("saved-scroll-" + location.href));
60+
if(saved){
61+
window.scrollTo({
62+
left: saved.scrollX,
63+
top: saved.scrollY,
64+
behavior: "instant"
65+
});
66+
}
67+
}
68+
window.addEventListener("load", scrollToLastPosition);
69+
5170
/**
5271
* Retrieves related documents (external CSS and JS files)
5372
*

LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -238,23 +238,11 @@
238238
reload: function (msg) {
239239
// just reload the page
240240
window.location.reload(msg.params.ignoreCache);
241-
},
242-
243-
/**
244-
* Navigate to a different page.
245-
* @param {Object} msg
246-
*/
247-
navigate: function (msg) {
248-
if (msg.params.url) {
249-
// navigate to a new page.
250-
window.location.replace(msg.params.url);
251-
}
252241
}
253242
};
254243

255244
// subscribe handler to method Page.reload
256245
MessageBroker.on("Page.reload", Page.reload);
257-
MessageBroker.on("Page.navigate", Page.navigate);
258246
MessageBroker.on("ConnectionClose", Page.close);
259247

260248

@@ -381,8 +369,12 @@
381369
function onDocumentClick(event) {
382370
var element = event.target;
383371
if (element && element.hasAttribute('data-brackets-id')) {
384-
MessageBroker.send({"tagId": element.getAttribute('data-brackets-id'),
385-
"clicked": true});
372+
MessageBroker.send({
373+
"tagId": element.getAttribute('data-brackets-id'),
374+
"nodeName": element.nodeName,
375+
"contentEditable": element.contentEditable === 'true',
376+
"clicked": true
377+
});
386378
}
387379
}
388380
window.document.addEventListener("click", onDocumentClick);
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
/*
2+
* GNU AGPL-3.0 License
3+
*
4+
* Copyright (c) 2021 - present core.ai . All rights reserved.
5+
* Original work Copyright (c) 2014 - 2021 Adobe Systems Incorporated. All rights reserved.
6+
*
7+
* This program is free software: you can redistribute it and/or modify it
8+
* under the terms of the GNU Affero General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful, but WITHOUT
13+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
15+
* for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
19+
*
20+
*/
21+
22+
// This is a transport injected into the browser via a script that handles the low
23+
// level communication between the live development protocol handlers on both sides.
24+
// The actual communication to phoenix is done via the loaded web worker below. We just post/receive all
25+
// messages that should be sent/received by live preview to the worker. The worker will use broadcast
26+
// channels in browser and web sockets in desktop builds to rely on the message to phoenix.
27+
/**
28+
* Communication Architecture in PHCode.dev Browser Environment
29+
* ------------------------------------------------------------
30+
*
31+
* First of all I like to apologize for this complexity, it is how it is due to the browser standards security
32+
* policy, intelligent tracking prevention in browsers and the inherent multiprocess communication problem.
33+
* The dining philosophers can however take rest as the mechanism is fully lockless thanks to how js handles events.
34+
*
35+
* Overview:
36+
* PHCode.dev operates with a multi-iframe setup to facilitate communication between different components
37+
* within the same domain(phcode.dev) and cross domain(phcode.dev<>phcode.live). Live previews have to be domain
38+
* isolated to phcode.live domain so that malicious project live previews doesn't steal phcode.dev cookies and
39+
* take control of the users account by just opening a live preview.
40+
* This setup includes a preview page(phcode.dev/live-preview-loader.html), a server iframe (phcode.live), and an actual
41+
* preview iframe where the user's code is rendered(phcode.live/user/projoject/live/preview.html).
42+
*
43+
* Components:
44+
* 1. Preview Page (phcode.dev):
45+
* - Serves as the primary interface for the user. The actual tab.
46+
* - Hosts two iframes: the server iframe and the actual preview iframe.
47+
*
48+
* 2. Server Iframe (phcode.live):
49+
* - Responsible for installing a service worker for virtual server, sandboxed to its specific tab.
50+
* - Acts as an intermediary in the communication chain.
51+
*
52+
* 3. Actual Preview Iframe: (phcode.live/user/projoject/live/preview.html)
53+
* - Renders the user's code.
54+
* - Utilizes a broadcast channel within the web worker to send messages. We use a web worker so
55+
* that live preview tab hearbeat messages are sent to the editor even if the user is debugging
56+
* the page causing js execution to halt in the debugging thread but not the worker thread.
57+
*
58+
* Communication Flow:
59+
* 1. Messages originate from the Actual Preview Iframe, where the user's script is loaded.
60+
* 2. These messages are sent to the Live Preview Server Iframe via a broadcast channel in the service worker.
61+
* 3. The Server Iframe then relays these messages to the parent PHCode.dev frame.
62+
* 4. Finally, the PHCode.dev frame forwards these messages to the PHCode.dev editor page.
63+
* - This step occurs if the editor page is loaded in a different tab and not as an in-editor live preview panel.
64+
*
65+
* Note on Communication Constraints and Solutions:
66+
* ------------------------------------------------
67+
* Cross-Domain Communication Limitations:
68+
* - The default security model of web browsers restricts cross-domain communication as a measure to preserve security.
69+
* - This means that iframes from different domains cannot freely communicate with each other due to
70+
* browser-enforced sandboxing.
71+
*
72+
* Use of Broadcast Channels within the Same Domain:
73+
* - To circumvent these cross-domain communication restrictions, PHCode.dev employs broadcast channels within
74+
* the same domain.
75+
*
76+
* Solution for Cross-Domain Communication:
77+
* - The architecture is designed to avoid direct cross-domain communication, which is restricted by
78+
* the browser's security model.
79+
* - Instead, a 'hoola hoop' method is used where the server Iframe (phcode.live) relays broadcast channel
80+
* messages in phcode.live to its cross domain parent window phcode.dev through window post message apis.
81+
* - The parent PHCode.dev frame further communicates with the PHCode.dev editor page, if its in a different tab.
82+
*
83+
* Working within Browser Security Framework:
84+
* - This approach allows the system to operate within the browser's security constraints.
85+
* - It eliminates the need for server-side assistance, thus enabling instant live preview
86+
* feedback in a purely client-side setting.
87+
**/
88+
89+
90+
(function (global) {
91+
92+
// The below line will be replaced with the transport scripts provided by the static server at
93+
// LivePreviewTransport.js:getRemoteScript() This is so that the actual live preview page doesnt get hold of
94+
// any phoenix web socket or broadcast channel ids from this closure programatically for security.
95+
96+
//Replace dynamic section start
97+
const TRANSPORT_CONFIG={};
98+
//Replace dynamic section end
99+
100+
function _debugLog(...args) {
101+
if(window.LIVE_PREVIEW_DEBUG_ENABLED) {
102+
console.log(...args);
103+
}
104+
}
105+
106+
const clientID = "" + Math.round( Math.random()*1000000000);
107+
108+
const worker = new Worker(TRANSPORT_CONFIG.LIVE_DEV_REMOTE_WORKER_SCRIPTS_FILE_NAME);
109+
let _workerMessageProcessor;
110+
worker.onmessage = (event) => {
111+
const type = event.data.type;
112+
switch (type) {
113+
case 'REDIRECT_PAGE': location.href = event.data.URL; break;
114+
default:
115+
if(_workerMessageProcessor){
116+
return _workerMessageProcessor(event);
117+
}
118+
console.error("Live Preview page loader: received unknown message from worker:", event);
119+
}
120+
};
121+
// message channel to phoenix connect on load itself. The channel id is injected from phoenix
122+
// via LivePreviewTransport.js while serving the instrumented html file
123+
worker.postMessage({
124+
type: "setupPhoenixComm",
125+
livePreviewDebugModeEnabled: TRANSPORT_CONFIG.LIVE_PREVIEW_DEBUG_ENABLED,
126+
broadcastChannel: TRANSPORT_CONFIG.LIVE_PREVIEW_BROADCAST_CHANNEL_ID, // in browser this will be present, but not in tauri
127+
websocketChannelURL: TRANSPORT_CONFIG.LIVE_PREVIEW_WEBSOCKET_CHANNEL_URL, // in tauri this will be present. not in browser
128+
clientID
129+
});
130+
function _postLivePreviewMessage(message) {
131+
worker.postMessage({type: "livePreview", message});
132+
}
133+
let sentTitle, sentFavIconURL;
134+
135+
function convertImgToBase64(url, callback) {
136+
if(!url){
137+
callback(null);
138+
return;
139+
}
140+
let canvas = document.createElement('CANVAS');
141+
const ctx = canvas.getContext('2d');
142+
const img = new Image();
143+
img.crossOrigin = 'Anonymous';
144+
img.onload = function() {
145+
canvas.height = img.height;
146+
canvas.width = img.width;
147+
ctx.drawImage(img, 0, 0);
148+
const dataURL = canvas.toDataURL();
149+
callback(dataURL);
150+
canvas = null;
151+
};
152+
img.src = url;
153+
}
154+
155+
setInterval(()=>{
156+
const favIcon = document.querySelector("link[rel~='icon']");
157+
const faviconUrl = favIcon && favIcon.href;
158+
if(sentFavIconURL !== faviconUrl){
159+
sentFavIconURL = faviconUrl;
160+
convertImgToBase64(faviconUrl, function(base64) {
161+
if(!base64){
162+
base64 = "favicon.ico";
163+
}
164+
worker.postMessage({
165+
type: "updateTitleIcon",
166+
faviconBase64: base64
167+
});
168+
});
169+
}
170+
171+
if(sentTitle!== document.title) {
172+
sentTitle = document.title;
173+
worker.postMessage({
174+
type: "updateTitleIcon",
175+
title: document.title
176+
});
177+
}
178+
}, 1000);
179+
180+
global._Brackets_LiveDev_Transport = {
181+
_channelOpen: false,
182+
183+
/**
184+
* @private
185+
* An object that contains callbacks to handle various transport events. See `setCallbacks()`.
186+
* @type {?{connect: ?function, message: ?function(string), close: ?function}}
187+
*/
188+
_callbacks: null,
189+
190+
/**
191+
* Sets the callbacks that should be called when various transport events occur. All callbacks
192+
* are optional, but you should at least implement "message" or nothing interesting will happen :)
193+
* @param {?{connect: ?function, message: ?function(string), close: ?function}} callbacks
194+
* The callbacks to set.
195+
* connect - called when a connection is established to Brackets
196+
* message(msgStr) - called with a string message sent from Brackets
197+
* close - called when Brackets closes the connection
198+
*/
199+
setCallbacks: function (callbacks) {
200+
this._callbacks = callbacks;
201+
},
202+
203+
/**
204+
* Connects to the LivePreviewTransport in Brackets.
205+
*/
206+
connect: function () {
207+
const self = this;
208+
209+
// Listen to the response
210+
_workerMessageProcessor = (event) => {
211+
// Print the result
212+
_debugLog("Live Preview: Browser received event from Phoenix: ", JSON.stringify(event.data));
213+
const type = event.data.type;
214+
switch (type) {
215+
case 'BROWSER_CONNECT': break; // do nothing. This is a loopback message from another live preview tab
216+
case 'BROWSER_MESSAGE': break; // do nothing. This is a loopback message from another live preview tab
217+
case 'BROWSER_CLOSE': break; // do nothing. This is a loopback message from another live preview tab
218+
case 'MESSAGE_FROM_PHOENIX':
219+
if (self._callbacks && self._callbacks.message) {
220+
const clientIDs = event.data.clientIDs,
221+
message = event.data.message;
222+
if(clientIDs.includes(clientID) || clientIDs.length === 0){
223+
// clientIDs.length = 0 if the message is intended for all clients
224+
self._callbacks.message(message);
225+
}
226+
}
227+
break;
228+
case 'PHOENIX_CLOSE':
229+
self._channelOpen = false;
230+
if (self._callbacks && self._callbacks.close) {
231+
self._callbacks.close();
232+
}
233+
break;
234+
}
235+
};
236+
_postLivePreviewMessage({
237+
type: 'BROWSER_CONNECT',
238+
url: global.location.href,
239+
clientID: clientID
240+
});
241+
self._channelOpen = true;
242+
if (self._callbacks && self._callbacks.connect) {
243+
self._callbacks.connect();
244+
}
245+
246+
// attach to browser tab/window closing event so that we send a cleanup request
247+
// to the service worker for the comm ports
248+
addEventListener( 'beforeunload', function() {
249+
if(self._channelOpen){
250+
self._channelOpen = false;
251+
_postLivePreviewMessage({
252+
type: 'BROWSER_CLOSE',
253+
clientID: clientID
254+
});
255+
}
256+
});
257+
},
258+
259+
/**
260+
* Sends a message over the transport.
261+
* @param {string} msgStr The message to send.
262+
*/
263+
send: function (msgStr) {
264+
_postLivePreviewMessage({
265+
type: 'BROWSER_MESSAGE',
266+
clientID: clientID,
267+
message: msgStr
268+
});
269+
},
270+
271+
/**
272+
* Establish web socket connection.
273+
*/
274+
enable: function () {
275+
this.connect();
276+
}
277+
};
278+
279+
function getAbsoluteUrl(url) {
280+
// Check if the URL is already absolute
281+
if (/^(?:[a-z]+:)?\/\//i.test(url)) {
282+
return url; // The URL is already absolute
283+
}
284+
285+
// If not, create an absolute URL using the current page's location as the base
286+
const absoluteUrl = new URL(url, window.location.href);
287+
return absoluteUrl.href;
288+
}
289+
290+
// This is only for tauri builds where the live preview is embedded in the phoenix editor iframe. on clicking
291+
// any urls that needs to be open in a browser window, we execute this. In browser, this is no-op as there is
292+
// no corresponding listener attached in phoenix browser server.
293+
document.addEventListener('click', function(event) {
294+
let targetElement = event.target;
295+
// Traverse one level up the DOM to find an anchor element if the target is not the anchor itself
296+
// eg when image inside anchor elements etc..: <a><img></img></a>
297+
if (targetElement !== null && targetElement.tagName !== 'A') {
298+
targetElement = targetElement.parentElement;
299+
}
300+
301+
if (targetElement && targetElement.tagName === 'A' && (targetElement.target === '_blank')) {
302+
const href = getAbsoluteUrl(targetElement.getAttribute('href'));
303+
window.parent.postMessage({
304+
handlerName: "ph-liveServer",
305+
eventName: 'embeddedIframeHrefClick',
306+
href: href
307+
}, "*");
308+
}
309+
});
310+
document.addEventListener('contextmenu', function(event) {
311+
(document.activeElement || document.body).focus();
312+
});
313+
document.addEventListener('keydown', function(event) {
314+
if (event.key === 'Escape' || event.key === 'Esc') { // Check for Escape key
315+
// Perform the desired action for the Escape key
316+
window.parent.postMessage({
317+
handlerName: "ph-liveServer",
318+
eventName: 'embeddedEscapeKeyPressed'
319+
}, "*");
320+
}
321+
});
322+
}(this));

LiveDevelopment/BrowserScripts/RemoteFunctions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ function RemoteFunctions(config, remoteWSPort) {
195195

196196
show: function () {
197197
if (!this.body) {
198-
this.body = this.createBody();
198+
this.createBody();
199199
}
200200
if (!this.body.parentNode) {
201201
window.document.body.appendChild(this.body);

0 commit comments

Comments
 (0)