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.
Files changed (248) hide show
  1. plexora/__init__.py +129 -0
  2. plexora/__main__.py +15 -0
  3. plexora/_url.py +105 -0
  4. plexora/api/__init__.py +55 -0
  5. plexora/api/dataset.py +398 -0
  6. plexora/api/http.py +19 -0
  7. plexora/api/plugin.py +541 -0
  8. plexora/api/store.py +221 -0
  9. plexora/cli.py +777 -0
  10. plexora/client/dist/354_bundle.js +2 -0
  11. plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
  12. plexora/client/dist/418_bundle.js +2 -0
  13. plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
  14. plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
  15. plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
  16. plexora/client/dist/770_bundle.js +1 -0
  17. plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
  18. plexora/client/dist/vendor_bundle.js +2 -0
  19. plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
  20. plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
  21. plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
  22. plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
  23. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
  24. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
  25. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
  26. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
  27. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
  28. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
  29. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
  30. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
  31. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
  32. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
  33. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
  34. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
  35. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
  36. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
  37. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
  38. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
  39. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
  40. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
  41. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
  42. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
  43. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
  44. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
  45. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
  46. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
  47. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
  48. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
  49. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
  50. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
  51. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
  52. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
  53. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
  54. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
  55. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
  56. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
  57. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
  58. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
  59. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
  60. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
  61. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
  62. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
  63. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
  64. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
  65. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
  66. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
  67. plexora/client/src/css/import.css +474 -0
  68. plexora/client/src/css/main.css +903 -0
  69. plexora/client/src/css/openProject.css +363 -0
  70. plexora/client/src/css/quickView.css +166 -0
  71. plexora/client/src/css/tokens.css +51 -0
  72. plexora/client/src/css/viewer.css +1655 -0
  73. plexora/client/src/img/apple-touch-icon.png +0 -0
  74. plexora/client/src/img/favicon.ico +0 -0
  75. plexora/client/src/img/logo.ai +8892 -33
  76. plexora/client/src/img/logo.svg +1 -0
  77. plexora/client/src/img/logo_with_text.ai +9011 -34
  78. plexora/client/src/img/logo_with_text.svg +1 -0
  79. plexora/client/src/js/main.js +704 -0
  80. plexora/client/src/js/pluginRegistry.js +108 -0
  81. plexora/client/src/js/services/appStatus.js +286 -0
  82. plexora/client/src/js/services/browsePicker.js +73 -0
  83. plexora/client/src/js/services/dataLayer.js +400 -0
  84. plexora/client/src/js/services/datasetContext.js +143 -0
  85. plexora/client/src/js/services/glRenderer.js +143 -0
  86. plexora/client/src/js/services/importFormValidation.js +257 -0
  87. plexora/client/src/js/services/numericData.js +100 -0
  88. plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
  89. plexora/client/src/js/services/simpleEventHandler.js +25 -0
  90. plexora/client/src/js/vendor.js +29 -0
  91. plexora/client/src/js/views/channelList.js +663 -0
  92. plexora/client/src/js/views/colorSwatchPicker.js +202 -0
  93. plexora/client/src/js/views/columnClassifier.js +146 -0
  94. plexora/client/src/js/views/coordinateField.js +133 -0
  95. plexora/client/src/js/views/dataSourceField.js +276 -0
  96. plexora/client/src/js/views/imageViewer.js +3306 -0
  97. plexora/client/src/js/views/miniMap.js +787 -0
  98. plexora/client/src/js/views/navbarControls.js +178 -0
  99. plexora/client/src/js/views/openProjectPage.js +239 -0
  100. plexora/client/src/js/views/projectEdit.js +335 -0
  101. plexora/client/src/js/views/quickViewLanding.js +136 -0
  102. plexora/client/src/js/views/rainbow.js +326 -0
  103. plexora/client/src/js/views/requirementsModal.js +572 -0
  104. plexora/client/src/js/views/rgbImageViewer.js +115 -0
  105. plexora/client/src/js/views/searchableSelect.js +411 -0
  106. plexora/client/src/js/views/segmentationProgress.js +122 -0
  107. plexora/client/src/js/views/toolLoader.js +747 -0
  108. plexora/client/src/js/views/viewerControls.js +689 -0
  109. plexora/client/src/js/views/viewerManager.js +434 -0
  110. plexora/client/src/js/views/viewerSidebar.js +1117 -0
  111. plexora/client/src/js/workers/tileDecoder.js +130 -0
  112. plexora/client/src/shaders/frag.glsl +528 -0
  113. plexora/client/src/shaders/vert.glsl +12 -0
  114. plexora/client/templates/base.html +212 -0
  115. plexora/client/templates/index.html +238 -0
  116. plexora/client/templates/open_project.html +96 -0
  117. plexora/client/templates/project_columns.html +86 -0
  118. plexora/client/templates/project_edit.html +196 -0
  119. plexora/client/templates/upload.html +118 -0
  120. plexora/connect.py +528 -0
  121. plexora/datasource.py +782 -0
  122. plexora/jupyter.py +406 -0
  123. plexora/notebook_env.py +232 -0
  124. plexora/paths.py +486 -0
  125. plexora/plugins/__init__.py +12 -0
  126. plexora/plugins/cell_explorer/__init__.py +100 -0
  127. plexora/plugins/cell_explorer/server/__init__.py +11 -0
  128. plexora/plugins/cell_explorer/server/routes.py +167 -0
  129. plexora/plugins/cell_explorer/server/state.py +281 -0
  130. plexora/plugins/cell_explorer/server/values.py +145 -0
  131. plexora/plugins/cell_explorer/server/variables.py +367 -0
  132. plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
  133. plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
  134. plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
  135. plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
  136. plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
  137. plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
  138. plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
  139. plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
  140. plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
  141. plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
  142. plexora/plugins/figure_builder/__init__.py +125 -0
  143. plexora/plugins/figure_builder/server/__init__.py +7 -0
  144. plexora/plugins/figure_builder/server/compose.py +365 -0
  145. plexora/plugins/figure_builder/server/export.py +784 -0
  146. plexora/plugins/figure_builder/server/export_jobs.py +148 -0
  147. plexora/plugins/figure_builder/server/operations.py +556 -0
  148. plexora/plugins/figure_builder/server/pixels.py +142 -0
  149. plexora/plugins/figure_builder/server/provenance.py +174 -0
  150. plexora/plugins/figure_builder/server/render.py +278 -0
  151. plexora/plugins/figure_builder/server/repository.py +701 -0
  152. plexora/plugins/figure_builder/server/routes.py +594 -0
  153. plexora/plugins/figure_builder/server/schema.py +1023 -0
  154. plexora/plugins/figure_builder/server/sources.py +120 -0
  155. plexora/plugins/figure_builder/server/textmetrics.py +155 -0
  156. plexora/plugins/figure_builder/static/figureActions.js +363 -0
  157. plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
  158. plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
  159. plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
  160. plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
  161. plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
  162. plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
  163. plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
  164. plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
  165. plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
  166. plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
  167. plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
  168. plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
  169. plexora/plugins/figure_builder/static/figureRichText.js +634 -0
  170. plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
  171. plexora/plugins/figure_builder/static/figureSchema.js +218 -0
  172. plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
  173. plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
  174. plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
  175. plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
  176. plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
  177. plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
  178. plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
  179. plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
  180. plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
  181. plexora/plugins/gating/__init__.py +76 -0
  182. plexora/plugins/gating/server/__init__.py +0 -0
  183. plexora/plugins/gating/server/anndata_gates.py +367 -0
  184. plexora/plugins/gating/server/database.py +16 -0
  185. plexora/plugins/gating/server/model.py +320 -0
  186. plexora/plugins/gating/server/routes.py +235 -0
  187. plexora/plugins/gating/static/csvGatingList.js +947 -0
  188. plexora/plugins/gating/static/gating.css +289 -0
  189. plexora/plugins/gating/static/gatingApi.js +221 -0
  190. plexora/plugins/gating/static/gatingSidebarController.js +513 -0
  191. plexora/plugins/gating/templates/gating/legacy.html +26 -0
  192. plexora/plugins/gating/templates/gating/panel.html +69 -0
  193. plexora/plugins/roi/__init__.py +95 -0
  194. plexora/plugins/roi/server/__init__.py +8 -0
  195. plexora/plugins/roi/server/adapters.py +655 -0
  196. plexora/plugins/roi/server/geojson.py +267 -0
  197. plexora/plugins/roi/server/geometry.py +181 -0
  198. plexora/plugins/roi/server/mapping.py +169 -0
  199. plexora/plugins/roi/server/operations.py +293 -0
  200. plexora/plugins/roi/server/repository.py +215 -0
  201. plexora/plugins/roi/server/routes.py +409 -0
  202. plexora/plugins/roi/server/schema.py +323 -0
  203. plexora/plugins/roi/static/roi.css +583 -0
  204. plexora/plugins/roi/static/roiApi.js +177 -0
  205. plexora/plugins/roi/static/roiFigureBridge.js +107 -0
  206. plexora/plugins/roi/static/roiGeometry.js +350 -0
  207. plexora/plugins/roi/static/roiRenderer.js +278 -0
  208. plexora/plugins/roi/static/roiSidebarController.js +1038 -0
  209. plexora/plugins/roi/static/roiState.js +518 -0
  210. plexora/plugins/roi/static/roiTools.js +993 -0
  211. plexora/plugins/roi/templates/roi/panel.html +240 -0
  212. plexora/proxy.py +57 -0
  213. plexora/server/models/adapters/__init__.py +94 -0
  214. plexora/server/models/adapters/anndata_adapter.py +388 -0
  215. plexora/server/models/adapters/base.py +93 -0
  216. plexora/server/models/adapters/classify.py +192 -0
  217. plexora/server/models/adapters/csv_adapter.py +105 -0
  218. plexora/server/models/adapters/inspection.py +265 -0
  219. plexora/server/models/adapters/spatialdata_adapter.py +248 -0
  220. plexora/server/models/centroid_tiles.py +305 -0
  221. plexora/server/models/data_model.py +1659 -0
  222. plexora/server/models/database_model.py +169 -0
  223. plexora/server/models/project.py +1270 -0
  224. plexora/server/plugins.py +199 -0
  225. plexora/server/routes/browse_routes.py +41 -0
  226. plexora/server/routes/data_routes.py +294 -0
  227. plexora/server/routes/import_routes.py +603 -0
  228. plexora/server/routes/page_routes.py +139 -0
  229. plexora/server/routes/project_routes.py +438 -0
  230. plexora/server/routes/quick_view_routes.py +62 -0
  231. plexora/server/routes/system_routes.py +39 -0
  232. plexora/server/routes/tool_routes.py +418 -0
  233. plexora/server/utils/addHEColumns.py +11 -0
  234. plexora/server/utils/fast_png.py +97 -0
  235. plexora/server/utils/fullConversion.py +50 -0
  236. plexora/server/utils/native_dialog.py +168 -0
  237. plexora/server/utils/pre_normalization.py +47 -0
  238. plexora/server/utils/segmentation_pyramid.py +598 -0
  239. plexora/server/utils/smallestenclosingcircle.py +126 -0
  240. plexora/server/utils/tiffsurgeon.py +372 -0
  241. plexora/server_cli.py +48 -0
  242. plexora/test.py +0 -0
  243. plexora-0.0.1.dist-info/METADATA +341 -0
  244. plexora-0.0.1.dist-info/RECORD +248 -0
  245. plexora-0.0.1.dist-info/WHEEL +5 -0
  246. plexora-0.0.1.dist-info/entry_points.txt +12 -0
  247. plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
  248. 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
+ }