Skip to content
This repository was archived by the owner on May 21, 2019. It is now read-only.

Commit ade67ce

Browse files
committed
Always display job's prompt on the screen.
1 parent 9e73e99 commit ade67ce

File tree

4 files changed

+113
-27
lines changed

4 files changed

+113
-27
lines changed

src/views/4_PromptComponent.tsx

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ interface State {
3030
latestKeyCode?: number;
3131
offsetTop?: number;
3232
suggestions?: Suggestion[];
33+
isSticky?: boolean;
3334
}
3435

3536

@@ -40,6 +41,20 @@ export class PromptComponent extends React.Component<Props, State> implements Ke
4041
onKeyDown: Function;
4142
};
4243

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+
4358
constructor(props: Props) {
4459
super(props);
4560
this.prompt = this.props.job.prompt;
@@ -48,6 +63,7 @@ export class PromptComponent extends React.Component<Props, State> implements Ke
4863
suggestions: [],
4964
highlightedSuggestionIndex: 0,
5065
latestKeyCode: undefined,
66+
isSticky: false,
5167
};
5268

5369
const keyDownSubject: Subject<KeyboardEvent> = new Subject();
@@ -137,9 +153,16 @@ export class PromptComponent extends React.Component<Props, State> implements Ke
137153
}
138154
});
139155

156+
this.intersectionObserver.observe(this.placeholderNode);
157+
140158
this.setDOMValueProgrammatically(this.prompt.value);
141159
}
142160

161+
componentWillUnmount() {
162+
this.intersectionObserver.unobserve(this.placeholderNode);
163+
this.intersectionObserver.disconnect();
164+
}
165+
143166
componentDidUpdate(prevProps: Props, prevState: State) {
144167
if (this.props.status !== e.Status.NotStarted) {
145168
return;
@@ -186,35 +209,38 @@ export class PromptComponent extends React.Component<Props, State> implements Ke
186209
decorationToggle = <DecorationToggleComponent decorateToggler={this.props.decorateToggler}/>;
187210
}
188211

189-
if (this.props.status !== e.Status.NotStarted && this.props.job.screenBuffer.size > 100) {
212+
if (this.state.isSticky) {
190213
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}}/>;
193217
}
194218

195219
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>
218244
</div>
219245
</div>
220246
);
@@ -233,6 +259,11 @@ export class PromptComponent extends React.Component<Props, State> implements Ke
233259
return this.refs["command"] as HTMLInputElement;
234260
}
235261

262+
private get placeholderNode(): Element {
263+
/* tslint:disable:no-string-literal */
264+
return this.refs["placeholder"] as Element;
265+
}
266+
236267
private setDOMValueProgrammatically(text: string): void {
237268
this.commandNode.innerText = text;
238269
setCaretPosition(this.commandNode, text.length);

src/views/css/functions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ export function darken(color: string, percent: number) {
1111
export function failurize(color: string) {
1212
return tinyColor(color).spin(140).saturate(20).toHexString();
1313
}
14+
15+
export function toDOMString(pixels: number) {
16+
return `${pixels}px`;
17+
}

src/views/css/main.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {darken, lighten, failurize} from "./functions";
55
import {Attributes} from "../../Interfaces";
66
import {suggestionsLimit} from "../../Autocompletion";
77

8+
export {toDOMString} from "./functions";
9+
810
export interface CSSObject {
911
pointerEvents?: string;
1012
marginTop?: number;
@@ -57,7 +59,7 @@ export const outputPadding = 10;
5759
const promptVerticalPadding = 5;
5860
const promptHorizontalPadding = 10;
5961
const promptHeight = 12 + (2 * promptVerticalPadding);
60-
const promptWrapperHeight = promptHeight + promptVerticalPadding;
62+
export const promptWrapperHeight = promptHeight + promptVerticalPadding;
6163
const promptBackgroundColor = lighten(colors.black, 5);
6264
const suggestionSize = 2 * fontSize;
6365
const defaultShadow = "0 2px 8px 1px rgba(0, 0, 0, 0.3)";
@@ -85,6 +87,7 @@ const icon = {
8587
};
8688

8789
const outputCutHeight = fontSize * 2.6;
90+
const outputCutZIndex = 0;
8891

8992
const decorationWidth = 30;
9093
const arrowZIndex = 2;
@@ -382,6 +385,7 @@ export const outputCut = (status: Status, isHovered: boolean) => Object.assign(
382385
paddingTop: (outputCutHeight - fontSize) / 3,
383386
color: lighten(backgroundColor, isHovered ? 35 : 30),
384387
cursor: "pointer",
388+
zIndex: outputCutZIndex,
385389
}
386390
);
387391

@@ -494,7 +498,11 @@ export const promptInfo = (status: Status) => {
494498
return styles;
495499
};
496500

497-
export const promptWrapper = (status: Status) => {
501+
export const promptPlaceholder = {
502+
height: promptWrapperHeight,
503+
};
504+
505+
export const promptWrapper = (status: Status, isSticky: boolean) => {
498506
const styles: CSSObject = {
499507
top: 0,
500508
paddingTop: promptVerticalPadding,
@@ -505,8 +513,16 @@ export const promptWrapper = (status: Status) => {
505513
gridTemplateColumns: `${decorationWidth}px 1fr 150px`,
506514
backgroundColor: promptBackgroundColor,
507515
minHeight: promptWrapperHeight,
516+
zIndex: outputCutZIndex + 1,
508517
};
509518

519+
if (isSticky) {
520+
styles.boxShadow = "0 5px 8px -3px rgba(0, 0, 0, 0.3)";
521+
styles.width = "100%";
522+
styles.position = "fixed";
523+
styles.top = titleBarHeight;
524+
}
525+
510526
if ([Status.Failure, Status.Interrupted].includes(status)) {
511527
styles.backgroundColor = failurize(promptBackgroundColor);
512528
}

typings/Overrides.d.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,41 @@ declare class Notification {
77
constructor(title: string, options: { body: string });
88
}
99

10+
interface IntersectionObserverEntry {
11+
readonly time: number;
12+
readonly rootBounds: any;
13+
readonly boundingClientRect: ClientRect;
14+
readonly intersectionRect: ClientRect;
15+
readonly intersectionRatio: number;
16+
readonly target: Element;
17+
}
18+
19+
interface IntersectionObserverInit {
20+
// The root to use for intersection. If not provided, use the top-level document’s viewport.
21+
root?: Element;
22+
// Same as margin, can be 1, 2, 3 or 4 components, possibly negative lengths. If an explicit
23+
// root element is specified, components may be percentages of the root element size. If no
24+
// explicit root element is specified, using a percentage here is an error.
25+
// "5px"
26+
// "10% 20%"
27+
// "-10px 5px 5px"
28+
// "-10px -10px 5px 5px"
29+
rootMargin?: string;
30+
// Threshold(s) at which to trigger callback, specified as a ratio, or list of ratios,
31+
// of (visible area / total area) of the observed element (hence all entries must be
32+
// in the range [0, 1]). Callback will be invoked when the visible ratio of the observed
33+
// element crosses a threshold in the list.
34+
threshold?: number | number[];
35+
}
36+
37+
declare class IntersectionObserver {
38+
constructor(handler: (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => void, options?: IntersectionObserverInit);
39+
observe(target: Element): void;
40+
unobserve(target: Element): void;
41+
disconnect(): void;
42+
takeRecords(): IntersectionObserverEntry[];
43+
}
44+
1045
interface Window {
1146
DEBUG: boolean;
1247
jobUnderAttention: KeyDownReceiver;

0 commit comments

Comments
 (0)