From 2811951bb5f0f03affaafb6c7d437e948a235830 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Fri, 27 Jun 2025 06:54:52 +0530 Subject: [PATCH 01/11] First working draft --- src/pages/notebook.tsx | 368 +++++++++++++++++++++++++++++++++++++++-- style/base.css | 58 +++++++ 2 files changed, 415 insertions(+), 11 deletions(-) diff --git a/src/pages/notebook.tsx b/src/pages/notebook.tsx index 4cb32d3b..695bdd6c 100644 --- a/src/pages/notebook.tsx +++ b/src/pages/notebook.tsx @@ -7,6 +7,281 @@ import { DownloadDropdownButton } from '../ui-components/DownloadDropdownButton' import { Commands } from '../commands'; import { SharingService } from '../sharing-service'; import { INotebookContent } from '@jupyterlab/nbformat'; +import { Widget } from '@lumino/widgets'; +import { IDisposable, DisposableDelegate } from '@lumino/disposable'; + +/** + * Creates a "View Only" header widget for read-only notebooks. + * @returns A Widget containing the "View Only" message. + */ +function createViewOnlyHeader(): Widget { + const widget = new Widget(); + widget.addClass('je-ViewOnlyHeader'); + const contentNode = document.createElement('div'); + contentNode.className = 'je-ViewOnlyHeader-content'; + contentNode.textContent = 'View Only'; + + widget.node.appendChild(contentNode); + + return widget; +} + +/** + * Checks if a notebook is read-only/shared based on its metadata. + * @param notebookPanel - The notebook panel to check + * @returns true if the notebook is read-only/shared, false otherwise + */ +// TODO: better typing for notebookPanel +function isReadOnlyNotebook(notebookPanel: any): boolean { + try { + const { context } = notebookPanel; + if (!context?.model) { + return false; + } + + const { model } = context; + let metadata = null; + + if (model.metadata && typeof model.metadata.get === 'function') { + const { metadata: modelMetadata } = model; + metadata = { + isSharedNotebook: modelMetadata.get('isSharedNotebook'), + sharedId: modelMetadata.get('sharedId'), + readableId: modelMetadata.get('readableId') + }; + } else if (model.metadata) { + ({ metadata } = model); + } else if (model.toJSON && model.toJSON().metadata) { + const { metadata: jsonMetadata } = model.toJSON(); + metadata = jsonMetadata; + } + + console.log('Notebook metadata:', metadata); + + return metadata?.isSharedNotebook === true; + } catch (error) { + console.warn('Error checking notebook read-only status:', error); + return false; + } +} + +/** + * Hides the "notebook is read-only" indicator from the toolbar/ + * @param notebookPanel - The notebook panel to remove the indicator from + */ +// TODO: better typing for notebookPanel +function hideReadOnlyIndicator(notebookPanel: any): void { + try { + const { toolbar } = notebookPanel; + if (toolbar?.layout?.widgets) { + const widgets = Array.from(toolbar.layout.widgets) as Widget[]; + for (const widget of widgets) { + const { node } = widget; + if ( + node && + typeof node.getAttribute === 'function' && + node.getAttribute('data-jp-item-name') === 'read-only-indicator' + ) { + if (typeof widget.hide === 'function') { + widget.hide(); + return; + } + } + } + } + } catch (error) { + console.warn('Error hiding read-only indicator:', error); + } +} + +/** +// * TODO find a better way to do this + * Applies border-radius styles for the "View Only" header in accordance with the notebook area. + * @param notebookPanel - The notebook panel to style + * @param headerWidget - The View Only header widget + */ +function applySeamlessHeaderStyling(notebookPanel: any, headerWidget: Widget): void { + try { + const { node } = notebookPanel; + const mainAreaWidget = node.querySelector('.jp-MainAreaWidget'); + + if (mainAreaWidget) { + const { style } = mainAreaWidget; + Object.assign(style, { + borderRadius: '0', + borderTopLeftRadius: '0', + borderTopRightRadius: '0', + borderBottomLeftRadius: '0', + borderBottomRightRadius: '0' + }); + } + + if (headerWidget.node) { + const { style } = headerWidget.node; + Object.assign(style, { + background: '#412c88', + backgroundColor: '#412c88', + borderRadius: '0px', + borderBottomLeftRadius: '0px', + borderBottomRightRadius: '0px', + borderTopLeftRadius: '12px', + borderTopRightRadius: '12px' + }); + + style.setProperty('border-bottom-left-radius', '0px', 'important'); + style.setProperty('border-bottom-right-radius', '0px', 'important'); + style.setProperty('border-top-left-radius', '12px', 'important'); + style.setProperty('border-top-right-radius', '12px', 'important'); + style.setProperty('background-color', '#412c88', 'important'); + } + + const contentHeader = node.querySelector('.jp-MainAreaWidget-contentHeader'); + if (contentHeader) { + const { style } = contentHeader; + Object.assign(style, { + background: 'transparent', + backgroundColor: 'transparent', + padding: '0', + margin: '0', + borderRadius: '0px', + borderTopLeftRadius: '0px', + borderTopRightRadius: '0px', + borderBottomLeftRadius: '0px', + borderBottomRightRadius: '0px' + }); + } + + const possibleWhiteContainers = [ + '.jp-MainAreaWidget-contentHeader > *', + '.lm-BoxPanel-child', + '.lm-Widget' + ]; + + possibleWhiteContainers.forEach(selector => { + const containers = node.querySelectorAll(selector); + containers.forEach((container: HTMLElement) => { + if (container?.contains?.(headerWidget.node)) { + const { style } = container; + Object.assign(style, { + borderRadius: '0px', + borderBottomLeftRadius: '0px', + borderBottomRightRadius: '0px', + background: 'transparent', + backgroundColor: 'transparent' + }); + } + }); + }); + + setTimeout(() => { + const contentSelectors = [ + '.jp-MainAreaWidget > :last-child', + '.jp-MainAreaWidget > .lm-Widget:last-child', + '.jp-NotebookPanel-notebook' + ]; + + for (const selector of contentSelectors) { + const contentElements = node.querySelectorAll(selector); + contentElements.forEach((element: HTMLElement) => { + if ( + element?.style && + !element.classList.contains('jp-Toolbar') && + !element.classList.contains('jp-MainAreaWidget-contentHeader') + ) { + const { style } = element; + Object.assign(style, { + borderRadius: '0px 0px 12px 12px', + borderTopLeftRadius: '0px', + borderTopRightRadius: '0px', + borderBottomLeftRadius: '12px', + borderBottomRightRadius: '12px' + }); + } + }); + } + + if (headerWidget.node) { + const { style } = headerWidget.node; + Object.assign(style, { + background: '#412c88', + backgroundColor: '#412c88', + borderRadius: '0px', + borderBottomLeftRadius: '0px', + borderBottomRightRadius: '0px', + borderTopLeftRadius: '12px', + borderTopRightRadius: '12px' + }); + + style.setProperty('border-bottom-left-radius', '0px', 'important'); + style.setProperty('border-bottom-right-radius', '0px', 'important'); + style.setProperty('border-top-left-radius', '12px', 'important'); + style.setProperty('border-top-right-radius', '12px', 'important'); + style.setProperty('background-color', '#412c88', 'important'); + + console.log('Re-applied aggressive header styling for persistence'); + } + }, 50); + } catch (error) { + console.warn('Error applying seamless header styling:', error); + } +} + +/** + * Adds a "View Only" header to a read-only notebook panel. + * @param notebookPanel - The notebook panel to add the header to + * @returns A disposable that can be used to remove the header + */ +function addViewOnlyHeaderToNotebook(notebookPanel: any): IDisposable | null { + try { + if (!isReadOnlyNotebook(notebookPanel)) { + console.log('Notebook is not read-only, skipping View Only header'); + return null; + } + + console.log('Adding View Only header to read-only notebook'); + + const headerWidget = createViewOnlyHeader(); + const { contentHeader, node } = notebookPanel; + + if (!contentHeader) { + console.error('NotebookPanel.contentHeader is not available'); + return null; + } + + console.log('ContentHeader available, inserting widget...'); + + node.classList.add('je-shared-notebook'); + contentHeader.insertWidget(0, headerWidget); + + if (contentHeader.node) { + const { style } = contentHeader.node; + Object.assign(style, { + display: 'flex', + flexDirection: 'column', + minHeight: 'auto' + }); + } + + setTimeout(() => { + hideReadOnlyIndicator(notebookPanel); + }, 100); + + applySeamlessHeaderStyling(notebookPanel, headerWidget); + + console.log('View Only header added successfully'); + + return new DisposableDelegate(() => { + console.log('Disposing View Only header'); + node.classList.remove('je-shared-notebook'); + if (!headerWidget.isDisposed) { + headerWidget.dispose(); + } + }); + } catch (error) { + console.error('Error adding View Only header:', error); + return null; + } +} export const notebookPlugin: JupyterFrontEndPlugin = { id: 'jupytereverywhere:notebook', @@ -17,8 +292,8 @@ export const notebookPlugin: JupyterFrontEndPlugin = { tracker: INotebookTracker, toolbarRegistry: IToolbarWidgetRegistry ) => { - const { commands, shell } = app; - const contents = app.serviceManager.contents; + const { commands, shell, serviceManager } = app; + const { contents } = serviceManager; const params = new URLSearchParams(window.location.search); let notebookId = params.get('notebook'); @@ -41,12 +316,10 @@ export const notebookPlugin: JupyterFrontEndPlugin = { console.log('Retrieving notebook from API...'); const notebookResponse = await sharingService.retrieve(id); - console.log('API Response received:', notebookResponse); // debug + console.log('API Response received:', notebookResponse); - const content: INotebookContent = notebookResponse.content; + const { content }: { content: INotebookContent } = notebookResponse; - // We make all cells read-only by setting editable: false - // by iterating over each cell in the notebook content. if (content.cells) { content.cells.forEach(cell => { cell.metadata = { @@ -56,16 +329,16 @@ export const notebookPlugin: JupyterFrontEndPlugin = { }); } + const { id: responseId, readable_id, domain_id } = notebookResponse; content.metadata = { ...content.metadata, isSharedNotebook: true, - sharedId: notebookResponse.id, - readableId: notebookResponse.readable_id, - domainId: notebookResponse.domain_id + sharedId: responseId, + readableId: readable_id, + domainId: domain_id }; - // Generate a meaningful filename for the shared notebook - const filename = `Shared_${notebookResponse.readable_id || notebookResponse.id}.ipynb`; + const filename = `Shared_${readable_id || responseId}.ipynb`; await contents.save(filename, { content, @@ -113,6 +386,79 @@ export const notebookPlugin: JupyterFrontEndPlugin = { } }; + /** + * Hook into notebook widgets to add a "View Only" header for read-only notebooks. + */ + tracker.widgetAdded.connect((sender, notebookPanel) => { + console.log('Notebook widget added, setting up View Only header check...'); + + notebookPanel.revealed + .then(() => { + console.log('Notebook revealed, waiting for metadata to load...'); + + setTimeout(() => { + console.log('Checking if notebook is read-only...'); + const disposable = addViewOnlyHeaderToNotebook(notebookPanel); + if (disposable) { + notebookPanel.disposed.connect(() => { + disposable.dispose(); + }); + } + + setTimeout(() => { + if (isReadOnlyNotebook(notebookPanel)) { + hideReadOnlyIndicator(notebookPanel); + + const { contentHeader } = notebookPanel; + const headerWidget = contentHeader?.node?.querySelector('.je-ViewOnlyHeader'); + + if (headerWidget) { + const headerElement = headerWidget as HTMLElement; + const { style } = headerElement; + + Object.assign(style, { + background: '#412c88', + backgroundColor: '#412c88', + borderRadius: '0px', + borderBottomLeftRadius: '0px', + borderBottomRightRadius: '0px', + borderTopLeftRadius: '12px', + borderTopRightRadius: '12px' + }); + + style.setProperty('border-bottom-left-radius', '0px', 'important'); + style.setProperty('border-bottom-right-radius', '0px', 'important'); + style.setProperty('border-top-left-radius', '12px', 'important'); + style.setProperty('border-top-right-radius', '12px', 'important'); + style.setProperty('background-color', '#412c88', 'important'); + + if (contentHeader?.node) { + const { style: headerStyle } = contentHeader.node; + Object.assign(headerStyle, { + borderRadius: '0px', + borderBottomLeftRadius: '0px', + borderBottomRightRadius: '0px', + background: 'transparent', + backgroundColor: 'transparent' + }); + + headerStyle.setProperty('border-radius', '0px', 'important'); + headerStyle.setProperty('background-color', 'transparent', 'important'); + console.log('Fixed white container in final reapplication'); + } + + console.log('Re-applied MOST aggressive header styling'); + applySeamlessHeaderStyling(notebookPanel, { node: headerWidget } as Widget); + } + } + }, 200); + }, 500); + }) + .catch(error => { + console.warn('Error waiting for notebook to be revealed:', error); + }); + }); + // If a notebook ID is provided in the URL, load it; otherwise, // create a new notebook if (notebookId) { diff --git a/style/base.css b/style/base.css index e8f844c2..a21b790f 100644 --- a/style/base.css +++ b/style/base.css @@ -81,3 +81,61 @@ margin-top: 10px; background: white; } + +/* Shared notebook styling */ +.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget { + border-radius: 0 !important; +} + +.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget, +.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget-contentHeader, +.jp-NotebookPanel.je-shared-notebook .lm-BoxPanel-child { + background: transparent; +} + +.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget-contentHeader { + background: transparent !important; + padding: 0; + margin: 0; +} + +.jp-NotebookPanel.je-shared-notebook [data-jp-item-name='read-only-indicator'] { + display: none; +} + +/* View Only header */ +.je-ViewOnlyHeader { + min-height: 40px; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + background: var(--je-slate-blue) !important; + border-radius: var(--je-round-corners) var(--je-round-corners) 0 0 !important; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.je-ViewOnlyHeader-content { + color: white; + font-family: var(--je-font-family); + font-size: 14px; + font-weight: 600; + text-align: center; + padding: 8px 16px; + width: 100%; +} + +/* Content header visibility */ +.jp-MainAreaWidget-contentHeader { + display: flex; + flex-direction: column; + min-height: auto; + background: transparent; +} + +.jp-MainAreaWidget-contentHeader .je-ViewOnlyHeader { + order: -1; + flex-shrink: 0; +} From 96f56863b25ee55161e8d589f6c74bae957d0ce2 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Tue, 1 Jul 2025 16:52:01 +0530 Subject: [PATCH 02/11] Fix CSS specificity rules --- style/base.css | 63 +++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/style/base.css b/style/base.css index a21b790f..4d3fa251 100644 --- a/style/base.css +++ b/style/base.css @@ -62,16 +62,7 @@ font-weight: 600; } -#jp-main-dock-panel[data-mode='single-document'] { - padding: 25px !important; - background: #d8b8dc; -} - -#jp-main-dock-panel[data-mode='single-document'] .jp-MainAreaWidget { - border-radius: var(--je-round-corners); - background: transparent; -} - +/* Main area widget base styles */ .jp-MainAreaWidget > .jp-Toolbar { border-radius: var(--je-round-corners); } @@ -82,27 +73,6 @@ background: white; } -/* Shared notebook styling */ -.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget { - border-radius: 0 !important; -} - -.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget, -.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget-contentHeader, -.jp-NotebookPanel.je-shared-notebook .lm-BoxPanel-child { - background: transparent; -} - -.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget-contentHeader { - background: transparent !important; - padding: 0; - margin: 0; -} - -.jp-NotebookPanel.je-shared-notebook [data-jp-item-name='read-only-indicator'] { - display: none; -} - /* View Only header */ .je-ViewOnlyHeader { min-height: 40px; @@ -139,3 +109,34 @@ order: -1; flex-shrink: 0; } + +/* Shared notebook styling */ +.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget { + border-radius: 0 !important; +} + +.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget, +.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget-contentHeader, +.jp-NotebookPanel.je-shared-notebook .lm-BoxPanel-child { + background: transparent; +} + +.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget-contentHeader { + background: transparent !important; + padding: 0; + margin: 0; +} + +.jp-NotebookPanel.je-shared-notebook [data-jp-item-name='read-only-indicator'] { + display: none; +} + +#jp-main-dock-panel[data-mode='single-document'] { + padding: 25px !important; + background: #d8b8dc; +} + +#jp-main-dock-panel[data-mode='single-document'] .jp-MainAreaWidget { + border-radius: var(--je-round-corners); + background: transparent; +} From 2a98d24882bd1a3d7a42ac2aad9261bfb5462aac Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Tue, 1 Jul 2025 22:42:04 +0100 Subject: [PATCH 03/11] Add custom view-only notebook factory --- src/index.ts | 10 +- src/pages/notebook.tsx | 362 ++------------------------------------- src/readonly-notebook.ts | 174 +++++++++++++++++++ src/sidebar.ts | 1 - style/base.css | 44 +---- 5 files changed, 200 insertions(+), 391 deletions(-) create mode 100644 src/readonly-notebook.ts diff --git a/src/index.ts b/src/index.ts index 77c2922e..ad890cd6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { Commands } from './commands'; import { competitions } from './pages/competitions'; import { notebookPlugin } from './pages/notebook'; import { generateDefaultNotebookName } from './notebook-name'; +import { readonlyNotebookFactoryPlugin } from './readonly-notebook'; /** * Generate a shareable URL for the currently active notebook. @@ -236,4 +237,11 @@ const plugin: JupyterFrontEndPlugin = { } }; -export default [plugin, notebookPlugin, files, competitions, customSidebar]; +export default [ + readonlyNotebookFactoryPlugin, + plugin, + notebookPlugin, + files, + competitions, + customSidebar +]; diff --git a/src/pages/notebook.tsx b/src/pages/notebook.tsx index 695bdd6c..c76aa165 100644 --- a/src/pages/notebook.tsx +++ b/src/pages/notebook.tsx @@ -1,295 +1,22 @@ import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; import { INotebookTracker } from '@jupyterlab/notebook'; +import { INotebookContent } from '@jupyterlab/nbformat'; import { SidebarIcon } from '../ui-components/SidebarIcon'; import { EverywhereIcons } from '../icons'; import { ToolbarButton, IToolbarWidgetRegistry } from '@jupyterlab/apputils'; import { DownloadDropdownButton } from '../ui-components/DownloadDropdownButton'; import { Commands } from '../commands'; import { SharingService } from '../sharing-service'; -import { INotebookContent } from '@jupyterlab/nbformat'; -import { Widget } from '@lumino/widgets'; -import { IDisposable, DisposableDelegate } from '@lumino/disposable'; - -/** - * Creates a "View Only" header widget for read-only notebooks. - * @returns A Widget containing the "View Only" message. - */ -function createViewOnlyHeader(): Widget { - const widget = new Widget(); - widget.addClass('je-ViewOnlyHeader'); - const contentNode = document.createElement('div'); - contentNode.className = 'je-ViewOnlyHeader-content'; - contentNode.textContent = 'View Only'; - - widget.node.appendChild(contentNode); - - return widget; -} - -/** - * Checks if a notebook is read-only/shared based on its metadata. - * @param notebookPanel - The notebook panel to check - * @returns true if the notebook is read-only/shared, false otherwise - */ -// TODO: better typing for notebookPanel -function isReadOnlyNotebook(notebookPanel: any): boolean { - try { - const { context } = notebookPanel; - if (!context?.model) { - return false; - } - - const { model } = context; - let metadata = null; - - if (model.metadata && typeof model.metadata.get === 'function') { - const { metadata: modelMetadata } = model; - metadata = { - isSharedNotebook: modelMetadata.get('isSharedNotebook'), - sharedId: modelMetadata.get('sharedId'), - readableId: modelMetadata.get('readableId') - }; - } else if (model.metadata) { - ({ metadata } = model); - } else if (model.toJSON && model.toJSON().metadata) { - const { metadata: jsonMetadata } = model.toJSON(); - metadata = jsonMetadata; - } - - console.log('Notebook metadata:', metadata); - - return metadata?.isSharedNotebook === true; - } catch (error) { - console.warn('Error checking notebook read-only status:', error); - return false; - } -} - -/** - * Hides the "notebook is read-only" indicator from the toolbar/ - * @param notebookPanel - The notebook panel to remove the indicator from - */ -// TODO: better typing for notebookPanel -function hideReadOnlyIndicator(notebookPanel: any): void { - try { - const { toolbar } = notebookPanel; - if (toolbar?.layout?.widgets) { - const widgets = Array.from(toolbar.layout.widgets) as Widget[]; - for (const widget of widgets) { - const { node } = widget; - if ( - node && - typeof node.getAttribute === 'function' && - node.getAttribute('data-jp-item-name') === 'read-only-indicator' - ) { - if (typeof widget.hide === 'function') { - widget.hide(); - return; - } - } - } - } - } catch (error) { - console.warn('Error hiding read-only indicator:', error); - } -} - -/** -// * TODO find a better way to do this - * Applies border-radius styles for the "View Only" header in accordance with the notebook area. - * @param notebookPanel - The notebook panel to style - * @param headerWidget - The View Only header widget - */ -function applySeamlessHeaderStyling(notebookPanel: any, headerWidget: Widget): void { - try { - const { node } = notebookPanel; - const mainAreaWidget = node.querySelector('.jp-MainAreaWidget'); - - if (mainAreaWidget) { - const { style } = mainAreaWidget; - Object.assign(style, { - borderRadius: '0', - borderTopLeftRadius: '0', - borderTopRightRadius: '0', - borderBottomLeftRadius: '0', - borderBottomRightRadius: '0' - }); - } - - if (headerWidget.node) { - const { style } = headerWidget.node; - Object.assign(style, { - background: '#412c88', - backgroundColor: '#412c88', - borderRadius: '0px', - borderBottomLeftRadius: '0px', - borderBottomRightRadius: '0px', - borderTopLeftRadius: '12px', - borderTopRightRadius: '12px' - }); - - style.setProperty('border-bottom-left-radius', '0px', 'important'); - style.setProperty('border-bottom-right-radius', '0px', 'important'); - style.setProperty('border-top-left-radius', '12px', 'important'); - style.setProperty('border-top-right-radius', '12px', 'important'); - style.setProperty('background-color', '#412c88', 'important'); - } - - const contentHeader = node.querySelector('.jp-MainAreaWidget-contentHeader'); - if (contentHeader) { - const { style } = contentHeader; - Object.assign(style, { - background: 'transparent', - backgroundColor: 'transparent', - padding: '0', - margin: '0', - borderRadius: '0px', - borderTopLeftRadius: '0px', - borderTopRightRadius: '0px', - borderBottomLeftRadius: '0px', - borderBottomRightRadius: '0px' - }); - } - - const possibleWhiteContainers = [ - '.jp-MainAreaWidget-contentHeader > *', - '.lm-BoxPanel-child', - '.lm-Widget' - ]; - - possibleWhiteContainers.forEach(selector => { - const containers = node.querySelectorAll(selector); - containers.forEach((container: HTMLElement) => { - if (container?.contains?.(headerWidget.node)) { - const { style } = container; - Object.assign(style, { - borderRadius: '0px', - borderBottomLeftRadius: '0px', - borderBottomRightRadius: '0px', - background: 'transparent', - backgroundColor: 'transparent' - }); - } - }); - }); - - setTimeout(() => { - const contentSelectors = [ - '.jp-MainAreaWidget > :last-child', - '.jp-MainAreaWidget > .lm-Widget:last-child', - '.jp-NotebookPanel-notebook' - ]; - - for (const selector of contentSelectors) { - const contentElements = node.querySelectorAll(selector); - contentElements.forEach((element: HTMLElement) => { - if ( - element?.style && - !element.classList.contains('jp-Toolbar') && - !element.classList.contains('jp-MainAreaWidget-contentHeader') - ) { - const { style } = element; - Object.assign(style, { - borderRadius: '0px 0px 12px 12px', - borderTopLeftRadius: '0px', - borderTopRightRadius: '0px', - borderBottomLeftRadius: '12px', - borderBottomRightRadius: '12px' - }); - } - }); - } - - if (headerWidget.node) { - const { style } = headerWidget.node; - Object.assign(style, { - background: '#412c88', - backgroundColor: '#412c88', - borderRadius: '0px', - borderBottomLeftRadius: '0px', - borderBottomRightRadius: '0px', - borderTopLeftRadius: '12px', - borderTopRightRadius: '12px' - }); - - style.setProperty('border-bottom-left-radius', '0px', 'important'); - style.setProperty('border-bottom-right-radius', '0px', 'important'); - style.setProperty('border-top-left-radius', '12px', 'important'); - style.setProperty('border-top-right-radius', '12px', 'important'); - style.setProperty('background-color', '#412c88', 'important'); - - console.log('Re-applied aggressive header styling for persistence'); - } - }, 50); - } catch (error) { - console.warn('Error applying seamless header styling:', error); - } -} - -/** - * Adds a "View Only" header to a read-only notebook panel. - * @param notebookPanel - The notebook panel to add the header to - * @returns A disposable that can be used to remove the header - */ -function addViewOnlyHeaderToNotebook(notebookPanel: any): IDisposable | null { - try { - if (!isReadOnlyNotebook(notebookPanel)) { - console.log('Notebook is not read-only, skipping View Only header'); - return null; - } - - console.log('Adding View Only header to read-only notebook'); - - const headerWidget = createViewOnlyHeader(); - const { contentHeader, node } = notebookPanel; - - if (!contentHeader) { - console.error('NotebookPanel.contentHeader is not available'); - return null; - } - - console.log('ContentHeader available, inserting widget...'); - - node.classList.add('je-shared-notebook'); - contentHeader.insertWidget(0, headerWidget); - - if (contentHeader.node) { - const { style } = contentHeader.node; - Object.assign(style, { - display: 'flex', - flexDirection: 'column', - minHeight: 'auto' - }); - } - - setTimeout(() => { - hideReadOnlyIndicator(notebookPanel); - }, 100); - - applySeamlessHeaderStyling(notebookPanel, headerWidget); - - console.log('View Only header added successfully'); - - return new DisposableDelegate(() => { - console.log('Disposing View Only header'); - node.classList.remove('je-shared-notebook'); - if (!headerWidget.isDisposed) { - headerWidget.dispose(); - } - }); - } catch (error) { - console.error('Error adding View Only header:', error); - return null; - } -} +import { READONLY_NOTEBOOK_FACTORY, IReadonlyNotebookTracker } from '../readonly-notebook'; export const notebookPlugin: JupyterFrontEndPlugin = { id: 'jupytereverywhere:notebook', autoStart: true, - requires: [INotebookTracker, IToolbarWidgetRegistry], + requires: [INotebookTracker, IReadonlyNotebookTracker, IToolbarWidgetRegistry], activate: ( app: JupyterFrontEnd, tracker: INotebookTracker, + readonlyTracker: IReadonlyNotebookTracker, toolbarRegistry: IToolbarWidgetRegistry ) => { const { commands, shell, serviceManager } = app; @@ -297,6 +24,7 @@ export const notebookPlugin: JupyterFrontEndPlugin = { const params = new URLSearchParams(window.location.search); let notebookId = params.get('notebook'); + notebookId = 'aaa'; if (notebookId?.endsWith('.ipynb')) { notebookId = notebookId.slice(0, -6); @@ -349,7 +77,7 @@ export const notebookPlugin: JupyterFrontEndPlugin = { await commands.execute('docmanager:open', { path: filename, - factory: 'Notebook' + factory: READONLY_NOTEBOOK_FACTORY }); console.log(`Successfully loaded shared notebook: ${filename}`); @@ -386,79 +114,6 @@ export const notebookPlugin: JupyterFrontEndPlugin = { } }; - /** - * Hook into notebook widgets to add a "View Only" header for read-only notebooks. - */ - tracker.widgetAdded.connect((sender, notebookPanel) => { - console.log('Notebook widget added, setting up View Only header check...'); - - notebookPanel.revealed - .then(() => { - console.log('Notebook revealed, waiting for metadata to load...'); - - setTimeout(() => { - console.log('Checking if notebook is read-only...'); - const disposable = addViewOnlyHeaderToNotebook(notebookPanel); - if (disposable) { - notebookPanel.disposed.connect(() => { - disposable.dispose(); - }); - } - - setTimeout(() => { - if (isReadOnlyNotebook(notebookPanel)) { - hideReadOnlyIndicator(notebookPanel); - - const { contentHeader } = notebookPanel; - const headerWidget = contentHeader?.node?.querySelector('.je-ViewOnlyHeader'); - - if (headerWidget) { - const headerElement = headerWidget as HTMLElement; - const { style } = headerElement; - - Object.assign(style, { - background: '#412c88', - backgroundColor: '#412c88', - borderRadius: '0px', - borderBottomLeftRadius: '0px', - borderBottomRightRadius: '0px', - borderTopLeftRadius: '12px', - borderTopRightRadius: '12px' - }); - - style.setProperty('border-bottom-left-radius', '0px', 'important'); - style.setProperty('border-bottom-right-radius', '0px', 'important'); - style.setProperty('border-top-left-radius', '12px', 'important'); - style.setProperty('border-top-right-radius', '12px', 'important'); - style.setProperty('background-color', '#412c88', 'important'); - - if (contentHeader?.node) { - const { style: headerStyle } = contentHeader.node; - Object.assign(headerStyle, { - borderRadius: '0px', - borderBottomLeftRadius: '0px', - borderBottomRightRadius: '0px', - background: 'transparent', - backgroundColor: 'transparent' - }); - - headerStyle.setProperty('border-radius', '0px', 'important'); - headerStyle.setProperty('background-color', 'transparent', 'important'); - console.log('Fixed white container in final reapplication'); - } - - console.log('Re-applied MOST aggressive header styling'); - applySeamlessHeaderStyling(notebookPanel, { node: headerWidget } as Widget); - } - } - }, 200); - }, 500); - }) - .catch(error => { - console.warn('Error waiting for notebook to be revealed:', error); - }); - }); - // If a notebook ID is provided in the URL, load it; otherwise, // create a new notebook if (notebookId) { @@ -471,8 +126,11 @@ export const notebookPlugin: JupyterFrontEndPlugin = { label: 'Notebook', icon: EverywhereIcons.notebook, execute: () => { + if (readonlyTracker.currentWidget) { + return shell.activateById(readonlyTracker.currentWidget.id); + } if (tracker.currentWidget) { - shell.activateById(tracker.currentWidget.id); + return shell.activateById(tracker.currentWidget.id); } } }); diff --git a/src/readonly-notebook.ts b/src/readonly-notebook.ts new file mode 100644 index 00000000..9a959fe1 --- /dev/null +++ b/src/readonly-notebook.ts @@ -0,0 +1,174 @@ +import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; +import { IEditorMimeTypeService } from '@jupyterlab/codeeditor'; +import { WidgetTracker, IWidgetTracker } from '@jupyterlab/apputils'; +import { IEditorServices } from '@jupyterlab/codeeditor'; +import { ABCWidgetFactory, DocumentRegistry, DocumentWidget } from '@jupyterlab/docregistry'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; +import { ITranslator } from '@jupyterlab/translation'; +import { INotebookModel, NotebookPanel, Notebook, StaticNotebook } from '@jupyterlab/notebook'; +import { createToolbarFactory, IToolbarWidgetRegistry } from '@jupyterlab/apputils'; +import { Widget } from '@lumino/widgets'; +import { Token } from '@lumino/coreutils'; + +export const READONLY_NOTEBOOK_FACTORY = 'ReadonlyNotebook'; + +const NOTEBOOK_PANEL_CLASS = 'jp-NotebookPanel'; + +const NOTEBOOK_PANEL_TOOLBAR_CLASS = 'jp-NotebookPanel-toolbar'; + +const NOTEBOOK_PANEL_NOTEBOOK_CLASS = 'jp-NotebookPanel-notebook'; + +export const IReadonlyNotebookTracker = new Token( + 'jupytereverywhere:readonly-notebook:IReadonlyNotebookTracker' +); + +export interface IReadonlyNotebookTracker extends IWidgetTracker {} + +/** + * Creates a "View Only" header widget for read-only notebooks. + */ +class ViewOnlyHeader extends Widget { + constructor() { + super(); + this.addClass('je-ViewOnlyHeader'); + const contentNode = document.createElement('div'); + contentNode.className = 'je-ViewOnlyHeader-content'; + contentNode.textContent = 'View Only'; + this.node.appendChild(contentNode); + } +} + +class ReadonlyNotebook extends StaticNotebook { + // Add any customization for read-only notebook here if needed +} + +class ReadonlyNotebookPanel extends DocumentWidget { + /** + * Construct a new readonly notebook panel. + */ + constructor(options: DocumentWidget.IOptions) { + super(options); + + this.addClass(NOTEBOOK_PANEL_CLASS); + this.toolbar.addClass(NOTEBOOK_PANEL_TOOLBAR_CLASS); + this.content.addClass(NOTEBOOK_PANEL_NOTEBOOK_CLASS); + + this.content.model = this.context.model; + const headerWidget = new ViewOnlyHeader(); + this.contentHeader.insertWidget(0, headerWidget); + this.contentHeader.addClass('je-ViewOnlyHeader-wrapper'); + } +} + +namespace ReadonlyNotebookWidgetFactory { + export interface IOptions extends DocumentRegistry.IWidgetFactoryOptions { + rendermime: IRenderMimeRegistry; + contentFactory: Notebook.IContentFactory; + mimeTypeService: IEditorMimeTypeService; + editorConfig?: StaticNotebook.IEditorConfig; + notebookConfig?: StaticNotebook.INotebookConfig; + translator?: ITranslator; + } +} + +class ReadonlyNotebookWidgetFactory extends ABCWidgetFactory< + ReadonlyNotebookPanel, + INotebookModel +> { + /** + * Construct a new notebook widget factory. + * + * @param options - The options used to construct the factory. + */ + constructor(private _options: ReadonlyNotebookWidgetFactory.IOptions) { + super(_options); + } + + /** + * Create a new widget. + */ + protected createNewWidget( + context: DocumentRegistry.IContext, + source?: ReadonlyNotebookPanel + ): ReadonlyNotebookPanel { + const translator = (context as any).translator; + const { contentFactory, mimeTypeService, rendermime } = this._options; + const nbOptions = { + rendermime: source + ? source.content.rendermime + : rendermime.clone({ resolver: context.urlResolver }), + contentFactory, + mimeTypeService, + editorConfig: source + ? source.content.editorConfig + : this._options.editorConfig || StaticNotebook.defaultEditorConfig, + notebookConfig: source + ? source.content.notebookConfig + : this._options.notebookConfig || StaticNotebook.defaultNotebookConfig, + translator + }; + const content = new ReadonlyNotebook(nbOptions); + + return new ReadonlyNotebookPanel({ context, content }); + } +} + +export const readonlyNotebookFactoryPlugin: JupyterFrontEndPlugin = { + id: 'jupytereverywhere:readonly-notebook', + requires: [ + NotebookPanel.IContentFactory, + IEditorServices, + IRenderMimeRegistry, + IToolbarWidgetRegistry, + ISettingRegistry, + ITranslator + ], + provides: IReadonlyNotebookTracker, + autoStart: true, + activate: ( + app: JupyterFrontEnd, + contentFactory: NotebookPanel.IContentFactory, + editorServices: IEditorServices, + rendermime: IRenderMimeRegistry, + toolbarRegistry: IToolbarWidgetRegistry, + settingRegistry: ISettingRegistry, + translator: ITranslator + ) => { + const PANEL_SETTINGS = '@jupyterlab/notebook-extension:panel'; + + const toolbarFactory = createToolbarFactory( + toolbarRegistry, + settingRegistry, + READONLY_NOTEBOOK_FACTORY, + PANEL_SETTINGS, + translator + ); + + const trans = translator.load('jupyterlab'); + + const factory = new ReadonlyNotebookWidgetFactory({ + name: READONLY_NOTEBOOK_FACTORY, + label: trans.__('Readonly Notebook'), + fileTypes: ['notebook'], + modelName: 'notebook', + preferKernel: false, + canStartKernel: false, + rendermime, + contentFactory, + editorConfig: StaticNotebook.defaultEditorConfig, + notebookConfig: StaticNotebook.defaultNotebookConfig, + mimeTypeService: editorServices.mimeTypeService, + toolbarFactory, + translator + }); + const tracker = new WidgetTracker({ + namespace: 'readonly-notebook' + }); + factory.widgetCreated.connect((sender, widget) => { + void tracker.add(widget); + }); + app.docRegistry.addWidgetFactory(factory); + return tracker; + } +}; diff --git a/src/sidebar.ts b/src/sidebar.ts index 04b9a6d5..ec6e18e2 100644 --- a/src/sidebar.ts +++ b/src/sidebar.ts @@ -24,7 +24,6 @@ export const customSidebar: JupyterFrontEndPlugin = { const newWidget = args.currentTitle ? leftHandler._findWidgetByTitle(args.currentTitle) : null; - console.log(newWidget); if (newWidget && newWidget instanceof SidebarIcon) { const cancel = newWidget.execute(); if (cancel) { diff --git a/style/base.css b/style/base.css index 4d3fa251..f97a3d0b 100644 --- a/style/base.css +++ b/style/base.css @@ -76,15 +76,9 @@ /* View Only header */ .je-ViewOnlyHeader { min-height: 40px; - width: 100%; display: flex; align-items: center; - justify-content: center; - background: var(--je-slate-blue) !important; - border-radius: var(--je-round-corners) var(--je-round-corners) 0 0 !important; - margin: 0; - padding: 0; - box-sizing: border-box; + background: var(--je-slate-blue); } .je-ViewOnlyHeader-content { @@ -97,38 +91,14 @@ width: 100%; } -/* Content header visibility */ -.jp-MainAreaWidget-contentHeader { - display: flex; - flex-direction: column; - min-height: auto; - background: transparent; -} - -.jp-MainAreaWidget-contentHeader .je-ViewOnlyHeader { - order: -1; - flex-shrink: 0; -} - -/* Shared notebook styling */ -.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget { - border-radius: 0 !important; -} - -.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget, -.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget-contentHeader, -.jp-NotebookPanel.je-shared-notebook .lm-BoxPanel-child { - background: transparent; +.jp-MainAreaWidget > .je-ViewOnlyHeader-wrapper { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } -.jp-NotebookPanel.je-shared-notebook .jp-MainAreaWidget-contentHeader { - background: transparent !important; - padding: 0; - margin: 0; -} - -.jp-NotebookPanel.je-shared-notebook [data-jp-item-name='read-only-indicator'] { - display: none; +.je-ViewOnlyHeader-wrapper + .jp-Notebook { + border-top-left-radius: 0; + border-top-right-radius: 0; } #jp-main-dock-panel[data-mode='single-document'] { From b5c3a4cceec5734220c13b6a01b197bc64d6f60b Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Tue, 1 Jul 2025 22:51:02 +0100 Subject: [PATCH 04/11] Fix --- src/pages/notebook.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/notebook.tsx b/src/pages/notebook.tsx index c76aa165..aeb448b9 100644 --- a/src/pages/notebook.tsx +++ b/src/pages/notebook.tsx @@ -24,7 +24,6 @@ export const notebookPlugin: JupyterFrontEndPlugin = { const params = new URLSearchParams(window.location.search); let notebookId = params.get('notebook'); - notebookId = 'aaa'; if (notebookId?.endsWith('.ipynb')) { notebookId = notebookId.slice(0, -6); From 3c6cc5a275c9bd6b720bf7d5146b75c21cd2c0bd Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Tue, 1 Jul 2025 22:58:40 +0100 Subject: [PATCH 05/11] Rename to view-only --- src/index.ts | 4 +- src/pages/notebook.tsx | 8 ++-- src/{readonly-notebook.ts => view-only.ts} | 56 +++++++++++----------- 3 files changed, 34 insertions(+), 34 deletions(-) rename src/{readonly-notebook.ts => view-only.ts} (74%) diff --git a/src/index.ts b/src/index.ts index ad890cd6..82d0d2c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import { Commands } from './commands'; import { competitions } from './pages/competitions'; import { notebookPlugin } from './pages/notebook'; import { generateDefaultNotebookName } from './notebook-name'; -import { readonlyNotebookFactoryPlugin } from './readonly-notebook'; +import { viewOnlyNotebookFactoryPlugin } from './view-only'; /** * Generate a shareable URL for the currently active notebook. @@ -238,7 +238,7 @@ const plugin: JupyterFrontEndPlugin = { }; export default [ - readonlyNotebookFactoryPlugin, + viewOnlyNotebookFactoryPlugin, plugin, notebookPlugin, files, diff --git a/src/pages/notebook.tsx b/src/pages/notebook.tsx index aeb448b9..8cc2d605 100644 --- a/src/pages/notebook.tsx +++ b/src/pages/notebook.tsx @@ -7,16 +7,16 @@ import { ToolbarButton, IToolbarWidgetRegistry } from '@jupyterlab/apputils'; import { DownloadDropdownButton } from '../ui-components/DownloadDropdownButton'; import { Commands } from '../commands'; import { SharingService } from '../sharing-service'; -import { READONLY_NOTEBOOK_FACTORY, IReadonlyNotebookTracker } from '../readonly-notebook'; +import { VIEW_ONLY_NOTEBOOK_FACTORY, IViewOnlyNotebookTracker } from '../view-only'; export const notebookPlugin: JupyterFrontEndPlugin = { id: 'jupytereverywhere:notebook', autoStart: true, - requires: [INotebookTracker, IReadonlyNotebookTracker, IToolbarWidgetRegistry], + requires: [INotebookTracker, IViewOnlyNotebookTracker, IToolbarWidgetRegistry], activate: ( app: JupyterFrontEnd, tracker: INotebookTracker, - readonlyTracker: IReadonlyNotebookTracker, + readonlyTracker: IViewOnlyNotebookTracker, toolbarRegistry: IToolbarWidgetRegistry ) => { const { commands, shell, serviceManager } = app; @@ -76,7 +76,7 @@ export const notebookPlugin: JupyterFrontEndPlugin = { await commands.execute('docmanager:open', { path: filename, - factory: READONLY_NOTEBOOK_FACTORY + factory: VIEW_ONLY_NOTEBOOK_FACTORY }); console.log(`Successfully loaded shared notebook: ${filename}`); diff --git a/src/readonly-notebook.ts b/src/view-only.ts similarity index 74% rename from src/readonly-notebook.ts rename to src/view-only.ts index 9a959fe1..5fe34d49 100644 --- a/src/readonly-notebook.ts +++ b/src/view-only.ts @@ -11,7 +11,7 @@ import { createToolbarFactory, IToolbarWidgetRegistry } from '@jupyterlab/apputi import { Widget } from '@lumino/widgets'; import { Token } from '@lumino/coreutils'; -export const READONLY_NOTEBOOK_FACTORY = 'ReadonlyNotebook'; +export const VIEW_ONLY_NOTEBOOK_FACTORY = 'ViewOnlyNotebook'; const NOTEBOOK_PANEL_CLASS = 'jp-NotebookPanel'; @@ -19,14 +19,14 @@ const NOTEBOOK_PANEL_TOOLBAR_CLASS = 'jp-NotebookPanel-toolbar'; const NOTEBOOK_PANEL_NOTEBOOK_CLASS = 'jp-NotebookPanel-notebook'; -export const IReadonlyNotebookTracker = new Token( - 'jupytereverywhere:readonly-notebook:IReadonlyNotebookTracker' +export const IViewOnlyNotebookTracker = new Token( + 'jupytereverywhere:view-only-notebook:IViewOnlyNotebookTracker' ); -export interface IReadonlyNotebookTracker extends IWidgetTracker {} +export interface IViewOnlyNotebookTracker extends IWidgetTracker {} /** - * Creates a "View Only" header widget for read-only notebooks. + * Creates a "View Only" header widget for view-only notebooks. */ class ViewOnlyHeader extends Widget { constructor() { @@ -39,15 +39,15 @@ class ViewOnlyHeader extends Widget { } } -class ReadonlyNotebook extends StaticNotebook { - // Add any customization for read-only notebook here if needed +class ViewOnlyNotebook extends StaticNotebook { + // Add any customization for view-only notebook here if needed } -class ReadonlyNotebookPanel extends DocumentWidget { +class ViewOnlyNotebookPanel extends DocumentWidget { /** - * Construct a new readonly notebook panel. + * Construct a new view-only notebook panel. */ - constructor(options: DocumentWidget.IOptions) { + constructor(options: DocumentWidget.IOptions) { super(options); this.addClass(NOTEBOOK_PANEL_CLASS); @@ -61,8 +61,8 @@ class ReadonlyNotebookPanel extends DocumentWidget { +namespace ViewOnlyNotebookWidgetFactory { + export interface IOptions extends DocumentRegistry.IWidgetFactoryOptions { rendermime: IRenderMimeRegistry; contentFactory: Notebook.IContentFactory; mimeTypeService: IEditorMimeTypeService; @@ -72,8 +72,8 @@ namespace ReadonlyNotebookWidgetFactory { } } -class ReadonlyNotebookWidgetFactory extends ABCWidgetFactory< - ReadonlyNotebookPanel, +class ViewOnlyNotebookWidgetFactory extends ABCWidgetFactory< + ViewOnlyNotebookPanel, INotebookModel > { /** @@ -81,7 +81,7 @@ class ReadonlyNotebookWidgetFactory extends ABCWidgetFactory< * * @param options - The options used to construct the factory. */ - constructor(private _options: ReadonlyNotebookWidgetFactory.IOptions) { + constructor(private _options: ViewOnlyNotebookWidgetFactory.IOptions) { super(_options); } @@ -90,8 +90,8 @@ class ReadonlyNotebookWidgetFactory extends ABCWidgetFactory< */ protected createNewWidget( context: DocumentRegistry.IContext, - source?: ReadonlyNotebookPanel - ): ReadonlyNotebookPanel { + source?: ViewOnlyNotebookPanel + ): ViewOnlyNotebookPanel { const translator = (context as any).translator; const { contentFactory, mimeTypeService, rendermime } = this._options; const nbOptions = { @@ -108,14 +108,14 @@ class ReadonlyNotebookWidgetFactory extends ABCWidgetFactory< : this._options.notebookConfig || StaticNotebook.defaultNotebookConfig, translator }; - const content = new ReadonlyNotebook(nbOptions); + const content = new ViewOnlyNotebook(nbOptions); - return new ReadonlyNotebookPanel({ context, content }); + return new ViewOnlyNotebookPanel({ context, content }); } } -export const readonlyNotebookFactoryPlugin: JupyterFrontEndPlugin = { - id: 'jupytereverywhere:readonly-notebook', +export const viewOnlyNotebookFactoryPlugin: JupyterFrontEndPlugin = { + id: 'jupytereverywhere:view-only-notebook', requires: [ NotebookPanel.IContentFactory, IEditorServices, @@ -124,7 +124,7 @@ export const readonlyNotebookFactoryPlugin: JupyterFrontEndPlugin({ - namespace: 'readonly-notebook' + const tracker = new WidgetTracker({ + namespace: 'view-only-notebook' }); factory.widgetCreated.connect((sender, widget) => { void tracker.add(widget); From a9d295c9904fb91254b646a72aef71acd97d8ab7 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Tue, 1 Jul 2025 23:03:53 +0100 Subject: [PATCH 06/11] Add share and download dropdown factories to view-only notebook --- src/pages/notebook.tsx | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/pages/notebook.tsx b/src/pages/notebook.tsx index 8cc2d605..8852e460 100644 --- a/src/pages/notebook.tsx +++ b/src/pages/notebook.tsx @@ -138,24 +138,26 @@ export const notebookPlugin: JupyterFrontEndPlugin = { app.shell.activateById(sidebarItem.id); app.restored.then(() => app.shell.activateById(sidebarItem.id)); - toolbarRegistry.addFactory( - 'Notebook', - 'downloadDropdown', - () => new DownloadDropdownButton(commands) - ); - - toolbarRegistry.addFactory( - 'Notebook', - 'share', - () => - new ToolbarButton({ - label: 'Share', - icon: EverywhereIcons.link, - tooltip: 'Share this notebook', - onClick: () => { - void commands.execute(Commands.shareNotebookCommand); - } - }) - ); + for (const toolbarName of ['Notebook', 'ViewOnlyNotebook']) { + toolbarRegistry.addFactory( + toolbarName, + 'downloadDropdown', + () => new DownloadDropdownButton(commands) + ); + + toolbarRegistry.addFactory( + toolbarName, + 'share', + () => + new ToolbarButton({ + label: 'Share', + icon: EverywhereIcons.link, + tooltip: 'Share this notebook', + onClick: () => { + void commands.execute(Commands.shareNotebookCommand); + } + }) + ); + } } }; From d3251edb37db7d87130d6a98c7ca75b24b827860 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Tue, 1 Jul 2025 23:06:11 +0100 Subject: [PATCH 07/11] Add specification for view-only notebook --- schema/plugin.json | 56 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/schema/plugin.json b/schema/plugin.json index e37b353f..5d6344cd 100644 --- a/schema/plugin.json +++ b/schema/plugin.json @@ -6,11 +6,63 @@ "properties": {}, "additionalProperties": false, "jupyter.lab.toolbars": { - "Notebook": [ + "ViewOnlyNotebook": [ { "name": "save", + "disabled": true + }, + { + "name": "cut", + "command": "notebook:cut-cell", + "disabled": true + }, + { + "name": "copy", + "command": "notebook:copy-cell", + "disabled": true + }, + { + "name": "paste", + "command": "notebook:paste-cell-below", + "disabled": true + }, + { + "name": "run", + "caption": "Run cell", + "disabled": true + }, + { + "name": "interrupt", + "caption": "Interrupt notebook", + "disabled": true + }, + { + "name": "restart", + "caption": "Restart notebook", + "disabled": true + }, + { + "name": "restart-and-run", + "caption": "Restart and run all cells", + "disabled": true + }, + { + "name": "cellType", "command": "", - "disabled": true, + "disabled": true + }, + { + "name": "share", + "rank": 35 + }, + { + "name": "downloadDropdown", + "rank": 36 + } + ], + "Notebook": [ + { + "name": "save", "rank": 10 }, { From 4ef8a804cf3a17afc14b9cffd43e1cca968d5aff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 2 Jul 2025 08:50:30 +0000 Subject: [PATCH 08/11] Update Playwright Snapshots --- .../application-shell-chromium-linux.png | Bin 11929 -> 12042 bytes .../read-only-notebook-chromium-linux.png | Bin 12428 -> 12898 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/ui-tests/tests/jupytereverywhere.spec.ts-snapshots/application-shell-chromium-linux.png b/ui-tests/tests/jupytereverywhere.spec.ts-snapshots/application-shell-chromium-linux.png index ec0925b6f6765d7b6aec3b2233034366a9a1f029..f0b281f1474794f8f448ad2b16a3daf478ef9bda 100644 GIT binary patch literal 12042 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 literal 11929 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 L{hI}U*kb+{Mo)uD literal 12428 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 From 7b6217ce1a0c03da2710463c53085c5426edcb80 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:56:22 +0100 Subject: [PATCH 09/11] Remove the writable `false` as we are using a custom factory anyways --- src/pages/notebook.tsx | 3 +-- .../read-only-notebook-chromium-linux.png | Bin 12898 -> 11470 bytes 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/notebook.tsx b/src/pages/notebook.tsx index 8852e460..364eb38e 100644 --- a/src/pages/notebook.tsx +++ b/src/pages/notebook.tsx @@ -70,8 +70,7 @@ export const notebookPlugin: JupyterFrontEndPlugin = { await contents.save(filename, { content, format: 'json', - type: 'notebook', - writable: false + type: 'notebook' }); await commands.execute('docmanager:open', { diff --git a/ui-tests/tests/jupytereverywhere.spec.ts-snapshots/read-only-notebook-chromium-linux.png b/ui-tests/tests/jupytereverywhere.spec.ts-snapshots/read-only-notebook-chromium-linux.png index e773f231cb9456dc8e47145c0018ba409cb9998e..2640cd86319ab430b812add1f3ae2313339c82a0 100644 GIT binary patch literal 11470 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 Sp@uD))^ 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 L{hI}U*kb+{Mo)uD From 44c7baa07b6f8a62280c3280e2a792304f0d8f77 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:18:07 +0100 Subject: [PATCH 10/11] Try to point to plugin settings --- src/view-only.ts | 2 +- .../read-only-notebook-chromium-linux.png | Bin 11470 -> 7286 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view-only.ts b/src/view-only.ts index 5fe34d49..c9f9ad20 100644 --- a/src/view-only.ts +++ b/src/view-only.ts @@ -135,7 +135,7 @@ export const viewOnlyNotebookFactoryPlugin: JupyterFrontEndPlugin { - const PANEL_SETTINGS = '@jupyterlab/notebook-extension:panel'; + const PANEL_SETTINGS = 'jupytereverywhere:plugin'; const toolbarFactory = createToolbarFactory( toolbarRegistry, diff --git a/ui-tests/tests/jupytereverywhere.spec.ts-snapshots/read-only-notebook-chromium-linux.png b/ui-tests/tests/jupytereverywhere.spec.ts-snapshots/read-only-notebook-chromium-linux.png index 2640cd86319ab430b812add1f3ae2313339c82a0..e1b3376f942616245f546057c1c33e4c40b0e936 100644 GIT binary patch delta 4421 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 Sp@uD) Date: Wed, 2 Jul 2025 11:28:34 +0100 Subject: [PATCH 11/11] Fix toolbar, save interaction, block markdown cell unrendering --- schema/plugin.json | 9 +++- src/pages/notebook.tsx | 12 ++++- src/view-only.ts | 46 ++++++++++++++++-- .../read-only-notebook-chromium-linux.png | Bin 7286 -> 9631 bytes 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/schema/plugin.json b/schema/plugin.json index 5d6344cd..3a75a089 100644 --- a/schema/plugin.json +++ b/schema/plugin.json @@ -3,7 +3,6 @@ "title": "jupytereverywhere", "description": "jupytereverywhere settings.", "type": "object", - "properties": {}, "additionalProperties": false, "jupyter.lab.toolbars": { "ViewOnlyNotebook": [ @@ -158,5 +157,13 @@ "disabled": true } ] + }, + "jupyter.lab.transform": true, + "properties": { + "toolbar": { + "title": "View-only notebook panel toolbar items", + "type": "array", + "default": [] + } } } diff --git a/src/pages/notebook.tsx b/src/pages/notebook.tsx index 364eb38e..90a1caef 100644 --- a/src/pages/notebook.tsx +++ b/src/pages/notebook.tsx @@ -47,6 +47,11 @@ export const notebookPlugin: JupyterFrontEndPlugin = { const { content }: { content: INotebookContent } = notebookResponse; + // We make all cells read-only by setting editable: false. + // This is still required with a custom widget factory as + // it is not trivial to coerce the cells to respect the `readOnly` + // property otherwise (Mike tried swapping `Notebook.ContentFactory` + // and it does not work without further hacks). if (content.cells) { content.cells.forEach(cell => { cell.metadata = { @@ -70,7 +75,12 @@ export const notebookPlugin: JupyterFrontEndPlugin = { await contents.save(filename, { content, format: 'json', - type: 'notebook' + type: 'notebook', + // Even though we have a custom view-only factory, we still + // want to indicate that notebook is read-only to avoid + // error on Ctrl + S and instead get a nice notification that + // the notebook cannot be saved unless using save-as. + writable: false }); await commands.execute('docmanager:open', { diff --git a/src/view-only.ts b/src/view-only.ts index c9f9ad20..8bab6a7d 100644 --- a/src/view-only.ts +++ b/src/view-only.ts @@ -1,15 +1,17 @@ import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; import { IEditorMimeTypeService } from '@jupyterlab/codeeditor'; import { WidgetTracker, IWidgetTracker } from '@jupyterlab/apputils'; +import { ReactiveToolbar, Toolbar } from '@jupyterlab/ui-components'; import { IEditorServices } from '@jupyterlab/codeeditor'; import { ABCWidgetFactory, DocumentRegistry, DocumentWidget } from '@jupyterlab/docregistry'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { ITranslator } from '@jupyterlab/translation'; -import { INotebookModel, NotebookPanel, Notebook, StaticNotebook } from '@jupyterlab/notebook'; +import { INotebookModel, Notebook, StaticNotebook } from '@jupyterlab/notebook'; import { createToolbarFactory, IToolbarWidgetRegistry } from '@jupyterlab/apputils'; import { Widget } from '@lumino/widgets'; import { Token } from '@lumino/coreutils'; +import { MarkdownCell } from '@jupyterlab/cells'; export const VIEW_ONLY_NOTEBOOK_FACTORY = 'ViewOnlyNotebook'; @@ -39,6 +41,27 @@ class ViewOnlyHeader extends Widget { } } +namespace FilteredToolbar { + export interface IOptions extends Toolbar.IOptions { + itemsToFilterOut: Set; + } +} + +class FilteredToolbar extends ReactiveToolbar { + constructor(options: FilteredToolbar.IOptions) { + super(options); + this._itemsToFilterOut = options.itemsToFilterOut; + } + insertItem(index: number, name: string, widget: Widget): boolean { + if (this._itemsToFilterOut?.has(name)) { + return false; + } + return super.insertItem(index, name, widget); + } + // This can be undefined during the super() call in constructor + private _itemsToFilterOut: Set | undefined; +} + class ViewOnlyNotebook extends StaticNotebook { // Add any customization for view-only notebook here if needed } @@ -48,7 +71,12 @@ class ViewOnlyNotebookPanel extends DocumentWidget) { - super(options); + super({ + ...options, + toolbar: new FilteredToolbar({ + itemsToFilterOut: new Set(['read-only-indicator']) + }) + }); this.addClass(NOTEBOOK_PANEL_CLASS); this.toolbar.addClass(NOTEBOOK_PANEL_TOOLBAR_CLASS); @@ -114,10 +142,17 @@ class ViewOnlyNotebookWidgetFactory extends ABCWidgetFactory< } } +class ViewOnlyContentFactory extends Notebook.ContentFactory { + createMarkdownCell(options: MarkdownCell.IOptions): MarkdownCell { + const cell = super.createMarkdownCell(options); + cell.showEditorForReadOnly = false; + return cell; + } +} + export const viewOnlyNotebookFactoryPlugin: JupyterFrontEndPlugin = { id: 'jupytereverywhere:view-only-notebook', requires: [ - NotebookPanel.IContentFactory, IEditorServices, IRenderMimeRegistry, IToolbarWidgetRegistry, @@ -128,13 +163,13 @@ export const viewOnlyNotebookFactoryPlugin: JupyterFrontEndPlugin { + // This needs to have a `toolbar` property with an array const PANEL_SETTINGS = 'jupytereverywhere:plugin'; const toolbarFactory = createToolbarFactory( @@ -146,6 +181,7 @@ export const viewOnlyNotebookFactoryPlugin: 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%0A&* ze^M_UT(|k>;k}Elbgbn$;f-ZMk%evEgcUVDLed1zC3kGV$;V@XwdPZYY~;0Xur6hS zyseZnkFAY%SY1ZFk4On`PgV-=t5`VW;bQ!;HfWihtsSnOc~krBG4hJBnl5?y!?^z^ zxW9rOD7(ERzGz$2s$^pZiAuM0uw4ziLyaI1x{Eicy`%`Q%WlMV)vpUW`4cIb-m&~w z12p5~JW|D4#_~^~p6Ew+mS-J7`@7Z1Rj#unETF$0K3#fN4s51Afm_$n8JUD{seUc^ zmeTBM1O4z@joy-oU)&(MePs3Yq(ZE1>HCg1W!rbPjfzaiR3T6o-j=C>7kp#5K>WS< zXyy9}p`REukG1eS2D1jk2;pth!lI%f?mSXj)4pDmRm*kVYbKb8w^1mUQ?e+?DTzbC z*B?CxyznM!R~~~(0tvpA8AB1}%}`S^Z&A~y1#xJFGy8rp4*Vq<{41(K_B!f_tS~C^ z)EmCW*#C2Z32>xvTAs+u%j3IR{uBY;-AZMJZ;0_d`8>Zi-D&8Xp2_Wd#upCj^Ix?W zGi&Fr@h84=x|-5>%k1m`1^!GY(Wi+x5jfXRflSscIsT9z48lPUo;*=F-9K`Yar8&G zMH8elSf<|wBoBvv?<Rynb}O(DWu`76;5&Bge%Usmr6AM6AI zzb~=MWcgeCMY(w-Y&SfoypCs|f3KXhyMqt<1NMgBvcatn#7RG&{IKbg9hK7kdLC+( zT3;8!Xh;K?Y&BT&4~<6$jxwfc@=@<`!`cLU I{a)060lxqxTL1t6