Skip to content

Commit 49a1647

Browse files
committed
feat: narrow quick open to previewable/server files in design mode + iframe-click dismiss
Move the file-type classification helpers (getExtension, isPDF, isSVG, isImage, isHTMLFile, isMarkdownFile, isPreviewableFile, isServerRenderedFile) from Phoenix-live-preview/utils.js into a shared src/utils/FileTypeUtils.js module so non-live-preview code can depend on them. The live-preview utils now re-exports those helpers and keeps only its iframe-specific pieces (LIVE_PREVIEW_IFRAME_ID, MDVIEWR_IFRAME_ID, focusActiveEditorIfFocusInLivePreview). Quick Open's floating (design-mode) picker now restricts results to files that actually make sense to open into the live preview — HTML, SVG, Markdown, PDF — plus server-rendered sources (PHP, ASP, JSP, Python, Ruby/ERB, ColdFusion). Everything else is hidden because opening it would just collapse design mode. Also dismiss the floating picker when focus moves into the live preview iframe: those clicks never surface as mousedown / focusin on the parent document, so they used to leave the picker stranded. Listen for window blur and close once focus settles outside the bar / results dropdown.
1 parent f7c997d commit 49a1647

3 files changed

Lines changed: 156 additions & 67 deletions

File tree

src/extensionsIntegrated/Phoenix-live-preview/utils.js

