Skip to content

Commit 02dd071

Browse files
authored
Add placeholders to code and markdown cells (#234)
* Add placeholders * Hide placeholder when cell is clicked * Add tests for placeholders * Update test config * Add snapshots * Update snapshots and enforce no cursor blinking * Update * Fix typo, fix markdown editor placeholder test * Align styles with figma * Blur markdown before taking snapshot; update code placeholder snapshot * Update snapshot
1 parent 8bfb432 commit 02dd071

13 files changed

+178
-51
lines changed

lite/jupyter-lite.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,22 @@
5151
},
5252
"@jupyterlab/notebook-extension:tracker": {
5353
"autoStartDefaultKernel": true,
54-
"windowingMode": "none"
54+
"windowingMode": "none",
55+
"codeCellConfig": {
56+
"lineNumbers": false,
57+
"lineWrap": false,
58+
"placeholder": "This is a code cell. Add code here"
59+
},
60+
"markdownCellConfig": {
61+
"lineNumbers": false,
62+
"matchBrackets": false,
63+
"placeholder": "This is a text cell. Add text here"
64+
},
65+
"rawCellConfig": {
66+
"lineNumbers": false,
67+
"matchBrackets": false,
68+
"placeholder": "This is a raw cell. Add text here"
69+
}
5570
},
5671
"@jupyterlab/application-extension:context-menu": {
5772
"disabled": true

src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import {
2929

3030
import { KERNEL_DISPLAY_NAMES, switchKernel } from './kernels';
3131
import { singleDocumentMode } from './single-mode';
32-
import { notebookFactoryPlugin } from './run-button';
32+
import { notebookFactoryPlugin } from './notebook-factory';
33+
import { placeholderPlugin } from './placeholders';
3334

3435
/**
3536
* Generate a shareable URL for the currently active notebook.
@@ -599,5 +600,6 @@ export default [
599600
// competitions,
600601
customSidebar,
601602
// helpPlugin,
602-
singleDocumentMode
603+
singleDocumentMode,
604+
placeholderPlugin
603605
];

src/notebook-factory.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application';
2+
import { MarkdownCell } from '@jupyterlab/cells';
3+
import { IEditorServices } from '@jupyterlab/codeeditor';
4+
import { Notebook, NotebookPanel } from '@jupyterlab/notebook';
5+
import { EMPTY_MARKDOWN_PLACEHOLDER, MarkdownCellWithCustomPlaceholder } from './placeholders';
6+
import { JEInputPrompt } from './run-button';
7+
8+
export namespace JENotebookContentFactory {
9+
export interface IOptions extends Notebook.ContentFactory.IOptions {
10+
app: JupyterFrontEnd;
11+
}
12+
}
13+
14+
export class JENotebookContentFactory extends Notebook.ContentFactory {
15+
private _app: JupyterFrontEnd;
16+
17+
constructor(options: JENotebookContentFactory.IOptions) {
18+
super(options);
19+
this._app = options.app;
20+
}
21+
22+
createInputPrompt(): JEInputPrompt {
23+
return new JEInputPrompt(this._app);
24+
}
25+
26+
createNotebook(options: Notebook.IOptions): Notebook {
27+
return new Notebook(options);
28+
}
29+
30+
createMarkdownCell(options: MarkdownCell.IOptions): MarkdownCell {
31+
const cell = new MarkdownCellWithCustomPlaceholder({
32+
...options,
33+
emptyPlaceholder: EMPTY_MARKDOWN_PLACEHOLDER
34+
}).initializeState();
35+
// Monkey patch until https://github.com/jupyterlab/jupyterlab/issues/17917 is solved
36+
cell['_updateRenderedInput'] = cell['updateRenderedInput'];
37+
return cell;
38+
}
39+
}
40+
41+
/**
42+
* Plugin that provides the custom notebook factory.
43+
*/
44+
export const notebookFactoryPlugin: JupyterFrontEndPlugin<NotebookPanel.IContentFactory> = {
45+
id: 'jupytereverywhere:notebook-factory',
46+
description: 'Provides notebook cell factory with input prompts',
47+
provides: NotebookPanel.IContentFactory,
48+
requires: [IEditorServices],
49+
autoStart: true,
50+
activate: (app: JupyterFrontEnd, editorServices: IEditorServices) => {
51+
const editorFactory = editorServices.factoryService.newInlineEditor;
52+
53+
const factory = new JENotebookContentFactory({
54+
editorFactory,
55+
app
56+
});
57+
58+
return factory;
59+
}
60+
};

src/placeholders.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { placeholder } from '@codemirror/view';
2+
import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application';
3+
import { MarkdownCell } from '@jupyterlab/cells';
4+
import { EditorExtensionRegistry, IEditorExtensionRegistry } from '@jupyterlab/codemirror';
5+
import { MimeModel } from '@jupyterlab/rendermime';
6+
7+
export const EMPTY_MARKDOWN_PLACEHOLDER = 'This is a text cell. Double-click to edit';
8+
9+
export const placeholderPlugin: JupyterFrontEndPlugin<void> = {
10+
id: '@jupyter-everywhere/codemirror-extension:placeholder',
11+
autoStart: true,
12+
requires: [IEditorExtensionRegistry],
13+
activate: (app: JupyterFrontEnd, extensions: IEditorExtensionRegistry) => {
14+
extensions.addExtension(
15+
Object.freeze({
16+
name: 'placeholder',
17+
default: null,
18+
factory: () =>
19+
EditorExtensionRegistry.createConfigurableExtension((text: string | null) =>
20+
text ? placeholder(text) : []
21+
),
22+
schema: {
23+
type: ['string', 'null'],
24+
title: 'Placeholder',
25+
description: 'Placeholder to show.'
26+
}
27+
})
28+
);
29+
}
30+
};
31+
32+
export class MarkdownCellWithCustomPlaceholder extends MarkdownCell {
33+
constructor(options: MarkdownCellWithCustomPlaceholder.IOptions) {
34+
super(options);
35+
this._emptyPlaceholder = options.emptyPlaceholder;
36+
}
37+
updateRenderedInput(): Promise<void> {
38+
if (this.placeholder) {
39+
return Promise.resolve();
40+
}
41+
42+
const model = this.model;
43+
const text = (model && model.sharedModel.getSource()) || this._emptyPlaceholder;
44+
// Do not re-render if the text has not changed.
45+
if (text !== this._previousText) {
46+
const mimeModel = new MimeModel({ data: { 'text/markdown': text } });
47+
this._previousText = text;
48+
return this.renderer.renderModel(mimeModel);
49+
}
50+
return Promise.resolve();
51+
}
52+
private _previousText: string = '';
53+
private _emptyPlaceholder: string;
54+
}
55+
56+
namespace MarkdownCellWithCustomPlaceholder {
57+
export interface IOptions extends MarkdownCell.IOptions {
58+
emptyPlaceholder: string;
59+
}
60+
}

src/run-button.ts

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,9 @@
3636
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
3737
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3838

39-
import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application';
40-
import { IEditorServices } from '@jupyterlab/codeeditor';
39+
import { JupyterFrontEnd } from '@jupyterlab/application';
4140
import { ToolbarButton } from '@jupyterlab/ui-components';
4241
import { Widget, PanelLayout } from '@lumino/widgets';
43-
import { Notebook, NotebookPanel } from '@jupyterlab/notebook';
4442
import { EverywhereIcons } from './icons';
4543

4644
const INPUT_PROMPT_CLASS = 'jp-InputPrompt';
@@ -113,47 +111,3 @@ export class JEInputPrompt extends Widget implements IInputPrompt {
113111
this._promptIndicator.executionCount = value;
114112
}
115113
}
116-
117-
export namespace JENotebookContentFactory {
118-
export interface IOptions extends Notebook.ContentFactory.IOptions {
119-
app: JupyterFrontEnd;
120-
}
121-
}
122-
123-
export class JENotebookContentFactory extends Notebook.ContentFactory {
124-
private _app: JupyterFrontEnd;
125-
126-
constructor(options: JENotebookContentFactory.IOptions) {
127-
super(options);
128-
this._app = options.app;
129-
}
130-
131-
createInputPrompt(): JEInputPrompt {
132-
return new JEInputPrompt(this._app);
133-
}
134-
135-
createNotebook(options: Notebook.IOptions): Notebook {
136-
return new Notebook(options);
137-
}
138-
}
139-
140-
/**
141-
* Plugin that provides the custom notebook factory with run buttons
142-
*/
143-
export const notebookFactoryPlugin: JupyterFrontEndPlugin<NotebookPanel.IContentFactory> = {
144-
id: 'jupytereverywhere:notebook-factory',
145-
description: 'Provides notebook cell factory with input prompts',
146-
provides: NotebookPanel.IContentFactory,
147-
requires: [IEditorServices],
148-
autoStart: true,
149-
activate: (app: JupyterFrontEnd, editorServices: IEditorServices) => {
150-
const editorFactory = editorServices.factoryService.newInlineEditor;
151-
152-
const factory = new JENotebookContentFactory({
153-
editorFactory,
154-
app
155-
});
156-
157-
return factory;
158-
}
159-
};

style/base.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,16 @@ consistent with code cells */
208208
min-height: var(--je-cell-height);
209209
}
210210

