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,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typing into a text annotation where it sits.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the <textarea> this used to be, which could only ever hold one style
|
|
5
|
+
* for the whole box. The editor is a contenteditable, and the two things that
|
|
6
|
+
* usually go wrong with one are handled head on:
|
|
7
|
+
*
|
|
8
|
+
* **The model is the state; the DOM is a view of it.** Formatting commands
|
|
9
|
+
* change `this.rich` and re-render, rather than asking `execCommand` to mutate
|
|
10
|
+
* the DOM and hoping to recognise what it produced. Browsers disagree about
|
|
11
|
+
* whether bold is <b>, <strong> or a style attribute, and a document whose
|
|
12
|
+
* shape depends on which browser typed into it cannot be normalised into
|
|
13
|
+
* anything canonical.
|
|
14
|
+
*
|
|
15
|
+
* **Paste is plain text only.** `workspace_body.html` already warns that
|
|
16
|
+
* contenteditable accepts pasted markup; reading `text/plain` and nothing else
|
|
17
|
+
* answers that outright, and a paste out of a word processor arrives as words
|
|
18
|
+
* rather than as forty spans of Calibri.
|
|
19
|
+
*
|
|
20
|
+
* It mounts in `#fb_overlay_layer` and must stay there:
|
|
21
|
+
* `FigureCanvas.render()` replaces the whole page surface on every change, and
|
|
22
|
+
* an editor mounted inside it would be destroyed by the autosave triggered by
|
|
23
|
+
* the last thing the user typed.
|
|
24
|
+
*/
|
|
25
|
+
class FigureTextEditor {
|
|
26
|
+
|
|
27
|
+
constructor({ overlayEl, canvas, state, onCommit }) {
|
|
28
|
+
this.overlayEl = overlayEl;
|
|
29
|
+
this.canvas = canvas;
|
|
30
|
+
this.state = state;
|
|
31
|
+
this.onCommit = onCommit || (() => {});
|
|
32
|
+
this.el = null;
|
|
33
|
+
this.annotationId = null;
|
|
34
|
+
this.rich = null;
|
|
35
|
+
this.started = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get active() { return Boolean(this.el); }
|
|
39
|
+
|
|
40
|
+
get annotation() {
|
|
41
|
+
return this.annotationId
|
|
42
|
+
? this.state.document.annotations[this.annotationId] : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// -- opening and closing -----------------------------------------------
|
|
46
|
+
|
|
47
|
+
open(annotationId) {
|
|
48
|
+
const annotation = this.state.document.annotations[annotationId];
|
|
49
|
+
if (!annotation || annotation.type !== "text" || !this.overlayEl) return;
|
|
50
|
+
this.close(false);
|
|
51
|
+
|
|
52
|
+
const target = this.canvas.surfaceEl.querySelector(
|
|
53
|
+
`[data-annotation-id="${annotationId}"]`);
|
|
54
|
+
if (!target) return;
|
|
55
|
+
|
|
56
|
+
this.annotationId = annotationId;
|
|
57
|
+
this.rich = FigureRichText.normalize(annotation.text || "", annotation.rich);
|
|
58
|
+
this.started = JSON.stringify(this.rich);
|
|
59
|
+
|
|
60
|
+
this.el = document.createElement("div");
|
|
61
|
+
this.el.className = "fb-text-editor";
|
|
62
|
+
this.el.contentEditable = "true";
|
|
63
|
+
this.el.spellcheck = false;
|
|
64
|
+
this.position(target);
|
|
65
|
+
this.render();
|
|
66
|
+
this.overlayEl.appendChild(this.el);
|
|
67
|
+
|
|
68
|
+
// The annotation itself is hidden while its editor is open, so the two
|
|
69
|
+
// are never on screen at once and half a pixel of disagreement between
|
|
70
|
+
// them does not read as a ghost.
|
|
71
|
+
target.style.visibility = "hidden";
|
|
72
|
+
|
|
73
|
+
this.el.addEventListener("keydown", (event) => this.keyDown(event));
|
|
74
|
+
this.el.addEventListener("paste", (event) => this.paste(event));
|
|
75
|
+
this.el.addEventListener("input", () => this.readBack());
|
|
76
|
+
this.el.addEventListener("blur", () => this.commit());
|
|
77
|
+
this.el.focus();
|
|
78
|
+
// Everything, highlighted, the moment the editor opens -- the same thing
|
|
79
|
+
// double-clicking a caption does in every other tool, and the thing that
|
|
80
|
+
// makes "double-click, type" replace the caption rather than append to
|
|
81
|
+
// it.
|
|
82
|
+
//
|
|
83
|
+
// Asserted twice. `focus()` and the click that opened the editor are
|
|
84
|
+
// both still settling, and a browser that has its own idea of what the
|
|
85
|
+
// double-click selected applies it after this handler returns; whichever
|
|
86
|
+
// range lands last is the one the user sees, so this makes it ours. The
|
|
87
|
+
// guard is the annotation id, so a quick second double-click on a
|
|
88
|
+
// different box cannot be overwritten by the first one's frame.
|
|
89
|
+
this.selectAll();
|
|
90
|
+
const opened = annotationId;
|
|
91
|
+
//: Set AFTER the call above, because `selectAll` goes through
|
|
92
|
+
//: `setOffsets` and would clear it. Any later `setOffsets` clears it
|
|
93
|
+
//: too, which is the point: a caller that placed the caret on purpose
|
|
94
|
+
//: -- Insert Symbol opening the editor to add one character at the end
|
|
95
|
+
//: -- must not be overruled a frame later by a select-all it did not
|
|
96
|
+
//: ask for, leaving the whole caption highlighted and one keystroke
|
|
97
|
+
//: from being replaced.
|
|
98
|
+
this._reassert = true;
|
|
99
|
+
window.requestAnimationFrame?.(() => {
|
|
100
|
+
if (this._reassert && this.el && this.annotationId === opened) {
|
|
101
|
+
this.selectAll();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Put the editor exactly over the annotation.
|
|
108
|
+
*
|
|
109
|
+
* Measured from the rendered element rather than computed from millimetres,
|
|
110
|
+
* because the annotation may be rotated -- and a rotated box's bounding
|
|
111
|
+
* rectangle is the only honest place to put an upright editor over it.
|
|
112
|
+
*/
|
|
113
|
+
position(target) {
|
|
114
|
+
const box = target.getBoundingClientRect();
|
|
115
|
+
const host = this.overlayEl.getBoundingClientRect();
|
|
116
|
+
const annotation = this.annotation;
|
|
117
|
+
this.el.style.left = (box.left - host.left) + "px";
|
|
118
|
+
this.el.style.top = (box.top - host.top) + "px";
|
|
119
|
+
this.el.style.width = Math.max(40, box.width) + "px";
|
|
120
|
+
this.el.style.minHeight = Math.max(16, box.height) + "px";
|
|
121
|
+
this.el.style.textAlign =
|
|
122
|
+
annotation.style.align === "justify" ? "justify" : annotation.style.align;
|
|
123
|
+
this.applyBoxType(annotation.style);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The BOX's type, set on the editor itself, as the inherited default.
|
|
128
|
+
*
|
|
129
|
+
* This is what makes the size you type at the size that gets drawn.
|
|
130
|
+
*
|
|
131
|
+
* Every run renders inside a <span> carrying its own resolved font, so the
|
|
132
|
+
* type was right for every character that was already there -- and wrong
|
|
133
|
+
* for every character typed anywhere a span did not already exist. An empty
|
|
134
|
+
* box renders as `<div class="fb-text-editor-line"><br></div>` with no span
|
|
135
|
+
* in it at all, so the first thing typed into a caption went straight into
|
|
136
|
+
* the line div and inherited the APP's type: the workspace's 13 px UI font,
|
|
137
|
+
* where the canvas was about to draw 14 pt. Place a text box, type, press
|
|
138
|
+
* Escape, and the words changed size in front of you. The same happened for
|
|
139
|
+
* a character typed at the start of a line, or into a blank line mid-block.
|
|
140
|
+
*
|
|
141
|
+
* `.fb-text-editor-line` used to hard-code `line-height: 1.2` for the same
|
|
142
|
+
* reason and with the same flaw: 1.2 is the DEFAULT leading, not this box's,
|
|
143
|
+
* so a caption set to 1.5 was typed at 1.2 and drawn at 1.5. It comes from
|
|
144
|
+
* the style now, which is where `FigureRichText.lineMetrics` reads it.
|
|
145
|
+
*
|
|
146
|
+
* The size is `FigureCanvas.fontPx` -- the same function the SVG on the
|
|
147
|
+
* canvas is drawn with, not a second conversion that could round differently.
|
|
148
|
+
*/
|
|
149
|
+
/**
|
|
150
|
+
* Follow the annotation after a re-render.
|
|
151
|
+
*
|
|
152
|
+
* The box can change under an open editor -- the sidebar's leading and
|
|
153
|
+
* alignment are box properties, and a commit re-lays the annotation out --
|
|
154
|
+
* and the editor was placed once, at `open`, and never looked again. So
|
|
155
|
+
* setting line spacing to 1.5 while typing moved the caption and left the
|
|
156
|
+
* editor over where it used to be, at the leading it used to have.
|
|
157
|
+
*
|
|
158
|
+
* Position and type only; the DOM is not re-rendered, because rebuilding it
|
|
159
|
+
* would put the caret back at the start in the middle of a sentence.
|
|
160
|
+
*/
|
|
161
|
+
reposition(target) {
|
|
162
|
+
if (!this.el || !target || !this.annotation) return;
|
|
163
|
+
this.position(target);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
applyBoxType(style) {
|
|
167
|
+
this.el.style.fontFamily = FigureRichText.cssStack(style.font_family);
|
|
168
|
+
this.el.style.fontSize = this.canvas.fontPx(style.font_size_pt) + "px";
|
|
169
|
+
this.el.style.lineHeight = String(
|
|
170
|
+
style.line_height || FigureRichText.LINE_HEIGHT);
|
|
171
|
+
this.el.style.color = style.color;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
close(restore) {
|
|
175
|
+
this.overlayEl?.querySelectorAll(".fb-text-editor")
|
|
176
|
+
.forEach((editor) => editor.remove());
|
|
177
|
+
if (restore !== false && this.annotationId) {
|
|
178
|
+
const target = this.canvas.surfaceEl.querySelector(
|
|
179
|
+
`[data-annotation-id="${this.annotationId}"]`);
|
|
180
|
+
if (target) target.style.visibility = "";
|
|
181
|
+
}
|
|
182
|
+
this.el = null;
|
|
183
|
+
this.annotationId = null;
|
|
184
|
+
this.rich = null;
|
|
185
|
+
this.started = null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// -- rendering the model ------------------------------------------------
|
|
189
|
+
|
|
190
|
+
render() {
|
|
191
|
+
const annotation = this.annotation;
|
|
192
|
+
if (!this.el || !annotation) return;
|
|
193
|
+
this.el.innerHTML = this.rich.lines.map((line) => {
|
|
194
|
+
const runs = line.runs.map((run) => this.runMarkup(run, annotation.style)).join("");
|
|
195
|
+
// A <br> keeps an empty line selectable: a <div> with nothing in it
|
|
196
|
+
// collapses to zero height and the caret cannot be put in it.
|
|
197
|
+
return `<div class="fb-text-editor-line">${runs || "<br>"}</div>`;
|
|
198
|
+
}).join("");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
runMarkup(run, style) {
|
|
202
|
+
const resolved = FigureRichText.resolveRun(run, style);
|
|
203
|
+
const css = [
|
|
204
|
+
`font-family:${FigureRichText.cssStack(resolved.family)}`,
|
|
205
|
+
`font-size:${this.canvas.fontPx(resolved.size_pt)}px`,
|
|
206
|
+
`color:${resolved.color}`,
|
|
207
|
+
resolved.bold ? "font-weight:bold" : "",
|
|
208
|
+
resolved.italic ? "font-style:italic" : "",
|
|
209
|
+
(resolved.underline || resolved.strike)
|
|
210
|
+
? `text-decoration:${resolved.underline ? "underline " : ""}`
|
|
211
|
+
+ `${resolved.strike ? "line-through" : ""}`
|
|
212
|
+
: "",
|
|
213
|
+
].filter(Boolean).join(";");
|
|
214
|
+
return `<span ${FigureRichText.runAttributes(run)} style="${css}"
|
|
215
|
+
>${FigureSchema.escapeHtml(run.text)}</span>`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Take whatever the browser did to the DOM back into the model. */
|
|
219
|
+
readBack() {
|
|
220
|
+
if (!this.el) return;
|
|
221
|
+
this.rich = FigureRichText.normalize(
|
|
222
|
+
"", FigureRichText.linesFromDom(this.el));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// -- keys ----------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
keyDown(event) {
|
|
228
|
+
// Every canvas and workspace shortcut is off while this has focus. The
|
|
229
|
+
// old textarea relied on both of them checking for a focused INPUT or
|
|
230
|
+
// TEXTAREA first; a contenteditable is a DIV, so neither would.
|
|
231
|
+
event.stopPropagation();
|
|
232
|
+
|
|
233
|
+
const accel = event.metaKey || event.ctrlKey;
|
|
234
|
+
if (event.key === "Escape") {
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
this.cancel();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (event.key === "Enter" && accel) {
|
|
240
|
+
event.preventDefault();
|
|
241
|
+
this.el.blur();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
245
|
+
// Handled in the model rather than left to the browser: engines
|
|
246
|
+
// disagree about whether Enter makes a <div>, a <p> or a <br>, and
|
|
247
|
+
// a line break is the one thing here that must be unambiguous.
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
this.splitLine();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (accel && !event.shiftKey && !event.altKey) {
|
|
253
|
+
const mark = { b: "bold", i: "italic", u: "underline" }[event.key.toLowerCase()];
|
|
254
|
+
if (mark) {
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
this.toggle(mark);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
paste(event) {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
const text = (event.clipboardData || window.clipboardData)?.getData("text/plain");
|
|
264
|
+
if (!text) return;
|
|
265
|
+
this.replaceSelection(FigureRichText.normalizeBreaks(text));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// -- editing the model ---------------------------------------------------
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Turn a mark on or off over the selection.
|
|
272
|
+
*
|
|
273
|
+
* Off when every character in the range already has it, on otherwise --
|
|
274
|
+
* which is what makes a second press of Cmd+B undo the first even when the
|
|
275
|
+
* selection started out half bold.
|
|
276
|
+
*/
|
|
277
|
+
toggle(mark) {
|
|
278
|
+
const range = this.offsets();
|
|
279
|
+
if (!range) return;
|
|
280
|
+
const format = FigureRichText.formatOfRange(
|
|
281
|
+
this.rich, range.start, range.end, this.annotation.style);
|
|
282
|
+
const value = format && format[mark] === true ? null : true;
|
|
283
|
+
this.applyFormat({ [mark]: value }, range);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Set one or more properties over the selection, or over everything when
|
|
287
|
+
* nothing is selected -- which is what the sidebar's controls do. */
|
|
288
|
+
applyFormat(patch, range) {
|
|
289
|
+
const at = range || this.offsets();
|
|
290
|
+
if (!at) return;
|
|
291
|
+
const whole = at.start === at.end;
|
|
292
|
+
const span = whole ? { start: 0, end: this.plainLength() } : at;
|
|
293
|
+
this.rich = FigureRichText.applyToRange(this.rich, span.start, span.end, patch);
|
|
294
|
+
this.render();
|
|
295
|
+
this.setOffsets(at);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
splitLine() {
|
|
299
|
+
const at = this.offsets();
|
|
300
|
+
if (!at) return;
|
|
301
|
+
if (at.end > at.start) this.replaceSelection("");
|
|
302
|
+
const cut = this.offsets().start;
|
|
303
|
+
const lines = [];
|
|
304
|
+
let offset = 0;
|
|
305
|
+
for (let index = 0; index < this.rich.lines.length; index += 1) {
|
|
306
|
+
const line = this.rich.lines[index];
|
|
307
|
+
const length = line.runs.reduce((total, run) => total + run.text.length, 0);
|
|
308
|
+
if (offset <= cut && cut <= offset + length) {
|
|
309
|
+
const local = cut - offset;
|
|
310
|
+
lines.push({ hard: line.hard, runs: this.sliceRuns(line.runs, 0, local) });
|
|
311
|
+
lines.push({ hard: true, runs: this.sliceRuns(line.runs, local, length) });
|
|
312
|
+
} else {
|
|
313
|
+
lines.push(line);
|
|
314
|
+
}
|
|
315
|
+
offset += length + 1;
|
|
316
|
+
}
|
|
317
|
+
this.rich = FigureRichText.normalize("", { lines: lines });
|
|
318
|
+
this.render();
|
|
319
|
+
this.setOffsets({ start: cut + 1, end: cut + 1 });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
sliceRuns(runs, from, to) {
|
|
323
|
+
const out = [];
|
|
324
|
+
let offset = 0;
|
|
325
|
+
for (const run of runs) {
|
|
326
|
+
const start = Math.max(from, offset);
|
|
327
|
+
const end = Math.min(to, offset + run.text.length);
|
|
328
|
+
if (end > start) {
|
|
329
|
+
out.push(FigureRichText.orderRun(
|
|
330
|
+
{ ...run, text: run.text.slice(start - offset, end - offset) }));
|
|
331
|
+
}
|
|
332
|
+
offset += run.text.length;
|
|
333
|
+
}
|
|
334
|
+
return FigureRichText.coalesce(out);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Replace the selection with plain text, keeping the marks at its start. */
|
|
338
|
+
replaceSelection(text) {
|
|
339
|
+
const at = this.offsets();
|
|
340
|
+
if (!at) return;
|
|
341
|
+
const pieces = text.split("\n");
|
|
342
|
+
const marks = FigureRichText.runsInRange(this.rich, at.start, at.start)[0] || {};
|
|
343
|
+
const carried = FigureRichText.orderRun({ ...marks, text: "" });
|
|
344
|
+
delete carried.text;
|
|
345
|
+
|
|
346
|
+
const lines = [];
|
|
347
|
+
let offset = 0;
|
|
348
|
+
let inserted = at.start;
|
|
349
|
+
for (const line of this.rich.lines) {
|
|
350
|
+
const length = line.runs.reduce((total, run) => total + run.text.length, 0);
|
|
351
|
+
const from = offset;
|
|
352
|
+
const to = offset + length;
|
|
353
|
+
if (at.end < from || at.start > to) {
|
|
354
|
+
lines.push(line);
|
|
355
|
+
} else if (at.start >= from && at.start <= to) {
|
|
356
|
+
const head = this.sliceRuns(line.runs, 0, at.start - from);
|
|
357
|
+
const tail = at.end <= to
|
|
358
|
+
? this.sliceRuns(line.runs, at.end - from, length) : [];
|
|
359
|
+
if (pieces.length === 1) {
|
|
360
|
+
lines.push({ hard: line.hard, runs: FigureRichText.coalesce(
|
|
361
|
+
head.concat(pieces[0] ? [{ ...carried, text: pieces[0] }] : [], tail)) });
|
|
362
|
+
inserted = at.start + pieces[0].length;
|
|
363
|
+
} else {
|
|
364
|
+
lines.push({ hard: line.hard, runs: FigureRichText.coalesce(
|
|
365
|
+
head.concat([{ ...carried, text: pieces[0] }])) });
|
|
366
|
+
for (const middle of pieces.slice(1, -1)) {
|
|
367
|
+
lines.push({ hard: true, runs: [{ ...carried, text: middle }] });
|
|
368
|
+
}
|
|
369
|
+
const last = pieces[pieces.length - 1];
|
|
370
|
+
lines.push({ hard: true, runs: FigureRichText.coalesce(
|
|
371
|
+
[{ ...carried, text: last }].concat(tail)) });
|
|
372
|
+
inserted = at.start + text.length;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
offset = to + 1;
|
|
376
|
+
}
|
|
377
|
+
this.rich = FigureRichText.normalize("", { lines: lines });
|
|
378
|
+
this.render();
|
|
379
|
+
this.setOffsets({ start: inserted, end: inserted });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
plainLength() {
|
|
383
|
+
return FigureRichText.plainText(this.rich).length;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// -- selection <-> offsets ----------------------------------------------
|
|
387
|
+
|
|
388
|
+
/** Every text node in the editor, with the plain-text offset it starts at.
|
|
389
|
+
*
|
|
390
|
+
* A line contributes its own length plus one for the newline that follows
|
|
391
|
+
* it, so these offsets are the same ones `applyToRange` works in. */
|
|
392
|
+
textNodes() {
|
|
393
|
+
const out = [];
|
|
394
|
+
let offset = 0;
|
|
395
|
+
const lines = Array.from(this.el.querySelectorAll(".fb-text-editor-line"));
|
|
396
|
+
lines.forEach((line, index) => {
|
|
397
|
+
if (index > 0) offset += 1;
|
|
398
|
+
const walk = (node) => {
|
|
399
|
+
if (node.nodeType === 3) {
|
|
400
|
+
out.push({ node: node, start: offset });
|
|
401
|
+
offset += node.nodeValue.length;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
Array.from(node.childNodes || []).forEach(walk);
|
|
405
|
+
};
|
|
406
|
+
walk(line);
|
|
407
|
+
out.push({ node: line, start: offset, end: true });
|
|
408
|
+
});
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
offsets() {
|
|
413
|
+
if (!this.el) return null;
|
|
414
|
+
const selection = window.getSelection?.();
|
|
415
|
+
if (!selection || !selection.rangeCount) return { start: 0, end: 0 };
|
|
416
|
+
const range = selection.getRangeAt(0);
|
|
417
|
+
if (!this.el.contains(range.startContainer)) return { start: 0, end: 0 };
|
|
418
|
+
const start = this.offsetOf(range.startContainer, range.startOffset);
|
|
419
|
+
const end = this.offsetOf(range.endContainer, range.endOffset);
|
|
420
|
+
return { start: Math.min(start, end), end: Math.max(start, end) };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
offsetOf(node, within) {
|
|
424
|
+
const nodes = this.textNodes();
|
|
425
|
+
for (const entry of nodes) {
|
|
426
|
+
if (entry.node === node && !entry.end) return entry.start + within;
|
|
427
|
+
}
|
|
428
|
+
// A caret in an element rather than a text node -- an empty line, or the
|
|
429
|
+
// gap after the last run. Take the line's own start.
|
|
430
|
+
for (const entry of nodes) {
|
|
431
|
+
if (entry.node === node) return entry.start;
|
|
432
|
+
}
|
|
433
|
+
return 0;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
setOffsets(at) {
|
|
437
|
+
if (!this.el || !window.getSelection) return;
|
|
438
|
+
// Somebody has said where the caret goes. See `open`.
|
|
439
|
+
this._reassert = false;
|
|
440
|
+
const place = (offset) => {
|
|
441
|
+
const nodes = this.textNodes().filter((entry) => !entry.end);
|
|
442
|
+
for (const entry of nodes) {
|
|
443
|
+
if (offset <= entry.start + entry.node.nodeValue.length) {
|
|
444
|
+
return { node: entry.node,
|
|
445
|
+
offset: Math.max(0, offset - entry.start) };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const last = nodes[nodes.length - 1];
|
|
449
|
+
return last
|
|
450
|
+
? { node: last.node, offset: last.node.nodeValue.length }
|
|
451
|
+
: { node: this.el, offset: 0 };
|
|
452
|
+
};
|
|
453
|
+
const from = place(at.start);
|
|
454
|
+
const to = place(at.end);
|
|
455
|
+
const range = document.createRange();
|
|
456
|
+
range.setStart(from.node, from.offset);
|
|
457
|
+
range.setEnd(to.node, to.offset);
|
|
458
|
+
const selection = window.getSelection();
|
|
459
|
+
selection.removeAllRanges();
|
|
460
|
+
selection.addRange(range);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
selectAll() {
|
|
464
|
+
this.setOffsets({ start: 0, end: this.plainLength() });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// -- committing ----------------------------------------------------------
|
|
468
|
+
|
|
469
|
+
cancel() {
|
|
470
|
+
this.close(true);
|
|
471
|
+
this.canvas.render();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
commit() {
|
|
475
|
+
const annotationId = this.annotationId;
|
|
476
|
+
const annotation = this.annotation;
|
|
477
|
+
if (!annotationId || !annotation) return;
|
|
478
|
+
this.readBack();
|
|
479
|
+
// Re-broken to the box's own width before it is stored: the lines ARE
|
|
480
|
+
// the document, so the wrap has to be resolved by the one thing that
|
|
481
|
+
// can measure a string before anything else reads them.
|
|
482
|
+
const rich = this.canvas.rewrapAnnotation({ ...annotation, rich: this.rich });
|
|
483
|
+
const started = this.started;
|
|
484
|
+
this.close(true);
|
|
485
|
+
|
|
486
|
+
// A text box placed and then left empty is invisible and unselectable,
|
|
487
|
+
// so cancelling out of a fresh one removes it rather than leaving a
|
|
488
|
+
// ghost on the page.
|
|
489
|
+
if (!FigureRichText.plainText(rich).trim()) {
|
|
490
|
+
if (!annotation.text) {
|
|
491
|
+
this.state.commit(
|
|
492
|
+
[{ op: "remove_annotations", annotation_ids: [annotationId] }],
|
|
493
|
+
(draft) => { delete draft.annotations[annotationId]; });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (JSON.stringify(rich) === started) {
|
|
498
|
+
this.canvas.render();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
this.onCommit(annotationId, rich);
|
|
502
|
+
}
|
|
503
|
+
}
|