Lines changed: 13 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -39,60 +39,10 @@ define(function (require, exports, module) {
3939
const LIVE_PREVIEW_IFRAME_ID = "panel-live-preview-frame";
4040
const MDVIEWR_IFRAME_ID = "panel-md-preview-frame";
4141
const EditorManager = require("editor/EditorManager");
42-
function getExtension(filePath) {
43-
filePath = filePath || '';
44-
let pathSplit = filePath.split('.');
45-
return pathSplit && pathSplit.length>1 ? pathSplit[pathSplit.length-1] : '';
46-
}
47-
48-
function isPreviewableFile(filePath) {
49-
// only svg images should appear in the live preview as it needs text editor.
50-
// All other image types should appear in the image previewer
51-
return isSVG(filePath) || isMarkdownFile(filePath) || isHTMLFile(filePath) || isPDF(filePath);
52-
}
53-
54-
function isPDF(filePath) {
55-
let extension = getExtension(filePath);
56-
return extension === "pdf";
57-
}
58-
59-
function isSVG(filePath) {
60-
let extension = getExtension(filePath);
61-
return extension === "svg";
62-
}
63-
64-
function isImage(filePath) {
65-
let extension = getExtension(filePath);
66-
return ["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp", "ico", "avif"]
67-
.includes(extension.toLowerCase());
68-
}
69-
70-
function isMarkdownFile(filePath) {
71-
let extension = getExtension(filePath);
72-
return ['md', 'markdown', 'mdx'].includes(extension.toLowerCase());
73-
}
74-
75-
function isHTMLFile(filePath) {
76-
let extension = getExtension(filePath);
77-
return ['html', 'htm', 'xhtml'].includes(extension.toLowerCase());
78-
}
79-
80-
function isServerRenderedFile(filePath) {
81-
let extension = getExtension(filePath);
82-
return [
83-
"shtml",
84-
"asp",
85-
"aspx",
86-
"php",
87-
"jsp",
88-
"jspx",
89-
"cfm",
90-
"cfc", // ColdFusion Component
91-
"rb", // Ruby file, used in Ruby on Rails for views with ERB
92-
"erb", // Embedded Ruby, used in Ruby on Rails views
93-
"py" // Python file, used in web frameworks like Django or Flask for views
94-
].includes(extension.toLowerCase());
95-
}
42+
// File-type classification helpers live in a shared utility module so
43+
// non-live-preview code (e.g. Quick Open in design mode) can use them
44+
// without depending on this extension.
45+
const FileTypeUtils = require("utils/FileTypeUtils");
9646

9747
function focusActiveEditorIfFocusInLivePreview() {
9848
const editor = EditorManager.getActiveEditor();
@@ -105,15 +55,14 @@ define(function (require, exports, module) {
10555
}
10656
}
10757

108-
exports.getExtension = getExtension;
109-
exports.isPreviewableFile = isPreviewableFile;
110-
exports.isImage = isImage;
111-
exports.isPDF = isPDF;
112-
exports.isSVG = isSVG;
113-
exports.isHTMLFile = isHTMLFile;
114-
exports.isServerRenderedFile = isServerRenderedFile;
115-
exports.isMarkdownFile = isMarkdownFile;
58+
// Re-export the shared helpers so existing callers keep working.
59+
exports.getExtension = FileTypeUtils.getExtension;
60+
exports.isPreviewableFile = FileTypeUtils.isPreviewableFile;
61+
exports.isImage = FileTypeUtils.isImage;
62+
exports.isPDF = FileTypeUtils.isPDF;
63+
exports.isSVG = FileTypeUtils.isSVG;
64+
exports.isHTMLFile = FileTypeUtils.isHTMLFile;
65+
exports.isServerRenderedFile = FileTypeUtils.isServerRenderedFile;
66+
exports.isMarkdownFile = FileTypeUtils.isMarkdownFile;
11667
exports.focusActiveEditorIfFocusInLivePreview = focusActiveEditorIfFocusInLivePreview;
11768
});
118-
119-

src/search/QuickOpen.js

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ define(function (require, exports, module) {
4444
LanguageManager = require("language/LanguageManager"),
4545
FileSystemError = require("filesystem/FileSystemError"),
4646
WorkspaceManager = require("view/WorkspaceManager"),
47+
FileTypeUtils = require("utils/FileTypeUtils"),
4748
ModalBar = require("widgets/ModalBar").ModalBar,
4849
QuickSearchField = require("search/QuickSearchField").QuickSearchField,
4950
StringMatch = require("utils/StringMatch"),
@@ -84,14 +85,48 @@ define(function (require, exports, module) {
8485
return closePromise;
8586
}
8687
let closePromise = null;
88+
function isInPicker(node) {
89+
if (!node) { return false; }
90+
if ($overlay[0].contains(node)) { return true; }
91+
if (node.closest && node.closest(".quick-search-container")) { return true; }
92+
return false;
93+
}
8794
function onFocusIn(e) {
8895
// Close when focus moves outside both the bar and the results
8996
// dropdown (which QuickSearchField appends to <body>, not into
9097
// our overlay).
91-
if ($overlay[0].contains(e.target)) { return; }
92-
if (e.target.closest && e.target.closest(".quick-search-container")) { return; }
98+
if (isInPicker(e.target)) { return; }
99+
close();
100+
}
101+
// The overlay itself uses `pointer-events: none` so the UI behind it
102+
// stays interactive (users can still click into the sidebar / live
103+
// preview). Clicking somewhere does not always move focus (e.g. live
104+
// preview iframes keep their own focus), so we also watch mousedown
105+
// on the document and dismiss whenever the click lands outside the
106+
// bar + dropdown.
107+
function onMousedown(e) {
108+
if (isInPicker(e.target)) { return; }
93109
close();
94110
}
111+
window.document.addEventListener("mousedown", onMousedown, true);
112+
// Clicks inside the live-preview iframe never surface as mousedown
113+
// events on the parent document — the iframe consumes them. Detect
114+
// that case via the window blur event (focus moves to the iframe's
115+
// content window) and a slight delay to let focus settle before
116+
// deciding whether we really moved out of the picker.
117+
function onWindowBlur() {
118+
window.setTimeout(function () {
119+
if (closed) { return; }
120+
const active = window.document.activeElement;
121+
if (active && isInPicker(active)) { return; }
122+
close();
123+
}, 0);
124+
}
125+
window.addEventListener("blur", onWindowBlur);
126+
closeHandlers.push(function () {
127+
window.document.removeEventListener("mousedown", onMousedown, true);
128+
window.removeEventListener("blur", onWindowBlur);
129+
});
95130
const bar = {
96131
on: function (evt, fn) {
97132
if (evt === "close" && typeof fn === "function") {
@@ -841,8 +876,15 @@ define(function (require, exports, module) {
841876
onHighlight: this._handleItemHighlight
842877
});
843878

844-
// Return files that are non-binary, or binary files that have a custom viewer
879+
// Return files that are non-binary, or binary files that have a custom viewer.
880+
// In design mode narrow to files that actually make sense to open into the
881+
// live preview (HTML/MD/SVG/PDF) or server-rendered sources — everything
882+
// else would just collapse design mode.
845883
function _filter(file) {
884+
if (_floating) {
885+
return FileTypeUtils.isPreviewableFile(file.fullPath) ||
886+
FileTypeUtils.isServerRenderedFile(file.fullPath);
887+
}
846888
return !LanguageManager.getLanguageForPath(file.fullPath).isBinary() ||
847889
MainViewFactory.findSuitableFactoryForPath(file.fullPath);
848890
}

src/utils/FileTypeUtils.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* GNU AGPL-3.0 License
3+
*
4+
* Copyright (c) 2021 - present core.ai . All rights reserved.
5+
*
6+
* This program is free software: you can redistribute it and/or modify it
7+
* under the terms of the GNU Affero General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
14+
* for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
18+
*
19+
*/
20+
21+
/**
22+
* File-type classification helpers shared by live preview, Quick Open in
23+
* design mode, and anywhere else that needs a quick "is this an HTML /
24+
* markdown / previewable / server-rendered file?" check based on extension.
25+
*/
26+
define(function (require, exports, module) {
27+
28+
function getExtension(filePath) {
29+
filePath = filePath || '';
30+
let pathSplit = filePath.split('.');
31+
return pathSplit && pathSplit.length > 1 ? pathSplit[pathSplit.length - 1] : '';
32+
}
33+
34+
function isPDF(filePath) {
35+
return getExtension(filePath).toLowerCase() === "pdf";
36+
}
37+
38+
function isSVG(filePath) {
39+
return getExtension(filePath).toLowerCase() === "svg";
40+
}
41+
42+
function isImage(filePath) {
43+
const extension = getExtension(filePath).toLowerCase();
44+
return ["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp", "ico", "avif"].includes(extension);
45+
}
46+
47+
function isMarkdownFile(filePath) {
48+
return ['md', 'markdown', 'mdx'].includes(getExtension(filePath).toLowerCase());
49+
}
50+
51+
function isHTMLFile(filePath) {
52+
return ['html', 'htm', 'xhtml'].includes(getExtension(filePath).toLowerCase());
53+
}
54+
55+
/**
56+
* True for file types that can be rendered directly in the live preview
57+
* panel (HTML, SVG, PDF, Markdown).
58+
* @param {string} filePath
59+
* @return {boolean}
60+
*/
61+
function isPreviewableFile(filePath) {
62+
// Only SVG images are previewable via this path; other images go to
63+
// the image previewer. Markdown / HTML / PDF also render.
64+
return isSVG(filePath) || isMarkdownFile(filePath) || isHTMLFile(filePath) || isPDF(filePath);
65+
}
66+
67+
/**
68+
* True for source files typically processed by a server-side runtime
69+
* (PHP, ASP, JSP, Ruby/ERB, Python, ColdFusion, etc.).
70+
* @param {string} filePath
71+
* @return {boolean}
72+
*/
73+
function isServerRenderedFile(filePath) {
74+
const extension = getExtension(filePath).toLowerCase();
75+
return [
76+
"shtml",
77+
"asp",
78+
"aspx",
79+
"php",
80+
"jsp",
81+
"jspx",
82+
"cfm",
83+
"cfc", // ColdFusion Component
84+
"rb", // Ruby file, used in Ruby on Rails for views with ERB
85+
"erb", // Embedded Ruby, used in Ruby on Rails views
86+
"py" // Python file, used in web frameworks like Django or Flask for views
87+
].includes(extension);
88+
}
89+
90+
exports.getExtension = getExtension;
91+
exports.isPDF = isPDF;
92+
exports.isSVG = isSVG;
93+
exports.isImage = isImage;
94+
exports.isHTMLFile = isHTMLFile;
95+
exports.isMarkdownFile = isMarkdownFile;
96+
exports.isPreviewableFile = isPreviewableFile;
97+
exports.isServerRenderedFile = isServerRenderedFile;
98+
});

0 commit comments

Comments
 (0)