211+
.jp-mod-focused .cm-placeholder {
212+
/* Hide placeholder when cell is clicked */
213+
display: none;
214+
}
215+
216+
.cm-editor .cm-placeholder {
217+
color: #828282 !important;
218+
font-weight: 600;
219+
}
220+
211221
.jp-MarkdownCell .jp-RenderedHTMLCommon,
212222
.jp-MarkdownCell .jp-InputArea-editor {
213223
min-height: 32px;

ui-tests/inject-test-config.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
set -eux
22
# Overwrite some settings to disable cursor blinking,
33
# which causes inadvertent playwright snapshot failures
4-
cat jupyter-lite.json | jq '.["jupyter-config-data"].["settingsOverrides"] += {"@jupyterlab/codemirror-extension:plugin":{ "defaultConfig": { "cursorBlinkRate": 0, "lineNumbers": false } } }' > jupyter-lite.json.tmp
4+
cat jupyter-lite.json | jq '.["jupyter-config-data"].["settingsOverrides"] *= {"@jupyterlab/notebook-extension:tracker": { "codeCellConfig": { "cursorBlinkRate": 0 }, "markdownCellConfig": { "cursorBlinkRate": 0 }, "rawCellConfig": { "cursorBlinkRate": 0 } }, "@jupyterlab/codemirror-extension:plugin":{ "defaultConfig": { "cursorBlinkRate": 0, "lineNumbers": false } } }' > jupyter-lite.json.tmp
55
mv jupyter-lite.json.tmp jupyter-lite.json

ui-tests/tests/jupytereverywhere.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,32 @@ test.describe('Kernel commands should use memory terminology', () => {
726726
});
727727
});
728728

