1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import { ArrayExt, each } from '@lumino/algorithm';
- import { ReadonlyPartialJSONValue } from '@lumino/coreutils';
- import { Message } from '@lumino/messaging';
- import { MimeData } from '@lumino/coreutils';
- import { AttachedProperty } from '@lumino/properties';
- import { ISignal, Signal } from '@lumino/signaling';
- import { Drag, IDragEvent } from '@lumino/dragdrop';
- import { PanelLayout, Widget } from '@lumino/widgets';
- import { h, VirtualDOM } from '@lumino/virtualdom';
- import {
- ICellModel,
- Cell,
- IMarkdownCellModel,
- CodeCell,
- MarkdownCell,
- ICodeCellModel,
- RawCell,
- IRawCellModel
- } from '@jupyterlab/cells';
- import { IEditorMimeTypeService, CodeEditor } from '@jupyterlab/codeeditor';
- import { IChangedArgs } from '@jupyterlab/coreutils';
- import * as nbformat from '@jupyterlab/nbformat';
- import { IObservableMap, IObservableList } from '@jupyterlab/observables';
- import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
- import { INotebookModel } from './model';
- /**
- * The data attribute added to a widget that has an active kernel.
- */
- const KERNEL_USER = 'jpKernelUser';
- /**
- * The data attribute added to a widget that can run code.
- */
- const CODE_RUNNER = 'jpCodeRunner';
- /**
- * The data attribute added to a widget that can undo.
- */
- const UNDOER = 'jpUndoer';
- /**
- * The class name added to notebook widgets.
- */
- const NB_CLASS = 'jp-Notebook';
- /**
- * The class name added to notebook widget cells.
- */
- const NB_CELL_CLASS = 'jp-Notebook-cell';
- /**
- * The class name added to a notebook in edit mode.
- */
- const EDIT_CLASS = 'jp-mod-editMode';
- /**
- * The class name added to a notebook in command mode.
- */
- const COMMAND_CLASS = 'jp-mod-commandMode';
- /**
- * The class name added to the active cell.
- */
- const ACTIVE_CLASS = 'jp-mod-active';
- /**
- * The class name added to selected cells.
- */
- const SELECTED_CLASS = 'jp-mod-selected';
- /**
- * The class name added to an active cell when there are other selected cells.
- */
- const OTHER_SELECTED_CLASS = 'jp-mod-multiSelected';
- /**
- * The class name added to unconfined images.
- */
- const UNCONFINED_CLASS = 'jp-mod-unconfined';
- /**
- * The class name added to a drop target.
- */
- const DROP_TARGET_CLASS = 'jp-mod-dropTarget';
- /**
- * The class name added to a drop source.
- */
- const DROP_SOURCE_CLASS = 'jp-mod-dropSource';
- /**
- * The class name added to drag images.
- */
- const DRAG_IMAGE_CLASS = 'jp-dragImage';
- /**
- * The class name added to singular drag images
- */
- const SINGLE_DRAG_IMAGE_CLASS = 'jp-dragImage-singlePrompt';
- /**
- * The class name added to the drag image cell content.
- */
- const CELL_DRAG_CONTENT_CLASS = 'jp-dragImage-content';
- /**
- * The class name added to the drag image cell content.
- */
- const CELL_DRAG_PROMPT_CLASS = 'jp-dragImage-prompt';
- /**
- * The class name added to the drag image cell content.
- */
- const CELL_DRAG_MULTIPLE_BACK = 'jp-dragImage-multipleBack';
- /**
- * The mimetype used for Jupyter cell data.
- */
- const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
- /**
- * The threshold in pixels to start a drag event.
- */
- const DRAG_THRESHOLD = 5;
- /**
- * The interactivity modes for the notebook.
- */
- export type NotebookMode = 'command' | 'edit';
- /**
- * A widget which renders static non-interactive notebooks.
- *
- * #### Notes
- * The widget model must be set separately and can be changed
- * at any time. Consumers of the widget must account for a
- * `null` model, and may want to listen to the `modelChanged`
- * signal.
- */
- export class StaticNotebook extends Widget {
- /**
- * Construct a notebook widget.
- */
- constructor(options: StaticNotebook.IOptions) {
- super();
- this.addClass(NB_CLASS);
- this.node.dataset[KERNEL_USER] = 'true';
- this.node.dataset[UNDOER] = 'true';
- this.rendermime = options.rendermime;
- this.layout = new Private.NotebookPanelLayout();
- this.contentFactory =
- options.contentFactory || StaticNotebook.defaultContentFactory;
- this.editorConfig =
- options.editorConfig || StaticNotebook.defaultEditorConfig;
- this.notebookConfig =
- options.notebookConfig || StaticNotebook.defaultNotebookConfig;
- this._mimetypeService = options.mimeTypeService;
- }
- /**
- * A signal emitted when the model of the notebook changes.
- */
- get modelChanged(): ISignal<this, void> {
- return this._modelChanged;
- }
- /**
- * A signal emitted when the model content changes.
- *
- * #### Notes
- * This is a convenience signal that follows the current model.
- */
- get modelContentChanged(): ISignal<this, void> {
- return this._modelContentChanged;
- }
- /**
- * The cell factory used by the widget.
- */
- readonly contentFactory: StaticNotebook.IContentFactory;
- /**
- * The Rendermime instance used by the widget.
- */
- readonly rendermime: IRenderMimeRegistry;
- /**
- * The model for the widget.
- */
- get model(): INotebookModel | null {
- return this._model;
- }
- set model(newValue: INotebookModel | null) {
- newValue = newValue || null;
- if (this._model === newValue) {
- return;
- }
- const oldValue = this._model;
- this._model = newValue;
- if (oldValue && oldValue.modelDB.isCollaborative) {
- void oldValue.modelDB.connected.then(() => {
- oldValue!.modelDB.collaborators!.changed.disconnect(
- this._onCollaboratorsChanged,
- this
- );
- });
- }
- if (newValue && newValue.modelDB.isCollaborative) {
- void newValue.modelDB.connected.then(() => {
- newValue!.modelDB.collaborators!.changed.connect(
- this._onCollaboratorsChanged,
- this
- );
- });
- }
- // Trigger private, protected, and public changes.
- this._onModelChanged(oldValue, newValue);
- this.onModelChanged(oldValue, newValue);
- this._modelChanged.emit(void 0);
- }
- /**
- * Get the mimetype for code cells.
- */
- get codeMimetype(): string {
- return this._mimetype;
- }
- /**
- * A read-only sequence of the widgets in the notebook.
- */
- get widgets(): ReadonlyArray<Cell> {
- return (this.layout as PanelLayout).widgets as ReadonlyArray<Cell>;
- }
- /**
- * A configuration object for cell editor settings.
- */
- get editorConfig(): StaticNotebook.IEditorConfig {
- return this._editorConfig;
- }
- set editorConfig(value: StaticNotebook.IEditorConfig) {
- this._editorConfig = value;
- this._updateEditorConfig();
- }
- /**
- * A configuration object for notebook settings.
- */
- get notebookConfig(): StaticNotebook.INotebookConfig {
- return this._notebookConfig;
- }
- set notebookConfig(value: StaticNotebook.INotebookConfig) {
- this._notebookConfig = value;
- this._updateNotebookConfig();
- }
- /**
- * Dispose of the resources held by the widget.
- */
- dispose() {
- // Do nothing if already disposed.
- if (this.isDisposed) {
- return;
- }
- this._model = null;
- super.dispose();
- }
- /**
- * Handle a new model.
- *
- * #### Notes
- * This method is called after the model change has been handled
- * internally and before the `modelChanged` signal is emitted.
- * The default implementation is a no-op.
- */
- protected onModelChanged(
- oldValue: INotebookModel | null,
- newValue: INotebookModel | null
- ): void {
- // No-op.
- }
- /**
- * Handle changes to the notebook model content.
- *
- * #### Notes
- * The default implementation emits the `modelContentChanged` signal.
- */
- protected onModelContentChanged(model: INotebookModel, args: void): void {
- this._modelContentChanged.emit(void 0);
- }
- /**
- * Handle changes to the notebook model metadata.
- *
- * #### Notes
- * The default implementation updates the mimetypes of the code cells
- * when the `language_info` metadata changes.
- */
- protected onMetadataChanged(
- sender: IObservableMap<ReadonlyPartialJSONValue | undefined>,
- args: IObservableMap.IChangedArgs<ReadonlyPartialJSONValue>
- ): void {
- switch (args.key) {
- case 'language_info':
- this._updateMimetype();
- break;
- default:
- break;
- }
- }
- /**
- * Handle a cell being inserted.
- *
- * The default implementation is a no-op
- */
- protected onCellInserted(index: number, cell: Cell): void {
- // This is a no-op.
- }
- /**
- * Handle a cell being moved.
- *
- * The default implementation is a no-op
- */
- protected onCellMoved(fromIndex: number, toIndex: number): void {
- // This is a no-op.
- }
- /**
- * Handle a cell being removed.
- *
- * The default implementation is a no-op
- */
- protected onCellRemoved(index: number, cell: Cell): void {
- // This is a no-op.
- }
- /**
- * Handle a new model on the widget.
- */
- private _onModelChanged(
- oldValue: INotebookModel | null,
- newValue: INotebookModel | null
- ): void {
- const layout = this.layout as PanelLayout;
- if (oldValue) {
- oldValue.cells.changed.disconnect(this._onCellsChanged, this);
- oldValue.metadata.changed.disconnect(this.onMetadataChanged, this);
- oldValue.contentChanged.disconnect(this.onModelContentChanged, this);
- // TODO: reuse existing cell widgets if possible. Remember to initially
- // clear the history of each cell if we do this.
- while (layout.widgets.length) {
- this._removeCell(0);
- }
- }
- if (!newValue) {
- this._mimetype = 'text/plain';
- return;
- }
- this._updateMimetype();
- const cells = newValue.cells;
- if (!cells.length) {
- cells.push(
- newValue.contentFactory.createCell(this.notebookConfig.defaultCell, {})
- );
- }
- each(cells, (cell: ICellModel, i: number) => {
- this._insertCell(i, cell);
- });
- cells.changed.connect(this._onCellsChanged, this);
- newValue.contentChanged.connect(this.onModelContentChanged, this);
- newValue.metadata.changed.connect(this.onMetadataChanged, this);
- }
- /**
- * Handle a change cells event.
- */
- private _onCellsChanged(
- sender: IObservableList<ICellModel>,
- args: IObservableList.IChangedArgs<ICellModel>
- ) {
- let index = 0;
- switch (args.type) {
- case 'add':
- index = args.newIndex;
- each(args.newValues, value => {
- this._insertCell(index++, value);
- });
- break;
- case 'move':
- this._moveCell(args.oldIndex, args.newIndex);
- break;
- case 'remove':
- each(args.oldValues, value => {
- this._removeCell(args.oldIndex);
- });
- // Add default cell if there are no cells remaining.
- if (!sender.length) {
- const model = this.model;
- // Add the cell in a new context to avoid triggering another
- // cell changed event during the handling of this signal.
- requestAnimationFrame(() => {
- if (model && !model.isDisposed && !model.cells.length) {
- model.cells.push(
- model.contentFactory.createCell(
- this.notebookConfig.defaultCell,
- {}
- )
- );
- }
- });
- }
- break;
- case 'set':
- // TODO: reuse existing widgets if possible.
- index = args.newIndex;
- each(args.newValues, value => {
- // Note: this ordering (insert then remove)
- // is important for getting the active cell
- // index for the editable notebook correct.
- this._insertCell(index, value);
- this._removeCell(index + 1);
- index++;
- });
- break;
- default:
- return;
- }
- }
- /**
- * Create a cell widget and insert into the notebook.
- */
- private _insertCell(index: number, cell: ICellModel): void {
- let widget: Cell;
- switch (cell.type) {
- case 'code':
- widget = this._createCodeCell(cell as ICodeCellModel);
- widget.model.mimeType = this._mimetype;
- break;
- case 'markdown':
- widget = this._createMarkdownCell(cell as IMarkdownCellModel);
- if (cell.value.text === '') {
- (widget as MarkdownCell).rendered = false;
- }
- break;
- default:
- widget = this._createRawCell(cell as IRawCellModel);
- }
- widget.addClass(NB_CELL_CLASS);
- const layout = this.layout as PanelLayout;
- layout.insertWidget(index, widget);
- this.onCellInserted(index, widget);
- }
- /**
- * Create a code cell widget from a code cell model.
- */
- private _createCodeCell(model: ICodeCellModel): CodeCell {
- const rendermime = this.rendermime;
- const contentFactory = this.contentFactory;
- const editorConfig = this.editorConfig.code;
- const options = {
- editorConfig,
- model,
- rendermime,
- contentFactory,
- updateEditorOnShow: false
- };
- const cell = this.contentFactory.createCodeCell(options, this);
- cell.syncCollapse = true;
- cell.syncEditable = true;
- cell.syncScrolled = true;
- return cell;
- }
- /**
- * Create a markdown cell widget from a markdown cell model.
- */
- private _createMarkdownCell(model: IMarkdownCellModel): MarkdownCell {
- const rendermime = this.rendermime;
- const contentFactory = this.contentFactory;
- const editorConfig = this.editorConfig.markdown;
- const options = {
- editorConfig,
- model,
- rendermime,
- contentFactory,
- updateEditorOnShow: false
- };
- const cell = this.contentFactory.createMarkdownCell(options, this);
- cell.syncCollapse = true;
- cell.syncEditable = true;
- return cell;
- }
- /**
- * Create a raw cell widget from a raw cell model.
- */
- private _createRawCell(model: IRawCellModel): RawCell {
- const contentFactory = this.contentFactory;
- const editorConfig = this.editorConfig.raw;
- const options = {
- editorConfig,
- model,
- contentFactory,
- updateEditorOnShow: false
- };
- const cell = this.contentFactory.createRawCell(options, this);
- cell.syncCollapse = true;
- cell.syncEditable = true;
- return cell;
- }
- /**
- * Move a cell widget.
- */
- private _moveCell(fromIndex: number, toIndex: number): void {
- const layout = this.layout as PanelLayout;
- layout.insertWidget(toIndex, layout.widgets[fromIndex]);
- this.onCellMoved(fromIndex, toIndex);
- }
- /**
- * Remove a cell widget.
- */
- private _removeCell(index: number): void {
- const layout = this.layout as PanelLayout;
- const widget = layout.widgets[index] as Cell;
- widget.parent = null;
- this.onCellRemoved(index, widget);
- widget.dispose();
- }
- /**
- * Update the mimetype of the notebook.
- */
- private _updateMimetype(): void {
- const info = this._model?.metadata.get(
- 'language_info'
- ) as nbformat.ILanguageInfoMetadata;
- if (!info) {
- return;
- }
- this._mimetype = this._mimetypeService.getMimeTypeByLanguage(info);
- each(this.widgets, widget => {
- if (widget.model.type === 'code') {
- widget.model.mimeType = this._mimetype;
- }
- });
- }
- /**
- * Handle an update to the collaborators.
- */
- private _onCollaboratorsChanged(): void {
- // If there are selections corresponding to non-collaborators,
- // they are stale and should be removed.
- for (let i = 0; i < this.widgets.length; i++) {
- const cell = this.widgets[i];
- for (const key of cell.model.selections.keys()) {
- if (false === this._model?.modelDB?.collaborators?.has(key)) {
- cell.model.selections.delete(key);
- }
- }
- }
- }
- /**
- * Update editor settings for notebook cells.
- */
- private _updateEditorConfig() {
- for (let i = 0; i < this.widgets.length; i++) {
- const cell = this.widgets[i];
- let config: Partial<CodeEditor.IConfig>;
- switch (cell.model.type) {
- case 'code':
- config = this._editorConfig.code;
- break;
- case 'markdown':
- config = this._editorConfig.markdown;
- break;
- default:
- config = this._editorConfig.raw;
- break;
- }
- Object.keys(config).forEach((key: keyof CodeEditor.IConfig) => {
- cell.editor.setOption(key, config[key] ?? null);
- });
- cell.editor.refresh();
- }
- }
- /**
- * Apply updated notebook settings.
- */
- private _updateNotebookConfig() {
- // Apply scrollPastEnd setting.
- this.toggleClass(
- 'jp-mod-scrollPastEnd',
- this._notebookConfig.scrollPastEnd
- );
- }
- private _editorConfig = StaticNotebook.defaultEditorConfig;
- private _notebookConfig = StaticNotebook.defaultNotebookConfig;
- private _mimetype = 'text/plain';
- private _model: INotebookModel | null = null;
- private _mimetypeService: IEditorMimeTypeService;
- private _modelChanged = new Signal<this, void>(this);
- private _modelContentChanged = new Signal<this, void>(this);
- }
- /**
- * The namespace for the `StaticNotebook` class statics.
- */
- export namespace StaticNotebook {
- /**
- * An options object for initializing a static notebook.
- */
- export interface IOptions {
- /**
- * The rendermime instance used by the widget.
- */
- rendermime: IRenderMimeRegistry;
- /**
- * The language preference for the model.
- */
- languagePreference?: string;
- /**
- * A factory for creating content.
- */
- contentFactory?: IContentFactory;
- /**
- * A configuration object for the cell editor settings.
- */
- editorConfig?: IEditorConfig;
- /**
- * A configuration object for notebook settings.
- */
- notebookConfig?: INotebookConfig;
- /**
- * The service used to look up mime types.
- */
- mimeTypeService: IEditorMimeTypeService;
- }
- /**
- * A factory for creating notebook content.
- *
- * #### Notes
- * This extends the content factory of the cell itself, which extends the content
- * factory of the output area and input area. The result is that there is a single
- * factory for creating all child content of a notebook.
- */
- export interface IContentFactory extends Cell.IContentFactory {
- /**
- * Create a new code cell widget.
- */
- createCodeCell(
- options: CodeCell.IOptions,
- parent: StaticNotebook
- ): CodeCell;
- /**
- * Create a new markdown cell widget.
- */
- createMarkdownCell(
- options: MarkdownCell.IOptions,
- parent: StaticNotebook
- ): MarkdownCell;
- /**
- * Create a new raw cell widget.
- */
- createRawCell(options: RawCell.IOptions, parent: StaticNotebook): RawCell;
- }
- /**
- * A config object for the cell editors.
- */
- export interface IEditorConfig {
- /**
- * Config options for code cells.
- */
- readonly code: Partial<CodeEditor.IConfig>;
- /**
- * Config options for markdown cells.
- */
- readonly markdown: Partial<CodeEditor.IConfig>;
- /**
- * Config options for raw cells.
- */
- readonly raw: Partial<CodeEditor.IConfig>;
- }
- /**
- * Default configuration options for cell editors.
- */
- export const defaultEditorConfig: IEditorConfig = {
- code: {
- ...CodeEditor.defaultConfig,
- lineWrap: 'off',
- matchBrackets: true,
- autoClosingBrackets: true
- },
- markdown: {
- ...CodeEditor.defaultConfig,
- lineWrap: 'on',
- matchBrackets: false,
- autoClosingBrackets: false
- },
- raw: {
- ...CodeEditor.defaultConfig,
- lineWrap: 'on',
- matchBrackets: false,
- autoClosingBrackets: false
- }
- };
- /**
- * A config object for the notebook widget
- */
- export interface INotebookConfig {
- /**
- * Enable scrolling past the last cell
- */
- scrollPastEnd: boolean;
- /**
- * The default type for new notebook cells.
- */
- defaultCell: nbformat.CellType;
- /**
- * Should timing be recorded in metadata
- */
- recordTiming: boolean;
- }
- /**
- * Default configuration options for notebooks.
- */
- export const defaultNotebookConfig: INotebookConfig = {
- scrollPastEnd: true,
- defaultCell: 'code',
- recordTiming: false
- };
- /**
- * The default implementation of an `IContentFactory`.
- */
- export class ContentFactory extends Cell.ContentFactory
- implements IContentFactory {
- /**
- * Create a new code cell widget.
- *
- * #### Notes
- * If no cell content factory is passed in with the options, the one on the
- * notebook content factory is used.
- */
- createCodeCell(
- options: CodeCell.IOptions,
- parent: StaticNotebook
- ): CodeCell {
- if (!options.contentFactory) {
- options.contentFactory = this;
- }
- return new CodeCell(options).initializeState();
- }
- /**
- * Create a new markdown cell widget.
- *
- * #### Notes
- * If no cell content factory is passed in with the options, the one on the
- * notebook content factory is used.
- */
- createMarkdownCell(
- options: MarkdownCell.IOptions,
- parent: StaticNotebook
- ): MarkdownCell {
- if (!options.contentFactory) {
- options.contentFactory = this;
- }
- return new MarkdownCell(options).initializeState();
- }
- /**
- * Create a new raw cell widget.
- *
- * #### Notes
- * If no cell content factory is passed in with the options, the one on the
- * notebook content factory is used.
- */
- createRawCell(options: RawCell.IOptions, parent: StaticNotebook): RawCell {
- if (!options.contentFactory) {
- options.contentFactory = this;
- }
- return new RawCell(options).initializeState();
- }
- }
- /**
- * A namespace for the staic notebook content factory.
- */
- export namespace ContentFactory {
- /**
- * Options for the content factory.
- */
- export interface IOptions extends Cell.ContentFactory.IOptions {}
- }
- /**
- * Default content factory for the static notebook widget.
- */
- export const defaultContentFactory: IContentFactory = new ContentFactory();
- }
- /**
- * A notebook widget that supports interactivity.
- */
- export class Notebook extends StaticNotebook {
- /**
- * Construct a notebook widget.
- */
- constructor(options: Notebook.IOptions) {
- super(Private.processNotebookOptions(options));
- this.node.tabIndex = -1; // Allow the widget to take focus.
- // Allow the node to scroll while dragging items.
- this.node.setAttribute('data-lm-dragscroll', 'true');
- }
- /**
- * A signal emitted when the active cell changes.
- *
- * #### Notes
- * This can be due to the active index changing or the
- * cell at the active index changing.
- */
- get activeCellChanged(): ISignal<this, Cell> {
- return this._activeCellChanged;
- }
- /**
- * A signal emitted when the state of the notebook changes.
- */
- get stateChanged(): ISignal<this, IChangedArgs<any>> {
- return this._stateChanged;
- }
- /**
- * A signal emitted when the selection state of the notebook changes.
- */
- get selectionChanged(): ISignal<this, void> {
- return this._selectionChanged;
- }
- /**
- * The interactivity mode of the notebook.
- */
- get mode(): NotebookMode {
- return this._mode;
- }
- set mode(newValue: NotebookMode) {
- const activeCell = this.activeCell;
- if (!activeCell) {
- newValue = 'command';
- }
- if (newValue === this._mode) {
- this._ensureFocus();
- return;
- }
- // Post an update request.
- this.update();
- const oldValue = this._mode;
- this._mode = newValue;
- if (newValue === 'edit') {
- // Edit mode deselects all cells.
- each(this.widgets, widget => {
- this.deselect(widget);
- });
- // Edit mode unrenders an active markdown widget.
- if (activeCell instanceof MarkdownCell) {
- activeCell.rendered = false;
- }
- activeCell!.inputHidden = false;
- } else {
- // Focus on the notebook document, which blurs the active cell.
- this.node.focus();
- }
- this._stateChanged.emit({ name: 'mode', oldValue, newValue });
- this._ensureFocus();
- }
- /**
- * The active cell index of the notebook.
- *
- * #### Notes
- * The index will be clamped to the bounds of the notebook cells.
- */
- get activeCellIndex(): number {
- if (!this.model) {
- return -1;
- }
- return this.model.cells.length ? this._activeCellIndex : -1;
- }
- set activeCellIndex(newValue: number) {
- const oldValue = this._activeCellIndex;
- if (!this.model || !this.model.cells.length) {
- newValue = -1;
- } else {
- newValue = Math.max(newValue, 0);
- newValue = Math.min(newValue, this.model.cells.length - 1);
- }
- this._activeCellIndex = newValue;
- const cell = this.widgets[newValue];
- if (cell !== this._activeCell) {
- // Post an update request.
- this.update();
- this._activeCell = cell;
- this._activeCellChanged.emit(cell);
- }
- if (this.mode === 'edit' && cell instanceof MarkdownCell) {
- cell.rendered = false;
- }
- this._ensureFocus();
- if (newValue === oldValue) {
- return;
- }
- this._trimSelections();
- this._stateChanged.emit({ name: 'activeCellIndex', oldValue, newValue });
- }
- /**
- * Get the active cell widget.
- *
- * #### Notes
- * This is a cell or `null` if there is no active cell.
- */
- get activeCell(): Cell | null {
- return this._activeCell;
- }
- /**
- * Dispose of the resources held by the widget.
- */
- dispose(): void {
- if (this.isDisposed) {
- return;
- }
- this._activeCell = null;
- super.dispose();
- }
- /**
- * Select a cell widget.
- *
- * #### Notes
- * It is a no-op if the value does not change.
- * It will emit the `selectionChanged` signal.
- */
- select(widget: Cell): void {
- if (Private.selectedProperty.get(widget)) {
- return;
- }
- Private.selectedProperty.set(widget, true);
- this._selectionChanged.emit(void 0);
- this.update();
- }
- /**
- * Deselect a cell widget.
- *
- * #### Notes
- * It is a no-op if the value does not change.
- * It will emit the `selectionChanged` signal.
- */
- deselect(widget: Cell): void {
- if (!Private.selectedProperty.get(widget)) {
- return;
- }
- Private.selectedProperty.set(widget, false);
- this._selectionChanged.emit(void 0);
- this.update();
- }
- /**
- * Whether a cell is selected.
- */
- isSelected(widget: Cell): boolean {
- return Private.selectedProperty.get(widget);
- }
- /**
- * Whether a cell is selected or is the active cell.
- */
- isSelectedOrActive(widget: Cell): boolean {
- if (widget === this._activeCell) {
- return true;
- }
- return Private.selectedProperty.get(widget);
- }
- /**
- * Deselect all of the cells.
- */
- deselectAll(): void {
- let changed = false;
- each(this.widgets, widget => {
- if (Private.selectedProperty.get(widget)) {
- changed = true;
- }
- Private.selectedProperty.set(widget, false);
- });
- if (changed) {
- this._selectionChanged.emit(void 0);
- }
- // Make sure we have a valid active cell.
- this.activeCellIndex = this.activeCellIndex; // eslint-disable-line
- this.update();
- }
- /**
- * Move the head of an existing contiguous selection to extend the selection.
- *
- * @param index - The new head of the existing selection.
- *
- * #### Notes
- * If there is no existing selection, the active cell is considered an
- * existing one-cell selection.
- *
- * If the new selection is a single cell, that cell becomes the active cell
- * and all cells are deselected.
- *
- * There is no change if there are no cells (i.e., activeCellIndex is -1).
- */
- extendContiguousSelectionTo(index: number): void {
- let { head, anchor } = this.getContiguousSelection();
- let i: number;
- // Handle the case of no current selection.
- if (anchor === null || head === null) {
- if (index === this.activeCellIndex) {
- // Already collapsed selection, nothing more to do.
- return;
- }
- // We will start a new selection below.
- head = this.activeCellIndex;
- anchor = this.activeCellIndex;
- }
- // Move the active cell. We do this before the collapsing shortcut below.
- this.activeCellIndex = index;
- // Make sure the index is valid, according to the rules for setting and clipping the
- // active cell index. This may change the index.
- index = this.activeCellIndex;
- // Collapse the selection if it is only the active cell.
- if (index === anchor) {
- this.deselectAll();
- return;
- }
- let selectionChanged = false;
- if (head < index) {
- if (head < anchor) {
- Private.selectedProperty.set(this.widgets[head], false);
- selectionChanged = true;
- }
- // Toggle everything strictly between head and index except anchor.
- for (i = head + 1; i < index; i++) {
- if (i !== anchor) {
- Private.selectedProperty.set(
- this.widgets[i],
- !Private.selectedProperty.get(this.widgets[i])
- );
- selectionChanged = true;
- }
- }
- } else if (index < head) {
- if (anchor < head) {
- Private.selectedProperty.set(this.widgets[head], false);
- selectionChanged = true;
- }
- // Toggle everything strictly between index and head except anchor.
- for (i = index + 1; i < head; i++) {
- if (i !== anchor) {
- Private.selectedProperty.set(
- this.widgets[i],
- !Private.selectedProperty.get(this.widgets[i])
- );
- selectionChanged = true;
- }
- }
- }
- // Anchor and index should *always* be selected.
- if (!Private.selectedProperty.get(this.widgets[anchor])) {
- selectionChanged = true;
- }
- Private.selectedProperty.set(this.widgets[anchor], true);
- if (!Private.selectedProperty.get(this.widgets[index])) {
- selectionChanged = true;
- }
- Private.selectedProperty.set(this.widgets[index], true);
- if (selectionChanged) {
- this._selectionChanged.emit(void 0);
- }
- }
- /**
- * Get the head and anchor of a contiguous cell selection.
- *
- * The head of a contiguous selection is always the active cell.
- *
- * If there are no cells selected, `{head: null, anchor: null}` is returned.
- *
- * Throws an error if the currently selected cells do not form a contiguous
- * selection.
- */
- getContiguousSelection():
- | { head: number; anchor: number }
- | { head: null; anchor: null } {
- const cells = this.widgets;
- const first = ArrayExt.findFirstIndex(cells, c => this.isSelected(c));
- // Return early if no cells are selected.
- if (first === -1) {
- return { head: null, anchor: null };
- }
- const last = ArrayExt.findLastIndex(
- cells,
- c => this.isSelected(c),
- -1,
- first
- );
- // Check that the selection is contiguous.
- for (let i = first; i <= last; i++) {
- if (!this.isSelected(cells[i])) {
- throw new Error('Selection not contiguous');
- }
- }
- // Check that the active cell is one of the endpoints of the selection.
- const activeIndex = this.activeCellIndex;
- if (first !== activeIndex && last !== activeIndex) {
- throw new Error('Active cell not at endpoint of selection');
- }
- // Determine the head and anchor of the selection.
- if (first === activeIndex) {
- return { head: first, anchor: last };
- } else {
- return { head: last, anchor: first };
- }
- }
- /**
- * Scroll so that the given position is centered.
- *
- * @param position - The vertical position in the notebook widget.
- *
- * @param threshold - An optional threshold for the scroll (0-50, defaults to
- * 25).
- *
- * #### Notes
- * If the position is within the threshold percentage of the widget height,
- * measured from the center of the widget, the scroll position will not be
- * changed. A threshold of 0 means we will always scroll so the position is
- * centered, and a threshold of 50 means scrolling only happens if position is
- * outside the current window.
- */
- scrollToPosition(position: number, threshold = 25): void {
- const node = this.node;
- const ar = node.getBoundingClientRect();
- const delta = position - ar.top - ar.height / 2;
- if (Math.abs(delta) > (ar.height * threshold) / 100) {
- node.scrollTop += delta;
- }
- }
- /**
- * Set URI fragment identifier.
- */
- setFragment(fragment: string): void {
- // Wait all cells are rendered then set fragment and update.
- void Promise.all(this.widgets.map(widget => widget.ready)).then(() => {
- this._fragment = fragment;
- this.update();
- });
- }
- /**
- * Handle the DOM events for the widget.
- *
- * @param event - The DOM event sent to the widget.
- *
- * #### Notes
- * This method implements the DOM `EventListener` interface and is
- * called in response to events on the notebook panel's node. It should
- * not be called directly by user code.
- */
- handleEvent(event: Event): void {
- if (!this.model) {
- return;
- }
- switch (event.type) {
- case 'contextmenu':
- if (event.eventPhase === Event.CAPTURING_PHASE) {
- this._evtContextMenuCapture(event as PointerEvent);
- }
- break;
- case 'mousedown':
- if (event.eventPhase === Event.CAPTURING_PHASE) {
- this._evtMouseDownCapture(event as MouseEvent);
- } else {
- this._evtMouseDown(event as MouseEvent);
- }
- break;
- case 'mouseup':
- if (event.currentTarget === document) {
- this._evtDocumentMouseup(event as MouseEvent);
- }
- break;
- case 'mousemove':
- if (event.currentTarget === document) {
- this._evtDocumentMousemove(event as MouseEvent);
- }
- break;
- case 'keydown':
- this._ensureFocus(true);
- break;
- case 'dblclick':
- this._evtDblClick(event as MouseEvent);
- break;
- case 'focusin':
- this._evtFocusIn(event as MouseEvent);
- break;
- case 'focusout':
- this._evtFocusOut(event as MouseEvent);
- break;
- case 'lm-dragenter':
- this._evtDragEnter(event as IDragEvent);
- break;
- case 'lm-dragleave':
- this._evtDragLeave(event as IDragEvent);
- break;
- case 'lm-dragover':
- this._evtDragOver(event as IDragEvent);
- break;
- case 'lm-drop':
- this._evtDrop(event as IDragEvent);
- break;
- default:
- break;
- }
- }
- /**
- * Handle `after-attach` messages for the widget.
- */
- protected onAfterAttach(msg: Message): void {
- super.onAfterAttach(msg);
- const node = this.node;
- node.addEventListener('contextmenu', this, true);
- node.addEventListener('mousedown', this, true);
- node.addEventListener('mousedown', this);
- node.addEventListener('keydown', this);
- node.addEventListener('dblclick', this);
- node.addEventListener('focusin', this);
- node.addEventListener('focusout', this);
- // Capture drag events for the notebook widget
- // in order to preempt the drag/drop handlers in the
- // code editor widgets, which can take text data.
- node.addEventListener('lm-dragenter', this, true);
- node.addEventListener('lm-dragleave', this, true);
- node.addEventListener('lm-dragover', this, true);
- node.addEventListener('lm-drop', this, true);
- }
- /**
- * Handle `before-detach` messages for the widget.
- */
- protected onBeforeDetach(msg: Message): void {
- const node = this.node;
- node.removeEventListener('contextmenu', this, true);
- node.removeEventListener('mousedown', this, true);
- node.removeEventListener('mousedown', this);
- node.removeEventListener('keydown', this);
- node.removeEventListener('dblclick', this);
- node.removeEventListener('focusin', this);
- node.removeEventListener('focusout', this);
- node.removeEventListener('lm-dragenter', this, true);
- node.removeEventListener('lm-dragleave', this, true);
- node.removeEventListener('lm-dragover', this, true);
- node.removeEventListener('lm-drop', this, true);
- document.removeEventListener('mousemove', this, true);
- document.removeEventListener('mouseup', this, true);
- }
- /**
- * A message handler invoked on an `'after-show'` message.
- */
- protected onAfterShow(msg: Message): void {
- this._checkCacheOnNextResize = true;
- }
- /**
- * A message handler invoked on a `'resize'` message.
- */
- protected onResize(msg: Widget.ResizeMessage): void {
- if (!this._checkCacheOnNextResize) {
- return super.onResize(msg);
- }
- this._checkCacheOnNextResize = false;
- const cache = this._cellLayoutStateCache;
- const width = parseInt(this.node.style.width, 10);
- if (cache) {
- if (width === cache.width) {
- // Cache identical, do nothing
- return;
- }
- }
- // Update cache
- this._cellLayoutStateCache = { width };
- // Fallback:
- for (const w of this.widgets) {
- if (w instanceof Cell) {
- w.editorWidget.update();
- }
- }
- }
- /**
- * A message handler invoked on an `'before-hide'` message.
- */
- protected onBeforeHide(msg: Message): void {
- // Update cache
- const width = parseInt(this.node.style.width, 10);
- this._cellLayoutStateCache = { width };
- }
- /**
- * Handle `'activate-request'` messages.
- */
- protected onActivateRequest(msg: Message): void {
- this._ensureFocus(true);
- }
- /**
- * Handle `update-request` messages sent to the widget.
- */
- protected onUpdateRequest(msg: Message): void {
- const activeCell = this.activeCell;
- // Set the appropriate classes on the cells.
- if (this.mode === 'edit') {
- this.addClass(EDIT_CLASS);
- this.removeClass(COMMAND_CLASS);
- } else {
- this.addClass(COMMAND_CLASS);
- this.removeClass(EDIT_CLASS);
- }
- if (activeCell) {
- activeCell.addClass(ACTIVE_CLASS);
- }
- let count = 0;
- each(this.widgets, widget => {
- if (widget !== activeCell) {
- widget.removeClass(ACTIVE_CLASS);
- }
- widget.removeClass(OTHER_SELECTED_CLASS);
- if (this.isSelectedOrActive(widget)) {
- widget.addClass(SELECTED_CLASS);
- count++;
- } else {
- widget.removeClass(SELECTED_CLASS);
- }
- });
- if (count > 1) {
- activeCell?.addClass(OTHER_SELECTED_CLASS);
- }
- if (this._fragment) {
- let el;
- try {
- el = this.node.querySelector(this._fragment);
- } catch (error) {
- console.warn('Unable to set URI fragment identifier', error);
- }
- if (el) {
- el.scrollIntoView();
- }
- this._fragment = '';
- }
- }
- /**
- * Handle a cell being inserted.
- */
- protected onCellInserted(index: number, cell: Cell): void {
- if (this.model && this.model.modelDB.isCollaborative) {
- const modelDB = this.model.modelDB;
- void modelDB.connected.then(() => {
- if (!cell.isDisposed) {
- // Setup the selection style for collaborators.
- const localCollaborator = modelDB.collaborators!.localCollaborator;
- cell.editor.uuid = localCollaborator.sessionId;
- cell.editor.selectionStyle = {
- ...CodeEditor.defaultSelectionStyle,
- color: localCollaborator.color
- };
- }
- });
- }
- cell.editor.edgeRequested.connect(this._onEdgeRequest, this);
- // If the insertion happened above, increment the active cell
- // index, otherwise it stays the same.
- this.activeCellIndex =
- index <= this.activeCellIndex
- ? this.activeCellIndex + 1
- : this.activeCellIndex;
- }
- /**
- * Handle a cell being moved.
- */
- protected onCellMoved(fromIndex: number, toIndex: number): void {
- const i = this.activeCellIndex;
- if (fromIndex === i) {
- this.activeCellIndex = toIndex;
- } else if (fromIndex < i && i <= toIndex) {
- this.activeCellIndex--;
- } else if (toIndex <= i && i < fromIndex) {
- this.activeCellIndex++;
- }
- }
- /**
- * Handle a cell being removed.
- */
- protected onCellRemoved(index: number, cell: Cell): void {
- // If the removal happened above, decrement the active
- // cell index, otherwise it stays the same.
- this.activeCellIndex =
- index <= this.activeCellIndex
- ? this.activeCellIndex - 1
- : this.activeCellIndex;
- if (this.isSelected(cell)) {
- this._selectionChanged.emit(void 0);
- }
- }
- /**
- * Handle a new model.
- */
- protected onModelChanged(
- oldValue: INotebookModel,
- newValue: INotebookModel
- ): void {
- super.onModelChanged(oldValue, newValue);
- // Try to set the active cell index to 0.
- // It will be set to `-1` if there is no new model or the model is empty.
- this.activeCellIndex = 0;
- }
- /**
- * Handle edge request signals from cells.
- */
- private _onEdgeRequest(
- editor: CodeEditor.IEditor,
- location: CodeEditor.EdgeLocation
- ): void {
- const prev = this.activeCellIndex;
- if (location === 'top') {
- this.activeCellIndex--;
- // Move the cursor to the first position on the last line.
- if (this.activeCellIndex < prev) {
- const editor = this.activeCell!.editor;
- const lastLine = editor.lineCount - 1;
- editor.setCursorPosition({ line: lastLine, column: 0 });
- }
- } else if (location === 'bottom') {
- this.activeCellIndex++;
- // Move the cursor to the first character.
- if (this.activeCellIndex > prev) {
- const editor = this.activeCell!.editor;
- editor.setCursorPosition({ line: 0, column: 0 });
- }
- }
- this.mode = 'edit';
- }
- /**
- * Ensure that the notebook has proper focus.
- */
- private _ensureFocus(force = false): void {
- const activeCell = this.activeCell;
- if (this.mode === 'edit' && activeCell) {
- if (!activeCell.editor.hasFocus()) {
- activeCell.editor.focus();
- }
- }
- if (force && !this.node.contains(document.activeElement)) {
- this.node.focus();
- }
- }
- /**
- * Find the cell index containing the target html element.
- *
- * #### Notes
- * Returns -1 if the cell is not found.
- */
- private _findCell(node: HTMLElement): number {
- // Trace up the DOM hierarchy to find the root cell node.
- // Then find the corresponding child and select it.
- let n: HTMLElement | null = node;
- while (n && n !== this.node) {
- if (n.classList.contains(NB_CELL_CLASS)) {
- const i = ArrayExt.findFirstIndex(
- this.widgets,
- widget => widget.node === n
- );
- if (i !== -1) {
- return i;
- }
- break;
- }
- n = n.parentElement;
- }
- return -1;
- }
- /**
- * Find the target of html mouse event and cell index containing this target.
- *
- * #### Notes
- * Returned index is -1 if the cell is not found.
- */
- private _findEventTargetAndCell(event: MouseEvent): [HTMLElement, number] {
- let target = event.target as HTMLElement;
- let index = this._findCell(target);
- if (index === -1) {
- // `event.target` sometimes gives an orphaned node in Firefox 57, which
- // can have `null` anywhere in its parent line. If we fail to find a cell
- // using `event.target`, try again using a target reconstructed from the
- // position of the click event.
- target = document.elementFromPoint(
- event.clientX,
- event.clientY
- ) as HTMLElement;
- index = this._findCell(target);
- }
- return [target, index];
- }
- /**
- * Handle `contextmenu` event.
- */
- private _evtContextMenuCapture(event: PointerEvent): void {
- // Allow the event to propagate un-modified if the user
- // is holding the shift-key (and probably requesting
- // the native context menu).
- if (event.shiftKey) {
- return;
- }
- const [target, index] = this._findEventTargetAndCell(event);
- const widget = this.widgets[index];
- if (widget && widget.editorWidget.node.contains(target)) {
- // Prevent CodeMirror from focusing the editor.
- // TODO: find an editor-agnostic solution.
- event.preventDefault();
- }
- }
- /**
- * Handle `mousedown` event in the capture phase for the widget.
- */
- private _evtMouseDownCapture(event: MouseEvent): void {
- const { button, shiftKey } = event;
- const [target, index] = this._findEventTargetAndCell(event);
- const widget = this.widgets[index];
- // On OS X, the context menu may be triggered with ctrl-left-click. In
- // Firefox, ctrl-left-click gives an event with button 2, but in Chrome,
- // ctrl-left-click gives an event with button 0 with the ctrl modifier.
- if (
- button === 2 &&
- !shiftKey &&
- widget &&
- widget.editorWidget.node.contains(target)
- ) {
- this.mode = 'command';
- // Prevent CodeMirror from focusing the editor.
- // TODO: find an editor-agnostic solution.
- event.preventDefault();
- }
- }
- /**
- * Handle `mousedown` events for the widget.
- */
- private _evtMouseDown(event: MouseEvent): void {
- const { button, shiftKey } = event;
- // We only handle main or secondary button actions.
- if (!(button === 0 || button === 2)) {
- return;
- }
- // Shift right-click gives the browser default behavior.
- if (shiftKey && button === 2) {
- return;
- }
- const [target, index] = this._findEventTargetAndCell(event);
- const widget = this.widgets[index];
- let targetArea: 'input' | 'prompt' | 'cell' | 'notebook';
- if (widget) {
- if (widget.editorWidget.node.contains(target)) {
- targetArea = 'input';
- } else if (widget.promptNode.contains(target)) {
- targetArea = 'prompt';
- } else {
- targetArea = 'cell';
- }
- } else {
- targetArea = 'notebook';
- }
- // Make sure we go to command mode if the click isn't in the cell editor If
- // we do click in the cell editor, the editor handles the focus event to
- // switch to edit mode.
- if (targetArea !== 'input') {
- this.mode = 'command';
- }
- if (targetArea === 'notebook') {
- this.deselectAll();
- } else if (targetArea === 'prompt' || targetArea === 'cell') {
- // We don't want to prevent the default selection behavior
- // if there is currently text selected in an output.
- const hasSelection = (window.getSelection() ?? '').toString() !== '';
- if (button === 0 && shiftKey && !hasSelection) {
- // Prevent browser selecting text in prompt or output
- event.preventDefault();
- // Shift-click - extend selection
- try {
- this.extendContiguousSelectionTo(index);
- } catch (e) {
- console.error(e);
- this.deselectAll();
- return;
- }
- // Enter selecting mode
- this._mouseMode = 'select';
- document.addEventListener('mouseup', this, true);
- document.addEventListener('mousemove', this, true);
- } else if (button === 0 && !shiftKey) {
- // Prepare to start a drag if we are on the drag region.
- if (targetArea === 'prompt') {
- // Prepare for a drag start
- this._dragData = {
- pressX: event.clientX,
- pressY: event.clientY,
- index: index
- };
- // Enter possible drag mode
- this._mouseMode = 'couldDrag';
- document.addEventListener('mouseup', this, true);
- document.addEventListener('mousemove', this, true);
- event.preventDefault();
- }
- if (!this.isSelectedOrActive(widget)) {
- this.deselectAll();
- this.activeCellIndex = index;
- }
- } else if (button === 2) {
- if (!this.isSelectedOrActive(widget)) {
- this.deselectAll();
- this.activeCellIndex = index;
- }
- event.preventDefault();
- }
- } else if (targetArea === 'input') {
- if (button === 2 && !this.isSelectedOrActive(widget)) {
- this.deselectAll();
- this.activeCellIndex = index;
- }
- }
- // If we didn't set focus above, make sure we get focus now.
- this._ensureFocus(true);
- }
- /**
- * Handle the `'mouseup'` event on the document.
- */
- private _evtDocumentMouseup(event: MouseEvent): void {
- event.preventDefault();
- event.stopPropagation();
- // Remove the event listeners we put on the document
- document.removeEventListener('mousemove', this, true);
- document.removeEventListener('mouseup', this, true);
- if (this._mouseMode === 'couldDrag') {
- // We didn't end up dragging if we are here, so treat it as a click event.
- const [, index] = this._findEventTargetAndCell(event);
- this.deselectAll();
- this.activeCellIndex = index;
- }
- this._mouseMode = null;
- }
- /**
- * Handle the `'mousemove'` event for the widget.
- */
- private _evtDocumentMousemove(event: MouseEvent): void {
- event.preventDefault();
- event.stopPropagation();
- // If in select mode, update the selection
- switch (this._mouseMode) {
- case 'select':
- const target = event.target as HTMLElement;
- const index = this._findCell(target);
- if (index !== -1) {
- this.extendContiguousSelectionTo(index);
- }
- break;
- case 'couldDrag':
- // Check for a drag initialization.
- const data = this._dragData!;
- const dx = Math.abs(event.clientX - data.pressX);
- const dy = Math.abs(event.clientY - data.pressY);
- if (dx >= DRAG_THRESHOLD || dy >= DRAG_THRESHOLD) {
- this._mouseMode = null;
- this._startDrag(data.index, event.clientX, event.clientY);
- }
- break;
- default:
- break;
- }
- }
- /**
- * Handle the `'lm-dragenter'` event for the widget.
- */
- private _evtDragEnter(event: IDragEvent): void {
- if (!event.mimeData.hasData(JUPYTER_CELL_MIME)) {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- const target = event.target as HTMLElement;
- const index = this._findCell(target);
- if (index === -1) {
- return;
- }
- const widget = (this.layout as PanelLayout).widgets[index];
- widget.node.classList.add(DROP_TARGET_CLASS);
- }
- /**
- * Handle the `'lm-dragleave'` event for the widget.
- */
- private _evtDragLeave(event: IDragEvent): void {
- if (!event.mimeData.hasData(JUPYTER_CELL_MIME)) {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- const elements = this.node.getElementsByClassName(DROP_TARGET_CLASS);
- if (elements.length) {
- (elements[0] as HTMLElement).classList.remove(DROP_TARGET_CLASS);
- }
- }
- /**
- * Handle the `'lm-dragover'` event for the widget.
- */
- private _evtDragOver(event: IDragEvent): void {
- if (!event.mimeData.hasData(JUPYTER_CELL_MIME)) {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- event.dropAction = event.proposedAction;
- const elements = this.node.getElementsByClassName(DROP_TARGET_CLASS);
- if (elements.length) {
- (elements[0] as HTMLElement).classList.remove(DROP_TARGET_CLASS);
- }
- const target = event.target as HTMLElement;
- const index = this._findCell(target);
- if (index === -1) {
- return;
- }
- const widget = (this.layout as PanelLayout).widgets[index];
- widget.node.classList.add(DROP_TARGET_CLASS);
- }
- /**
- * Handle the `'lm-drop'` event for the widget.
- */
- private _evtDrop(event: IDragEvent): void {
- if (!event.mimeData.hasData(JUPYTER_CELL_MIME)) {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- if (event.proposedAction === 'none') {
- event.dropAction = 'none';
- return;
- }
- let target = event.target as HTMLElement;
- while (target && target.parentElement) {
- if (target.classList.contains(DROP_TARGET_CLASS)) {
- target.classList.remove(DROP_TARGET_CLASS);
- break;
- }
- target = target.parentElement;
- }
- // Model presence should be checked before calling event handlers
- const model = this.model!;
- const source: Notebook = event.source;
- if (source === this) {
- // Handle the case where we are moving cells within
- // the same notebook.
- event.dropAction = 'move';
- const toMove: Cell[] = event.mimeData.getData('internal:cells');
- // Compute the to/from indices for the move.
- let fromIndex = ArrayExt.firstIndexOf(this.widgets, toMove[0]);
- let toIndex = this._findCell(target);
- // This check is needed for consistency with the view.
- if (toIndex !== -1 && toIndex > fromIndex) {
- toIndex -= 1;
- } else if (toIndex === -1) {
- // If the drop is within the notebook but not on any cell,
- // most often this means it is past the cell areas, so
- // set it to move the cells to the end of the notebook.
- toIndex = this.widgets.length - 1;
- }
- // Don't move if we are within the block of selected cells.
- if (toIndex >= fromIndex && toIndex < fromIndex + toMove.length) {
- return;
- }
- // Move the cells one by one
- model.cells.beginCompoundOperation();
- if (fromIndex < toIndex) {
- each(toMove, cellWidget => {
- model.cells.move(fromIndex, toIndex);
- });
- } else if (fromIndex > toIndex) {
- each(toMove, cellWidget => {
- model.cells.move(fromIndex++, toIndex++);
- });
- }
- model.cells.endCompoundOperation();
- } else {
- // Handle the case where we are copying cells between
- // notebooks.
- event.dropAction = 'copy';
- // Find the target cell and insert the copied cells.
- let index = this._findCell(target);
- if (index === -1) {
- index = this.widgets.length;
- }
- const start = index;
- const values = event.mimeData.getData(JUPYTER_CELL_MIME);
- const factory = model.contentFactory;
- // Insert the copies of the original cells.
- model.cells.beginCompoundOperation();
- each(values, (cell: nbformat.ICell) => {
- let value: ICellModel;
- switch (cell.cell_type) {
- case 'code':
- value = factory.createCodeCell({ cell });
- break;
- case 'markdown':
- value = factory.createMarkdownCell({ cell });
- break;
- default:
- value = factory.createRawCell({ cell });
- break;
- }
- model.cells.insert(index++, value);
- });
- model.cells.endCompoundOperation();
- // Select the inserted cells.
- this.deselectAll();
- this.activeCellIndex = start;
- this.extendContiguousSelectionTo(index - 1);
- }
- }
- /**
- * Start a drag event.
- */
- private _startDrag(index: number, clientX: number, clientY: number): void {
- const cells = this.model!.cells;
- const selected: nbformat.ICell[] = [];
- const toMove: Cell[] = [];
- each(this.widgets, (widget, i) => {
- const cell = cells.get(i);
- if (this.isSelectedOrActive(widget)) {
- widget.addClass(DROP_SOURCE_CLASS);
- selected.push(cell.toJSON());
- toMove.push(widget);
- }
- });
- const activeCell = this.activeCell;
- let dragImage: HTMLElement | null = null;
- let countString: string;
- if (activeCell?.model.type === 'code') {
- const executionCount = (activeCell.model as ICodeCellModel)
- .executionCount;
- countString = ' ';
- if (executionCount) {
- countString = executionCount.toString();
- }
- } else {
- countString = '';
- }
- // Create the drag image.
- dragImage = Private.createDragImage(
- selected.length,
- countString,
- activeCell?.model.value.text.split('\n')[0].slice(0, 26) ?? ''
- );
- // Set up the drag event.
- this._drag = new Drag({
- mimeData: new MimeData(),
- dragImage,
- supportedActions: 'copy-move',
- proposedAction: 'copy',
- source: this
- });
- this._drag.mimeData.setData(JUPYTER_CELL_MIME, selected);
- // Add mimeData for the fully reified cell widgets, for the
- // case where the target is in the same notebook and we
- // can just move the cells.
- this._drag.mimeData.setData('internal:cells', toMove);
- // Add mimeData for the text content of the selected cells,
- // allowing for drag/drop into plain text fields.
- const textContent = toMove.map(cell => cell.model.value.text).join('\n');
- this._drag.mimeData.setData('text/plain', textContent);
- // Remove mousemove and mouseup listeners and start the drag.
- document.removeEventListener('mousemove', this, true);
- document.removeEventListener('mouseup', this, true);
- this._mouseMode = null;
- void this._drag.start(clientX, clientY).then(action => {
- if (this.isDisposed) {
- return;
- }
- this._drag = null;
- each(toMove, widget => {
- widget.removeClass(DROP_SOURCE_CLASS);
- });
- });
- }
- /**
- * Handle `focus` events for the widget.
- */
- private _evtFocusIn(event: MouseEvent): void {
- const target = event.target as HTMLElement;
- const index = this._findCell(target);
- if (index !== -1) {
- const widget = this.widgets[index];
- // If the editor itself does not have focus, ensure command mode.
- if (!widget.editorWidget.node.contains(target)) {
- this.mode = 'command';
- }
- this.activeCellIndex = index;
- // If the editor has focus, ensure edit mode.
- const node = widget.editorWidget.node;
- if (node.contains(target)) {
- this.mode = 'edit';
- }
- } else {
- // No cell has focus, ensure command mode.
- this.mode = 'command';
- }
- if (this.mode === 'command' && target !== this.node) {
- delete this.node.dataset[CODE_RUNNER];
- } else {
- this.node.dataset[CODE_RUNNER] = 'true';
- }
- }
- /**
- * Handle `focusout` events for the notebook.
- */
- private _evtFocusOut(event: MouseEvent): void {
- const relatedTarget = event.relatedTarget as HTMLElement;
- // Bail if the window is losing focus, to preserve edit mode. This test
- // assumes that we explicitly focus things rather than calling blur()
- if (!relatedTarget) {
- return;
- }
- // Bail if the item gaining focus is another cell,
- // and we should not be entering command mode.
- const index = this._findCell(relatedTarget);
- if (index !== -1) {
- const widget = this.widgets[index];
- if (widget.editorWidget.node.contains(relatedTarget)) {
- return;
- }
- }
- // Otherwise enter command mode if not already.
- if (this.mode !== 'command') {
- this.mode = 'command';
- // Switching to command mode currently focuses the notebook element, so
- // refocus the relatedTarget so the focus actually switches as intended.
- if (relatedTarget) {
- relatedTarget.focus();
- }
- }
- }
- /**
- * Handle `dblclick` events for the widget.
- */
- private _evtDblClick(event: MouseEvent): void {
- const model = this.model;
- if (!model) {
- return;
- }
- this.deselectAll();
- const [target, index] = this._findEventTargetAndCell(event);
- if (index === -1) {
- return;
- }
- this.activeCellIndex = index;
- if (model.cells.get(index).type === 'markdown') {
- const widget = this.widgets[index] as MarkdownCell;
- widget.rendered = false;
- } else if (target.localName === 'img') {
- target.classList.toggle(UNCONFINED_CLASS);
- }
- }
- /**
- * Remove selections from inactive cells to avoid
- * spurious cursors.
- */
- private _trimSelections(): void {
- for (let i = 0; i < this.widgets.length; i++) {
- if (i !== this._activeCellIndex) {
- const cell = this.widgets[i];
- cell.model.selections.delete(cell.editor.uuid);
- }
- }
- }
- private _activeCellIndex = -1;
- private _activeCell: Cell | null = null;
- private _mode: NotebookMode = 'command';
- private _drag: Drag | null = null;
- private _fragment = '';
- private _dragData: {
- pressX: number;
- pressY: number;
- index: number;
- } | null = null;
- private _mouseMode: 'select' | 'couldDrag' | null = null;
- private _activeCellChanged = new Signal<this, Cell>(this);
- private _stateChanged = new Signal<this, IChangedArgs<any>>(this);
- private _selectionChanged = new Signal<this, void>(this);
- // Attributes for optimized cell refresh:
- private _cellLayoutStateCache?: { width: number };
- private _checkCacheOnNextResize = false;
- }
- /**
- * The namespace for the `Notebook` class statics.
- */
- export namespace Notebook {
- /**
- * An options object for initializing a notebook widget.
- */
- export interface IOptions extends StaticNotebook.IOptions {}
- /**
- * The content factory for the notebook widget.
- */
- export interface IContentFactory extends StaticNotebook.IContentFactory {}
- /**
- * The default implementation of a notebook content factory..
- *
- * #### Notes
- * Override methods on this class to customize the default notebook factory
- * methods that create notebook content.
- */
- export class ContentFactory extends StaticNotebook.ContentFactory {}
- /**
- * A namespace for the notebook content factory.
- */
- export namespace ContentFactory {
- /**
- * An options object for initializing a notebook content factory.
- */
- export interface IOptions extends StaticNotebook.ContentFactory.IOptions {}
- }
- export const defaultContentFactory: IContentFactory = new ContentFactory();
- }
- /**
- * A namespace for private data.
- */
- namespace Private {
- /**
- * An attached property for the selected state of a cell.
- */
- export const selectedProperty = new AttachedProperty<Cell, boolean>({
- name: 'selected',
- create: () => false
- });
- /**
- * A custom panel layout for the notebook.
- */
- export class NotebookPanelLayout extends PanelLayout {
- /**
- * A message handler invoked on an `'update-request'` message.
- *
- * #### Notes
- * This is a reimplementation of the base class method,
- * and is a no-op.
- */
- protected onUpdateRequest(msg: Message): void {
- // This is a no-op.
- }
- }
- /**
- * Create a cell drag image.
- */
- export function createDragImage(
- count: number,
- promptNumber: string,
- cellContent: string
- ): HTMLElement {
- if (count > 1) {
- if (promptNumber !== '') {
- return VirtualDOM.realize(
- h.div(
- h.div(
- { className: DRAG_IMAGE_CLASS },
- h.span(
- { className: CELL_DRAG_PROMPT_CLASS },
- '[' + promptNumber + ']:'
- ),
- h.span({ className: CELL_DRAG_CONTENT_CLASS }, cellContent)
- ),
- h.div({ className: CELL_DRAG_MULTIPLE_BACK }, '')
- )
- );
- } else {
- return VirtualDOM.realize(
- h.div(
- h.div(
- { className: DRAG_IMAGE_CLASS },
- h.span({ className: CELL_DRAG_PROMPT_CLASS }),
- h.span({ className: CELL_DRAG_CONTENT_CLASS }, cellContent)
- ),
- h.div({ className: CELL_DRAG_MULTIPLE_BACK }, '')
- )
- );
- }
- } else {
- if (promptNumber !== '') {
- return VirtualDOM.realize(
- h.div(
- h.div(
- { className: `${DRAG_IMAGE_CLASS} ${SINGLE_DRAG_IMAGE_CLASS}` },
- h.span(
- { className: CELL_DRAG_PROMPT_CLASS },
- '[' + promptNumber + ']:'
- ),
- h.span({ className: CELL_DRAG_CONTENT_CLASS }, cellContent)
- )
- )
- );
- } else {
- return VirtualDOM.realize(
- h.div(
- h.div(
- { className: `${DRAG_IMAGE_CLASS} ${SINGLE_DRAG_IMAGE_CLASS}` },
- h.span({ className: CELL_DRAG_PROMPT_CLASS }),
- h.span({ className: CELL_DRAG_CONTENT_CLASS }, cellContent)
- )
- )
- );
- }
- }
- }
- /**
- * Process the `IOptions` passed to the notebook widget.
- *
- * #### Notes
- * This defaults the content factory to that in the `Notebook` namespace.
- */
- export function processNotebookOptions(options: Notebook.IOptions) {
- if (options.contentFactory) {
- return options;
- } else {
- return {
- rendermime: options.rendermime,
- languagePreference: options.languagePreference,
- contentFactory: Notebook.defaultContentFactory,
- mimeTypeService: options.mimeTypeService
- };
- }
- }
- }
|