@@ -30,6 +30,7 @@ interface State {
30
30
latestKeyCode ?: number ;
31
31
offsetTop ?: number ;
32
32
suggestions ?: Suggestion [ ] ;
33
+ isSticky ?: boolean ;
33
34
}
34
35
35
36
@@ -40,6 +41,20 @@ export class PromptComponent extends React.Component<Props, State> implements Ke
40
41
onKeyDown : Function ;
41
42
} ;
42
43
44
+ private intersectionObserver = new IntersectionObserver (
45
+ ( entries ) => {
46
+ const entry = entries [ 0 ] ;
47
+ const nearTop = entry . boundingClientRect . bottom < 100 ;
48
+ const isVisible = entry . intersectionRatio === 1 ;
49
+
50
+ this . setState ( { isSticky : nearTop && ! isVisible } ) ;
51
+ } ,
52
+ {
53
+ threshold : 1 ,
54
+ rootMargin : css . toDOMString ( css . promptWrapperHeight ) ,
55
+ }
56
+ ) ;
57
+
43
58
constructor ( props : Props ) {
44
59
super ( props ) ;
45
60
this . prompt = this . props . job . prompt ;
@@ -48,6 +63,7 @@ export class PromptComponent extends React.Component<Props, State> implements Ke
48
63
suggestions : [ ] ,
49
64
highlightedSuggestionIndex : 0 ,
50
65
latestKeyCode : undefined ,
66
+ isSticky : false ,
51
67
} ;
52
68
53
69
const keyDownSubject : Subject < KeyboardEvent > = new Subject ( ) ;
@@ -137,9 +153,16 @@ export class PromptComponent extends React.Component<Props, State> implements Ke
137
153
}
138
154
} ) ;
139
155
156
+ this . intersectionObserver . observe ( this . placeholderNode ) ;
157
+
140
158
this . setDOMValueProgrammatically ( this . prompt . value ) ;
141
159
}
142
160
161
+ componentWillUnmount ( ) {
162
+ this . intersectionObserver . unobserve ( this . placeholderNode ) ;
163
+ this . intersectionObserver . disconnect ( ) ;
164
+ }
165
+
143
166
componentDidUpdate ( prevProps : Props , prevState : State ) {
144
167
if ( this . props . status !== e . Status . NotStarted ) {
145
168
return ;
@@ -186,35 +209,38 @@ export class PromptComponent extends React.Component<Props, State> implements Ke
186
209
decorationToggle = < DecorationToggleComponent decorateToggler = { this . props . decorateToggler } /> ;
187
210
}
188
211
189
- if ( this . props . status !== e . Status . NotStarted && this . props . job . screenBuffer . size > 100 ) {
212
+ if ( this . state . isSticky ) {
190
213
scrollToTop = < span style = { css . action }
191
- onClick = { this . handleScrollToTop . bind ( this ) }
192
- dangerouslySetInnerHTML = { { __html : fontAwesome . longArrowUp } } /> ;
214
+ title = "Scroll to beginning of output."
215
+ onClick = { this . handleScrollToTop . bind ( this ) }
216
+ dangerouslySetInnerHTML = { { __html : fontAwesome . longArrowUp } } /> ;
193
217
}
194
218
195
219
return (
196
- < div className = "prompt-wrapper" id = { this . props . job . id } style = { css . promptWrapper ( this . props . status ) } >
197
- < div style = { css . arrow ( this . props . status ) } >
198
- < div style = { css . arrowInner ( this . props . status ) } > </ div >
199
- </ div >
200
- < div style = { css . promptInfo ( this . props . status ) }
201
- title = { JSON . stringify ( this . props . status ) }
202
- dangerouslySetInnerHTML = { { __html : this . props . status === Status . Interrupted ? fontAwesome . close : "" } } > </ div >
203
- < div className = "prompt"
204
- style = { css . prompt }
205
- onKeyDown = { event => this . handlers . onKeyDown ( event ) }
206
- onInput = { this . handleInput . bind ( this ) }
207
- onKeyPress = { ( ) => this . props . status === e . Status . InProgress && stopBubblingUp ( event ) }
208
- onDrop = { this . handleDrop . bind ( this ) }
209
- type = "text"
210
- ref = "command"
211
- contentEditable = { this . props . status === e . Status . NotStarted || this . props . status === e . Status . InProgress } > </ div >
212
- { autocompletedPreview }
213
- { inlineSynopsis }
214
- { autocomplete }
215
- < div style = { css . actions } >
216
- { decorationToggle }
217
- { scrollToTop }
220
+ < div className = "prompt-placeholder" ref = "placeholder" id = { this . props . job . id } style = { css . promptPlaceholder } >
221
+ < div className = "prompt-wrapper" style = { css . promptWrapper ( this . props . status , this . state . isSticky ) } >
222
+ < div style = { css . arrow ( this . props . status ) } >
223
+ < div style = { css . arrowInner ( this . props . status ) } > </ div >
224
+ </ div >
225
+ < div style = { css . promptInfo ( this . props . status ) }
226
+ title = { JSON . stringify ( this . props . status ) }
227
+ dangerouslySetInnerHTML = { { __html : this . props . status === Status . Interrupted ? fontAwesome . close : "" } } > </ div >
228
+ < div className = "prompt"
229
+ style = { css . prompt }
230
+ onKeyDown = { event => this . handlers . onKeyDown ( event ) }
231
+ onInput = { this . handleInput . bind ( this ) }
232
+ onKeyPress = { ( ) => this . props . status === e . Status . InProgress && stopBubblingUp ( event ) }
233
+ onDrop = { this . handleDrop . bind ( this ) }
234
+ type = "text"
235
+ ref = "command"
236
+ contentEditable = { this . props . status === e . Status . NotStarted || this . props . status === e . Status . InProgress } > </ div >
237
+ { autocompletedPreview }
238
+ { inlineSynopsis }
239
+ { autocomplete }
240
+ < div style = { css . actions } >
241
+ { decorationToggle }
242
+ { scrollToTop }
243
+ </ div >
218
244
</ div >
219
245
</ div >
220
246
) ;
@@ -233,6 +259,11 @@ export class PromptComponent extends React.Component<Props, State> implements Ke
233
259
return this . refs [ "command" ] as HTMLInputElement ;
234
260
}
235
261
262
+ private get placeholderNode ( ) : Element {
263
+ /* tslint:disable:no-string-literal */
264
+ return this . refs [ "placeholder" ] as Element ;
265
+ }
266
+
236
267
private setDOMValueProgrammatically ( text : string ) : void {
237
268
this . commandNode . innerText = text ;
238
269
setCaretPosition ( this . commandNode , text . length ) ;
0 commit comments