729+
test.describe('Placeholders in cells', () => {
730+
test.beforeEach(async ({ page }) => {
731+
await page.waitForSelector('.jp-NotebookPanel');
732+
});
733+
test('Code cell editor placeholder', async ({ page }) => {
734+
await runCommand(page, 'notebook:enter-command-mode');
735+
736+
const cell = page.locator('.jp-CodeCell').first();
737+
expect(await cell.screenshot()).toMatchSnapshot('code-editor-placeholder.png');
738+
});
739+
test('Markdown cell editor placeholder', async ({ page }) => {
740+
await runCommand(page, 'notebook:change-cell-to-markdown');
741+
await runCommand(page, 'notebook:enter-command-mode');
742+
743+
const cell = page.locator('.jp-MarkdownCell').first();
744+
expect(await cell.screenshot()).toMatchSnapshot('markdown-editor-placeholder.png');
745+
});
746+
test('Rendered Markdown cell placeholder', async ({ page }) => {
747+
await runCommand(page, 'notebook:change-cell-to-markdown');
748+
await runCommand(page, 'notebook:run-cell');
749+
750+
const cell = page.locator('.jp-MarkdownCell').first();
751+
expect(await cell.screenshot()).toMatchSnapshot('rendered-markdown-placeholder.png');
752+
});
753+
});
754+
729755
test.describe('Per cell run buttons', () => {
730756
test('Clicking the run button executes code and shows output', async ({ page }) => {
731757
await page.waitForSelector('.jp-NotebookPanel');
3.22 KB
Loading
4.19 KB
Loading

0 commit comments

Comments
 (0)