mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-20 01:11:25 +00:00
Initial Move from Azure DevOps to GitHub
This commit is contained in:
115
src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx
Normal file
115
src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { createGlobalStyle } from "styled-components";
|
||||
|
||||
const AzureTheme = createGlobalStyle`
|
||||
:root {
|
||||
/* --theme-primary-bg-hover: #0078d4;
|
||||
--theme-primary-bg-focus: #0078d4;
|
||||
--theme-primary-shadow-hover: #0078d4; */
|
||||
|
||||
--theme-app-bg: white;
|
||||
--theme-app-fg: var(--nt-color-midnight);
|
||||
--theme-app-border: var(--nt-color-grey-light);
|
||||
|
||||
--theme-primary-bg: var(--nt-color-grey-lightest);
|
||||
--theme-primary-bg-hover: var(--nt-color-grey-lighter);
|
||||
--theme-primary-bg-focus: var(--nt-color-grey-light);
|
||||
|
||||
--theme-primary-fg: var(--nt-color-midnight-light);
|
||||
--theme-primary-fg-hover: var(--nt-color-midnight);
|
||||
--theme-primary-fg-focus: var(--theme-app-fg);
|
||||
|
||||
--theme-secondary-bg: var(--theme-primary-bg);
|
||||
--theme-secondary-bg-hover: var(--theme-primary-bg-hover);
|
||||
--theme-secondary-bg-focus: var(--theme-primary-bg-focus);
|
||||
|
||||
--theme-secondary-fg: var(--nt-color-midnight-lighter);
|
||||
--theme-secondary-fg-hover: var(--nt-color-midnight-light);
|
||||
--theme-secondary-fg-focus: var(--theme-primary-fg);
|
||||
|
||||
/* --theme-primary-shadow-hover: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--theme-primary-shadow-focus: 0px 2px 4px rgba(0, 0, 0, 0.1); */
|
||||
|
||||
--theme-title-bar-bg: var(--theme-primary-bg-hover);
|
||||
|
||||
--theme-menu-bg: var(--theme-primary-bg);
|
||||
--theme-menu-bg-hover: var(--theme-primary-bg-hover);
|
||||
--theme-menu-bg-focus: var(--theme-primary-bg-focus);
|
||||
/* --theme-menu-shadow: var(--theme-primary-shadow-hover); */
|
||||
|
||||
--theme-menu-fg: var(--theme-app-fg);
|
||||
--theme-menu-fg-hover: var(--theme-app-fg);
|
||||
--theme-menu-fg-focus: var(--theme-app-fg);
|
||||
|
||||
--theme-cell-bg: var(--theme-app-bg);
|
||||
/* --theme-cell-shadow-hover: var(--theme-primary-shadow-hover); */
|
||||
/* --theme-cell-shadow-focus: var(--theme-primary-shadow-focus); */
|
||||
|
||||
--theme-cell-prompt-bg: var(--theme-primary-bg);
|
||||
--theme-cell-prompt-bg-hover: var(--theme-primary-bg-hover);
|
||||
--theme-cell-prompt-bg-focus: var(--theme-primary-bg-focus);
|
||||
|
||||
--theme-cell-prompt-fg: var(--theme-secondary-fg);
|
||||
--theme-cell-prompt-fg-hover: var(--theme-secondary-fg-hover);
|
||||
--theme-cell-prompt-fg-focus: var(--theme-secondary-fg-focus);
|
||||
|
||||
--theme-cell-toolbar-bg: var(--theme-primary-bg);
|
||||
--theme-cell-toolbar-bg-hover: var(--theme-primary-bg-hover);
|
||||
--theme-cell-toolbar-bg-focus: var(--theme-primary-bg-focus);
|
||||
|
||||
--theme-cell-toolbar-fg: var(--theme-secondary-fg);
|
||||
--theme-cell-toolbar-fg-hover: var(--theme-secondary-fg-hover);
|
||||
--theme-cell-toolbar-fg-focus: var(--theme-secondary-fg-focus);
|
||||
|
||||
--theme-cell-menu-bg: var(--theme-primary-bg);
|
||||
--theme-cell-menu-bg-hover: var(--theme-primary-bg-hover);
|
||||
--theme-cell-menu-bg-focus: var(--theme-primary-bg-focus);
|
||||
|
||||
--theme-cell-menu-fg: var(--theme-primary-fg);
|
||||
--theme-cell-menu-fg-hover: var(--theme-primary-fg-hover);
|
||||
--theme-cell-menu-fg-focus: var(--theme-primary-fg-focus);
|
||||
|
||||
--theme-cell-input-bg: var(--theme-secondary-bg);
|
||||
--theme-cell-input-fg: var(--theme-app-fg);
|
||||
|
||||
--theme-cell-output-bg: var(--theme-app-bg);
|
||||
--theme-cell-output-fg: var(--theme-primary-fg);
|
||||
|
||||
--theme-cell-creator-bg: var(--theme-app-bg);
|
||||
|
||||
--theme-cell-creator-fg: var(--theme-secondary-fg);
|
||||
--theme-cell-creator-fg-hover: var(--theme-secondary-fg-hover);
|
||||
--theme-cell-creator-fg-focus: var(--theme-secondary-fg-focus);
|
||||
|
||||
--theme-pager-bg: #fafafa;
|
||||
|
||||
--cm-background: #fafafa;
|
||||
--cm-color: black;
|
||||
|
||||
--cm-gutter-bg: white;
|
||||
|
||||
--cm-comment: #a86;
|
||||
--cm-keyword: blue;
|
||||
--cm-string: #a22;
|
||||
--cm-builtin: #077;
|
||||
--cm-special: #0aa;
|
||||
--cm-variable: black;
|
||||
--cm-number: #3a3;
|
||||
--cm-meta: #555;
|
||||
--cm-link: #3a3;
|
||||
--cm-operator: black;
|
||||
--cm-def: black;
|
||||
|
||||
--cm-activeline-bg: #e8f2ff;
|
||||
--cm-matchingbracket-outline: grey;
|
||||
--cm-matchingbracket-color: black;
|
||||
|
||||
--cm-hint-color: var(--cm-color);
|
||||
--cm-hint-color-active: var(--cm-color);
|
||||
--cm-hint-bg: var(--theme-app-bg);
|
||||
--cm-hint-bg-active: #abd1ff;
|
||||
|
||||
--status-bar: #eeedee;
|
||||
}
|
||||
`;
|
||||
|
||||
export { AzureTheme };
|
||||
@@ -0,0 +1,56 @@
|
||||
.NotebookReadOnlyRender {
|
||||
.nteract-cell-container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.nteract-cell {
|
||||
padding: 0.5px;
|
||||
border: 1px solid #ffffff;
|
||||
border-left: 3px solid #ffffff;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.CodeMirror-lines {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.nteract-cell:hover {
|
||||
border: 1px solid #0078d4;
|
||||
border-left: 3px solid #0078d4;
|
||||
|
||||
.CodeMirror-scroll {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs {
|
||||
border-top: 1px solid #d7d7d7;
|
||||
}
|
||||
|
||||
.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-cell-outputs {
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ffffff;
|
||||
|
||||
pre {
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-md-cell {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.nteract-cell:hover.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import * as React from "react";
|
||||
import "./base.css";
|
||||
import "./default.css";
|
||||
|
||||
import { CodeCell, RawCell, Cells, MarkdownCell } from "@nteract/stateful-components";
|
||||
import { AzureTheme } from "./AzureTheme";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import { actions, ContentRef } from "@nteract/core";
|
||||
import loadTransform from "../NotebookComponent/loadTransform";
|
||||
import CodeMirrorEditor from "@nteract/editor";
|
||||
import "./NotebookReadOnlyRenderer.less";
|
||||
|
||||
export interface NotebookRendererProps {
|
||||
contentRef: any;
|
||||
}
|
||||
|
||||
interface PassedEditorProps {
|
||||
id: string;
|
||||
contentRef: ContentRef;
|
||||
editorFocused: boolean;
|
||||
value: string;
|
||||
channels: any;
|
||||
kernelStatus: string;
|
||||
theme: string;
|
||||
onChange: (text: string) => void;
|
||||
onFocusChange: (focused: boolean) => void;
|
||||
className: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the class that uses nteract to render a read-only notebook.
|
||||
*/
|
||||
class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
||||
componentDidMount() {
|
||||
loadTransform(this.props as any);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<div className="NotebookReadOnlyRender">
|
||||
<Cells contentRef={this.props.contentRef}>
|
||||
{{
|
||||
code: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
|
||||
<CodeCell id={id} contentRef={contentRef}>
|
||||
{{
|
||||
editor: {
|
||||
codemirror: (props: PassedEditorProps) => <CodeMirrorEditor {...props} readOnly={"nocursor"} />
|
||||
},
|
||||
prompt: ({ id, contentRef }) => <></>
|
||||
}}
|
||||
</CodeCell>
|
||||
),
|
||||
markdown: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
|
||||
<MarkdownCell id={id} contentRef={contentRef} cell_type="markdown">
|
||||
{{
|
||||
editor: {}
|
||||
}}
|
||||
</MarkdownCell>
|
||||
),
|
||||
raw: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
|
||||
<RawCell id={id} contentRef={contentRef} cell_type="raw">
|
||||
{{
|
||||
editor: {
|
||||
codemirror: (props: PassedEditorProps) => <CodeMirrorEditor {...props} readOnly={"nocursor"} />
|
||||
}
|
||||
}}
|
||||
</RawCell>
|
||||
)
|
||||
}}
|
||||
</Cells>
|
||||
<AzureTheme />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererProps) => {
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => {
|
||||
return {
|
||||
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
|
||||
return dispatch(
|
||||
actions.addTransform({
|
||||
mediaType: transform.MIMETYPE,
|
||||
component: transform
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
return mapDispatchToProps;
|
||||
};
|
||||
|
||||
export default connect(null, makeMapDispatchToProps)(NotebookReadOnlyRenderer);
|
||||
108
src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.less
Normal file
108
src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.less
Normal file
@@ -0,0 +1,108 @@
|
||||
// CommandBar
|
||||
@HoverColor: #d7d7d7;
|
||||
@HighlightColor: #0078d4;
|
||||
|
||||
.NotebookRendererContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.NotebookRenderer {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
|
||||
.nteract-cells {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.nteract-cell-container {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.nteract-cell {
|
||||
padding: 0.5px;
|
||||
border: 1px solid #ffffff;
|
||||
border-left: 3px solid #ffffff;
|
||||
|
||||
.CellContextMenuButton {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
margin: 0px 0px 0px -100%;
|
||||
float: right;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-scroll, .CodeMirror-linenumber, .CodeMirror-gutters {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.nteract-cell:hover {
|
||||
border: 1px solid @HoverColor;
|
||||
border-left: 3px solid @HoverColor;
|
||||
|
||||
.CellContextMenuButton {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-cell-container.selected {
|
||||
.nteract-cell {
|
||||
border: 1px solid @HighlightColor;
|
||||
border-left: 3px solid @HighlightColor;
|
||||
}
|
||||
}
|
||||
|
||||
// White background when hovered or selected
|
||||
.nteract-cell:hover, .nteract-cell-container.selected .nteract-cell {
|
||||
.CodeMirror-scroll, .CodeMirror-linenumber, .CodeMirror-gutters {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
color: #015CDA;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs {
|
||||
border-top: 1px solid @HoverColor;
|
||||
}
|
||||
|
||||
.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-cell-outputs {
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ffffff;
|
||||
|
||||
pre {
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-md-cell {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.nteract-cell:hover.nteract-md-cell {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.nteract-md-cell .ntreact-cell-source {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Undo tree.less
|
||||
.expanded::before {
|
||||
content: '';
|
||||
}
|
||||
165
src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx
Normal file
165
src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import * as React from "react";
|
||||
import "./base.css";
|
||||
import "./default.css";
|
||||
|
||||
import { RawCell, Cells, CodeCell, MarkdownCell } from "@nteract/stateful-components";
|
||||
import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror";
|
||||
|
||||
import Prompt from "./Prompt";
|
||||
import { promptContent } from "./PromptContent";
|
||||
|
||||
import { AzureTheme } from "./AzureTheme";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import HTML5Backend from "react-dnd-html5-backend";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import { actions, ContentRef } from "@nteract/core";
|
||||
import { CellId } from "@nteract/commutable";
|
||||
import loadTransform from "../NotebookComponent/loadTransform";
|
||||
import DraggableCell from "./decorators/draggable";
|
||||
import CellCreator from "./decorators/CellCreator";
|
||||
import KeyboardShortcuts from "./decorators/kbd-shortcuts";
|
||||
|
||||
import CellToolbar from "./Toolbar";
|
||||
import StatusBar from "./StatusBar";
|
||||
|
||||
import HijackScroll from "./decorators/hijack-scroll";
|
||||
import { CellType } from "@nteract/commutable/src";
|
||||
|
||||
import "./NotebookRenderer.less";
|
||||
import HoverableCell from "./decorators/HoverableCell";
|
||||
import CellLabeler from "./decorators/CellLabeler";
|
||||
|
||||
export interface NotebookRendererProps {
|
||||
contentRef: any;
|
||||
}
|
||||
|
||||
interface PassedEditorProps {
|
||||
id: string;
|
||||
contentRef: ContentRef;
|
||||
editorFocused: boolean;
|
||||
value: string;
|
||||
channels: any;
|
||||
kernelStatus: string;
|
||||
theme: string;
|
||||
onChange: (text: string) => void;
|
||||
onFocusChange: (focused: boolean) => void;
|
||||
className: string;
|
||||
}
|
||||
|
||||
const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, children: React.ReactNode) => {
|
||||
const Cell = () => (
|
||||
<DraggableCell id={id} contentRef={contentRef}>
|
||||
<HijackScroll id={id} contentRef={contentRef}>
|
||||
<CellCreator id={id} contentRef={contentRef}>
|
||||
<CellLabeler id={id} contentRef={contentRef}>
|
||||
<HoverableCell id={id} contentRef={contentRef}>
|
||||
{children}
|
||||
</HoverableCell>
|
||||
</CellLabeler>
|
||||
</CellCreator>
|
||||
</HijackScroll>
|
||||
</DraggableCell>
|
||||
);
|
||||
|
||||
Cell.defaultProps = { cell_type };
|
||||
return <Cell />;
|
||||
};
|
||||
|
||||
class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
||||
constructor(props: NotebookRendererProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hoveredCellId: undefined
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
loadTransform(this.props as any);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<div className="NotebookRendererContainer">
|
||||
<div className="NotebookRenderer">
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<KeyboardShortcuts contentRef={this.props.contentRef}>
|
||||
<Cells contentRef={this.props.contentRef}>
|
||||
{{
|
||||
code: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) =>
|
||||
decorate(
|
||||
id,
|
||||
contentRef,
|
||||
"code",
|
||||
<CodeCell id={id} contentRef={contentRef} cell_type="code">
|
||||
{{
|
||||
editor: {
|
||||
codemirror: (props: PassedEditorProps) => (
|
||||
<CodeMirrorEditor {...props} lineNumbers={true} />
|
||||
)
|
||||
},
|
||||
prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => (
|
||||
<Prompt id={id} contentRef={contentRef} isHovered={false}>
|
||||
{promptContent}
|
||||
</Prompt>
|
||||
),
|
||||
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />
|
||||
}}
|
||||
</CodeCell>
|
||||
),
|
||||
markdown: ({ id, contentRef }: { id: any; contentRef: ContentRef }) =>
|
||||
decorate(
|
||||
id,
|
||||
contentRef,
|
||||
"markdown",
|
||||
<MarkdownCell id={id} contentRef={contentRef} cell_type="markdown">
|
||||
{{
|
||||
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />
|
||||
}}
|
||||
</MarkdownCell>
|
||||
),
|
||||
|
||||
raw: ({ id, contentRef }: { id: any; contentRef: ContentRef }) =>
|
||||
decorate(
|
||||
id,
|
||||
contentRef,
|
||||
"raw",
|
||||
<RawCell id={id} contentRef={contentRef} cell_type="raw">
|
||||
{{
|
||||
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />
|
||||
}}
|
||||
</RawCell>
|
||||
)
|
||||
}}
|
||||
</Cells>
|
||||
</KeyboardShortcuts>
|
||||
<AzureTheme />
|
||||
</DndProvider>
|
||||
</div>
|
||||
<StatusBar contentRef={this.props.contentRef} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererProps) => {
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => {
|
||||
return {
|
||||
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
|
||||
return dispatch(
|
||||
actions.addTransform({
|
||||
mediaType: transform.MIMETYPE,
|
||||
component: transform
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
return mapDispatchToProps;
|
||||
};
|
||||
|
||||
export default connect(null, makeMapDispatchToProps)(BaseNotebookRenderer);
|
||||
30
src/Explorer/Notebook/NotebookRenderer/Prompt.less
Normal file
30
src/Explorer/Notebook/NotebookRenderer/Prompt.less
Normal file
@@ -0,0 +1,30 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.runCellButton {
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
z-index: 300;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
.ms-Button-flexContainer {
|
||||
align-items: start;
|
||||
padding-top: 11px;
|
||||
|
||||
.ms-Button-icon {
|
||||
color: #0078D4;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.greyStopButton {
|
||||
.runCellButton .ms-Button-flexContainer .ms-Button-icon {
|
||||
color: @BaseMediumHigh;
|
||||
}
|
||||
|
||||
.ms-Spinner .ms-Spinner-circle {
|
||||
border-top-color: @BaseMediumHigh;
|
||||
}
|
||||
}
|
||||
90
src/Explorer/Notebook/NotebookRenderer/Prompt.tsx
Normal file
90
src/Explorer/Notebook/NotebookRenderer/Prompt.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
|
||||
import { ContentRef, selectors, actions } from "@nteract/core";
|
||||
import { CdbAppState } from "../NotebookComponent/types";
|
||||
|
||||
export interface PassedPromptProps {
|
||||
id: string;
|
||||
contentRef: ContentRef;
|
||||
status?: string;
|
||||
executionCount?: number;
|
||||
isHovered?: boolean;
|
||||
runCell?: () => void;
|
||||
stopCell?: () => void;
|
||||
}
|
||||
|
||||
interface ComponentProps {
|
||||
id: string;
|
||||
contentRef: ContentRef;
|
||||
isHovered?: boolean;
|
||||
children: (props: PassedPromptProps) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
status?: string;
|
||||
executionCount?: number;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
executeCell: () => void;
|
||||
stopExecution: () => void;
|
||||
}
|
||||
|
||||
type Props = StateProps & DispatchProps & ComponentProps;
|
||||
|
||||
export class PromptPure extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<div className="nteract-cell-prompt">
|
||||
{this.props.children({
|
||||
id: this.props.id,
|
||||
contentRef: this.props.contentRef,
|
||||
status: this.props.status,
|
||||
executionCount: this.props.executionCount,
|
||||
runCell: this.props.executeCell,
|
||||
stopCell: this.props.stopExecution,
|
||||
isHovered: this.props.isHovered
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const makeMapStateToProps = (state: CdbAppState, ownProps: ComponentProps): ((state: CdbAppState) => StateProps) => {
|
||||
const mapStateToProps = (state: CdbAppState) => {
|
||||
const { contentRef, id } = ownProps;
|
||||
const model = selectors.model(state, { contentRef });
|
||||
|
||||
let status;
|
||||
let executionCount;
|
||||
|
||||
if (model && model.type === "notebook") {
|
||||
status = model.transient.getIn(["cellMap", id, "status"]);
|
||||
const cell = selectors.notebook.cellById(model, { id });
|
||||
if (cell) {
|
||||
executionCount = cell.get("execution_count", undefined);
|
||||
}
|
||||
}
|
||||
|
||||
const isHovered = state.cdb.hoveredCellId === id;
|
||||
|
||||
return {
|
||||
status,
|
||||
executionCount,
|
||||
isHovered
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: Dispatch,
|
||||
{ id, contentRef }: { id: string; contentRef: ContentRef }
|
||||
): DispatchProps => ({
|
||||
executeCell: () => dispatch(actions.executeCell({ id, contentRef })),
|
||||
stopExecution: () => dispatch(actions.interruptKernel({}))
|
||||
});
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(PromptPure);
|
||||
56
src/Explorer/Notebook/NotebookRenderer/PromptContent.tsx
Normal file
56
src/Explorer/Notebook/NotebookRenderer/PromptContent.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from "react";
|
||||
import { IconButton } from "office-ui-fabric-react/lib/Button";
|
||||
import { Spinner, SpinnerSize } from "office-ui-fabric-react/lib/Spinner";
|
||||
import "./Prompt.less";
|
||||
import { PassedPromptProps } from "./Prompt";
|
||||
|
||||
export const promptContent = (props: PassedPromptProps): JSX.Element => {
|
||||
if (props.status === "busy") {
|
||||
const stopButtonText: string = "Stop cell execution";
|
||||
return (
|
||||
<div
|
||||
style={{ position: "sticky", width: "100%", maxHeight: "100%", left: 0, top: 0, zIndex: 300 }}
|
||||
className={props.isHovered ? "" : "greyStopButton"}
|
||||
>
|
||||
<IconButton
|
||||
className="runCellButton"
|
||||
iconProps={{ iconName: "CircleStopSolid" }}
|
||||
title={stopButtonText}
|
||||
ariaLabel={stopButtonText}
|
||||
onClick={props.stopCell}
|
||||
style={{ position: "absolute" }}
|
||||
/>
|
||||
<Spinner size={SpinnerSize.large} style={{ position: "absolute", width: "100%", paddingTop: 5 }} />
|
||||
</div>
|
||||
);
|
||||
} else if (props.isHovered) {
|
||||
const playButtonText: string = "Run cell";
|
||||
return (
|
||||
<IconButton
|
||||
className="runCellButton"
|
||||
iconProps={{ iconName: "MSNVideosSolid" }}
|
||||
title={playButtonText}
|
||||
ariaLabel={playButtonText}
|
||||
onClick={props.runCell}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <div style={{ paddingTop: 7 }}>{promptText(props)}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate what text goes inside the prompt based on the props to the prompt
|
||||
*/
|
||||
const promptText = (props: PassedPromptProps): string => {
|
||||
if (props.status === "busy") {
|
||||
return "[*]";
|
||||
}
|
||||
if (props.status === "queued") {
|
||||
return "[…]";
|
||||
}
|
||||
if (typeof props.executionCount === "number") {
|
||||
return `[${props.executionCount}]`;
|
||||
}
|
||||
return "[ ]";
|
||||
};
|
||||
55
src/Explorer/Notebook/NotebookRenderer/StatusBar.test.tsx
Normal file
55
src/Explorer/Notebook/NotebookRenderer/StatusBar.test.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { StatusBar } from "./StatusBar";
|
||||
|
||||
describe("StatusBar", () => {
|
||||
test("can render on a dummyNotebook", () => {
|
||||
const lastSaved = new Date();
|
||||
const kernelSpecDisplayName = "python3";
|
||||
|
||||
const component = shallow(
|
||||
<StatusBar kernelStatus="kernel status" lastSaved={lastSaved} kernelSpecDisplayName={kernelSpecDisplayName} />
|
||||
);
|
||||
|
||||
expect(component).not.toBeNull();
|
||||
});
|
||||
test("Update if kernelSpecDisplayName has changed", () => {
|
||||
const lastSaved = new Date();
|
||||
const kernelSpecDisplayName = "python3";
|
||||
|
||||
const component = shallow(
|
||||
<StatusBar kernelStatus="kernel status" lastSaved={lastSaved} kernelSpecDisplayName={kernelSpecDisplayName} />
|
||||
);
|
||||
|
||||
const shouldUpdate = component.instance().shouldComponentUpdate(
|
||||
{
|
||||
lastSaved,
|
||||
kernelSpecDisplayName: "javascript",
|
||||
kernelStatus: "kernelStatus"
|
||||
},
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(shouldUpdate).toBe(true);
|
||||
});
|
||||
test("update if kernelStatus has changed", () => {
|
||||
const lastSaved = new Date();
|
||||
const kernelSpecDisplayName = "python3";
|
||||
|
||||
const component = shallow(
|
||||
<StatusBar kernelStatus="kernel status" lastSaved={lastSaved} kernelSpecDisplayName={kernelSpecDisplayName} />
|
||||
);
|
||||
|
||||
const shouldUpdate = component.instance().shouldComponentUpdate(
|
||||
{
|
||||
lastSaved: new Date(),
|
||||
kernelSpecDisplayName: "python3",
|
||||
kernelStatus: "kernelStatus"
|
||||
},
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(shouldUpdate).toBe(true);
|
||||
});
|
||||
});
|
||||
128
src/Explorer/Notebook/NotebookRenderer/StatusBar.tsx
Normal file
128
src/Explorer/Notebook/NotebookRenderer/StatusBar.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { AppState, ContentRef, selectors } from "@nteract/core";
|
||||
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
|
||||
interface Props {
|
||||
lastSaved?: Date | null;
|
||||
kernelSpecDisplayName: string;
|
||||
kernelStatus: string;
|
||||
}
|
||||
|
||||
const NOT_CONNECTED = "not connected";
|
||||
|
||||
import styled from "styled-components";
|
||||
|
||||
export const LeftStatus = styled.div`
|
||||
float: left;
|
||||
display: block;
|
||||
padding-left: 10px;
|
||||
`;
|
||||
export const RightStatus = styled.div`
|
||||
float: right;
|
||||
padding-right: 10px;
|
||||
display: block;
|
||||
`;
|
||||
|
||||
export const Bar = styled.div`
|
||||
padding: 8px 0px 2px;
|
||||
border-top: 1px solid ${StyleConstants.BaseMedium};
|
||||
border-left: 1px solid ${StyleConstants.BaseMedium};
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 0.5em;
|
||||
background: var(--status-bar);
|
||||
z-index: 99;
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const BarContainer = styled.div`
|
||||
padding-left: 4px;
|
||||
`;
|
||||
|
||||
export class StatusBar extends React.Component<Props> {
|
||||
shouldComponentUpdate(nextProps: Props): boolean {
|
||||
if (this.props.lastSaved !== nextProps.lastSaved || this.props.kernelStatus !== nextProps.kernelStatus) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const name = this.props.kernelSpecDisplayName || "Loading...";
|
||||
|
||||
return (
|
||||
<BarContainer>
|
||||
<Bar data-test="notebookStatusBar">
|
||||
<RightStatus>
|
||||
{this.props.lastSaved ? (
|
||||
<p data-test="saveStatus"> Last saved {distanceInWordsToNow(this.props.lastSaved)} </p>
|
||||
) : (
|
||||
<p> Not saved yet </p>
|
||||
)}
|
||||
</RightStatus>
|
||||
<LeftStatus>
|
||||
<p data-test="kernelStatus">
|
||||
{name} | {this.props.kernelStatus}
|
||||
</p>
|
||||
</LeftStatus>
|
||||
</Bar>
|
||||
</BarContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface InitialProps {
|
||||
contentRef: ContentRef;
|
||||
}
|
||||
|
||||
const makeMapStateToProps = (initialState: AppState, initialProps: InitialProps): ((state: AppState) => Props) => {
|
||||
const { contentRef } = initialProps;
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const content = selectors.content(state, { contentRef });
|
||||
|
||||
if (!content || content.type !== "notebook") {
|
||||
return {
|
||||
kernelStatus: NOT_CONNECTED,
|
||||
kernelSpecDisplayName: "no kernel",
|
||||
lastSaved: null
|
||||
};
|
||||
}
|
||||
|
||||
const kernelRef = content.model.kernelRef;
|
||||
let kernel = null;
|
||||
if (kernelRef) {
|
||||
kernel = selectors.kernel(state, { kernelRef });
|
||||
}
|
||||
|
||||
const lastSaved = content && content.lastSaved ? content.lastSaved : null;
|
||||
|
||||
const kernelStatus = kernel != null && kernel.status != null ? kernel.status : NOT_CONNECTED;
|
||||
|
||||
// TODO: We need kernels associated to the kernelspec they came from
|
||||
// so we can pluck off the display_name and provide it here
|
||||
let kernelSpecDisplayName = " ";
|
||||
if (kernelStatus === NOT_CONNECTED) {
|
||||
kernelSpecDisplayName = "no kernel";
|
||||
} else if (kernel != null && kernel.kernelSpecName != null) {
|
||||
kernelSpecDisplayName = kernel.kernelSpecName;
|
||||
} else if (content && content.type === "notebook") {
|
||||
kernelSpecDisplayName = selectors.notebook.displayName(content.model) || " ";
|
||||
}
|
||||
|
||||
return {
|
||||
kernelSpecDisplayName,
|
||||
kernelStatus,
|
||||
lastSaved
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export default connect(makeMapStateToProps)(StatusBar);
|
||||
181
src/Explorer/Notebook/NotebookRenderer/Toolbar.tsx
Normal file
181
src/Explorer/Notebook/NotebookRenderer/Toolbar.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { ContentRef } from "@nteract/types";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
|
||||
import { IconButton } from "office-ui-fabric-react/lib/Button";
|
||||
import {
|
||||
DirectionalHint,
|
||||
IContextualMenuItem,
|
||||
ContextualMenuItemType
|
||||
} from "office-ui-fabric-react/lib/ContextualMenu";
|
||||
import { actions, AppState, DocumentRecordProps } from "@nteract/core";
|
||||
import { CellToolbarContext } from "@nteract/stateful-components";
|
||||
import { CellType, CellId } from "@nteract/commutable";
|
||||
import * as selectors from "@nteract/selectors";
|
||||
import { RecordOf } from "immutable";
|
||||
|
||||
export interface ComponentProps {
|
||||
contentRef: ContentRef;
|
||||
id: CellId;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
executeCell: () => void;
|
||||
insertCodeCellAbove: () => void;
|
||||
insertCodeCellBelow: () => void;
|
||||
insertTextCellAbove: () => void;
|
||||
insertTextCellBelow: () => void;
|
||||
moveCell: (destinationId: CellId, above: boolean) => void;
|
||||
clearOutputs: () => void;
|
||||
deleteCell: () => void;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
cellType: CellType;
|
||||
cellIdAbove: CellId;
|
||||
cellIdBelow: CellId;
|
||||
}
|
||||
|
||||
class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & StateProps> {
|
||||
static contextType = CellToolbarContext;
|
||||
|
||||
render(): JSX.Element {
|
||||
let items: IContextualMenuItem[] = [];
|
||||
|
||||
if (this.props.cellType === "code") {
|
||||
items = items.concat([
|
||||
{
|
||||
key: "Run",
|
||||
text: "Run",
|
||||
onClick: this.props.executeCell
|
||||
},
|
||||
{
|
||||
key: "Clear Outputs",
|
||||
text: "Clear Outputs",
|
||||
onClick: this.props.clearOutputs
|
||||
},
|
||||
{
|
||||
key: "Divider",
|
||||
itemType: ContextualMenuItemType.Divider
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
items = items.concat([
|
||||
{
|
||||
key: "Divider",
|
||||
itemType: ContextualMenuItemType.Divider
|
||||
},
|
||||
{
|
||||
key: "Insert Code Cell Above",
|
||||
text: "Insert Code Cell Above",
|
||||
onClick: this.props.insertCodeCellAbove
|
||||
},
|
||||
{
|
||||
key: "Insert Code Cell Below",
|
||||
text: "Insert Code Cell Below",
|
||||
onClick: this.props.insertCodeCellBelow
|
||||
},
|
||||
{
|
||||
key: "Insert Text Cell Above",
|
||||
text: "Insert Text Cell Above",
|
||||
onClick: this.props.insertTextCellAbove
|
||||
},
|
||||
{
|
||||
key: "Insert Text Cell Below",
|
||||
text: "Insert Text Cell Below",
|
||||
onClick: this.props.insertTextCellBelow
|
||||
},
|
||||
{
|
||||
key: "Divider",
|
||||
itemType: ContextualMenuItemType.Divider
|
||||
}
|
||||
]);
|
||||
|
||||
const moveItems: IContextualMenuItem[] = [];
|
||||
if (this.props.cellIdAbove !== undefined) {
|
||||
moveItems.push({
|
||||
key: "Move Cell Up",
|
||||
text: "Move Cell Up",
|
||||
onClick: () => this.props.moveCell(this.props.cellIdAbove, true)
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.cellIdBelow !== undefined) {
|
||||
moveItems.push({
|
||||
key: "Move Cell Down",
|
||||
text: "Move Cell Down",
|
||||
onClick: () => this.props.moveCell(this.props.cellIdBelow, false)
|
||||
});
|
||||
}
|
||||
|
||||
if (moveItems.length > 0) {
|
||||
moveItems.push({
|
||||
key: "Divider",
|
||||
itemType: ContextualMenuItemType.Divider
|
||||
});
|
||||
items = items.concat(moveItems);
|
||||
}
|
||||
|
||||
items.push({
|
||||
key: "Delete Cell",
|
||||
text: "Delete Cell",
|
||||
onClick: this.props.deleteCell
|
||||
});
|
||||
|
||||
const menuItemLabel = "More";
|
||||
return (
|
||||
<IconButton
|
||||
name="More"
|
||||
className="CellContextMenuButton"
|
||||
ariaLabel={menuItemLabel}
|
||||
menuIconProps={{
|
||||
iconName: menuItemLabel,
|
||||
styles: { root: { fontSize: "18px", fontWeight: "bold" } }
|
||||
}}
|
||||
menuProps={{
|
||||
isBeakVisible: false,
|
||||
directionalHint: DirectionalHint.bottomRightEdge,
|
||||
items
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: Dispatch,
|
||||
{ id, contentRef }: { id: CellId; contentRef: ContentRef }
|
||||
): DispatchProps => ({
|
||||
executeCell: () => dispatch(actions.executeCell({ id, contentRef })),
|
||||
insertCodeCellAbove: () => dispatch(actions.createCellAbove({ id, contentRef, cellType: "code" })),
|
||||
insertCodeCellBelow: () => dispatch(actions.createCellBelow({ id, contentRef, cellType: "code", source: "" })),
|
||||
insertTextCellAbove: () => dispatch(actions.createCellAbove({ id, contentRef, cellType: "markdown" })),
|
||||
insertTextCellBelow: () => dispatch(actions.createCellBelow({ id, contentRef, cellType: "markdown", source: "" })),
|
||||
moveCell: (destinationId: CellId, above: boolean) =>
|
||||
dispatch(actions.moveCell({ id, contentRef, destinationId, above })),
|
||||
clearOutputs: () => dispatch(actions.clearOutputs({ id, contentRef })),
|
||||
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef }))
|
||||
});
|
||||
|
||||
const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => {
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const cellType = selectors.cell.cellFromState(state, { id: ownProps.id, contentRef: ownProps.contentRef })
|
||||
.cell_type;
|
||||
const model = selectors.model(state, { contentRef: ownProps.contentRef });
|
||||
const cellOrder = selectors.notebook.cellOrder(model as RecordOf<DocumentRecordProps>);
|
||||
const cellIndex = cellOrder.indexOf(ownProps.id);
|
||||
const cellIdAbove = cellIndex ? cellOrder.get(cellIndex - 1, undefined) : undefined;
|
||||
const cellIdBelow = cellIndex !== undefined ? cellOrder.get(cellIndex + 1, undefined) : undefined;
|
||||
|
||||
return {
|
||||
cellType,
|
||||
cellIdAbove,
|
||||
cellIdBelow
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(BaseToolbar);
|
||||
143
src/Explorer/Notebook/NotebookRenderer/base.css
Normal file
143
src/Explorer/Notebook/NotebookRenderer/base.css
Normal file
@@ -0,0 +1,143 @@
|
||||
.nteract-cell-prompt {
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
/* For creating a buffer area for <Prompt blank /> */
|
||||
min-height: 22px;
|
||||
width: var(--prompt-width, 50px);
|
||||
padding: 2px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs {
|
||||
padding: 10px 10px 10px calc(var(--prompt-width, 50px) + 10px);
|
||||
word-wrap: break-word;
|
||||
overflow-y: hidden;
|
||||
outline: none;
|
||||
/* When expanded, this is overtaken to 100% */
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs code {
|
||||
white-space: pre-wrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs pre {
|
||||
white-space: pre-wrap;
|
||||
font-size: 14px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.nteract-cell {
|
||||
position: relative;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.nteract-cells {
|
||||
padding-bottom: 10px;
|
||||
padding: var(--nt-spacing-m, 10px);
|
||||
}
|
||||
|
||||
.nteract-cell-input .nteract-cell-source {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/** Adaptation for the R kernel's inline lists **/
|
||||
.nteract-cell-outputs .list-inline li {
|
||||
display: inline;
|
||||
padding-right: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nteract-cell-input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.nteract-cell-input.invisible {
|
||||
height: 34px;
|
||||
}
|
||||
.nteract-cell-input .nteract-cell-prompt {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* for nested paragraphs in block quotes */
|
||||
.nteract-cell-outputs blockquote p {
|
||||
display: inline;
|
||||
}
|
||||
.nteract-cell-outputs dd {
|
||||
display: block;
|
||||
-webkit-margin-start: 40px;
|
||||
}
|
||||
.nteract-cell-outputs dl {
|
||||
display: block;
|
||||
-webkit-margin-before: 1__qem;
|
||||
-webkit-margin-after: 1em;
|
||||
-webkit-margin-start: 0;
|
||||
-webkit-margin-end: 0;
|
||||
}
|
||||
.nteract-cell-outputs dt {
|
||||
display: block;
|
||||
}
|
||||
.nteract-cell-outputs dl {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.nteract-cell-outputs dt {
|
||||
font-weight: bold;
|
||||
float: left;
|
||||
width: 20%;
|
||||
/* adjust the width; make sure the total of both is 100% */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.nteract-cell-outputs dd {
|
||||
float: left;
|
||||
width: 80%;
|
||||
/* adjust the width; make sure the total of both is 100% */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs kbd {
|
||||
display: inline-block;
|
||||
padding: 0.1em 0.5em;
|
||||
margin: 0 0.2em;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs th,
|
||||
.nteract-cell-outputs td,
|
||||
/* for legacy output handling */
|
||||
.nteract-cell-outputs .th,
|
||||
.nteract-cell-outputs .td {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs blockquote {
|
||||
padding: 0.75em 0.5em 0.75em 1em;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs blockquote::before {
|
||||
display: block;
|
||||
height: 0;
|
||||
margin-left: -0.95em;
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import { actions, selectors, ContentRef, AppState } from "@nteract/core";
|
||||
import { CellType } from "@nteract/commutable";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
|
||||
import styled from "styled-components";
|
||||
import AddCodeCellIcon from "../../../../../images/notebook/add-code-cell.svg";
|
||||
import AddTextCellIcon from "../../../../../images/notebook/add-text-cell.svg";
|
||||
|
||||
interface ComponentProps {
|
||||
id: string;
|
||||
contentRef: ContentRef;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
isFirstCell: boolean;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
createCellAppend: (payload: { cellType: CellType; contentRef: ContentRef }) => void;
|
||||
createCellAbove: (payload: { cellType: CellType; id?: string; contentRef: ContentRef }) => void;
|
||||
createCellBelow: (payload: { cellType: CellType; id?: string; source: string; contentRef: ContentRef }) => void;
|
||||
}
|
||||
|
||||
export const CellCreatorMenu = styled.div`
|
||||
display: none;
|
||||
pointer-events: all;
|
||||
position: relative;
|
||||
top: 0px;
|
||||
/**
|
||||
* Now that the cell-creator is added as a decorator we need
|
||||
* this x-index to ensure that it is always shown on the top
|
||||
* of other cells.
|
||||
*/
|
||||
z-index: 50;
|
||||
|
||||
button:first-child {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-block;
|
||||
|
||||
width: 109px;
|
||||
height: 24px;
|
||||
padding: 0px 4px;
|
||||
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
|
||||
border: 1px solid #0078d4;
|
||||
outline: none;
|
||||
background: var(--theme-cell-creator-bg);
|
||||
color: #0078d4;
|
||||
}
|
||||
|
||||
button span {
|
||||
color: var(--theme-cell-creator-fg);
|
||||
}
|
||||
|
||||
button span:hover {
|
||||
color: var(--theme-cell-creator-fg-hover);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.octicon {
|
||||
transition: color 0.5s;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Divider = styled.div`
|
||||
display: none;
|
||||
position: relative;
|
||||
top: 12px;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
border-top: 1px solid rgba(204, 204, 204, 0.8);
|
||||
`;
|
||||
|
||||
const CreatorHoverMask = styled.div`
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
height: 0px;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
const CreatorHoverRegion = styled.div`
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
top: 5px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
|
||||
&:hover ${CellCreatorMenu} {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&:hover ${Divider} {
|
||||
display: inherit;
|
||||
}
|
||||
`;
|
||||
|
||||
const FirstCreatorContainer = styled.div`
|
||||
height: 20px;
|
||||
`;
|
||||
|
||||
interface CellCreatorProps {
|
||||
above: boolean;
|
||||
createCell: (type: "markdown" | "code", above: boolean) => void;
|
||||
}
|
||||
|
||||
export class PureCellCreator extends React.PureComponent<CellCreatorProps> {
|
||||
createMarkdownCell = () => {
|
||||
this.props.createCell("markdown", this.props.above);
|
||||
};
|
||||
|
||||
createCodeCell = () => {
|
||||
this.props.createCell("code", this.props.above);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CreatorHoverMask>
|
||||
<CreatorHoverRegion>
|
||||
<Divider />
|
||||
<CellCreatorMenu>
|
||||
<button onClick={this.createCodeCell} className="add-code-cell">
|
||||
<span className="octicon">
|
||||
<img src={AddCodeCellIcon} alt="Add code cell" />
|
||||
</span>
|
||||
Add code
|
||||
</button>
|
||||
<button onClick={this.createMarkdownCell} className="add-text-cell">
|
||||
<span className="octicon">
|
||||
<img src={AddTextCellIcon} alt="Add text cell" />
|
||||
</span>
|
||||
Add text
|
||||
</button>
|
||||
</CellCreatorMenu>
|
||||
</CreatorHoverRegion>
|
||||
</CreatorHoverMask>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CellCreator extends React.PureComponent<ComponentProps & DispatchProps & StateProps> {
|
||||
createCell = (type: "code" | "markdown", above: boolean): void => {
|
||||
const { createCellBelow, createCellAppend, createCellAbove, id, contentRef } = this.props;
|
||||
|
||||
if (id === undefined || typeof id !== "string") {
|
||||
createCellAppend({ cellType: type, contentRef });
|
||||
return;
|
||||
}
|
||||
|
||||
above
|
||||
? createCellAbove({ cellType: type, id, contentRef })
|
||||
: createCellBelow({ cellType: type, id, source: "", contentRef });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.props.isFirstCell && (
|
||||
<FirstCreatorContainer>
|
||||
<PureCellCreator above={true} createCell={this.createCell} />
|
||||
</FirstCreatorContainer>
|
||||
)}
|
||||
{this.props.children}
|
||||
<PureCellCreator above={false} createCell={this.createCell} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState, ownProps: ComponentProps) => {
|
||||
const { id, contentRef } = ownProps;
|
||||
const model = selectors.model(state, { contentRef });
|
||||
let isFirstCell = false;
|
||||
|
||||
if (model && model.type === "notebook") {
|
||||
const cellOrder = selectors.notebook.cellOrder(model);
|
||||
const cellIndex = cellOrder.findIndex(cellId => cellId === id);
|
||||
isFirstCell = cellIndex === 0;
|
||||
}
|
||||
|
||||
return {
|
||||
isFirstCell
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
createCellAbove: (payload: { cellType: CellType; id?: string; contentRef: ContentRef }) =>
|
||||
dispatch(actions.createCellAbove(payload)),
|
||||
createCellAppend: (payload: { cellType: CellType; contentRef: ContentRef }) =>
|
||||
dispatch(actions.createCellAppend(payload)),
|
||||
createCellBelow: (payload: { cellType: CellType; id?: string; source: string; contentRef: ContentRef }) =>
|
||||
dispatch(actions.createCellBelow(payload))
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CellCreator);
|
||||
@@ -0,0 +1,8 @@
|
||||
@import "../../../../../less/Common/Constants.less";
|
||||
|
||||
.CellLabeler .CellLabel {
|
||||
margin-left: 5px;
|
||||
font-family: @DataExplorerFont;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import "./CellLabeler.less";
|
||||
|
||||
import { AppState, ContentRef, selectors, DocumentRecordProps } from "@nteract/core";
|
||||
import { RecordOf } from "immutable";
|
||||
|
||||
interface ComponentProps {
|
||||
id: string;
|
||||
contentRef: ContentRef; // TODO: Make this per contentRef?
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
cellIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays "Cell <index>"
|
||||
*/
|
||||
class CellLabeler extends React.Component<ComponentProps & StateProps> {
|
||||
render() {
|
||||
return (
|
||||
<div className="CellLabeler">
|
||||
<div className="CellLabel">Cell {this.props.cellIndex + 1}</div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => {
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const model = selectors.model(state, { contentRef: ownProps.contentRef });
|
||||
const cellOrder = selectors.notebook.cellOrder(model as RecordOf<DocumentRecordProps>);
|
||||
const cellIndex = cellOrder.indexOf(ownProps.id);
|
||||
|
||||
return {
|
||||
cellIndex
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export default connect(makeMapStateToProps, undefined)(CellLabeler);
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
|
||||
import { ContentRef } from "@nteract/core";
|
||||
import * as actions from "../../NotebookComponent/actions";
|
||||
|
||||
interface ComponentProps {
|
||||
id: string;
|
||||
contentRef: ContentRef; // TODO: Make this per contentRef?
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
hover: () => void;
|
||||
unHover: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* HoverableCell sets the hovered cell
|
||||
*/
|
||||
class HoverableCell extends React.Component<ComponentProps & DispatchProps> {
|
||||
render() {
|
||||
return (
|
||||
<div className="HoverableCell" onMouseEnter={this.props.hover} onMouseLeave={this.props.unHover}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: Dispatch,
|
||||
{ id, contentRef }: { id: string; contentRef: ContentRef }
|
||||
): DispatchProps => ({
|
||||
hover: () => dispatch(actions.setHoveredCell({ cellId: id })),
|
||||
unHover: () => dispatch(actions.setHoveredCell({ cellId: undefined }))
|
||||
});
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(HoverableCell);
|
||||
@@ -0,0 +1,234 @@
|
||||
import { actions, ContentRef } from "@nteract/core";
|
||||
import React from "react";
|
||||
import {
|
||||
ConnectDragPreview,
|
||||
ConnectDragSource,
|
||||
ConnectDropTarget,
|
||||
DragSource,
|
||||
DragSourceConnector,
|
||||
DragSourceMonitor,
|
||||
DropTarget,
|
||||
DropTargetConnector,
|
||||
DropTargetMonitor
|
||||
} from "react-dnd";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import styled, { StyledComponent } from "styled-components";
|
||||
|
||||
/**
|
||||
The cell drag preview image is just a little stylized version of
|
||||
|
||||
[ ]
|
||||
|
||||
It matches nteract's default light theme
|
||||
|
||||
*/
|
||||
const cellDragPreviewImage = [
|
||||
"data:image/png;base64,",
|
||||
"iVBORw0KGgoAAAANSUhEUgAAADsAAAAzCAYAAAApdnDeAAAAAXNSR0IArs4c6QAA",
|
||||
"AwNJREFUaAXtmlFL3EAUhe9MZptuoha3rLWgYC0W+lj/T3+26INvXbrI2oBdE9km",
|
||||
"O9Nzxu1S0LI70AQScyFmDDfkfvdMZpNwlCCccwq7f21MaVM4FPtkU0o59RdoJBMx",
|
||||
"WZINBg+DQWGKCAk+2kIKFh9JlSzLYVmOilEpR1Kh/iUbQFiNQTSbzWJrbYJximOJ",
|
||||
"cSaulpVRoqh4K8JhjprIVJWqFlCpQNG51roYj8cLjJcGf5RMZWC1TYw1o2LxcEmy",
|
||||
"0jeEo3ZFWVHIx0ji4eeKHFOx8l4sVVVZnBE6tWLHq7xO7FY86YpPeVjeo5y61tlR",
|
||||
"JyhXEOQhF/lw6BGWixHvUWXVTpdgyUMu8q1h/ZJbqQhdiLsESx4FLvL9gcV6q3Cs",
|
||||
"0liq2IHuBHjItYIV3rMvJnrYrkrdK9sr24EO9NO4AyI+i/CilOXbTi1xeXXFTyAS",
|
||||
"GSOfzs42XmM+v5fJ5JvP29/fl8PDw43nhCbUpuzFxYXs7OxKmqZb1WQGkc/P80K+",
|
||||
"T6dbnROaVJuyfPY+Pj7aup7h66HP/1Uu5O7u59bnhSTWpmxIEU3l9rBNdbrp6/TK",
|
||||
"Nt3xpq7XK9tUp5u+Tm2/s/jYJdfX12LwBHVycrKRK89zmeJhYnZ7K3Fcz3e/2mDP",
|
||||
"z7/waZEf8zaC+gSkKa3l4OBA3uztbXdOYFZtsKcfToNKSZNUPp6GnRN0AST3C1Ro",
|
||||
"x9qS3yvbFqVC6+yVDe1YW/J7ZduiVGidvbKhHWtLfq9sW5QKrdMri9cxB6OFhQmO",
|
||||
"TrDuBHjIRT5CEZZj0i7xOkYnWGeCPOQiHqC8lc/R60cLnNPuvjOkns7dk4t8/Jfv",
|
||||
"s46mRlWqQiudxebVV3gAj7C9hXsmgZeztnfe/91YODEr3IoF/JY/sE2gbGaVLci3",
|
||||
"hh0tRtWNvsm16JmNcOs6N9dW72LP7yOtWbEhjAUkZ+icoJ5HbE6+NSxMjKWe6cKb",
|
||||
"GkUWgMwiFbXSlRpFkXelUlF4F70rVd7Bd4oZ/LL8xiDmtPV2Nwyf2zOlTfHERY7i",
|
||||
"Haa1+w2+iFqx0aIgvgAAAABJRU5ErkJggg=="
|
||||
].join("");
|
||||
|
||||
interface Props {
|
||||
focusCell: (payload: any) => void;
|
||||
id: string;
|
||||
moveCell: (payload: any) => void;
|
||||
children: React.ReactNode;
|
||||
contentRef: ContentRef;
|
||||
}
|
||||
|
||||
interface DnDSourceProps {
|
||||
connectDragPreview: ConnectDragPreview;
|
||||
connectDragSource: ConnectDragSource;
|
||||
isDragging: boolean;
|
||||
}
|
||||
|
||||
interface DnDTargetProps {
|
||||
connectDropTarget: ConnectDropTarget;
|
||||
isOver: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hoverUpperHalf: boolean;
|
||||
}
|
||||
|
||||
const cellSource = {
|
||||
beginDrag(props: Props) {
|
||||
return {
|
||||
id: props.id
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const DragHandle = styled.div.attrs({
|
||||
role: "presentation"
|
||||
})`
|
||||
position: absolute;
|
||||
z-index: 200;
|
||||
width: var(--prompt-width, 50px);
|
||||
height: 20px;
|
||||
cursor: move;
|
||||
`;
|
||||
|
||||
interface DragAreaProps {
|
||||
isDragging: boolean;
|
||||
isOver: boolean;
|
||||
hoverUpperHalf: boolean;
|
||||
}
|
||||
|
||||
const DragArea = styled.div.attrs<DragAreaProps>(props => ({
|
||||
style: {
|
||||
opacity: props.isDragging ? 0.25 : 1,
|
||||
borderTop: props.isOver && props.hoverUpperHalf ? "3px lightgray solid" : "3px transparent solid",
|
||||
borderBottom: props.isOver && !props.hoverUpperHalf ? "3px lightgray solid" : "3px transparent solid"
|
||||
}
|
||||
}))`
|
||||
padding: 10px;
|
||||
margin-top: -15px;
|
||||
` as StyledComponent<"div", any, DragAreaProps, never>; // Somehow setting the type on `attrs` isn't propagating properly;
|
||||
|
||||
// This is the div that DragHandle's absolute position will anchor
|
||||
const DragHandleAnchor = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export function isDragUpper(props: Props, monitor: DropTargetMonitor, el: HTMLElement): boolean {
|
||||
const hoverBoundingRect = el.getBoundingClientRect();
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
const hoverClientY = clientOffset!.y - hoverBoundingRect.top;
|
||||
|
||||
return hoverClientY < hoverMiddleY;
|
||||
}
|
||||
|
||||
export const cellTarget = {
|
||||
drop(props: Props, monitor: DropTargetMonitor, component: any): void {
|
||||
if (monitor) {
|
||||
const hoverUpperHalf = isDragUpper(props, monitor, component.el);
|
||||
// DropTargetSpec monitor definition could be undefined. we'll need a check for monitor in order to pass validation.
|
||||
props.moveCell({
|
||||
id: monitor.getItem().id,
|
||||
destinationId: props.id,
|
||||
above: hoverUpperHalf,
|
||||
contentRef: props.contentRef
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
hover(props: Props, monitor: DropTargetMonitor, component: any): void {
|
||||
if (monitor) {
|
||||
component.setState({
|
||||
hoverUpperHalf: isDragUpper(props, monitor, component.el)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function collectSource(
|
||||
connect: DragSourceConnector,
|
||||
monitor: DragSourceMonitor
|
||||
): {
|
||||
connectDragSource: ConnectDragSource;
|
||||
isDragging: boolean;
|
||||
connectDragPreview: ConnectDragPreview;
|
||||
} {
|
||||
return {
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging(),
|
||||
connectDragPreview: connect.dragPreview()
|
||||
};
|
||||
}
|
||||
|
||||
function collectTarget(
|
||||
connect: DropTargetConnector,
|
||||
monitor: DropTargetMonitor
|
||||
): {
|
||||
connectDropTarget: ConnectDropTarget;
|
||||
isOver: boolean;
|
||||
} {
|
||||
return {
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
isOver: monitor.isOver()
|
||||
};
|
||||
}
|
||||
|
||||
export class DraggableCellView extends React.Component<Props & DnDSourceProps & DnDTargetProps, State> {
|
||||
el?: HTMLDivElement | null;
|
||||
|
||||
state = {
|
||||
hoverUpperHalf: true
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
const connectDragPreview = this.props.connectDragPreview;
|
||||
const img = new (window as any).Image();
|
||||
|
||||
img.src = cellDragPreviewImage;
|
||||
|
||||
img.onload = /*dragImageLoaded*/ () => {
|
||||
connectDragPreview(img);
|
||||
};
|
||||
}
|
||||
|
||||
selectCell = () => {
|
||||
const { focusCell, id, contentRef } = this.props;
|
||||
focusCell({ id, contentRef });
|
||||
};
|
||||
|
||||
render() {
|
||||
return this.props.connectDropTarget(
|
||||
// Sadly connectDropTarget _has_ to take a React element for a DOM element (no styled-divs)
|
||||
<div>
|
||||
<DragArea
|
||||
isDragging={this.props.isDragging}
|
||||
hoverUpperHalf={this.state.hoverUpperHalf}
|
||||
isOver={this.props.isOver}
|
||||
ref={el => {
|
||||
this.el = el;
|
||||
}}
|
||||
>
|
||||
<DragHandleAnchor>
|
||||
{this.props.connectDragSource(
|
||||
// Same thing with connectDragSource... It also needs a React Element that matches a DOM element
|
||||
<div>
|
||||
<DragHandle onClick={this.selectCell} />
|
||||
</div>
|
||||
)}
|
||||
{this.props.children}
|
||||
</DragHandleAnchor>
|
||||
</DragArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const source = DragSource<Props, DnDSourceProps>("CELL", cellSource, collectSource);
|
||||
const target = DropTarget<Props, DnDTargetProps>("CELL", cellTarget, collectTarget);
|
||||
|
||||
export const makeMapDispatchToProps = (initialDispatch: Dispatch) => {
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
moveCell: (payload: actions.MoveCell["payload"]) => dispatch(actions.moveCell(payload)),
|
||||
focusCell: (payload: actions.FocusCell["payload"]) => dispatch(actions.focusCell(payload))
|
||||
});
|
||||
return mapDispatchToProps;
|
||||
};
|
||||
|
||||
export default connect(null, makeMapDispatchToProps)(source(target(DraggableCellView)));
|
||||
@@ -0,0 +1,97 @@
|
||||
/* eslint jsx-a11y/no-static-element-interactions: 0 */
|
||||
/* eslint jsx-a11y/click-events-have-key-events: 0 */
|
||||
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
|
||||
import { actions, selectors, ContentRef, AppState } from "@nteract/core";
|
||||
|
||||
interface ComponentProps {
|
||||
id: string;
|
||||
contentRef: ContentRef;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
focused: boolean;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
selectCell: () => void;
|
||||
}
|
||||
|
||||
type Props = ComponentProps & DispatchProps & StateProps;
|
||||
|
||||
export class HijackScroll extends React.Component<Props> {
|
||||
el: HTMLDivElement | null = null;
|
||||
|
||||
scrollIntoViewIfNeeded(prevFocused?: boolean): void {
|
||||
// Check if the element is being hovered over.
|
||||
const hovered = this.el && this.el.parentElement && this.el.parentElement.querySelector(":hover") === this.el;
|
||||
|
||||
if (
|
||||
this.props.focused &&
|
||||
prevFocused !== this.props.focused &&
|
||||
// Don't scroll into view if already hovered over, this prevents
|
||||
// accidentally selecting text within the codemirror area
|
||||
!hovered
|
||||
) {
|
||||
if (this.el && "scrollIntoViewIfNeeded" in this.el) {
|
||||
// This is only valid in Chrome, WebKit
|
||||
(this.el as any).scrollIntoViewIfNeeded();
|
||||
} else if (this.el) {
|
||||
// Make a best guess effort for older platforms
|
||||
this.el.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
this.scrollIntoViewIfNeeded(prevProps.focused);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.scrollIntoViewIfNeeded();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
onClick={this.props.selectCell}
|
||||
role="presentation"
|
||||
ref={el => {
|
||||
this.el = el;
|
||||
}}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const makeMapStateToProps = (initialState: AppState, ownProps: ComponentProps) => {
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const { id, contentRef } = ownProps;
|
||||
const model = selectors.model(state, { contentRef });
|
||||
let focused = false;
|
||||
|
||||
if (model && model.type === "notebook") {
|
||||
focused = model.cellFocused === id;
|
||||
}
|
||||
|
||||
return {
|
||||
focused
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const makeMapDispatchToProps = (initialDispatch: Dispatch, ownProps: ComponentProps) => {
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
selectCell: () => dispatch(actions.focusCell({ id: ownProps.id, contentRef: ownProps.contentRef }))
|
||||
});
|
||||
return mapDispatchToProps;
|
||||
};
|
||||
|
||||
export default connect(makeMapStateToProps, makeMapDispatchToProps)(HijackScroll);
|
||||
@@ -0,0 +1,142 @@
|
||||
import Immutable from "immutable";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
|
||||
import { CellId } from "@nteract/commutable";
|
||||
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
|
||||
|
||||
interface ComponentProps {
|
||||
contentRef: ContentRef;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
cellMap: Immutable.Map<string, any>;
|
||||
cellOrder: Immutable.List<string>;
|
||||
focusedCell?: string | null;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
executeFocusedCell: (payload: { contentRef: ContentRef }) => void;
|
||||
focusNextCell: (payload: { id?: CellId; createCellIfUndefined: boolean; contentRef: ContentRef }) => void;
|
||||
focusNextCellEditor: (payload: { id?: CellId; contentRef: ContentRef }) => void;
|
||||
}
|
||||
|
||||
type Props = ComponentProps & StateProps & DispatchProps;
|
||||
|
||||
export class KeyboardShortcuts extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.keyDown = this.keyDown.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props) {
|
||||
const newContentRef = this.props.contentRef !== nextProps.contentRef;
|
||||
const newFocusedCell = this.props.focusedCell !== nextProps.focusedCell;
|
||||
const newCellOrder = this.props.cellOrder && this.props.cellOrder.size !== nextProps.cellOrder.size;
|
||||
return newContentRef || newFocusedCell || newCellOrder;
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
document.addEventListener("keydown", this.keyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
document.removeEventListener("keydown", this.keyDown);
|
||||
}
|
||||
|
||||
keyDown(e: KeyboardEvent): void {
|
||||
// If enter is not pressed, do nothing
|
||||
if (e.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
executeFocusedCell,
|
||||
focusNextCell,
|
||||
focusNextCellEditor,
|
||||
contentRef,
|
||||
cellOrder,
|
||||
focusedCell,
|
||||
cellMap
|
||||
} = this.props;
|
||||
|
||||
let ctrlKeyPressed = e.ctrlKey;
|
||||
// Allow cmd + enter (macOS) to operate like ctrl + enter
|
||||
if (process.platform === "darwin") {
|
||||
ctrlKeyPressed = (e.metaKey || e.ctrlKey) && !(e.metaKey && e.ctrlKey);
|
||||
}
|
||||
|
||||
const shiftXORctrl = (e.shiftKey || ctrlKeyPressed) && !(e.shiftKey && ctrlKeyPressed);
|
||||
if (!shiftXORctrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if (focusedCell) {
|
||||
// NOTE: Order matters here because we need it to execute _before_ we
|
||||
// focus the next cell
|
||||
executeFocusedCell({ contentRef });
|
||||
|
||||
if (e.shiftKey) {
|
||||
/** Get the next cell and check if it is a markdown cell. */
|
||||
const focusedCellIndex = cellOrder.indexOf(focusedCell);
|
||||
const nextCellId = cellOrder.get(focusedCellIndex + 1);
|
||||
const nextCell = nextCellId ? cellMap.get(nextCellId) : undefined;
|
||||
|
||||
/** Always focus the next cell. */
|
||||
focusNextCell({
|
||||
id: undefined,
|
||||
createCellIfUndefined: true,
|
||||
contentRef
|
||||
});
|
||||
|
||||
/** Only focus the next editor if it is a code cell or a cell
|
||||
* created at the bottom of the notebook. */
|
||||
if (nextCell === undefined || (nextCell && nextCell.get("cell_type") === "code")) {
|
||||
focusNextCellEditor({ id: focusedCell, contentRef });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <React.Fragment>{this.props.children}</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
export const makeMapStateToProps = (state: AppState, ownProps: ComponentProps) => {
|
||||
const { contentRef } = ownProps;
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const model = selectors.model(state, { contentRef });
|
||||
|
||||
let cellOrder = Immutable.List();
|
||||
let cellMap = Immutable.Map<string, any>();
|
||||
let focusedCell;
|
||||
|
||||
if (model && model.type === "notebook") {
|
||||
cellOrder = model.notebook.cellOrder;
|
||||
cellMap = selectors.notebook.cellMap(model);
|
||||
focusedCell = selectors.notebook.cellFocused(model);
|
||||
}
|
||||
|
||||
return {
|
||||
cellOrder,
|
||||
cellMap,
|
||||
focusedCell
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
executeFocusedCell: (payload: { contentRef: ContentRef }) => dispatch(actions.executeFocusedCell(payload)),
|
||||
focusNextCell: (payload: { id?: CellId; createCellIfUndefined: boolean; contentRef: ContentRef }) =>
|
||||
dispatch(actions.focusNextCell(payload)),
|
||||
focusNextCellEditor: (payload: { id?: CellId; contentRef: ContentRef }) =>
|
||||
dispatch(actions.focusNextCellEditor(payload))
|
||||
});
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(KeyboardShortcuts);
|
||||
181
src/Explorer/Notebook/NotebookRenderer/default.css
Normal file
181
src/Explorer/Notebook/NotebookRenderer/default.css
Normal file
@@ -0,0 +1,181 @@
|
||||
.nteract-cell-prompt {
|
||||
font-family: monospace;
|
||||
color: var(--theme-cell-prompt-fg, black);
|
||||
background-color: var(--theme-cell-prompt-bg, #fafafa);
|
||||
}
|
||||
|
||||
.nteract-cell-pagers {
|
||||
background-color: var(--theme-pager-bg, #fafafa);
|
||||
}
|
||||
|
||||
.nteract-cell-outputs a {
|
||||
color: var(--link-color-unvisited, blue);
|
||||
}
|
||||
|
||||
.nteract-cell-outputs a:visited {
|
||||
color: var(--link-color-visited, blue);
|
||||
}
|
||||
|
||||
.nteract-cell-outputs code {
|
||||
font-family: "Source Code Pro", monospace;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs kbd {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #fff inset;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs th,
|
||||
.nteract-cell-outputs td,
|
||||
/* for legacy output handling */
|
||||
.nteract-cell-outputs .th,
|
||||
.nteract-cell-outputs .td {
|
||||
border: 1px solid var(--theme-app-border, #cbcbcb);
|
||||
}
|
||||
|
||||
.nteract-cell-outputs blockquote {
|
||||
padding: 0.75em 0.5em 0.75em 1em;
|
||||
}
|
||||
|
||||
.nteract-cell-outputs blockquote::before {
|
||||
display: block;
|
||||
height: 0;
|
||||
margin-left: -0.95em;
|
||||
}
|
||||
|
||||
.nteract-cell-input .nteract-cell-source {
|
||||
background-color: var(--theme-cell-input-bg, #fafafa);
|
||||
}
|
||||
|
||||
.nteract-cells {
|
||||
font-family: "Source Sans Pro", Helvetica Neue, Helvetica, sans-serif;
|
||||
font-size: 16px;
|
||||
background-color: var(--theme-app-bg);
|
||||
color: var(--theme-app-fg);
|
||||
}
|
||||
|
||||
.nteract-cell {
|
||||
background: var(--theme-cell-bg, white);
|
||||
}
|
||||
|
||||
.nteract-cell-container.selected .nteract-cell-prompt {
|
||||
background-color: var(--theme-cell-prompt-bg-focus, hsl(0, 0%, 90%));
|
||||
color: var(--theme-cell-prompt-fg-focus, hsl(0, 0%, 51%));
|
||||
}
|
||||
|
||||
.nteract-cell-container:hover:not(.selected) .nteract-cell-prompt,
|
||||
.nteract-cell-container:active:not(.selected) .nteract-cell-prompt {
|
||||
background-color: var(--theme-cell-prompt-bg-hover, hsl(0, 0%, 94%));
|
||||
color: var(--theme-cell-prompt-fg-hover, hsl(0, 0%, 15%));
|
||||
}
|
||||
|
||||
.nteract-cell-outputs {
|
||||
background-color: var(--theme-cell-output-bg);
|
||||
}
|
||||
|
||||
.nteract-cell-container.selected .nteract-cell-outputs {
|
||||
background-color: var(--theme-cell-output-bg-focus);
|
||||
}
|
||||
|
||||
.nteract-cell-container:hover:not(.selected) .nteract-cell-outputs,
|
||||
.nteract-cell-container:active:not(.selected) .nteract-cell-outputs {
|
||||
background-color: var(--theme-cell-output-bg-hover);
|
||||
}
|
||||
|
||||
.nteract-cell:focus .nteract-cell-prompt {
|
||||
background-color: var(--theme-cell-prompt-bg-focus, hsl(0, 0%, 90%));
|
||||
color: var(--theme-cell-prompt-fg-focus, hsl(0, 0%, 51%));
|
||||
}
|
||||
|
||||
@media print {
|
||||
/* make sure all cells look the same in print regarless of focus */
|
||||
.nteract-cell-container .nteract-cell-prompt,
|
||||
.nteract-cell-container.selected .nteract-cell-prompt,
|
||||
.nteract-cell-container:focus .nteract-cell-prompt,
|
||||
.nteract-cell-container:hover:not(.selected) .nteract-cell-prompt {
|
||||
background-color: var(--theme-cell-prompt-bg, white);
|
||||
color: var(--theme-cell-prompt-fg, black);
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-cell-toolbar {
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
|
||||
.nteract-cell-container:not(.selected) .nteract-cell-toolbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nteract-cell-container:hover:not(.selected) .nteract-cell-toolbar,
|
||||
.nteract-cell-container.selected .nteract-cell-toolbar {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.nteract-cell-toolbar > div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.nteract-cell-toolbar:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.nteract-cell-toolbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nteract-cell-toolbar button {
|
||||
display: inline-block;
|
||||
|
||||
width: 22px;
|
||||
height: 20px;
|
||||
padding: 0px 4px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.nteract-cell-toolbar span {
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
color: var(--theme-cell-toolbar-fg);
|
||||
}
|
||||
|
||||
.nteract-cell-toolbar button span:hover {
|
||||
color: var(--theme-cell-toolbar-fg-hover);
|
||||
}
|
||||
|
||||
.nteract-cell-toolbar .octicon {
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.nteract-cell-toolbar span.spacer {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: 1px 5px 3px 5px;
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
.nteract-cell-toolbar {
|
||||
z-index: 9;
|
||||
position: sticky; /* keep visible with large code cells that need scrolling */
|
||||
float: right;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 34px;
|
||||
margin: 0 0 0 -100%; /* allow code cell to completely overlap (underlap?) */
|
||||
padding: 0 0 0 50px; /* give users extra room to move their mouse to the
|
||||
toolbar without causing the cell to go out of
|
||||
focus/hide the toolbar before they get there */
|
||||
}
|
||||
|
||||
.nteract-cell.hidden .nteract-cell-toolbar {
|
||||
display: none;
|
||||
}
|
||||
Reference in New Issue
Block a user