@@ -52,6 +52,88 @@ define(function (require, exports, module) {
5252 var _providerRegistrationHandler = new ProviderRegistrationHandler ( ) ,
5353 _registerQuickOpenProvider = _providerRegistrationHandler . registerProvider . bind ( _providerRegistrationHandler ) ;
5454
55+ /**
56+ * Build a ModalBar-compatible stub that renders a Spotlight-style floating
57+ * picker on top of the viewport (used while in design mode, since the
58+ * standard ModalBar mounts into #editor-holder which is collapsed).
59+ * Implements the small subset of the ModalBar API that QuickOpen relies on:
60+ * `on("close", fn)`, `close()`, `prepareClose()`, and `getRoot()`.
61+ * @private
62+ */
63+ function _createFloatingQuickOpenBar ( templateHTML ) {
64+ const $overlay = $ ( "<div class='quick-open-floating-overlay'/>" ) ;
65+ const $bar = $ ( "<div class='quick-open-floating-bar'/>" ) . html ( templateHTML ) ;
66+ $overlay . append ( $bar ) . appendTo ( "body" ) ;
67+ const closeHandlers = [ ] ;
68+ let closed = false ;
69+ function close ( ) {
70+ if ( closed ) { return closePromise ; }
71+ closed = true ;
72+ // QuickNavigateDialog.close() returns this.closePromise, which is
73+ // the 3rd arg of the ModalBar "close" event payload. Pass a
74+ // resolved jQuery Deferred so callers can chain .done() safely.
75+ closePromise = $ . Deferred ( ) . resolve ( ) . promise ( ) ;
76+ closeHandlers . forEach ( function ( fn ) {
77+ try {
78+ // Match ModalBar's trigger signature: event, reason, promise
79+ fn ( { } , undefined , closePromise ) ;
80+ } catch ( e ) { /* swallow */ }
81+ } ) ;
82+ $overlay . remove ( ) ;
83+ window . document . body . removeEventListener ( "focusin" , onFocusIn , true ) ;
84+ return closePromise ;
85+ }
86+ let closePromise = null ;
87+ function onFocusIn ( e ) {
88+ // Close when focus moves outside both the bar and the results
89+ // dropdown (which QuickSearchField appends to <body>, not into
90+ // our overlay).
91+ if ( $overlay [ 0 ] . contains ( e . target ) ) { return ; }
92+ if ( e . target . closest && e . target . closest ( ".quick-search-container" ) ) { return ; }
93+ close ( ) ;
94+ }
95+ const bar = {
96+ on : function ( evt , fn ) {
97+ if ( evt === "close" && typeof fn === "function" ) {
98+ closeHandlers . push ( fn ) ;
99+ }
100+ } ,
101+ prepareClose : function ( ) { /* no-op — floating bar doesn't reserve editor space */ } ,
102+ close : close ,
103+ getRoot : function ( ) { return $bar ; }
104+ } ;
105+ // Close the whole picker on the FIRST Escape rather than just the
106+ // dropdown. Use capture so we run before QuickSearchField's keydown
107+ // handler (which does internal dropdown dismiss handling) can
108+ // consume the event.
109+ function onEscape ( e ) {
110+ if ( e . key === "Escape" || e . keyCode === 27 ) {
111+ close ( ) ;
112+ e . preventDefault ( ) ;
113+ e . stopPropagation ( ) ;
114+ if ( e . stopImmediatePropagation ) { e . stopImmediatePropagation ( ) ; }
115+ }
116+ }
117+ $overlay [ 0 ] . addEventListener ( "keydown" , onEscape , true ) ;
118+ // Global safety net: while overlay is up, first Escape anywhere closes it.
119+ window . document . addEventListener ( "keydown" , onEscape , true ) ;
120+ closeHandlers . push ( function ( ) {
121+ window . document . removeEventListener ( "keydown" , onEscape , true ) ;
122+ } ) ;
123+ $overlay . on ( "mousedown" , function ( e ) {
124+ if ( e . target === $overlay [ 0 ] ) {
125+ close ( ) ;
126+ }
127+ } ) ;
128+ window . document . body . addEventListener ( "focusin" , onFocusIn , true ) ;
129+ // Move focus into the input once the overlay is in the DOM.
130+ window . setTimeout ( function ( ) {
131+ const $input = $bar . find ( "input" ) . first ( ) ;
132+ if ( $input . length ) { $input . focus ( ) ; }
133+ } , 0 ) ;
134+ return bar ;
135+ }
136+
55137 /**
56138 * Represents the symbol kind
57139 * @type {Object }
@@ -637,6 +719,14 @@ define(function (require, exports, module) {
637719 initialString = initialString || "" ;
638720 initialString = prefix + initialString ;
639721
722+ // If the underlying search field was already torn down (e.g. the bar
723+ // was closed via focus-loss but the isOpen flag wasn't updated in
724+ // time), fall back to closing cleanly so the caller reopens fresh.
725+ if ( ! this . searchField || ! this . searchField . $input || ! this . $searchField || ! this . $searchField [ 0 ] ) {
726+ this . isOpen = false ;
727+ return ;
728+ }
729+
640730 this . searchField . setText ( initialString ) ;
641731
642732 // Select just the text after the prefix
@@ -707,17 +797,44 @@ define(function (require, exports, module) {
707797 placeholder='${ Strings . CMD_QUICK_OPEN } \u2026' style='width: 30em'>
708798 <span class='find-dialog-label'></span>
709799 </div>` ;
710- this . modalBar = new ModalBar ( searchBarHTML , true ) ;
800+ // In design mode the editor area is collapsed and the normal ModalBar
801+ // (which slides in before #editor-holder) has no room to render. Use a
802+ // Spotlight-style floating overlay instead so the picker stays usable.
803+ if ( WorkspaceManager . isInDesignMode ( ) ) {
804+ this . modalBar = _createFloatingQuickOpenBar ( searchBarHTML ) ;
805+ } else {
806+ this . modalBar = new ModalBar ( searchBarHTML , true ) ;
807+ }
711808
712809 this . modalBar . on ( "close" , this . _handleCloseBar ) ;
713810
714811 this . $searchField = $ ( "input#quickOpenSearch" ) ;
715812 this . $indexingSpinner = $ ( "#indexing-spinner" ) ;
716813
814+ const _floating = WorkspaceManager . isInDesignMode ( ) ;
815+ let _verticalAdjust = this . modalBar . getRoot ( ) . outerHeight ( ) ;
816+ if ( _floating ) {
817+ // QuickSearchField computes dropdownTop as
818+ // input.offset().top + input.height + verticalAdjust
819+ // For the standard ModalBar that lands flush with the bar's bottom
820+ // because the input sits at the bar's bottom edge. In the floating
821+ // bar the input is vertically centered inside, so `bar.height` as
822+ // verticalAdjust overshoots and the dropdown floats detached. Use
823+ // (bar.bottom - input.bottom) instead so the dropdown starts right
824+ // below the bar.
825+ const $root = this . modalBar . getRoot ( ) ;
826+ const barBottom = $root . offset ( ) . top + $root . outerHeight ( ) ;
827+ const inputBottom = this . $searchField . offset ( ) . top + this . $searchField . outerHeight ( ) ;
828+ _verticalAdjust = Math . max ( 0 , barBottom - inputBottom ) ;
829+ }
717830 this . searchField = new QuickSearchField ( this . $searchField , {
718831 maxResults : 20 ,
719832 firstHighlightIndex : 0 ,
720- verticalAdjust : this . modalBar . getRoot ( ) . outerHeight ( ) ,
833+ verticalAdjust : _verticalAdjust ,
834+ // In design mode the floating bar is the visible container for the
835+ // picker; align the results dropdown to that element instead of the
836+ // bare <input> so the two edges match.
837+ $positionEl : _floating ? this . modalBar . getRoot ( ) : undefined ,
721838 resultProvider : this . _filterCallback ,
722839 formatter : this . _resultsFormatterCallback ,
723840 onCommit : this . _handleItemSelect ,
@@ -780,14 +897,6 @@ define(function (require, exports, module) {
780897 }
781898
782899 function doFileSearch ( ) {
783- // Design mode hides the editor area where Quick Open's modal bar and
784- // result interactions need to land. Exit design mode first so the user
785- // sees the picker on the normal editor chrome.
786- // TODO: allow Quick Open to float above the live preview so users can
787- // navigate without leaving design mode.
788- if ( WorkspaceManager . isInDesignMode ( ) ) {
789- CommandManager . execute ( Commands . VIEW_TOGGLE_DESIGN_MODE ) ;
790- }
791900 beginSearch ( "" , getCurrentEditorSelectedText ( ) ) ;
792901 }
793902
0 commit comments