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,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text sidebar: font, size, colour, spacing, alignment, decoration.
|
|
3
|
+
*
|
|
4
|
+
* CONTEXTUAL, and that is what distinguishes it from the fixed properties
|
|
5
|
+
* column this workspace deliberately removed. It appears while a text object is
|
|
6
|
+
* selected or being typed into and goes away again, so the canvas keeps the
|
|
7
|
+
* full width of the window for everything else anyone does here. The reason the
|
|
8
|
+
* old column went is in `workspace_body.html`: three hundred permanent pixels
|
|
9
|
+
* to show controls that are relevant some of the time.
|
|
10
|
+
*
|
|
11
|
+
* It carries text FORMATTING and nothing else. Arrange, align, group,
|
|
12
|
+
* duplicate, lock and delete apply to every object kind, so they belong to the
|
|
13
|
+
* bar that floats over the selection -- one shared component driven by what the
|
|
14
|
+
* selection can do, rather than a list rebuilt per type.
|
|
15
|
+
*
|
|
16
|
+
* GEOMETRY is on the same side of that line, and it took a second pass to hold
|
|
17
|
+
* it: this panel grew a "Box" group with a Rotation field in it, and the bar's
|
|
18
|
+
* Transform popover has rotation, width and height for every object kind. Two
|
|
19
|
+
* controls for one number, one of which only exists while the object is a
|
|
20
|
+
* caption. Rotation has gone from here. What is left in "Box" is `autofit`,
|
|
21
|
+
* which is not geometry at all -- it says the TEXT decides the height, so it
|
|
22
|
+
* belongs with the text.
|
|
23
|
+
*
|
|
24
|
+
* Which formatting a control changes depends on what is selected, which is the
|
|
25
|
+
* behaviour people expect from every text editor:
|
|
26
|
+
*
|
|
27
|
+
* * text highlighted inside the editor -> just that range;
|
|
28
|
+
* * the caret sitting in the editor, or the box merely selected -> the whole
|
|
29
|
+
* object.
|
|
30
|
+
*
|
|
31
|
+
* A property whose value differs across the selection shows as blank rather
|
|
32
|
+
* than as one of the values it is not.
|
|
33
|
+
*/
|
|
34
|
+
class FigureTextPanel {
|
|
35
|
+
|
|
36
|
+
constructor({ root, canvas, state, editor, onStyle, onClose }) {
|
|
37
|
+
this.root = root;
|
|
38
|
+
this.canvas = canvas;
|
|
39
|
+
this.state = state;
|
|
40
|
+
this.editor = editor;
|
|
41
|
+
this.onStyle = onStyle || (() => {});
|
|
42
|
+
this.onClose = onClose || (() => {});
|
|
43
|
+
this.annotationId = null;
|
|
44
|
+
//: Shut by hand for THIS object. Cleared when the selection moves, so
|
|
45
|
+
//: closing the panel is "not for this caption" rather than "never
|
|
46
|
+
//: again" -- a contextual panel that stayed shut until some other
|
|
47
|
+
//: switch was found would be a feature with no way back on.
|
|
48
|
+
this.dismissed = false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setup() {
|
|
52
|
+
if (!this.root) return;
|
|
53
|
+
this.root.addEventListener("input", (event) => this.changed(event));
|
|
54
|
+
this.root.addEventListener("change", (event) => this.changed(event));
|
|
55
|
+
this.root.addEventListener("click", (event) => this.clicked(event));
|
|
56
|
+
this.root.addEventListener("keydown", (event) => this.keyDown(event));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get annotation() {
|
|
60
|
+
return this.annotationId
|
|
61
|
+
? this.state.document.annotations[this.annotationId] : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// -- what it shows -------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Draw the panel for a selection, or empty it when that is not one text
|
|
68
|
+
* object. Called on every selection change and after every render.
|
|
69
|
+
*
|
|
70
|
+
* It does NOT decide whether it is on screen. Whoever owns the strip beside
|
|
71
|
+
* the rail does, because only one panel can be in it at a time: this one
|
|
72
|
+
* says whether it has anything to show, through `wants`, and the workspace
|
|
73
|
+
* settles it against the tray. Two panels each hiding and showing
|
|
74
|
+
* themselves is how they ended up stacked on top of each other.
|
|
75
|
+
*/
|
|
76
|
+
update(ids) {
|
|
77
|
+
if (!this.root) return;
|
|
78
|
+
const single = ids.length === 1
|
|
79
|
+
? this.state.document.annotations[ids[0]] : null;
|
|
80
|
+
const annotation = single && single.type === "text" ? single : null;
|
|
81
|
+
if (annotation && annotation.annotation_id !== this.annotationId) {
|
|
82
|
+
this.dismissed = false;
|
|
83
|
+
}
|
|
84
|
+
if (!annotation) {
|
|
85
|
+
this.annotationId = null;
|
|
86
|
+
this.root.innerHTML = "";
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.annotationId = annotation.annotation_id;
|
|
90
|
+
this.render();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Whether there is anything here worth the strip. */
|
|
94
|
+
get wants() {
|
|
95
|
+
return Boolean(this.annotationId) && !this.dismissed;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Bring it back after it was shut -- what double-clicking a caption does,
|
|
99
|
+
* since typing into one is the moment its formatting is wanted. */
|
|
100
|
+
reveal() {
|
|
101
|
+
this.dismissed = false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* What the controls should read.
|
|
106
|
+
*
|
|
107
|
+
* Taken from the highlighted range when there is one, so the panel is
|
|
108
|
+
* describing what a change would actually affect. `null` for a property the
|
|
109
|
+
* range disagrees about -- which is what makes a mixed selection show a
|
|
110
|
+
* blank box instead of quietly claiming one of the two values.
|
|
111
|
+
*/
|
|
112
|
+
current() {
|
|
113
|
+
const annotation = this.annotation;
|
|
114
|
+
const style = annotation.style;
|
|
115
|
+
// What the WHOLE object reads as, when there is no highlighted range to
|
|
116
|
+
// describe instead. The four decoration flags used to be hard-coded
|
|
117
|
+
// false here, which made every one of them a one-way switch: the click
|
|
118
|
+
// handler reads `aria-pressed` to decide whether it is setting the mark
|
|
119
|
+
// or clearing it, and a button that is never pressed can only ever set.
|
|
120
|
+
// Bold went on and would not come off.
|
|
121
|
+
const rich = FigureRichText.normalize(annotation.text || "", annotation.rich);
|
|
122
|
+
const box = FigureRichText.formatOfRange(
|
|
123
|
+
rich, 0, FigureRichText.plainText(rich).length, style) || {
|
|
124
|
+
family: style.font_family, size_pt: style.font_size_pt,
|
|
125
|
+
color: style.color, bold: false, italic: false,
|
|
126
|
+
underline: false, strike: false,
|
|
127
|
+
};
|
|
128
|
+
if (!this.editor?.active || this.editor.annotationId !== this.annotationId) {
|
|
129
|
+
return box;
|
|
130
|
+
}
|
|
131
|
+
const at = this.editor.offsets();
|
|
132
|
+
if (!at || at.start === at.end) return box;
|
|
133
|
+
return FigureRichText.formatOfRange(
|
|
134
|
+
this.editor.rich, at.start, at.end, style) || box;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* One property per ROW: its name ranged left, its control ranged right.
|
|
139
|
+
*
|
|
140
|
+
* The panel was built as three titled groups -- FONT, PARAGRAPH, BOX --
|
|
141
|
+
* with the controls stacked underneath each title and only some of them
|
|
142
|
+
* labelled. Two of the eight properties said what they were, and the rest
|
|
143
|
+
* were left to their icons: a number field with nothing beside it, a colour
|
|
144
|
+
* well with nothing beside it, and three segmented tracks of arrows that a
|
|
145
|
+
* reader has to decode before they can tell horizontal alignment from
|
|
146
|
+
* vertical. The group titles were the wrong thing to spend the width on --
|
|
147
|
+
* "FONT" over a font menu says nothing that the menu does not.
|
|
148
|
+
*
|
|
149
|
+
* Every row now names itself, and every control starts at the same left
|
|
150
|
+
* edge, so the panel is scanned down one column of names rather than read.
|
|
151
|
+
* That is what the reference design does and it is the whole of why it is
|
|
152
|
+
* legible at this width.
|
|
153
|
+
*/
|
|
154
|
+
render() {
|
|
155
|
+
const annotation = this.annotation;
|
|
156
|
+
const style = annotation.style;
|
|
157
|
+
const now = this.current();
|
|
158
|
+
const escape = FigureSchema.escapeHtml.bind(FigureSchema);
|
|
159
|
+
const focus = this.focused();
|
|
160
|
+
|
|
161
|
+
const families = FigureRichText.FAMILIES.map((name) =>
|
|
162
|
+
`<option value="${name}"${now.family === name ? " selected" : ""}
|
|
163
|
+
>${escape(FigureTextPanel.FAMILY_LABELS[name] || name)}</option>`).join("");
|
|
164
|
+
|
|
165
|
+
this.root.innerHTML = `
|
|
166
|
+
<header class="fb-side-heading">
|
|
167
|
+
<span class="fb-side-title">Text</span>
|
|
168
|
+
<button type="button" class="fb-icon-button" data-close="1"
|
|
169
|
+
title="Close">
|
|
170
|
+
<span class="fas fa-xmark" aria-hidden="true"></span>
|
|
171
|
+
</button>
|
|
172
|
+
</header>
|
|
173
|
+
|
|
174
|
+
${this.field("Font", "fb_text_font",
|
|
175
|
+
`<select class="fb-input" id="fb_text_font" data-field="family"
|
|
176
|
+
aria-label="Font family">${families}</select>`)}
|
|
177
|
+
|
|
178
|
+
${this.field("Size", "fb_text_size", this.stepper(now.size_pt))}
|
|
179
|
+
|
|
180
|
+
${this.field("Color", "fb_text_color", `
|
|
181
|
+
<input class="fb-input fb-input-color" id="fb_text_color" type="color"
|
|
182
|
+
data-field="color" value="${escape(now.color || "#000000")}"
|
|
183
|
+
aria-label="Text color">
|
|
184
|
+
<input class="fb-input fb-input-hex" id="fb_text_hex" type="text"
|
|
185
|
+
data-field="color_hex" spellcheck="false" maxlength="7"
|
|
186
|
+
value="${escape(now.color || "")}" placeholder="Mixed"
|
|
187
|
+
aria-label="Color, as a hex code">`)}
|
|
188
|
+
|
|
189
|
+
${this.field("Line spacing", "fb_text_leading",
|
|
190
|
+
this.leadingSelect(style.line_height || FigureRichText.LINE_HEIGHT))}
|
|
191
|
+
|
|
192
|
+
${this.field("Horizontal align", null, this.segmented("Horizontal alignment",
|
|
193
|
+
[["left", "Align left"], ["center", "Center"],
|
|
194
|
+
["right", "Align right"], ["justify", "Justify"]].map(([value, title]) =>
|
|
195
|
+
this.styleButton("align", value, `align-${value}`,
|
|
196
|
+
title, style.align === value))))}
|
|
197
|
+
|
|
198
|
+
${this.field("Vertical align", null, this.segmented("Vertical alignment",
|
|
199
|
+
[["top", "arrow-up", "Align to the top"],
|
|
200
|
+
["middle", "arrows-up-down", "Center vertically"],
|
|
201
|
+
["bottom", "arrow-down", "Align to the bottom"]].map(
|
|
202
|
+
([value, icon, title]) => this.styleButton(
|
|
203
|
+
"valign", value, icon, title, style.valign === value))))}
|
|
204
|
+
|
|
205
|
+
${this.field("Decoration", null, this.segmented("Decoration", [
|
|
206
|
+
// "(⌘B)" and not "Bold ⌘B": a shortcut inside a tooltip is
|
|
207
|
+
// parenthesised, the same way FigureContextBar.button writes
|
|
208
|
+
// one. Two spaces and a glyph is how a MENU ROW carries it, and
|
|
209
|
+
// a menu row draws the key in its own right-hand column.
|
|
210
|
+
this.toggleButton("bold", "bold", "Bold (⌘B)", now.bold),
|
|
211
|
+
this.toggleButton("italic", "italic", "Italic (⌘I)", now.italic),
|
|
212
|
+
this.toggleButton("underline", "underline", "Underline (⌘U)", now.underline),
|
|
213
|
+
this.toggleButton("strike", "strikethrough", "Strikethrough", now.strike),
|
|
214
|
+
]))}
|
|
215
|
+
|
|
216
|
+
${this.field("Auto height", "fb_text_autofit",
|
|
217
|
+
`<input type="checkbox" id="fb_text_autofit" data-style="autofit"
|
|
218
|
+
${style.autofit ? "checked" : ""}
|
|
219
|
+
aria-label="Fit the box height to the text">`)}`;
|
|
220
|
+
|
|
221
|
+
this.restore(focus);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** One row: the property's name, then its control. `forId` is the control
|
|
225
|
+
* the name belongs to, and is null for the segmented tracks -- a <label>
|
|
226
|
+
* can only point at one element, and a track is four buttons that carry
|
|
227
|
+
* their own. */
|
|
228
|
+
field(name, forId, control) {
|
|
229
|
+
const escape = FigureSchema.escapeHtml.bind(FigureSchema);
|
|
230
|
+
const label = forId
|
|
231
|
+
? `<label class="fb-field-name" for="${forId}">${escape(name)}</label>`
|
|
232
|
+
: `<span class="fb-field-name">${escape(name)}</span>`;
|
|
233
|
+
return `<div class="fb-field">${label}
|
|
234
|
+
<div class="fb-field-control">${control}</div></div>`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Minus, the number, plus.
|
|
239
|
+
*
|
|
240
|
+
* A bare number field is a control you have to select and retype to change
|
|
241
|
+
* by one point, and its spin buttons are two arrows four pixels tall that
|
|
242
|
+
* only appear on hover. Type size is the property on this panel most likely
|
|
243
|
+
* to be nudged rather than set, so it gets the buttons the reference design
|
|
244
|
+
* gives it. The field stays editable -- 7.5 is a real answer and no stepper
|
|
245
|
+
* gets to it.
|
|
246
|
+
*
|
|
247
|
+
* A TEXT input and not a number one, which is not a detail: typing "20" put
|
|
248
|
+
* 02 in the field. `focused()` keeps the caret across the panel's redraw by
|
|
249
|
+
* reading `selectionStart`, and that property is null on a number input --
|
|
250
|
+
* the HTML spec makes it null for every type that does not have a "selection
|
|
251
|
+
* API", number among them. So the caret was restored to offset 0 after the
|
|
252
|
+
* first digit and the second went in front of it. The platform's own
|
|
253
|
+
* spinners were already suppressed in CSS, so `type="number"` was buying
|
|
254
|
+
* nothing but that bug; `inputmode="decimal"` keeps the numeric keypad on a
|
|
255
|
+
* touch device, and Up/Down are handled in `keyDown`.
|
|
256
|
+
*/
|
|
257
|
+
stepper(value) {
|
|
258
|
+
return `<div class="fb-stepper">
|
|
259
|
+
<button type="button" class="fb-stepper-button" data-step="-1"
|
|
260
|
+
aria-label="Smaller">
|
|
261
|
+
<span class="fas fa-minus" aria-hidden="true"></span></button>
|
|
262
|
+
<input class="fb-stepper-value" id="fb_text_size" type="text"
|
|
263
|
+
inputmode="decimal" spellcheck="false" data-field="size_pt"
|
|
264
|
+
value="${value === null ? "" : value}" placeholder="Mixed"
|
|
265
|
+
aria-label="Type size, in points">
|
|
266
|
+
<button type="button" class="fb-stepper-button" data-step="1"
|
|
267
|
+
aria-label="Larger">
|
|
268
|
+
<span class="fas fa-plus" aria-hidden="true"></span></button>
|
|
269
|
+
</div>`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The usual leadings, plus whatever this caption is actually set to. The
|
|
273
|
+
* extra option is what keeps a menu from being a value editor that can
|
|
274
|
+
* silently round: a figure set to 1.35 by an earlier build, or through the
|
|
275
|
+
* REST surface, still shows 1.35 and still selects it. */
|
|
276
|
+
leadingSelect(current) {
|
|
277
|
+
const values = FigureTextPanel.LEADINGS.slice();
|
|
278
|
+
if (!values.includes(current)) values.push(current);
|
|
279
|
+
values.sort((a, b) => a - b);
|
|
280
|
+
return `<select class="fb-input" id="fb_text_leading" data-style="line_height"
|
|
281
|
+
aria-label="Line spacing">${values.map((value) =>
|
|
282
|
+
`<option value="${value}"${value === current ? " selected" : ""}
|
|
283
|
+
>${value.toFixed(2).replace(/\.?0+$/, "")}</option>`).join("")}</select>`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
static get LEADINGS() { return [1, 1.15, 1.2, 1.5, 2]; }
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Which control had the keyboard, and where its caret was.
|
|
290
|
+
*
|
|
291
|
+
* The panel redraws on every document change, and a document change is what
|
|
292
|
+
* every control on it causes -- so typing a hex code rebuilt the field
|
|
293
|
+
* under the caret at the second character. Only the id and the caret are
|
|
294
|
+
* kept, because the markup is rebuilt from the model either way; this is
|
|
295
|
+
* about not losing the user's place in it.
|
|
296
|
+
*/
|
|
297
|
+
focused() {
|
|
298
|
+
const active = document.activeElement;
|
|
299
|
+
if (!active || !this.root.contains(active) || !active.id) return null;
|
|
300
|
+
let caret = null;
|
|
301
|
+
// Guarded because `selectionStart` is null -- or throws outright -- on
|
|
302
|
+
// any input type without a selection API, and this reads whichever one
|
|
303
|
+
// happens to have the keyboard.
|
|
304
|
+
try {
|
|
305
|
+
if (Number.isInteger(active.selectionStart)) {
|
|
306
|
+
caret = [active.selectionStart, active.selectionEnd];
|
|
307
|
+
}
|
|
308
|
+
} catch (error) { caret = null; }
|
|
309
|
+
return { id: active.id, caret: caret };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
restore(focus) {
|
|
313
|
+
if (!focus) return;
|
|
314
|
+
const again = this.root.querySelector(`#${focus.id}`);
|
|
315
|
+
if (!again) return;
|
|
316
|
+
again.focus();
|
|
317
|
+
if (focus.caret) again.setSelectionRange(focus.caret[0], focus.caret[1]);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** A row of buttons that read as one control. Four loose icon buttons and a
|
|
321
|
+
* segmented track hold the same things; only the second one says that
|
|
322
|
+
* exactly one of them is the current answer. */
|
|
323
|
+
segmented(label, buttons) {
|
|
324
|
+
return `<div class="fb-segmented" role="group"
|
|
325
|
+
aria-label="${FigureSchema.escapeHtml(label)}"
|
|
326
|
+
>${buttons.join("")}</div>`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
toggleButton(mark, icon, title, on) {
|
|
330
|
+
return `<button type="button" class="fb-segmented-button${on ? " is-on" : ""}"
|
|
331
|
+
data-mark="${mark}" title="${FigureSchema.escapeHtml(title)}"
|
|
332
|
+
aria-label="${FigureSchema.escapeHtml(title)}"
|
|
333
|
+
aria-pressed="${on ? "true" : "false"}">
|
|
334
|
+
<span class="fas fa-${icon}" aria-hidden="true"></span></button>`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
styleButton(field, value, icon, title, on) {
|
|
338
|
+
return `<button type="button" class="fb-segmented-button${on ? " is-on" : ""}"
|
|
339
|
+
data-style-set="${field}" data-value="${value}"
|
|
340
|
+
title="${FigureSchema.escapeHtml(title)}"
|
|
341
|
+
aria-label="${FigureSchema.escapeHtml(title)}"
|
|
342
|
+
aria-pressed="${on ? "true" : "false"}">
|
|
343
|
+
<span class="fas fa-${icon}" aria-hidden="true"></span></button>`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
static get FAMILY_LABELS() {
|
|
347
|
+
return {
|
|
348
|
+
"Helvetica": "Helvetica / Arial",
|
|
349
|
+
"Times-Roman": "Times",
|
|
350
|
+
"Courier": "Courier",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// -- acting --------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
clicked(event) {
|
|
357
|
+
if (event.target.closest?.("[data-close]")) {
|
|
358
|
+
this.dismissed = true;
|
|
359
|
+
this.onClose();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const step = event.target.closest?.("[data-step]");
|
|
363
|
+
if (step) {
|
|
364
|
+
this.step(Number(step.dataset.step));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const mark = event.target.closest?.("[data-mark]");
|
|
368
|
+
if (mark) {
|
|
369
|
+
this.applyRun({ [mark.dataset.mark]:
|
|
370
|
+
mark.getAttribute("aria-pressed") === "true" ? null : true });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const set = event.target.closest?.("[data-style-set]");
|
|
374
|
+
if (set) this.applyStyle({ [set.dataset.styleSet]: set.dataset.value });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Up and down step the size field.
|
|
379
|
+
*
|
|
380
|
+
* Which is the one thing `type="number"` gave for free and is worth putting
|
|
381
|
+
* back by hand -- a size is adjusted against what it looks like on the page,
|
|
382
|
+
* so holding a key and watching the caption grow is how it is actually set.
|
|
383
|
+
* Shift takes ten at a time, the same multiplier the arrow keys use to nudge
|
|
384
|
+
* an object on the canvas.
|
|
385
|
+
*/
|
|
386
|
+
keyDown(event) {
|
|
387
|
+
if (event.target.dataset?.field !== "size_pt") return;
|
|
388
|
+
const by = { ArrowUp: 1, ArrowDown: -1 }[event.key];
|
|
389
|
+
if (!by) return;
|
|
390
|
+
event.preventDefault();
|
|
391
|
+
this.step(event.shiftKey ? by * 10 : by);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** One point bigger or smaller, from whatever the field currently reads.
|
|
395
|
+
* From the FIELD and not from the annotation, because the field is what
|
|
396
|
+
* shows blank for a selection whose sizes disagree -- and stepping "mixed"
|
|
397
|
+
* has to start somewhere the user can see. */
|
|
398
|
+
step(by) {
|
|
399
|
+
const input = this.root.querySelector("#fb_text_size");
|
|
400
|
+
const from = parseFloat(input && input.value);
|
|
401
|
+
const base = Number.isFinite(from) ? from : FigureRichText.DEFAULT_SIZE_PT;
|
|
402
|
+
this.applyRun({ size_pt: Math.min(200, Math.max(1, base + by)) });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
changed(event) {
|
|
406
|
+
const target = event.target;
|
|
407
|
+
if (target.dataset.field === "color_hex") {
|
|
408
|
+
// Only a complete code applies. This fires on every keystroke, and
|
|
409
|
+
// "#f" is not a colour -- committing the partial ones would put six
|
|
410
|
+
// entries in the undo history for one colour and repaint the
|
|
411
|
+
// caption black on the way past.
|
|
412
|
+
const value = target.value.trim().toLowerCase();
|
|
413
|
+
if (/^#[0-9a-f]{6}$/.test(value)) this.applyRun({ color: value });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (target.dataset.field === "size_pt") {
|
|
417
|
+
// On `change` -- the field being left, or Enter -- and never on
|
|
418
|
+
// `input`. A size is typed a digit at a time and every prefix of it
|
|
419
|
+
// is a valid number, so committing keystrokes set the caption to
|
|
420
|
+
// 2 pt on the way to 20: a visible flash of tiny type, and two
|
|
421
|
+
// entries in the undo history for one decision. The hex field above
|
|
422
|
+
// has the same shape of guard, and can be stricter because it knows
|
|
423
|
+
// what a finished value looks like.
|
|
424
|
+
if (event.type !== "change") return;
|
|
425
|
+
const value = parseFloat(target.value);
|
|
426
|
+
// Not a number, so there is nothing to commit -- redraw the row so
|
|
427
|
+
// the field goes back to reading what the caption actually is,
|
|
428
|
+
// rather than being left showing what was typed over it.
|
|
429
|
+
if (!isFinite(value)) return this.render();
|
|
430
|
+
this.applyRun({ size_pt: Math.min(200, Math.max(1, value)) });
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (target.dataset.field) {
|
|
434
|
+
this.applyRun({ [target.dataset.field]: target.value });
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (target.dataset.style) {
|
|
438
|
+
const key = target.dataset.style;
|
|
439
|
+
this.applyStyle({ [key]: key === "autofit"
|
|
440
|
+
? target.checked : parseFloat(target.value) });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* A property that lives on a RUN -- font, size, colour, bold and friends.
|
|
446
|
+
*
|
|
447
|
+
* Routed through the open editor when there is one, because only the editor
|
|
448
|
+
* knows what is highlighted. With no editor open the whole object is the
|
|
449
|
+
* target, and the change goes onto the box's own style so that runs which
|
|
450
|
+
* never overrode it follow along.
|
|
451
|
+
*/
|
|
452
|
+
applyRun(patch) {
|
|
453
|
+
const annotation = this.annotation;
|
|
454
|
+
if (!annotation) return;
|
|
455
|
+
if (this.editor?.active && this.editor.annotationId === this.annotationId) {
|
|
456
|
+
this.editor.applyFormat(patch);
|
|
457
|
+
this.render();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const style = {};
|
|
461
|
+
if ("family" in patch) style.font_family = patch.family;
|
|
462
|
+
if ("size_pt" in patch) style.font_size_pt = patch.size_pt;
|
|
463
|
+
if ("color" in patch) style.color = patch.color;
|
|
464
|
+
const marks = ["bold", "italic", "underline", "strike"]
|
|
465
|
+
.filter((mark) => mark in patch);
|
|
466
|
+
if (marks.length) {
|
|
467
|
+
// A box-level mark is written onto every run instead of onto the
|
|
468
|
+
// box, so that a run which set it explicitly and one which merely
|
|
469
|
+
// inherited it cannot end up disagreeing after the next edit.
|
|
470
|
+
//
|
|
471
|
+
// The length comes from the NORMALISED rich text, not from
|
|
472
|
+
// `annotation.rich`: a text box saved by an older build has only a
|
|
473
|
+
// flat `text`, `plainText(undefined)` is "", and the range would be
|
|
474
|
+
// [0, 0) -- so bolding a caption that had never been formatted
|
|
475
|
+
// before did nothing at all.
|
|
476
|
+
const rich = FigureRichText.normalize(
|
|
477
|
+
annotation.text || "", annotation.rich);
|
|
478
|
+
const whole = FigureRichText.applyToRange(
|
|
479
|
+
rich, 0, FigureRichText.plainText(rich).length,
|
|
480
|
+
Object.fromEntries(marks.map((mark) => [mark, patch[mark]])));
|
|
481
|
+
this.onStyle(this.annotationId, { rich: whole });
|
|
482
|
+
}
|
|
483
|
+
if (Object.keys(style).length) this.onStyle(this.annotationId, { style: style });
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** A property that lives on the BOX -- alignment, spacing, autofit. */
|
|
487
|
+
applyStyle(patch) {
|
|
488
|
+
if (!this.annotationId) return;
|
|
489
|
+
this.onStyle(this.annotationId, { style: patch });
|
|
490
|
+
}
|
|
491
|
+
}
|