plexora 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plexora/__init__.py +129 -0
- plexora/__main__.py +15 -0
- plexora/_url.py +105 -0
- plexora/api/__init__.py +55 -0
- plexora/api/dataset.py +398 -0
- plexora/api/http.py +19 -0
- plexora/api/plugin.py +541 -0
- plexora/api/store.py +221 -0
- plexora/cli.py +777 -0
- plexora/client/dist/354_bundle.js +2 -0
- plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
- plexora/client/dist/418_bundle.js +2 -0
- plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
- plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
- plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
- plexora/client/dist/770_bundle.js +1 -0
- plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
- plexora/client/dist/vendor_bundle.js +2 -0
- plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
- plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
- plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
- plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
- plexora/client/src/css/import.css +474 -0
- plexora/client/src/css/main.css +903 -0
- plexora/client/src/css/openProject.css +363 -0
- plexora/client/src/css/quickView.css +166 -0
- plexora/client/src/css/tokens.css +51 -0
- plexora/client/src/css/viewer.css +1655 -0
- plexora/client/src/img/apple-touch-icon.png +0 -0
- plexora/client/src/img/favicon.ico +0 -0
- plexora/client/src/img/logo.ai +8892 -33
- plexora/client/src/img/logo.svg +1 -0
- plexora/client/src/img/logo_with_text.ai +9011 -34
- plexora/client/src/img/logo_with_text.svg +1 -0
- plexora/client/src/js/main.js +704 -0
- plexora/client/src/js/pluginRegistry.js +108 -0
- plexora/client/src/js/services/appStatus.js +286 -0
- plexora/client/src/js/services/browsePicker.js +73 -0
- plexora/client/src/js/services/dataLayer.js +400 -0
- plexora/client/src/js/services/datasetContext.js +143 -0
- plexora/client/src/js/services/glRenderer.js +143 -0
- plexora/client/src/js/services/importFormValidation.js +257 -0
- plexora/client/src/js/services/numericData.js +100 -0
- plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
- plexora/client/src/js/services/simpleEventHandler.js +25 -0
- plexora/client/src/js/vendor.js +29 -0
- plexora/client/src/js/views/channelList.js +663 -0
- plexora/client/src/js/views/colorSwatchPicker.js +202 -0
- plexora/client/src/js/views/columnClassifier.js +146 -0
- plexora/client/src/js/views/coordinateField.js +133 -0
- plexora/client/src/js/views/dataSourceField.js +276 -0
- plexora/client/src/js/views/imageViewer.js +3306 -0
- plexora/client/src/js/views/miniMap.js +787 -0
- plexora/client/src/js/views/navbarControls.js +178 -0
- plexora/client/src/js/views/openProjectPage.js +239 -0
- plexora/client/src/js/views/projectEdit.js +335 -0
- plexora/client/src/js/views/quickViewLanding.js +136 -0
- plexora/client/src/js/views/rainbow.js +326 -0
- plexora/client/src/js/views/requirementsModal.js +572 -0
- plexora/client/src/js/views/rgbImageViewer.js +115 -0
- plexora/client/src/js/views/searchableSelect.js +411 -0
- plexora/client/src/js/views/segmentationProgress.js +122 -0
- plexora/client/src/js/views/toolLoader.js +747 -0
- plexora/client/src/js/views/viewerControls.js +689 -0
- plexora/client/src/js/views/viewerManager.js +434 -0
- plexora/client/src/js/views/viewerSidebar.js +1117 -0
- plexora/client/src/js/workers/tileDecoder.js +130 -0
- plexora/client/src/shaders/frag.glsl +528 -0
- plexora/client/src/shaders/vert.glsl +12 -0
- plexora/client/templates/base.html +212 -0
- plexora/client/templates/index.html +238 -0
- plexora/client/templates/open_project.html +96 -0
- plexora/client/templates/project_columns.html +86 -0
- plexora/client/templates/project_edit.html +196 -0
- plexora/client/templates/upload.html +118 -0
- plexora/connect.py +528 -0
- plexora/datasource.py +782 -0
- plexora/jupyter.py +406 -0
- plexora/notebook_env.py +232 -0
- plexora/paths.py +486 -0
- plexora/plugins/__init__.py +12 -0
- plexora/plugins/cell_explorer/__init__.py +100 -0
- plexora/plugins/cell_explorer/server/__init__.py +11 -0
- plexora/plugins/cell_explorer/server/routes.py +167 -0
- plexora/plugins/cell_explorer/server/state.py +281 -0
- plexora/plugins/cell_explorer/server/values.py +145 -0
- plexora/plugins/cell_explorer/server/variables.py +367 -0
- plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
- plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
- plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
- plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
- plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
- plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
- plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
- plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
- plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
- plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
- plexora/plugins/figure_builder/__init__.py +125 -0
- plexora/plugins/figure_builder/server/__init__.py +7 -0
- plexora/plugins/figure_builder/server/compose.py +365 -0
- plexora/plugins/figure_builder/server/export.py +784 -0
- plexora/plugins/figure_builder/server/export_jobs.py +148 -0
- plexora/plugins/figure_builder/server/operations.py +556 -0
- plexora/plugins/figure_builder/server/pixels.py +142 -0
- plexora/plugins/figure_builder/server/provenance.py +174 -0
- plexora/plugins/figure_builder/server/render.py +278 -0
- plexora/plugins/figure_builder/server/repository.py +701 -0
- plexora/plugins/figure_builder/server/routes.py +594 -0
- plexora/plugins/figure_builder/server/schema.py +1023 -0
- plexora/plugins/figure_builder/server/sources.py +120 -0
- plexora/plugins/figure_builder/server/textmetrics.py +155 -0
- plexora/plugins/figure_builder/static/figureActions.js +363 -0
- plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
- plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
- plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
- plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
- plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
- plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
- plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
- plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
- plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
- plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
- plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
- plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
- plexora/plugins/figure_builder/static/figureRichText.js +634 -0
- plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
- plexora/plugins/figure_builder/static/figureSchema.js +218 -0
- plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
- plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
- plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
- plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
- plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
- plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
- plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
- plexora/plugins/gating/__init__.py +76 -0
- plexora/plugins/gating/server/__init__.py +0 -0
- plexora/plugins/gating/server/anndata_gates.py +367 -0
- plexora/plugins/gating/server/database.py +16 -0
- plexora/plugins/gating/server/model.py +320 -0
- plexora/plugins/gating/server/routes.py +235 -0
- plexora/plugins/gating/static/csvGatingList.js +947 -0
- plexora/plugins/gating/static/gating.css +289 -0
- plexora/plugins/gating/static/gatingApi.js +221 -0
- plexora/plugins/gating/static/gatingSidebarController.js +513 -0
- plexora/plugins/gating/templates/gating/legacy.html +26 -0
- plexora/plugins/gating/templates/gating/panel.html +69 -0
- plexora/plugins/roi/__init__.py +95 -0
- plexora/plugins/roi/server/__init__.py +8 -0
- plexora/plugins/roi/server/adapters.py +655 -0
- plexora/plugins/roi/server/geojson.py +267 -0
- plexora/plugins/roi/server/geometry.py +181 -0
- plexora/plugins/roi/server/mapping.py +169 -0
- plexora/plugins/roi/server/operations.py +293 -0
- plexora/plugins/roi/server/repository.py +215 -0
- plexora/plugins/roi/server/routes.py +409 -0
- plexora/plugins/roi/server/schema.py +323 -0
- plexora/plugins/roi/static/roi.css +583 -0
- plexora/plugins/roi/static/roiApi.js +177 -0
- plexora/plugins/roi/static/roiFigureBridge.js +107 -0
- plexora/plugins/roi/static/roiGeometry.js +350 -0
- plexora/plugins/roi/static/roiRenderer.js +278 -0
- plexora/plugins/roi/static/roiSidebarController.js +1038 -0
- plexora/plugins/roi/static/roiState.js +518 -0
- plexora/plugins/roi/static/roiTools.js +993 -0
- plexora/plugins/roi/templates/roi/panel.html +240 -0
- plexora/proxy.py +57 -0
- plexora/server/models/adapters/__init__.py +94 -0
- plexora/server/models/adapters/anndata_adapter.py +388 -0
- plexora/server/models/adapters/base.py +93 -0
- plexora/server/models/adapters/classify.py +192 -0
- plexora/server/models/adapters/csv_adapter.py +105 -0
- plexora/server/models/adapters/inspection.py +265 -0
- plexora/server/models/adapters/spatialdata_adapter.py +248 -0
- plexora/server/models/centroid_tiles.py +305 -0
- plexora/server/models/data_model.py +1659 -0
- plexora/server/models/database_model.py +169 -0
- plexora/server/models/project.py +1270 -0
- plexora/server/plugins.py +199 -0
- plexora/server/routes/browse_routes.py +41 -0
- plexora/server/routes/data_routes.py +294 -0
- plexora/server/routes/import_routes.py +603 -0
- plexora/server/routes/page_routes.py +139 -0
- plexora/server/routes/project_routes.py +438 -0
- plexora/server/routes/quick_view_routes.py +62 -0
- plexora/server/routes/system_routes.py +39 -0
- plexora/server/routes/tool_routes.py +418 -0
- plexora/server/utils/addHEColumns.py +11 -0
- plexora/server/utils/fast_png.py +97 -0
- plexora/server/utils/fullConversion.py +50 -0
- plexora/server/utils/native_dialog.py +168 -0
- plexora/server/utils/pre_normalization.py +47 -0
- plexora/server/utils/segmentation_pyramid.py +598 -0
- plexora/server/utils/smallestenclosingcircle.py +126 -0
- plexora/server/utils/tiffsurgeon.py +372 -0
- plexora/server_cli.py +48 -0
- plexora/test.py +0 -0
- plexora-0.0.1.dist-info/METADATA +341 -0
- plexora-0.0.1.dist-info/RECORD +248 -0
- plexora-0.0.1.dist-info/WHEEL +5 -0
- plexora-0.0.1.dist-info/entry_points.txt +12 -0
- plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
- plexora-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* toolLoader.js - Always loaded, unlike a plugin's own scripts, which are fetched
|
|
3
|
+
* lazily and named by the plugin's descriptor (see plexora/api/plugin.py's
|
|
4
|
+
* Plugin.scripts and asset_urls).
|
|
5
|
+
* Intercepts the navbar Tools-menu links so opening/closing an addon tool mid-session
|
|
6
|
+
* never navigates: on first open it fetches the tool's sidebar HTML, stylesheets and
|
|
7
|
+
* scripts from `/<datasource>/tools/<tool>/panel`, injects all three, and hands off to
|
|
8
|
+
* main.js's `__plexora.activatePlugin()` to register the plugin exactly as it
|
|
9
|
+
* would be at boot. Later opens just toggle visibility -- nothing is re-fetched.
|
|
10
|
+
*
|
|
11
|
+
* Each tool gets its OWN mount inside the shared slot. That was not so while
|
|
12
|
+
* gating was the only plugin: the fragment was written straight to
|
|
13
|
+
* `slot.innerHTML`, which is a whole-slot replace. With a second plugin that is
|
|
14
|
+
* destructive in a way nothing reports -- opening B wipes A's panel out of the
|
|
15
|
+
* DOM while A's controller keeps the element handles it took at setup(), and the
|
|
16
|
+
* re-open path here (which only unhides the slot) then shows an empty panel and
|
|
17
|
+
* a live controller wired to nodes that are no longer on the page.
|
|
18
|
+
*
|
|
19
|
+
* THREE STATES, kept apart on purpose, because collapsing any two of them is
|
|
20
|
+
* what made a second plugin unusable:
|
|
21
|
+
*
|
|
22
|
+
* LOADED - the tool's record, panel DOM, controller and cached data exist. It
|
|
23
|
+
* has a card in the sidebar and draws nothing.
|
|
24
|
+
* VISIBLE - it contributes a layer to the image. Several tools may be visible
|
|
25
|
+
* at once, and the card order is the order they stack in.
|
|
26
|
+
* ACTIVE - it is the one the shared Cells control, the opacity slider, picking
|
|
27
|
+
* and the gate flows act on, and the one whose panel is expanded.
|
|
28
|
+
* Exactly one, or none.
|
|
29
|
+
*
|
|
30
|
+
* Opening a tool makes it all three and stands the previous one down to LOADED,
|
|
31
|
+
* so the default is still one thing on screen. Turning another card's eye back on
|
|
32
|
+
* is what stacks them -- one click, and it is the whole feature. That click also
|
|
33
|
+
* PINS the layer, and a pinned layer is exempt from the stand-down: the default
|
|
34
|
+
* is for the first switch, not a rule that keeps dismantling a stack somebody
|
|
35
|
+
* built on purpose.
|
|
36
|
+
*
|
|
37
|
+
* The one exception to single-active is a COEXISTING PAIR: two tools opened
|
|
38
|
+
* together through openToolAlongside(), which stay expanded and drawn side by
|
|
39
|
+
* side while the selection moves freely between them. Cell Explorer's Open ROIs
|
|
40
|
+
* button is the only caller, because its ROI composition card summarises the
|
|
41
|
+
* cells under an overlay that has to still be drawn for the answer to mean
|
|
42
|
+
* anything. Opening a third tool, or closing either half, dissolves the pair --
|
|
43
|
+
* the exception never outlives the pairing that justified it.
|
|
44
|
+
*
|
|
45
|
+
* Visibility of the PANELS is therefore a function of each entry's own
|
|
46
|
+
* `collapsed` flag, applied in `paint()`, rather than something each call site
|
|
47
|
+
* toggles for itself.
|
|
48
|
+
*
|
|
49
|
+
* Switching away also has to TELL the tool. A sidebar panel can be hidden and
|
|
50
|
+
* left running, but a tool that reaches outside its panel -- viewer-canvas
|
|
51
|
+
* pointer handlers, document-level keyboard shortcuts -- has to stand those down
|
|
52
|
+
* or it keeps eating input for a panel the user cannot see. That is what
|
|
53
|
+
* `onHide()` is for (see pluginRegistry.js); `onVisibilityChange()` is its
|
|
54
|
+
* counterpart for the eye toggle, and `ctx.onCleanup` remains the full-teardown
|
|
55
|
+
* path for when a plugin is removed outright.
|
|
56
|
+
*
|
|
57
|
+
* Deliberately kept off the `__plexora` object: that object is created fresh by
|
|
58
|
+
* main.js (`const __plexora = window.__plexora = {...}`), which -- because this
|
|
59
|
+
* script runs first in document order -- would clobber anything stored on it before
|
|
60
|
+
* main.js's own init() runs.
|
|
61
|
+
*/
|
|
62
|
+
window.PlexoraToolLoader = (function () {
|
|
63
|
+
//: toolName -> { slotIds, sidebarController, visible, collapsed, pinned }
|
|
64
|
+
const loadedTools = new Map();
|
|
65
|
+
|
|
66
|
+
//: The tool the shared controls point at, or null when none is. Every
|
|
67
|
+
//: activation decision is derived from this rather than tracked per element.
|
|
68
|
+
let activeToolName = null;
|
|
69
|
+
|
|
70
|
+
//: The one sanctioned exception to single-active: a Set of exactly two tool
|
|
71
|
+
//: names that stay expanded and drawn together. Cell Explorer opens ROI this
|
|
72
|
+
//: way, because its ROI composition card only means anything while the
|
|
73
|
+
//: metadata overlay it summarises is still on screen underneath. Kept
|
|
74
|
+
//: deliberately narrow -- only openToolAlongside() forms a pair, and a third
|
|
75
|
+
//: tool or closing either half dissolves it rather than leaving a standing
|
|
76
|
+
//: exception behind.
|
|
77
|
+
let coexistPair = null;
|
|
78
|
+
|
|
79
|
+
/** The other half of the engaged pair, or null. */
|
|
80
|
+
function pairPartner(name) {
|
|
81
|
+
if (!coexistPair || !coexistPair.has(name)) return null;
|
|
82
|
+
let partner = null;
|
|
83
|
+
coexistPair.forEach((other) => { if (other !== name) partner = other; });
|
|
84
|
+
return partner;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Whether `name` is sharing the screen with whichever tool is selected. A
|
|
88
|
+
* pair is only ever engaged AROUND the active tool, so a name still in
|
|
89
|
+
* coexistPair while the active tool is not in it is stale, not coexisting. */
|
|
90
|
+
function isCoexisting(name) {
|
|
91
|
+
return Boolean(coexistPair && coexistPair.has(name)
|
|
92
|
+
&& coexistPair.has(activeToolName));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const HIDDEN = "tool-panel-hidden";
|
|
96
|
+
const MOUNT_ATTR = "data-tool-panel";
|
|
97
|
+
const CARD_ATTR = "data-tool-card";
|
|
98
|
+
|
|
99
|
+
//: The one slot that gets cards. `tool_panel_legacy_slot` is off-screen
|
|
100
|
+
//: scaffolding (gating mounts its download panel there); wrapping that in a
|
|
101
|
+
//: card would put a draggable header on something nobody can see.
|
|
102
|
+
const CARD_SLOT = "tool_panel_slot";
|
|
103
|
+
|
|
104
|
+
//: The drag-to-restack binding, created once the first card exists.
|
|
105
|
+
let sortable = null;
|
|
106
|
+
|
|
107
|
+
function detach(node) {
|
|
108
|
+
if (!node) return;
|
|
109
|
+
if (node.remove) node.remove();
|
|
110
|
+
else node.parentNode?.removeChild?.(node);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The tool's human name, taken from the Tools-menu link that opens it, so
|
|
114
|
+
* a plugin does not have to send its label twice. */
|
|
115
|
+
function toolLabel(toolName) {
|
|
116
|
+
const link = document.querySelector?.(`a[data-tool="${toolName}"]`);
|
|
117
|
+
const text = link?.textContent?.trim?.();
|
|
118
|
+
return text || toolName;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** `icons` is one class string per glyph. More than one is how a button that
|
|
122
|
+
* has two states is built here -- see the eye in buildCard. */
|
|
123
|
+
function iconButton(className, title, icons, onClick) {
|
|
124
|
+
const button = document.createElement("button");
|
|
125
|
+
button.type = "button";
|
|
126
|
+
button.className = className;
|
|
127
|
+
button.title = title;
|
|
128
|
+
button.innerHTML = [].concat(icons)
|
|
129
|
+
.map((icon) => `<span class="${icon}"></span>`).join("");
|
|
130
|
+
button.addEventListener("click", onClick);
|
|
131
|
+
return button;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* One tool's card: a grip, a collapse chevron, the tool's name, an eye and a
|
|
136
|
+
* remove button, over the panel the plugin rendered.
|
|
137
|
+
*
|
|
138
|
+
* The panel is wrapped, never re-parented later: collapsing is a class on
|
|
139
|
+
* the card, so the controller's element handles -- taken once at setup() --
|
|
140
|
+
* stay valid for the life of the tool. That is what makes expanding instant
|
|
141
|
+
* instead of a re-render.
|
|
142
|
+
*/
|
|
143
|
+
function buildCard(toolName, mount) {
|
|
144
|
+
const card = document.createElement("section");
|
|
145
|
+
card.className = "tool-card";
|
|
146
|
+
card.setAttribute(CARD_ATTR, toolName);
|
|
147
|
+
|
|
148
|
+
const header = document.createElement("div");
|
|
149
|
+
header.className = "tool-card-header";
|
|
150
|
+
|
|
151
|
+
const grip = document.createElement("span");
|
|
152
|
+
grip.className = "tool-card-grip fas fa-grip-vertical";
|
|
153
|
+
grip.title = "Drag to restack the layers";
|
|
154
|
+
header.appendChild(grip);
|
|
155
|
+
|
|
156
|
+
header.appendChild(iconButton(
|
|
157
|
+
"tool-card-collapse", "Collapse or expand this panel", "fas fa-chevron-down",
|
|
158
|
+
() => setToolCollapsed(toolName, !loadedTools.get(toolName)?.collapsed)));
|
|
159
|
+
|
|
160
|
+
const title = document.createElement("button");
|
|
161
|
+
title.type = "button";
|
|
162
|
+
title.className = "tool-card-title";
|
|
163
|
+
title.textContent = toolLabel(toolName);
|
|
164
|
+
// Selecting a card is what moves the shared controls onto it -- and,
|
|
165
|
+
// being the single-active path, folds the previous one away.
|
|
166
|
+
title.addEventListener("click", () => show(toolName));
|
|
167
|
+
header.appendChild(title);
|
|
168
|
+
|
|
169
|
+
// Both glyphs go in, and CSS shows whichever the card's is-layer-off
|
|
170
|
+
// class calls for. Rewriting one glyph's class from JS does NOT work:
|
|
171
|
+
// FontAwesome is loaded as JS (vendor.js), so it replaces every
|
|
172
|
+
// `<span class="fas fa-...">` with an `<svg>` before anyone can click
|
|
173
|
+
// anything -- the span the swap went looking for is no longer on the
|
|
174
|
+
// page, and nothing reports that. It is why a hidden layer used to sit
|
|
175
|
+
// under an open eye.
|
|
176
|
+
header.appendChild(iconButton(
|
|
177
|
+
"tool-card-eye", "Show or hide this tool's layer",
|
|
178
|
+
["fas fa-eye tool-card-eye-on", "fas fa-eye-slash tool-card-eye-off"],
|
|
179
|
+
() => setToolVisible(toolName, !loadedTools.get(toolName)?.visible)));
|
|
180
|
+
|
|
181
|
+
header.appendChild(iconButton(
|
|
182
|
+
"tool-card-remove", "Remove this tool", "fas fa-xmark",
|
|
183
|
+
() => removeTool(toolName)));
|
|
184
|
+
|
|
185
|
+
card.appendChild(header);
|
|
186
|
+
|
|
187
|
+
const body = document.createElement("div");
|
|
188
|
+
body.className = "tool-card-body";
|
|
189
|
+
body.appendChild(mount);
|
|
190
|
+
card.appendChild(body);
|
|
191
|
+
return card;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function cardFor(toolName) {
|
|
195
|
+
const slot = document.getElementById(CARD_SLOT);
|
|
196
|
+
return slot?.querySelector?.(`[${CARD_ATTR}="${toolName}"]`) || null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** One tool's wrapper inside one slot, created on demand. */
|
|
200
|
+
function mountFor(slotId, toolName, create) {
|
|
201
|
+
const slot = document.getElementById(slotId);
|
|
202
|
+
if (!slot) return null;
|
|
203
|
+
let mount = slot.querySelector?.(`[${MOUNT_ATTR}="${toolName}"]`) || null;
|
|
204
|
+
if (!mount && create) {
|
|
205
|
+
mount = document.createElement("div");
|
|
206
|
+
mount.className = "tool-panel-mount";
|
|
207
|
+
mount.setAttribute(MOUNT_ATTR, toolName);
|
|
208
|
+
if (slotId === CARD_SLOT) {
|
|
209
|
+
// At the TOP of the slot: the cards read downwards and the top
|
|
210
|
+
// one is the topmost layer, so a tool the user just opened is
|
|
211
|
+
// over everything already loaded.
|
|
212
|
+
const card = buildCard(toolName, mount);
|
|
213
|
+
if (slot.firstChild) slot.insertBefore(card, slot.firstChild);
|
|
214
|
+
else slot.appendChild(card);
|
|
215
|
+
} else {
|
|
216
|
+
slot.appendChild(mount);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return mount;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Show every expanded tool's panel and hide the collapsed ones.
|
|
224
|
+
*
|
|
225
|
+
* A slot is hidden when nothing in it is showing, which is what the class on
|
|
226
|
+
* the slot itself has always meant -- the server renders it that way for a
|
|
227
|
+
* page opened with no tool. The card slot is the exception: a collapsed card
|
|
228
|
+
* still shows its header, so that slot stays open for as long as it holds a
|
|
229
|
+
* card at all.
|
|
230
|
+
*/
|
|
231
|
+
function paint() {
|
|
232
|
+
const slotIds = new Set();
|
|
233
|
+
loadedTools.forEach((entry) => entry.slotIds.forEach((id) => slotIds.add(id)));
|
|
234
|
+
|
|
235
|
+
slotIds.forEach((slotId) => {
|
|
236
|
+
const slot = document.getElementById(slotId);
|
|
237
|
+
if (!slot) return;
|
|
238
|
+
const isCardSlot = slotId === CARD_SLOT;
|
|
239
|
+
let showing = false;
|
|
240
|
+
loadedTools.forEach((entry, name) => {
|
|
241
|
+
if (!entry.slotIds.includes(slotId)) return;
|
|
242
|
+
// In the card slot a panel shows unless its own card is folded.
|
|
243
|
+
// In the off-screen legacy slot there are no cards, so only the
|
|
244
|
+
// active tool's mount shows -- which is what it always did.
|
|
245
|
+
const visible = isCardSlot
|
|
246
|
+
? !entry.collapsed
|
|
247
|
+
: (name === activeToolName || isCoexisting(name));
|
|
248
|
+
showing = showing || visible || isCardSlot;
|
|
249
|
+
const mount = mountFor(slotId, name);
|
|
250
|
+
if (mount) mount.classList.toggle(HIDDEN, !visible);
|
|
251
|
+
});
|
|
252
|
+
slot.classList.toggle(HIDDEN, !showing);
|
|
253
|
+
});
|
|
254
|
+
paintCards();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The card headers: which is folded, which is selected, which layer is off.
|
|
259
|
+
*
|
|
260
|
+
* Three classes and nothing else -- the chevron's direction and which of the
|
|
261
|
+
* eye's two glyphs shows both hang off these in CSS, so there is no glyph
|
|
262
|
+
* here to keep in step and no way for the icons to drift out of it.
|
|
263
|
+
*/
|
|
264
|
+
function paintCards() {
|
|
265
|
+
loadedTools.forEach((entry, name) => {
|
|
266
|
+
const card = cardFor(name);
|
|
267
|
+
if (!card) return;
|
|
268
|
+
card.classList.toggle("is-collapsed", Boolean(entry.collapsed));
|
|
269
|
+
// Both halves of a coexisting pair read as selected: only one of
|
|
270
|
+
// them owns the shared controls, but the user opened them together
|
|
271
|
+
// and one greyed-out card would look like something had failed.
|
|
272
|
+
card.classList.toggle("is-active",
|
|
273
|
+
name === activeToolName || isCoexisting(name));
|
|
274
|
+
card.classList.toggle("is-layer-off", !entry.visible);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Sidebar order is layer order, and the top card is the top layer.
|
|
280
|
+
*
|
|
281
|
+
* Core stacks bottom-first, so the DOM order is reversed on the way out
|
|
282
|
+
* rather than the cards being built upside down -- a list whose first row is
|
|
283
|
+
* the bottom of the picture reads backwards to everyone.
|
|
284
|
+
*/
|
|
285
|
+
function syncLayerOrder() {
|
|
286
|
+
const slot = document.getElementById(CARD_SLOT);
|
|
287
|
+
if (!slot?.children) return;
|
|
288
|
+
const names = [];
|
|
289
|
+
Array.from(slot.children).forEach((child) => {
|
|
290
|
+
const name = child.getAttribute?.(CARD_ATTR);
|
|
291
|
+
if (name && loadedTools.has(name)) names.push(name);
|
|
292
|
+
});
|
|
293
|
+
names.reverse();
|
|
294
|
+
try {
|
|
295
|
+
window.__plexora?.setToolLayerOrder?.(names);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error("toolLoader: setToolLayerOrder() failed", error);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Drag-to-restack, on the same vendored library the column classifier uses
|
|
302
|
+
* (columnClassifier.js). Handle-only, so a click anywhere else in the
|
|
303
|
+
* header still reaches the button it landed on. */
|
|
304
|
+
function ensureSortable() {
|
|
305
|
+
const slot = document.getElementById(CARD_SLOT);
|
|
306
|
+
if (!slot || sortable || typeof window.Sortable !== "function") return;
|
|
307
|
+
sortable = new window.Sortable(slot, {
|
|
308
|
+
handle: ".tool-card-grip",
|
|
309
|
+
draggable: ".tool-card",
|
|
310
|
+
animation: 150,
|
|
311
|
+
onSort: syncLayerOrder,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Push one tool's on/off state everywhere it has to land.
|
|
317
|
+
*
|
|
318
|
+
* Core switches the cell layer for a plugin that has one; a tool that draws
|
|
319
|
+
* its own overlay (ROI) has nothing for core to switch, so its controller is
|
|
320
|
+
* told directly. Without the second half the eye on ROI's card would be a
|
|
321
|
+
* button that does nothing.
|
|
322
|
+
*/
|
|
323
|
+
function applyToolVisible(toolName, on) {
|
|
324
|
+
const entry = loadedTools.get(toolName);
|
|
325
|
+
if (!entry) return;
|
|
326
|
+
entry.visible = Boolean(on);
|
|
327
|
+
try {
|
|
328
|
+
window.__plexora?.setToolLayerVisible?.(toolName, entry.visible);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error("toolLoader: setToolLayerVisible() failed", error);
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
entry.sidebarController?.onVisibilityChange?.(entry.visible);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.error("toolLoader: onVisibilityChange() failed", error);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The card's eye. Unlike every other path that changes visibility, this one
|
|
341
|
+
* is a DECISION, and it is recorded as one.
|
|
342
|
+
*
|
|
343
|
+
* `pinned` is what stops the single-active default from undoing a stack the
|
|
344
|
+
* user built on purpose. Opening a tool turns the previous one's layer off,
|
|
345
|
+
* which is right the first time -- but once somebody has explicitly put a
|
|
346
|
+
* second layer back on to compare the two, clicking between their cards must
|
|
347
|
+
* stop taking it away again. Same shape as a layer's `userMode`: an
|
|
348
|
+
* automatic choice fills a gap, an explicit one is kept.
|
|
349
|
+
*/
|
|
350
|
+
function setToolVisible(toolName, on) {
|
|
351
|
+
const entry = loadedTools.get(toolName);
|
|
352
|
+
if (!entry) return;
|
|
353
|
+
entry.pinned = Boolean(on);
|
|
354
|
+
if (entry.visible === Boolean(on)) return;
|
|
355
|
+
applyToolVisible(toolName, on);
|
|
356
|
+
paintCards();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function setToolCollapsed(toolName, collapsed) {
|
|
360
|
+
const entry = loadedTools.get(toolName);
|
|
361
|
+
if (!entry) return;
|
|
362
|
+
entry.collapsed = Boolean(collapsed);
|
|
363
|
+
paint();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Put one tool back to LOADED: folded away, no longer drawing unless the
|
|
367
|
+
* user pinned it, and told so anything it hung outside its own panel --
|
|
368
|
+
* canvas handlers, document shortcuts -- stands down with it. */
|
|
369
|
+
function fold(toolName) {
|
|
370
|
+
const entry = loadedTools.get(toolName);
|
|
371
|
+
if (!entry) return;
|
|
372
|
+
entry.collapsed = true;
|
|
373
|
+
if (!entry.pinned) applyToolVisible(toolName, false);
|
|
374
|
+
try {
|
|
375
|
+
entry.sidebarController?.onHide?.();
|
|
376
|
+
} catch (error) {
|
|
377
|
+
console.error("toolLoader: onHide() failed", error);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Stand the selected tool down, unless it is the one being opened.
|
|
383
|
+
*
|
|
384
|
+
* Called before another tool is shown rather than when its own close button
|
|
385
|
+
* is pressed, so a tool never has to know it was switched away from.
|
|
386
|
+
*
|
|
387
|
+
* Single-active by default: the outgoing tool folds up AND its layer goes
|
|
388
|
+
* off, so opening a second tool shows one picture rather than two stacked
|
|
389
|
+
* ones nobody asked to compare. It stays LOADED -- its data, its colours and
|
|
390
|
+
* its panel are all still there -- so turning it back on is one click.
|
|
391
|
+
*
|
|
392
|
+
* Unless the user has pinned it with the eye, which is the one thing that
|
|
393
|
+
* outranks the default -- see setToolVisible -- or the outgoing tool is
|
|
394
|
+
* half of a coexisting pair, which is the other.
|
|
395
|
+
*/
|
|
396
|
+
function standDown(except) {
|
|
397
|
+
if (!activeToolName || activeToolName === except) return;
|
|
398
|
+
const previous = activeToolName;
|
|
399
|
+
// Moving the selection between the two halves of a coexisting pair is
|
|
400
|
+
// not a switch away from anything: both cards stay open and both layers
|
|
401
|
+
// stay drawn, and only which one the shared controls point at changes.
|
|
402
|
+
if (except && isCoexisting(previous) && coexistPair.has(except)) return;
|
|
403
|
+
// A third tool does end the arrangement, and it folds BOTH halves --
|
|
404
|
+
// otherwise the exception outlives the pairing that justified it and the
|
|
405
|
+
// user is left with a stray layer nobody asked to keep.
|
|
406
|
+
const partner = pairPartner(previous);
|
|
407
|
+
coexistPair = null;
|
|
408
|
+
activeToolName = null;
|
|
409
|
+
fold(previous);
|
|
410
|
+
if (partner) fold(partner);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function show(toolName) {
|
|
414
|
+
standDown(toolName);
|
|
415
|
+
const entry = loadedTools.get(toolName);
|
|
416
|
+
if (entry) {
|
|
417
|
+
entry.collapsed = false;
|
|
418
|
+
applyToolVisible(toolName, true);
|
|
419
|
+
}
|
|
420
|
+
activeToolName = toolName;
|
|
421
|
+
paint();
|
|
422
|
+
syncLayerOrder();
|
|
423
|
+
// Before onShow(), not after: a controller that re-applies its cell
|
|
424
|
+
// colours there is talking to controls that have to be pointing at this
|
|
425
|
+
// tool already, and core's own Cells control has to have adopted this
|
|
426
|
+
// layer's mode before the panel reads it back. See main.js's
|
|
427
|
+
// setActiveTool.
|
|
428
|
+
try {
|
|
429
|
+
window.__plexora?.setActiveTool?.(toolName);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
console.error("toolLoader: setActiveTool() failed", error);
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
loadedTools.get(toolName)?.sidebarController?.onShow?.();
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.error("toolLoader: onShow() failed", error);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function loadScript(src) {
|
|
441
|
+
if (document.querySelector(`script[src="${src}"]`)) return Promise.resolve();
|
|
442
|
+
return new Promise((resolve, reject) => {
|
|
443
|
+
const script = document.createElement("script");
|
|
444
|
+
script.src = src;
|
|
445
|
+
script.onload = () => resolve();
|
|
446
|
+
script.onerror = () => reject(new Error(`toolLoader: failed to load ${src}`));
|
|
447
|
+
document.head.appendChild(script);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function loadStyle(href) {
|
|
452
|
+
if (document.querySelector(`link[rel="stylesheet"][href="${href}"]`)) return Promise.resolve();
|
|
453
|
+
return new Promise((resolve) => {
|
|
454
|
+
const link = document.createElement("link");
|
|
455
|
+
link.rel = "stylesheet";
|
|
456
|
+
link.href = href;
|
|
457
|
+
// Resolves either way, unlike loadScript: a plugin whose stylesheet
|
|
458
|
+
// is missing still works, so refusing to open it would turn a
|
|
459
|
+
// cosmetic fault into a broken tool. The console line is what says
|
|
460
|
+
// an unstyled panel was not intended.
|
|
461
|
+
link.onload = () => resolve();
|
|
462
|
+
link.onerror = () => {
|
|
463
|
+
console.error(`toolLoader: failed to load ${href}`);
|
|
464
|
+
resolve();
|
|
465
|
+
};
|
|
466
|
+
document.head.appendChild(link);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function openTool(toolName, linkEl) {
|
|
471
|
+
if (loadedTools.has(toolName)) {
|
|
472
|
+
show(toolName);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
linkEl?.classList.add("tool-loading");
|
|
477
|
+
try {
|
|
478
|
+
const datasource = window.flaskVariables?.datasource;
|
|
479
|
+
const baseUrl = window.PLEXORA_BASE_URL || "";
|
|
480
|
+
const response = await fetch(`${baseUrl}/${datasource}/tools/${toolName}/panel`);
|
|
481
|
+
const payload = await response.json();
|
|
482
|
+
|
|
483
|
+
if (payload.needs) {
|
|
484
|
+
// The tool is installed and compatible but the project is
|
|
485
|
+
// missing something it declared. Ask for exactly that, then
|
|
486
|
+
// re-enter -- navigating away to collect a column name would
|
|
487
|
+
// tear down and rebuild the whole viewer to answer one
|
|
488
|
+
// question, which is the reason this lazy path exists.
|
|
489
|
+
const satisfied = await window.PlexoraRequirements.collect(
|
|
490
|
+
datasource, payload.needs);
|
|
491
|
+
if (!satisfied) return;
|
|
492
|
+
return openTool(toolName, linkEl);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (payload.redirect) {
|
|
496
|
+
// Unknown datasource or an uninstalled tool -- the server has
|
|
497
|
+
// decided where to send us.
|
|
498
|
+
window.location.href = payload.redirect;
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Stylesheets before the markup, and awaited: the fragments below
|
|
503
|
+
// are shown as soon as their slots are unhidden, and a plugin's own
|
|
504
|
+
// CSS is the only thing that styles them. Skipping this is what
|
|
505
|
+
// made gating's panels render raw -- the file input as a bare
|
|
506
|
+
// "Choose File", the download panel with no surface -- whenever the
|
|
507
|
+
// tool was opened from the Tools menu rather than loaded with
|
|
508
|
+
// ?tool=..., which is the path base.html covers.
|
|
509
|
+
await Promise.all((payload.styles || []).map(loadStyle));
|
|
510
|
+
|
|
511
|
+
const slotIds = Object.keys(payload.fragments || {});
|
|
512
|
+
slotIds.forEach((slotId) => {
|
|
513
|
+
// Into this tool's own mount, never the slot: the slot is shared
|
|
514
|
+
// and writing the whole of it destroys any other tool's panel.
|
|
515
|
+
const mount = mountFor(slotId, toolName, true);
|
|
516
|
+
if (mount) mount.innerHTML = payload.fragments[slotId];
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
for (const src of payload.scripts || []) {
|
|
520
|
+
await loadScript(src);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// main.js runs before any tool can be opened (deferred, but earlier in
|
|
524
|
+
// document order isn't guaranteed here -- this script loads first --
|
|
525
|
+
// so wait on its own readiness promise rather than assuming it's done).
|
|
526
|
+
await window.__plexoraReady;
|
|
527
|
+
const moduleDef = window.Plexora?.plugins?.get(toolName);
|
|
528
|
+
if (!moduleDef || !window.__plexora?.activatePlugin) return;
|
|
529
|
+
const { sidebarController } = await window.__plexora.activatePlugin(moduleDef);
|
|
530
|
+
|
|
531
|
+
loadedTools.set(toolName, {
|
|
532
|
+
slotIds,
|
|
533
|
+
sidebarController,
|
|
534
|
+
visible: true,
|
|
535
|
+
collapsed: false,
|
|
536
|
+
pinned: false,
|
|
537
|
+
});
|
|
538
|
+
ensureSortable();
|
|
539
|
+
show(toolName);
|
|
540
|
+
} finally {
|
|
541
|
+
linkEl?.classList.remove("tool-loading");
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Open `toolName` alongside `anchorToolName` rather than in place of it.
|
|
547
|
+
*
|
|
548
|
+
* Single-active is the right default for tools that are alternative views of
|
|
549
|
+
* one picture, but Cell Explorer and ROI are not alternatives: the ROI
|
|
550
|
+
* composition card only means anything while the metadata overlay it
|
|
551
|
+
* summarises is still drawn underneath. Rather than weaken standDown() for
|
|
552
|
+
* everyone, the exception is a named pair that only this entry point can
|
|
553
|
+
* form -- opening ROI from the Tools menu still behaves exactly as before.
|
|
554
|
+
*
|
|
555
|
+
* The pair is set BEFORE openTool(), because openTool() reaches show() ->
|
|
556
|
+
* standDown() synchronously on the already-loaded path, and standDown() is
|
|
557
|
+
* the call the pair exists to short-circuit.
|
|
558
|
+
*/
|
|
559
|
+
async function openToolAlongside(toolName, anchorToolName) {
|
|
560
|
+
if (!toolName) return;
|
|
561
|
+
if (!anchorToolName || anchorToolName === toolName
|
|
562
|
+
|| !loadedTools.has(anchorToolName)) {
|
|
563
|
+
// Nothing to ride along with -- an ordinary open.
|
|
564
|
+
return openTool(toolName);
|
|
565
|
+
}
|
|
566
|
+
if (activeToolName !== anchorToolName) show(anchorToolName);
|
|
567
|
+
const anchor = loadedTools.get(anchorToolName);
|
|
568
|
+
if (anchor) anchor.collapsed = false;
|
|
569
|
+
coexistPair = new Set([anchorToolName, toolName]);
|
|
570
|
+
try {
|
|
571
|
+
await openTool(toolName);
|
|
572
|
+
} finally {
|
|
573
|
+
// openTool() returns without loading anything on the redirect and
|
|
574
|
+
// requirements-declined paths; there is no pair if the second half
|
|
575
|
+
// never arrived.
|
|
576
|
+
if (!loadedTools.has(toolName)) coexistPair = null;
|
|
577
|
+
paint();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* The panel's own close button: fold the card away and stop drawing, but
|
|
583
|
+
* keep the tool loaded.
|
|
584
|
+
*
|
|
585
|
+
* Removing it outright is the card's X instead. A close that unloaded
|
|
586
|
+
* everything would throw away a cached column and a rebuilt lookup table to
|
|
587
|
+
* reclaim a strip of sidebar, and there would be no sign the tool had ever
|
|
588
|
+
* been open.
|
|
589
|
+
*/
|
|
590
|
+
function hideTool(toolName) {
|
|
591
|
+
const entry = loadedTools.get(toolName);
|
|
592
|
+
if (!entry) return;
|
|
593
|
+
// Explicit, so it clears the pin the same way the eye does -- a close
|
|
594
|
+
// that left the layer drawing would be a button whose name is a lie.
|
|
595
|
+
entry.pinned = false;
|
|
596
|
+
const partner = pairPartner(toolName);
|
|
597
|
+
if (partner) coexistPair = null;
|
|
598
|
+
if (activeToolName === toolName) {
|
|
599
|
+
standDown();
|
|
600
|
+
} else {
|
|
601
|
+
entry.collapsed = true;
|
|
602
|
+
applyToolVisible(toolName, false);
|
|
603
|
+
}
|
|
604
|
+
// The surviving half of a pair becomes the sole selected tool. Without
|
|
605
|
+
// this, closing ROI from its own panel would leave Cell Explorer
|
|
606
|
+
// expanded but unselected, with the shared controls pointing at nothing.
|
|
607
|
+
if (partner && loadedTools.has(partner) && activeToolName !== partner) {
|
|
608
|
+
show(partner);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
paint();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Unload a tool: tear the plugin down, take its card and every one of its
|
|
616
|
+
* mounts off the page, and forget it.
|
|
617
|
+
*
|
|
618
|
+
* Re-opening from the Tools menu afterwards goes the full way round again --
|
|
619
|
+
* the script tags persist so loadScript no-ops, re-registering the same name
|
|
620
|
+
* in the plugin registry is harmless, and a fresh instance and controller are
|
|
621
|
+
* built. The mounts have to go from EVERY slot for that to work, including
|
|
622
|
+
* the off-screen legacy one: a stale wrapper there is found by mountFor,
|
|
623
|
+
* returned as if it were new, and the freshly fetched fragment is written
|
|
624
|
+
* into markup the new controller never saw.
|
|
625
|
+
*/
|
|
626
|
+
function removeTool(toolName) {
|
|
627
|
+
const entry = loadedTools.get(toolName);
|
|
628
|
+
if (!entry) return;
|
|
629
|
+
// Read before the pair is dissolved: closing one half of a coexisting
|
|
630
|
+
// pair has to leave the other one selected, not drop the selection to
|
|
631
|
+
// nothing the way standDown() on its own would.
|
|
632
|
+
const partner = pairPartner(toolName);
|
|
633
|
+
// Only when this tool is IN the pair -- removing an unrelated third tool
|
|
634
|
+
// has nothing to say about two others sharing the screen.
|
|
635
|
+
if (partner) coexistPair = null;
|
|
636
|
+
if (activeToolName === toolName) standDown();
|
|
637
|
+
try {
|
|
638
|
+
window.__plexora?.deactivatePlugin?.(toolName);
|
|
639
|
+
} catch (error) {
|
|
640
|
+
console.error("toolLoader: deactivatePlugin() failed", error);
|
|
641
|
+
}
|
|
642
|
+
entry.slotIds.forEach((slotId) => {
|
|
643
|
+
const mount = mountFor(slotId, toolName);
|
|
644
|
+
const card = slotId === CARD_SLOT ? cardFor(toolName) : null;
|
|
645
|
+
detach(card || mount);
|
|
646
|
+
});
|
|
647
|
+
loadedTools.delete(toolName);
|
|
648
|
+
if (partner && loadedTools.has(partner) && activeToolName !== partner) {
|
|
649
|
+
show(partner); //: paints and re-syncs layer order for us
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
paint();
|
|
653
|
+
syncLayerOrder();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Wrap a server-rendered panel in the same per-tool mount and card the lazy
|
|
658
|
+
* path creates, so both paths leave the slot in the same shape.
|
|
659
|
+
*
|
|
660
|
+
* Without this, a page opened with ?tool=gating has gating's markup sitting
|
|
661
|
+
* loose in the slot; opening a second tool would append its card alongside
|
|
662
|
+
* and `paint()` would have nothing to hide the first one by.
|
|
663
|
+
*/
|
|
664
|
+
function adopt(slotId, toolName) {
|
|
665
|
+
const slot = document.getElementById(slotId);
|
|
666
|
+
if (!slot || !slot.appendChild) return;
|
|
667
|
+
if (slot.querySelector?.(`[${MOUNT_ATTR}="${toolName}"]`)) return;
|
|
668
|
+
const mount = document.createElement("div");
|
|
669
|
+
mount.className = "tool-panel-mount";
|
|
670
|
+
mount.setAttribute(MOUNT_ATTR, toolName);
|
|
671
|
+
while (slot.firstChild) mount.appendChild(slot.firstChild);
|
|
672
|
+
slot.appendChild(slotId === CARD_SLOT ? buildCard(toolName, mount) : mount);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Called by main.js when a tool was already active at boot (a direct/bookmarked
|
|
676
|
+
// ?tool=gating load rendered its panel server-side, not through openTool() above) --
|
|
677
|
+
// without this, the close button's hideToolPanel() would find nothing to hide, and
|
|
678
|
+
// a later Tools-menu click would re-fetch/re-activate a module that's already live.
|
|
679
|
+
function registerLoaded(toolName, slotIds, sidebarController) {
|
|
680
|
+
if (loadedTools.has(toolName)) return;
|
|
681
|
+
// Only the slots the server actually filled. main.js works the list out
|
|
682
|
+
// from `data-tool-mount`, which index.html stamps on EVERY slot with the
|
|
683
|
+
// active tool's name -- so a plugin that declared one panel is still
|
|
684
|
+
// named on all of them. Adopting an empty slot builds a card with
|
|
685
|
+
// nothing in it: a header, a grip and an eye over a panel that does not
|
|
686
|
+
// exist. A plugin whose controls live somewhere other than the sidebar
|
|
687
|
+
// (figure_builder puts them on the image) is entitled to no card at all,
|
|
688
|
+
// and this is what makes the boot path agree with the lazy one, which
|
|
689
|
+
// only ever sees the slots the payload named.
|
|
690
|
+
//
|
|
691
|
+
// `children`, not `firstChild` or textContent: a server-rendered slot
|
|
692
|
+
// with no panel in it still holds the template's whitespace, which is a
|
|
693
|
+
// text node and would read as content.
|
|
694
|
+
const filled = slotIds.filter(
|
|
695
|
+
(slotId) => Boolean(document.getElementById(slotId)?.children?.length));
|
|
696
|
+
filled.forEach((slotId) => adopt(slotId, toolName));
|
|
697
|
+
loadedTools.set(toolName, {
|
|
698
|
+
slotIds: filled,
|
|
699
|
+
sidebarController,
|
|
700
|
+
visible: true,
|
|
701
|
+
collapsed: false,
|
|
702
|
+
pinned: false,
|
|
703
|
+
});
|
|
704
|
+
ensureSortable();
|
|
705
|
+
// Through show(), so this path is not a second, quieter version of the
|
|
706
|
+
// menu one. A server-rendered panel is every bit as visible as a lazily
|
|
707
|
+
// opened one and needs the same onShow(): ROI attaches its viewer-canvas
|
|
708
|
+
// and document handlers there, so setting activeToolName directly gave
|
|
709
|
+
// a ?tool=roi page a panel that looked right and a pen that drew
|
|
710
|
+
// nothing -- no pointer handlers, no shortcuts, and no error to say so.
|
|
711
|
+
show(toolName);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
715
|
+
document.querySelectorAll("a[data-tool]").forEach((link) => {
|
|
716
|
+
link.addEventListener("click", (event) => {
|
|
717
|
+
event.preventDefault();
|
|
718
|
+
openTool(link.dataset.tool, link);
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
hideToolPanel: hideTool,
|
|
725
|
+
registerLoaded,
|
|
726
|
+
/** Which tool's panels are showing, for a tool deciding whether its own
|
|
727
|
+
* global shortcuts should be listening. */
|
|
728
|
+
activeTool: () => activeToolName,
|
|
729
|
+
/** Whether a tool's layer is currently drawn, for a controller that has
|
|
730
|
+
* to answer the same question its card's eye does. */
|
|
731
|
+
isToolVisible: (name) => Boolean(loadedTools.get(name)?.visible),
|
|
732
|
+
setToolVisible,
|
|
733
|
+
setToolCollapsed,
|
|
734
|
+
removeTool,
|
|
735
|
+
/** Open `toolName` WITHOUT standing `anchorToolName` down -- the one
|
|
736
|
+
* sanctioned exception to single-active. Both cards stay expanded and
|
|
737
|
+
* both layers stay drawn until a third tool opens or either half is
|
|
738
|
+
* closed. Cell Explorer's Open ROIs button is the caller. */
|
|
739
|
+
openToolAlongside,
|
|
740
|
+
/** Whether this tool is sharing the screen with another right now. */
|
|
741
|
+
isCoexisting,
|
|
742
|
+
/** The tool it is sharing with, or null. */
|
|
743
|
+
coexistPartner: (name) => (isCoexisting(name) ? pairPartner(name) : null),
|
|
744
|
+
/** Every loaded tool, top card first. */
|
|
745
|
+
loadedTools: () => Array.from(loadedTools.keys()),
|
|
746
|
+
};
|
|
747
|
+
})();
|