omni-viewer-core 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/THIRD_PARTY_NOTICES.md +49 -0
- package/dist/assets/audio-engine/audio_engine.mjs +2 -0
- package/dist/assets/audio-engine/audio_engine.wasm +0 -0
- package/dist/assets/pdfjs/pdf.worker.min.mjs +21 -0
- package/dist/host/index.d.ts +133 -0
- package/dist/host/index.js +10 -0
- package/dist/i18n/catalog.en.d.ts +1 -0
- package/dist/i18n/catalog.en.js +411 -0
- package/dist/i18n/catalog.ko.d.ts +1 -0
- package/dist/i18n/catalog.ko.js +68 -0
- package/dist/i18n/index.d.ts +18 -0
- package/dist/i18n/index.js +41 -0
- package/dist/licenses/emscripten.txt +101 -0
- package/dist/licenses/kissfft.txt +28 -0
- package/dist/licenses/pdfjs-dist.txt +177 -0
- package/dist/parsers/a2l/index.d.ts +1 -0
- package/dist/parsers/a2l/index.js +1 -0
- package/dist/parsers/archive/index.d.ts +22 -0
- package/dist/parsers/archive/index.js +46 -0
- package/dist/parsers/archive/model.d.ts +29 -0
- package/dist/parsers/archive/model.js +1 -0
- package/dist/parsers/archive/worker.d.ts +21 -0
- package/dist/parsers/archive/worker.js +1 -0
- package/dist/parsers/arxml/index.d.ts +1 -0
- package/dist/parsers/arxml/index.js +1 -0
- package/dist/parsers/asc/index.d.ts +1 -0
- package/dist/parsers/asc/index.js +1 -0
- package/dist/parsers/audio/index.d.ts +9 -0
- package/dist/parsers/audio/index.js +46 -0
- package/dist/parsers/automotive/index.d.ts +33 -0
- package/dist/parsers/automotive/index.js +973 -0
- package/dist/parsers/blf/index.d.ts +1 -0
- package/dist/parsers/blf/index.js +1 -0
- package/dist/parsers/csv/delimiter.d.ts +31 -0
- package/dist/parsers/csv/delimiter.js +151 -0
- package/dist/parsers/csv/index.d.ts +30 -0
- package/dist/parsers/csv/index.js +108 -0
- package/dist/parsers/csv/parseText.d.ts +20 -0
- package/dist/parsers/csv/parseText.js +108 -0
- package/dist/parsers/csv/sort.d.ts +26 -0
- package/dist/parsers/csv/sort.js +224 -0
- package/dist/parsers/csv/statistics.d.ts +49 -0
- package/dist/parsers/csv/statistics.js +150 -0
- package/dist/parsers/dbc/index.d.ts +56 -0
- package/dist/parsers/dbc/index.js +94 -0
- package/dist/parsers/doc-binary/index.d.ts +35 -0
- package/dist/parsers/doc-binary/index.js +267 -0
- package/dist/parsers/doc-binary/vscode-parser.d.ts +142 -0
- package/dist/parsers/doc-binary/vscode-parser.js +3747 -0
- package/dist/parsers/excel/index.d.ts +17 -0
- package/dist/parsers/excel/index.js +406 -0
- package/dist/parsers/excel/model.d.ts +80 -0
- package/dist/parsers/excel/model.js +48 -0
- package/dist/parsers/excel/serialize.d.ts +3 -0
- package/dist/parsers/excel/serialize.js +78 -0
- package/dist/parsers/hdf5/index.d.ts +75 -0
- package/dist/parsers/hdf5/index.js +658 -0
- package/dist/parsers/hdf5/node.d.ts +21 -0
- package/dist/parsers/hdf5/node.js +95 -0
- package/dist/parsers/json/index.d.ts +15 -0
- package/dist/parsers/json/index.js +102 -0
- package/dist/parsers/json/model.d.ts +42 -0
- package/dist/parsers/json/model.js +140 -0
- package/dist/parsers/json/parse.d.ts +22 -0
- package/dist/parsers/json/parse.js +279 -0
- package/dist/parsers/json/tokenizer.d.ts +17 -0
- package/dist/parsers/json/tokenizer.js +175 -0
- package/dist/parsers/jsonl/index.d.ts +14 -0
- package/dist/parsers/jsonl/index.js +18 -0
- package/dist/parsers/jsonl/model.d.ts +23 -0
- package/dist/parsers/jsonl/model.js +1 -0
- package/dist/parsers/jsonl/parse.d.ts +7 -0
- package/dist/parsers/jsonl/parse.js +34 -0
- package/dist/parsers/markdown/index.d.ts +37 -0
- package/dist/parsers/markdown/index.js +59 -0
- package/dist/parsers/mat/index.d.ts +31 -0
- package/dist/parsers/mat/index.js +426 -0
- package/dist/parsers/mermaid/index.d.ts +5 -0
- package/dist/parsers/mermaid/index.js +3 -0
- package/dist/parsers/mf4/index.d.ts +1 -0
- package/dist/parsers/mf4/index.js +1 -0
- package/dist/parsers/parquet/index.d.ts +21 -0
- package/dist/parsers/parquet/index.js +68 -0
- package/dist/parsers/pcap/index.d.ts +1 -0
- package/dist/parsers/pcap/index.js +1 -0
- package/dist/parsers/pcapng/index.d.ts +1 -0
- package/dist/parsers/pcapng/index.js +1 -0
- package/dist/parsers/plantuml/index.d.ts +5 -0
- package/dist/parsers/plantuml/index.js +4 -0
- package/dist/parsers/ppt-binary/index.d.ts +5 -0
- package/dist/parsers/ppt-binary/index.js +62 -0
- package/dist/parsers/ppt-binary/pptBinaryBuildContext.d.ts +12 -0
- package/dist/parsers/ppt-binary/pptBinaryBuildContext.js +40 -0
- package/dist/parsers/ppt-binary/pptBinaryContainer.d.ts +4 -0
- package/dist/parsers/ppt-binary/pptBinaryContainer.js +174 -0
- package/dist/parsers/ppt-binary/pptBinaryTypes.d.ts +125 -0
- package/dist/parsers/ppt-binary/pptBinaryTypes.js +1 -0
- package/dist/parsers/ppt-binary/pptSlideLayouts.d.ts +9 -0
- package/dist/parsers/ppt-binary/pptSlideLayouts.js +510 -0
- package/dist/parsers/ppt-binary/vscode-parser.d.ts +116 -0
- package/dist/parsers/ppt-binary/vscode-parser.js +2853 -0
- package/dist/parsers/ppt-binary/vscode.d.ts +4 -0
- package/dist/parsers/ppt-binary/vscode.js +12 -0
- package/dist/parsers/pptx/index.d.ts +15 -0
- package/dist/parsers/pptx/index.js +88 -0
- package/dist/parsers/pptx/vscode-parser.d.ts +167 -0
- package/dist/parsers/pptx/vscode-parser.js +1503 -0
- package/dist/parsers/pptx/vscode.d.ts +4 -0
- package/dist/parsers/pptx/vscode.js +18 -0
- package/dist/parsers/pptx/zip-reader.d.ts +3 -0
- package/dist/parsers/pptx/zip-reader.js +20 -0
- package/dist/parsers/proto/index.d.ts +90 -0
- package/dist/parsers/proto/index.js +197 -0
- package/dist/parsers/psd/index.d.ts +16 -0
- package/dist/parsers/psd/index.js +10 -0
- package/dist/parsers/shapefile/dbf.d.ts +23 -0
- package/dist/parsers/shapefile/dbf.js +112 -0
- package/dist/parsers/shapefile/index.d.ts +44 -0
- package/dist/parsers/shapefile/index.js +119 -0
- package/dist/parsers/shapefile/reproject.d.ts +10 -0
- package/dist/parsers/shapefile/reproject.js +41 -0
- package/dist/parsers/slide-model.d.ts +106 -0
- package/dist/parsers/slide-model.js +1 -0
- package/dist/parsers/toml/index.d.ts +15 -0
- package/dist/parsers/toml/index.js +18 -0
- package/dist/parsers/toml/model.d.ts +15 -0
- package/dist/parsers/toml/model.js +1 -0
- package/dist/parsers/toml/parse.d.ts +16 -0
- package/dist/parsers/toml/parse.js +266 -0
- package/dist/parsers/types.d.ts +103 -0
- package/dist/parsers/types.js +106 -0
- package/dist/parsers/video/index.d.ts +6 -0
- package/dist/parsers/video/index.js +2 -0
- package/dist/parsers/yaml/index.d.ts +16 -0
- package/dist/parsers/yaml/index.js +58 -0
- package/dist/parsers/yaml/model.d.ts +22 -0
- package/dist/parsers/yaml/model.js +1 -0
- package/dist/parsers/yaml/self-loading.d.ts +3 -0
- package/dist/parsers/yaml/self-loading.js +19 -0
- package/dist/parsers/zip-scan.d.ts +8 -0
- package/dist/parsers/zip-scan.js +80 -0
- package/dist/registry/container.d.ts +17 -0
- package/dist/registry/container.js +49 -0
- package/dist/registry/index.d.ts +133 -0
- package/dist/registry/index.js +348 -0
- package/dist/registry/probe.d.ts +3 -0
- package/dist/registry/probe.js +177 -0
- package/dist/registry/sniff.d.ts +17 -0
- package/dist/registry/sniff.js +57 -0
- package/dist/styles/archive.css +10 -0
- package/dist/styles/audio.css +66 -0
- package/dist/styles/automotive.css +1 -0
- package/dist/styles/csv.css +262 -0
- package/dist/styles/excel.css +212 -0
- package/dist/styles/fallback.css +48 -0
- package/dist/styles/hdf5.css +3 -0
- package/dist/styles/hwp.css +12 -0
- package/dist/styles/image.css +115 -0
- package/dist/styles/json.css +342 -0
- package/dist/styles/jsonl.css +1 -0
- package/dist/styles/markdown.css +10 -0
- package/dist/styles/mat.css +2 -0
- package/dist/styles/mermaid.css +1 -0
- package/dist/styles/parquet.css +2 -0
- package/dist/styles/pdf.css +551 -0
- package/dist/styles/plantuml.css +1 -0
- package/dist/styles/ppt.css +1 -0
- package/dist/styles/proto.css +2 -0
- package/dist/styles/psd.css +1 -0
- package/dist/styles/shapefile.css +25 -0
- package/dist/styles/toml.css +1 -0
- package/dist/styles/video.css +74 -0
- package/dist/styles/word.css +685 -0
- package/dist/styles/yaml.css +1 -0
- package/dist/viewers/a2l/index.d.ts +15 -0
- package/dist/viewers/a2l/index.js +5 -0
- package/dist/viewers/archive/archive-preview-decoder.d.ts +4 -0
- package/dist/viewers/archive/archive-preview-decoder.js +223 -0
- package/dist/viewers/archive/controller.d.ts +24 -0
- package/dist/viewers/archive/controller.js +18 -0
- package/dist/viewers/archive/index.d.ts +25 -0
- package/dist/viewers/archive/index.js +230 -0
- package/dist/viewers/archive/preview.d.ts +2 -0
- package/dist/viewers/archive/preview.js +2 -0
- package/dist/viewers/archive/styles.d.ts +1 -0
- package/dist/viewers/archive/styles.js +11 -0
- package/dist/viewers/arxml/index.d.ts +15 -0
- package/dist/viewers/arxml/index.js +5 -0
- package/dist/viewers/asc/index.d.ts +15 -0
- package/dist/viewers/asc/index.js +5 -0
- package/dist/viewers/audio/controller.d.ts +33 -0
- package/dist/viewers/audio/controller.js +49 -0
- package/dist/viewers/audio/engine.d.ts +44 -0
- package/dist/viewers/audio/engine.js +108 -0
- package/dist/viewers/audio/index.d.ts +92 -0
- package/dist/viewers/audio/index.js +448 -0
- package/dist/viewers/audio/self-loading.d.ts +7 -0
- package/dist/viewers/audio/self-loading.js +42 -0
- package/dist/viewers/audio/styles.d.ts +1 -0
- package/dist/viewers/audio/styles.js +73 -0
- package/dist/viewers/automotive/index.d.ts +8 -0
- package/dist/viewers/automotive/index.js +152 -0
- package/dist/viewers/automotive/styles.d.ts +1 -0
- package/dist/viewers/automotive/styles.js +1 -0
- package/dist/viewers/avro/index.d.ts +15 -0
- package/dist/viewers/avro/index.js +5 -0
- package/dist/viewers/bag/index.d.ts +15 -0
- package/dist/viewers/bag/index.js +5 -0
- package/dist/viewers/blf/index.d.ts +15 -0
- package/dist/viewers/blf/index.js +5 -0
- package/dist/viewers/csv/controller.d.ts +126 -0
- package/dist/viewers/csv/controller.js +598 -0
- package/dist/viewers/csv/index.d.ts +48 -0
- package/dist/viewers/csv/index.js +846 -0
- package/dist/viewers/csv/styles.d.ts +1 -0
- package/dist/viewers/csv/styles.js +267 -0
- package/dist/viewers/db3/index.d.ts +15 -0
- package/dist/viewers/db3/index.js +5 -0
- package/dist/viewers/dbc/index.d.ts +15 -0
- package/dist/viewers/dbc/index.js +5 -0
- package/dist/viewers/diagram.d.ts +14 -0
- package/dist/viewers/diagram.js +95 -0
- package/dist/viewers/excel/controller.d.ts +108 -0
- package/dist/viewers/excel/controller.js +548 -0
- package/dist/viewers/excel/grid.d.ts +28 -0
- package/dist/viewers/excel/grid.js +99 -0
- package/dist/viewers/excel/index.d.ts +37 -0
- package/dist/viewers/excel/index.js +620 -0
- package/dist/viewers/excel/self-loading.d.ts +6 -0
- package/dist/viewers/excel/self-loading.js +13 -0
- package/dist/viewers/excel/styles.d.ts +1 -0
- package/dist/viewers/excel/styles.js +216 -0
- package/dist/viewers/fallback/index.d.ts +17 -0
- package/dist/viewers/fallback/index.js +130 -0
- package/dist/viewers/fallback/styles.d.ts +1 -0
- package/dist/viewers/fallback/styles.js +49 -0
- package/dist/viewers/hdf5/index.d.ts +19 -0
- package/dist/viewers/hdf5/index.js +170 -0
- package/dist/viewers/hdf5/styles.d.ts +1 -0
- package/dist/viewers/hdf5/styles.js +4 -0
- package/dist/viewers/hwp/controller.d.ts +19 -0
- package/dist/viewers/hwp/controller.js +22 -0
- package/dist/viewers/hwp/index.d.ts +39 -0
- package/dist/viewers/hwp/index.js +271 -0
- package/dist/viewers/hwp/self-loading.d.ts +2 -0
- package/dist/viewers/hwp/self-loading.js +6 -0
- package/dist/viewers/hwp/styles.d.ts +1 -0
- package/dist/viewers/hwp/styles.js +13 -0
- package/dist/viewers/image/controller.d.ts +210 -0
- package/dist/viewers/image/controller.js +336 -0
- package/dist/viewers/image/decode.d.ts +54 -0
- package/dist/viewers/image/decode.js +150 -0
- package/dist/viewers/image/index.d.ts +38 -0
- package/dist/viewers/image/index.js +749 -0
- package/dist/viewers/image/limits.d.ts +18 -0
- package/dist/viewers/image/limits.js +20 -0
- package/dist/viewers/image/styles.d.ts +1 -0
- package/dist/viewers/image/styles.js +119 -0
- package/dist/viewers/json/controller.d.ts +89 -0
- package/dist/viewers/json/controller.js +411 -0
- package/dist/viewers/json/index.d.ts +25 -0
- package/dist/viewers/json/index.js +503 -0
- package/dist/viewers/json/styles.d.ts +1 -0
- package/dist/viewers/json/styles.js +346 -0
- package/dist/viewers/json/transforms.d.ts +27 -0
- package/dist/viewers/json/transforms.js +253 -0
- package/dist/viewers/jsonl/controller.d.ts +48 -0
- package/dist/viewers/jsonl/controller.js +136 -0
- package/dist/viewers/jsonl/index.d.ts +22 -0
- package/dist/viewers/jsonl/index.js +184 -0
- package/dist/viewers/jsonl/styles.d.ts +1 -0
- package/dist/viewers/jsonl/styles.js +1 -0
- package/dist/viewers/markdown/controller.d.ts +50 -0
- package/dist/viewers/markdown/controller.js +62 -0
- package/dist/viewers/markdown/index.d.ts +64 -0
- package/dist/viewers/markdown/index.js +425 -0
- package/dist/viewers/markdown/math.d.ts +21 -0
- package/dist/viewers/markdown/math.js +100 -0
- package/dist/viewers/markdown/self-loading.d.ts +5 -0
- package/dist/viewers/markdown/self-loading.js +48 -0
- package/dist/viewers/markdown/styles.d.ts +1 -0
- package/dist/viewers/markdown/styles.js +11 -0
- package/dist/viewers/mat/index.d.ts +19 -0
- package/dist/viewers/mat/index.js +113 -0
- package/dist/viewers/mat/styles.d.ts +1 -0
- package/dist/viewers/mat/styles.js +3 -0
- package/dist/viewers/media.d.ts +9 -0
- package/dist/viewers/media.js +77 -0
- package/dist/viewers/mermaid/index.d.ts +17 -0
- package/dist/viewers/mermaid/index.js +6 -0
- package/dist/viewers/mermaid/self-loading.d.ts +2 -0
- package/dist/viewers/mermaid/self-loading.js +2 -0
- package/dist/viewers/mf4/index.d.ts +15 -0
- package/dist/viewers/mf4/index.js +5 -0
- package/dist/viewers/parquet/controller.d.ts +37 -0
- package/dist/viewers/parquet/controller.js +26 -0
- package/dist/viewers/parquet/index.d.ts +23 -0
- package/dist/viewers/parquet/index.js +134 -0
- package/dist/viewers/parquet/styles.d.ts +1 -0
- package/dist/viewers/parquet/styles.js +3 -0
- package/dist/viewers/pcap/index.d.ts +15 -0
- package/dist/viewers/pcap/index.js +5 -0
- package/dist/viewers/pcapng/index.d.ts +15 -0
- package/dist/viewers/pcapng/index.js +5 -0
- package/dist/viewers/pdf/controller.d.ts +120 -0
- package/dist/viewers/pdf/controller.js +191 -0
- package/dist/viewers/pdf/editing.d.ts +26 -0
- package/dist/viewers/pdf/editing.js +265 -0
- package/dist/viewers/pdf/index.d.ts +88 -0
- package/dist/viewers/pdf/index.js +1400 -0
- package/dist/viewers/pdf/self-loading.d.ts +6 -0
- package/dist/viewers/pdf/self-loading.js +14 -0
- package/dist/viewers/pdf/styles.d.ts +1 -0
- package/dist/viewers/pdf/styles.js +555 -0
- package/dist/viewers/plantuml/index.d.ts +17 -0
- package/dist/viewers/plantuml/index.js +6 -0
- package/dist/viewers/plantuml/self-loading.d.ts +2 -0
- package/dist/viewers/plantuml/self-loading.js +2 -0
- package/dist/viewers/ppt/chart.d.ts +8 -0
- package/dist/viewers/ppt/chart.js +83 -0
- package/dist/viewers/ppt/controller.d.ts +34 -0
- package/dist/viewers/ppt/controller.js +27 -0
- package/dist/viewers/ppt/index.d.ts +33 -0
- package/dist/viewers/ppt/index.js +101 -0
- package/dist/viewers/ppt/render.d.ts +5 -0
- package/dist/viewers/ppt/render.js +144 -0
- package/dist/viewers/ppt/self-loading.d.ts +9 -0
- package/dist/viewers/ppt/self-loading.js +4 -0
- package/dist/viewers/ppt/styles.d.ts +1 -0
- package/dist/viewers/ppt/styles.js +1 -0
- package/dist/viewers/proto/index.d.ts +17 -0
- package/dist/viewers/proto/index.js +170 -0
- package/dist/viewers/proto/styles.d.ts +1 -0
- package/dist/viewers/proto/styles.js +2 -0
- package/dist/viewers/psd/index.d.ts +27 -0
- package/dist/viewers/psd/index.js +39 -0
- package/dist/viewers/psd/self-loading.d.ts +10 -0
- package/dist/viewers/psd/self-loading.js +20 -0
- package/dist/viewers/psd/styles.d.ts +1 -0
- package/dist/viewers/psd/styles.js +1 -0
- package/dist/viewers/reqif/index.d.ts +15 -0
- package/dist/viewers/reqif/index.js +5 -0
- package/dist/viewers/shapefile/index.d.ts +35 -0
- package/dist/viewers/shapefile/index.js +336 -0
- package/dist/viewers/shapefile/self-loading.d.ts +7 -0
- package/dist/viewers/shapefile/self-loading.js +32 -0
- package/dist/viewers/shapefile/styles.d.ts +1 -0
- package/dist/viewers/shapefile/styles.js +28 -0
- package/dist/viewers/stp/index.d.ts +15 -0
- package/dist/viewers/stp/index.js +5 -0
- package/dist/viewers/structured-styles.d.ts +1 -0
- package/dist/viewers/structured-styles.js +1 -0
- package/dist/viewers/structured.d.ts +36 -0
- package/dist/viewers/structured.js +138 -0
- package/dist/viewers/toml/controller.d.ts +38 -0
- package/dist/viewers/toml/controller.js +61 -0
- package/dist/viewers/toml/index.d.ts +18 -0
- package/dist/viewers/toml/index.js +34 -0
- package/dist/viewers/types.d.ts +36 -0
- package/dist/viewers/types.js +9 -0
- package/dist/viewers/video/controller.d.ts +29 -0
- package/dist/viewers/video/controller.js +54 -0
- package/dist/viewers/video/index.d.ts +18 -0
- package/dist/viewers/video/index.js +305 -0
- package/dist/viewers/video/styles.d.ts +1 -0
- package/dist/viewers/video/styles.js +79 -0
- package/dist/viewers/word/controller.d.ts +19 -0
- package/dist/viewers/word/controller.js +22 -0
- package/dist/viewers/word/docx-preprocess.d.ts +63 -0
- package/dist/viewers/word/docx-preprocess.js +136 -0
- package/dist/viewers/word/index.d.ts +32 -0
- package/dist/viewers/word/index.js +187 -0
- package/dist/viewers/word/normalize-docx.d.ts +2 -0
- package/dist/viewers/word/normalize-docx.js +50 -0
- package/dist/viewers/word/paginate.d.ts +1 -0
- package/dist/viewers/word/paginate.js +1035 -0
- package/dist/viewers/word/self-loading.d.ts +2 -0
- package/dist/viewers/word/self-loading.js +7 -0
- package/dist/viewers/word/styles.d.ts +1 -0
- package/dist/viewers/word/styles.js +686 -0
- package/dist/viewers/yaml/controller.d.ts +42 -0
- package/dist/viewers/yaml/controller.js +41 -0
- package/dist/viewers/yaml/index.d.ts +21 -0
- package/dist/viewers/yaml/index.js +20 -0
- package/package.json +375 -0
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
// CSV viewer — DOM renderer consuming CsvController (DESIGN.md §3-②).
|
|
2
|
+
//
|
|
3
|
+
// CSP rules: no eval, no inline handlers, no innerHTML — all rendering goes
|
|
4
|
+
// through createElement/textContent. Mount is async and renders inside a
|
|
5
|
+
// shadow root by default (§6 reverse-contamination contract).
|
|
6
|
+
//
|
|
7
|
+
// Optional services: `clipboard`. Degraded mode (core contract, ADR 24):
|
|
8
|
+
// without it the copy buttons render disabled with an explanatory tooltip —
|
|
9
|
+
// never hidden.
|
|
10
|
+
import { MountAbortedError, VIEWER_ROOT_CLASS } from '../types.js';
|
|
11
|
+
import { formatPercent, formatStatNumber, DELIMITER_PRIORITY } from '../../parsers/csv/index.js';
|
|
12
|
+
import { utf8ByteLength } from '../../parsers/types.js';
|
|
13
|
+
import { createCsvController, CSV_MIN_SEARCH_LENGTH } from './controller.js';
|
|
14
|
+
import { csvViewerCss } from './styles.js';
|
|
15
|
+
export { createCsvController, DEFAULT_PAGE_SIZE } from './controller.js';
|
|
16
|
+
export { csvViewerCss } from './styles.js';
|
|
17
|
+
/** Viewer metadata — single source for the registry codegen (DESIGN.md §7). */
|
|
18
|
+
export const CSV_VIEWER_META = {
|
|
19
|
+
id: 'csv',
|
|
20
|
+
displayNameKey: 'csv.tableView',
|
|
21
|
+
extensions: ['csv', 'tsv'],
|
|
22
|
+
priority: 10,
|
|
23
|
+
requiredServices: [],
|
|
24
|
+
optionalServices: ['clipboard', 'save', 'writeback'],
|
|
25
|
+
inputOwnership: 'borrows'
|
|
26
|
+
};
|
|
27
|
+
/** Export file name: strip the extension, append -export, pick the extension
|
|
28
|
+
* by the active delimiter (docs/viewers/csv.md §6). */
|
|
29
|
+
export function exportFileName(sourceName, delimiter) {
|
|
30
|
+
const stem = sourceName.replace(/\.[^./\\]+$/, '') || sourceName;
|
|
31
|
+
return `${stem}-export${delimiter === '\t' ? '.tsv' : '.csv'}`;
|
|
32
|
+
}
|
|
33
|
+
/** Suggested filename for Save As. The host's FileSaveService presents the
|
|
34
|
+
* destination picker; this only supplies its initial filename. */
|
|
35
|
+
export function saveAsFileName(sourceName, delimiter) {
|
|
36
|
+
const stem = sourceName.replace(/\.[^./\\]+$/, '') || sourceName;
|
|
37
|
+
return `${stem}${delimiter === '\t' ? '.tsv' : '.csv'}`;
|
|
38
|
+
}
|
|
39
|
+
/** Clipboard copy guard (docs/viewers/csv.md §6): larger payloads are refused. */
|
|
40
|
+
export const COPY_PAYLOAD_LIMIT_BYTES = 1024 * 1024;
|
|
41
|
+
/** Cells at least this long (or containing a newline) get a full-content tooltip. */
|
|
42
|
+
export const CELL_TOOLTIP_MIN_CHARS = 50;
|
|
43
|
+
const MIN_COLUMN_WIDTH = 40;
|
|
44
|
+
const FALLBACK_COLUMN_WIDTH = 120;
|
|
45
|
+
/** Header single-click sort is deferred briefly so a double-click can win and
|
|
46
|
+
* open the rename editor instead (docs §6). */
|
|
47
|
+
export const SORT_CLICK_DELAY_MS = 250;
|
|
48
|
+
/** Raw-view typing settles this long before the text is reparsed into the
|
|
49
|
+
* document (docs §6 — the edit only marks dirty; saving stays explicit). */
|
|
50
|
+
export const RAW_EDIT_DEBOUNCE_MS = 400;
|
|
51
|
+
const DELIMITER_LABEL_KEYS = {
|
|
52
|
+
',': 'csv.delimiter.comma',
|
|
53
|
+
';': 'csv.delimiter.semicolon',
|
|
54
|
+
'\t': 'csv.delimiter.tab',
|
|
55
|
+
'|': 'csv.delimiter.pipe'
|
|
56
|
+
};
|
|
57
|
+
export async function mountCsvViewer(input, container, ctx, options = {}) {
|
|
58
|
+
if (options.signal?.aborted)
|
|
59
|
+
throw new MountAbortedError();
|
|
60
|
+
const controllerOptions = {
|
|
61
|
+
fileName: input.fileName
|
|
62
|
+
};
|
|
63
|
+
if (options.limits !== undefined)
|
|
64
|
+
controllerOptions.limits = options.limits;
|
|
65
|
+
if (options.pageSize !== undefined)
|
|
66
|
+
controllerOptions.pageSize = options.pageSize;
|
|
67
|
+
const controller = createCsvController(input.data, controllerOptions);
|
|
68
|
+
const isolation = options.styleIsolation ?? 'shadow';
|
|
69
|
+
let root;
|
|
70
|
+
if (isolation === 'shadow' && typeof container.attachShadow === 'function') {
|
|
71
|
+
root = container.shadowRoot ?? container.attachShadow({ mode: 'open' });
|
|
72
|
+
const style = document.createElement('style');
|
|
73
|
+
style.textContent = csvViewerCss;
|
|
74
|
+
root.appendChild(style);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
container.classList.add(VIEWER_ROOT_CLASS, 'omni-viewer--csv');
|
|
78
|
+
root = container;
|
|
79
|
+
}
|
|
80
|
+
const t = ctx.i18n.t.bind(ctx.i18n);
|
|
81
|
+
const disposers = [];
|
|
82
|
+
let toastTimer = null;
|
|
83
|
+
/** Session-scoped column widths (px), per data column; null entry = auto.
|
|
84
|
+
* Non-null array means the table runs in fixed layout (docs §6). */
|
|
85
|
+
let columnWidths = null;
|
|
86
|
+
let activeResizeCleanup = null;
|
|
87
|
+
let pendingSortTimer = null;
|
|
88
|
+
let contextMenuCleanup = null;
|
|
89
|
+
let rawTextarea = null;
|
|
90
|
+
let rawEditTimer = null;
|
|
91
|
+
const el = (tag, className, text) => {
|
|
92
|
+
const node = document.createElement(tag);
|
|
93
|
+
if (className)
|
|
94
|
+
node.className = className;
|
|
95
|
+
if (text !== undefined)
|
|
96
|
+
node.textContent = text;
|
|
97
|
+
return node;
|
|
98
|
+
};
|
|
99
|
+
const on = (target, type, handler) => {
|
|
100
|
+
target.addEventListener(type, handler);
|
|
101
|
+
disposers.push(() => target.removeEventListener(type, handler));
|
|
102
|
+
};
|
|
103
|
+
// --- static frame -------------------------------------------------------
|
|
104
|
+
const frame = el('div', 'omni-csv');
|
|
105
|
+
frame.style.position = 'relative';
|
|
106
|
+
const toolbar = el('div', 'omni-csv__toolbar');
|
|
107
|
+
const delimiterLabel = el('label', undefined, t('csv.delimiter'));
|
|
108
|
+
const delimiterSelect = el('select');
|
|
109
|
+
// "Auto detect" re-runs detection; the select value always shows the
|
|
110
|
+
// delimiter currently in effect, so choosing auto snaps back to the
|
|
111
|
+
// detected one (docs/viewers/csv.md §2).
|
|
112
|
+
const autoOpt = el('option', undefined, t('csv.delimiter.auto'));
|
|
113
|
+
autoOpt.value = 'auto';
|
|
114
|
+
delimiterSelect.appendChild(autoOpt);
|
|
115
|
+
for (const d of DELIMITER_PRIORITY) {
|
|
116
|
+
const opt = el('option', undefined, t(DELIMITER_LABEL_KEYS[d]));
|
|
117
|
+
opt.value = d;
|
|
118
|
+
delimiterSelect.appendChild(opt);
|
|
119
|
+
}
|
|
120
|
+
delimiterLabel.appendChild(delimiterSelect);
|
|
121
|
+
on(delimiterSelect, 'change', () => {
|
|
122
|
+
const value = delimiterSelect.value;
|
|
123
|
+
controller.dispatch({
|
|
124
|
+
type: 'set-delimiter',
|
|
125
|
+
delimiter: value === 'auto' ? 'auto' : value
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
const searchInput = el('input', 'omni-csv__search');
|
|
129
|
+
searchInput.type = 'search';
|
|
130
|
+
searchInput.minLength = CSV_MIN_SEARCH_LENGTH;
|
|
131
|
+
searchInput.placeholder = t('csv.search');
|
|
132
|
+
searchInput.setAttribute('aria-label', t('csv.search'));
|
|
133
|
+
on(searchInput, 'input', () => {
|
|
134
|
+
controller.dispatch({ type: 'set-search', search: searchInput.value });
|
|
135
|
+
});
|
|
136
|
+
const rowNumToggle = el('button', undefined, t('csv.rowNumbers'));
|
|
137
|
+
rowNumToggle.type = 'button';
|
|
138
|
+
on(rowNumToggle, 'click', () => controller.dispatch({ type: 'toggle-row-numbers' }));
|
|
139
|
+
const pageSizeLabel = el('label', undefined, t('csv.pageSize'));
|
|
140
|
+
const pageSizeSelect = el('select');
|
|
141
|
+
for (const size of [100, 200, 500, 1000]) {
|
|
142
|
+
const opt = el('option', undefined, String(size));
|
|
143
|
+
opt.value = String(size);
|
|
144
|
+
pageSizeSelect.appendChild(opt);
|
|
145
|
+
}
|
|
146
|
+
pageSizeLabel.appendChild(pageSizeSelect);
|
|
147
|
+
on(pageSizeSelect, 'change', () => {
|
|
148
|
+
controller.dispatch({
|
|
149
|
+
type: 'set-page-size',
|
|
150
|
+
pageSize: Number(pageSizeSelect.value)
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
const viewToggle = el('button', undefined, t('csv.rawView'));
|
|
154
|
+
viewToggle.type = 'button';
|
|
155
|
+
on(viewToggle, 'click', () => {
|
|
156
|
+
controller.dispatch({
|
|
157
|
+
type: 'set-view-mode',
|
|
158
|
+
mode: controller.state.viewMode === 'table' ? 'raw' : 'table'
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
const statsToggle = el('button', undefined, t('csv.statistics'));
|
|
162
|
+
statsToggle.type = 'button';
|
|
163
|
+
on(statsToggle, 'click', () => controller.dispatch({ type: 'toggle-stats' }));
|
|
164
|
+
const copyTsvBtn = el('button', undefined, t('csv.copyTsv'));
|
|
165
|
+
copyTsvBtn.type = 'button';
|
|
166
|
+
const copyJsonBtn = el('button', undefined, t('csv.copyJson'));
|
|
167
|
+
copyJsonBtn.type = 'button';
|
|
168
|
+
const clipboard = ctx.clipboard;
|
|
169
|
+
if (clipboard) {
|
|
170
|
+
on(copyTsvBtn, 'click', () => void copy(clipboard, controller.toTsv()));
|
|
171
|
+
on(copyJsonBtn, 'click', () => void copy(clipboard, controller.toJson()));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
// Degraded mode: disabled with reason, identical on every platform.
|
|
175
|
+
for (const btn of [copyTsvBtn, copyJsonBtn]) {
|
|
176
|
+
btn.disabled = true;
|
|
177
|
+
btn.title = t('common.noClipboard');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const undoBtn = el('button', undefined, '↶');
|
|
181
|
+
undoBtn.type = 'button';
|
|
182
|
+
undoBtn.setAttribute('aria-label', t('csv.undo'));
|
|
183
|
+
on(undoBtn, 'click', () => controller.dispatch({ type: 'undo' }));
|
|
184
|
+
const redoBtn = el('button', undefined, '↷');
|
|
185
|
+
redoBtn.type = 'button';
|
|
186
|
+
redoBtn.setAttribute('aria-label', t('csv.redo'));
|
|
187
|
+
on(redoBtn, 'click', () => controller.dispatch({ type: 'redo' }));
|
|
188
|
+
const addRowBtn = el('button', undefined, t('csv.addRow'));
|
|
189
|
+
addRowBtn.type = 'button';
|
|
190
|
+
on(addRowBtn, 'click', () => controller.dispatch({ type: 'insert-row', afterRowId: 'end' }));
|
|
191
|
+
const addColumnBtn = el('button', undefined, t('csv.addColumn'));
|
|
192
|
+
addColumnBtn.type = 'button';
|
|
193
|
+
on(addColumnBtn, 'click', () => controller.dispatch({
|
|
194
|
+
type: 'insert-column',
|
|
195
|
+
columnIndex: controller.state.columnCount
|
|
196
|
+
}));
|
|
197
|
+
const saveBtn = el('button', undefined, t('csv.save'));
|
|
198
|
+
saveBtn.type = 'button';
|
|
199
|
+
const writeback = ctx.writeback;
|
|
200
|
+
if (writeback) {
|
|
201
|
+
on(saveBtn, 'click', () => void saveDocument(writeback));
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// Degraded mode (docs §6 결정 1): editing works, saving points to export.
|
|
205
|
+
saveBtn.disabled = true;
|
|
206
|
+
saveBtn.title = t('common.noWriteback');
|
|
207
|
+
}
|
|
208
|
+
const saveAsBtn = el('button', undefined, t('csv.saveAs'));
|
|
209
|
+
saveAsBtn.type = 'button';
|
|
210
|
+
const save = ctx.save;
|
|
211
|
+
if (save) {
|
|
212
|
+
on(saveAsBtn, 'click', () => void saveAsDocument(save));
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
saveAsBtn.disabled = true;
|
|
216
|
+
saveAsBtn.title = t('common.noFileSave');
|
|
217
|
+
}
|
|
218
|
+
const dirtyBadge = el('span', 'omni-csv__dirty');
|
|
219
|
+
async function saveDocument(service) {
|
|
220
|
+
try {
|
|
221
|
+
// Save covers the whole document in original order — never the
|
|
222
|
+
// filtered/sorted display result (docs §6).
|
|
223
|
+
const bytes = new TextEncoder().encode(controller.toDocumentCsv());
|
|
224
|
+
await service.write(bytes);
|
|
225
|
+
controller.markSaved();
|
|
226
|
+
showToast(t('common.savedToOriginal'));
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
ctx.logger.log('error', `csv save failed: ${String(error)}`);
|
|
230
|
+
showToast(t('common.saveFailed'));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** Save As delegates the destination picker to the host adapter. Unlike
|
|
234
|
+
* export, it always writes the whole edited document in document order. */
|
|
235
|
+
async function saveAsDocument(service) {
|
|
236
|
+
const state = controller.state;
|
|
237
|
+
const name = saveAsFileName(input.fileName, state.delimiter);
|
|
238
|
+
const mime = state.delimiter === '\t' ? 'text/tab-separated-values' : 'text/csv';
|
|
239
|
+
try {
|
|
240
|
+
const bytes = new TextEncoder().encode(controller.toDocumentCsv());
|
|
241
|
+
await service.saveFile(name, bytes, mime);
|
|
242
|
+
controller.markSaved();
|
|
243
|
+
showToast(t('common.saved', { name }));
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
ctx.logger.log('error', `csv save-as failed: ${String(error)}`);
|
|
247
|
+
showToast(t('common.saveFailed'));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Undo/redo shortcuts while editing (shadow-root local, disposed with us).
|
|
251
|
+
on(frame, 'keydown', (e) => {
|
|
252
|
+
if (!(e.ctrlKey || e.metaKey) || e.key.toLowerCase() !== 'z')
|
|
253
|
+
return;
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
controller.dispatch({ type: e.shiftKey ? 'redo' : 'undo' });
|
|
256
|
+
});
|
|
257
|
+
const exportBtn = el('button', undefined, t('csv.exportFile'));
|
|
258
|
+
exportBtn.type = 'button';
|
|
259
|
+
exportBtn.title = t('csv.exportFile.hint');
|
|
260
|
+
exportBtn.setAttribute('aria-label', t('csv.exportFile.hint'));
|
|
261
|
+
if (save) {
|
|
262
|
+
on(exportBtn, 'click', () => void exportFile(save));
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
// Degraded mode (ADR 24): disabled with reason, never hidden.
|
|
266
|
+
exportBtn.disabled = true;
|
|
267
|
+
exportBtn.title = t('common.noFileSave');
|
|
268
|
+
}
|
|
269
|
+
async function exportFile(service) {
|
|
270
|
+
const state = controller.state;
|
|
271
|
+
const name = exportFileName(input.fileName, state.delimiter);
|
|
272
|
+
const mime = state.delimiter === '\t' ? 'text/tab-separated-values' : 'text/csv';
|
|
273
|
+
try {
|
|
274
|
+
const bytes = new TextEncoder().encode(controller.toCsv());
|
|
275
|
+
await service.saveFile(name, bytes, mime);
|
|
276
|
+
showToast(t('common.saved', { name }));
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
ctx.logger.log('error', `csv export failed: ${String(error)}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function copy(service, payload) {
|
|
283
|
+
const payloadBytes = utf8ByteLength(payload);
|
|
284
|
+
if (payloadBytes > COPY_PAYLOAD_LIMIT_BYTES) {
|
|
285
|
+
showToast(t('common.copyTooLarge', { size: payloadBytes }));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
await service.writeText(payload);
|
|
290
|
+
showToast(t('common.copied'));
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
ctx.logger.log('error', `csv copy failed: ${String(error)}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const meta = el('span', 'omni-csv__meta');
|
|
297
|
+
toolbar.append(delimiterLabel, searchInput, viewToggle, statsToggle, rowNumToggle, pageSizeLabel, undoBtn, redoBtn, addRowBtn, addColumnBtn, saveBtn, saveAsBtn, copyTsvBtn, copyJsonBtn, exportBtn, el('span', 'omni-csv__spacer'), dirtyBadge, meta);
|
|
298
|
+
const diagnosticsBar = el('div', 'omni-csv__diagnostics');
|
|
299
|
+
const body = el('div', 'omni-csv__body');
|
|
300
|
+
const footer = el('div', 'omni-csv__footer');
|
|
301
|
+
const prevBtn = el('button', undefined, '‹');
|
|
302
|
+
prevBtn.type = 'button';
|
|
303
|
+
prevBtn.setAttribute('aria-label', t('common.prevPage'));
|
|
304
|
+
const nextBtn = el('button', undefined, '›');
|
|
305
|
+
nextBtn.type = 'button';
|
|
306
|
+
nextBtn.setAttribute('aria-label', t('common.nextPage'));
|
|
307
|
+
const pageInfo = el('span', 'omni-csv__meta');
|
|
308
|
+
on(prevBtn, 'click', () => controller.dispatch({ type: 'prev-page' }));
|
|
309
|
+
on(nextBtn, 'click', () => controller.dispatch({ type: 'next-page' }));
|
|
310
|
+
footer.append(prevBtn, pageInfo, nextBtn);
|
|
311
|
+
frame.append(toolbar, diagnosticsBar, body, footer);
|
|
312
|
+
root.appendChild(frame);
|
|
313
|
+
function showToast(message) {
|
|
314
|
+
const existing = frame.querySelector('.omni-csv__toast');
|
|
315
|
+
existing?.remove();
|
|
316
|
+
const toast = el('div', 'omni-csv__toast', message);
|
|
317
|
+
frame.appendChild(toast);
|
|
318
|
+
if (toastTimer)
|
|
319
|
+
clearTimeout(toastTimer);
|
|
320
|
+
toastTimer = setTimeout(() => toast.remove(), 1500);
|
|
321
|
+
}
|
|
322
|
+
function closeContextMenu() {
|
|
323
|
+
contextMenuCleanup?.();
|
|
324
|
+
contextMenuCleanup = null;
|
|
325
|
+
}
|
|
326
|
+
/** Right-click cell menu: row insert/delete on body rows, column
|
|
327
|
+
* insert/delete wherever a data column was hit — headers included
|
|
328
|
+
* (docs §6). Either half is omitted when its target is absent
|
|
329
|
+
* (header cell → no row ops, row-number cell → no column ops). */
|
|
330
|
+
function openCellContextMenu(event, rowId, columnIndex) {
|
|
331
|
+
closeContextMenu();
|
|
332
|
+
const menu = el('div', 'omni-csv__menu');
|
|
333
|
+
menu.setAttribute('role', 'menu');
|
|
334
|
+
const item = (label, action) => {
|
|
335
|
+
const btn = el('button', undefined, label);
|
|
336
|
+
btn.type = 'button';
|
|
337
|
+
btn.setAttribute('role', 'menuitem');
|
|
338
|
+
btn.addEventListener('click', () => {
|
|
339
|
+
closeContextMenu();
|
|
340
|
+
action();
|
|
341
|
+
});
|
|
342
|
+
menu.appendChild(btn);
|
|
343
|
+
};
|
|
344
|
+
if (rowId !== null) {
|
|
345
|
+
item(t('csv.insertRowBelow'), () => controller.dispatch({ type: 'insert-row', afterRowId: rowId }));
|
|
346
|
+
item(t('csv.deleteRow'), () => controller.dispatch({ type: 'delete-row', rowId }));
|
|
347
|
+
}
|
|
348
|
+
if (columnIndex !== null) {
|
|
349
|
+
const atColumn = columnIndex;
|
|
350
|
+
item(t('csv.insertColumnLeft'), () => controller.dispatch({ type: 'insert-column', columnIndex: atColumn }));
|
|
351
|
+
item(t('csv.insertColumnRight'), () => controller.dispatch({ type: 'insert-column', columnIndex: atColumn + 1 }));
|
|
352
|
+
// The controller refuses to delete the last column; don't offer it.
|
|
353
|
+
if (controller.state.columnCount > 1) {
|
|
354
|
+
item(t('csv.deleteColumn'), () => controller.dispatch({ type: 'delete-column', columnIndex: atColumn }));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (menu.childElementCount === 0)
|
|
358
|
+
return;
|
|
359
|
+
const frameRect = frame.getBoundingClientRect();
|
|
360
|
+
menu.style.left = `${event.clientX - frameRect.left}px`;
|
|
361
|
+
menu.style.top = `${event.clientY - frameRect.top}px`;
|
|
362
|
+
frame.appendChild(menu);
|
|
363
|
+
const onOutside = (e) => {
|
|
364
|
+
// Events crossing the shadow boundary have their `target` retargeted
|
|
365
|
+
// to the host, so `menu.contains(e.target)` is unreliable here.
|
|
366
|
+
// `composedPath()` keeps the real nodes traversed through the shadow.
|
|
367
|
+
if (e.composedPath().includes(menu))
|
|
368
|
+
return;
|
|
369
|
+
closeContextMenu();
|
|
370
|
+
};
|
|
371
|
+
const onKey = (e) => {
|
|
372
|
+
if (e.key === 'Escape')
|
|
373
|
+
closeContextMenu();
|
|
374
|
+
};
|
|
375
|
+
window.addEventListener('pointerdown', onOutside, true);
|
|
376
|
+
window.addEventListener('keydown', onKey, true);
|
|
377
|
+
contextMenuCleanup = () => {
|
|
378
|
+
window.removeEventListener('pointerdown', onOutside, true);
|
|
379
|
+
window.removeEventListener('keydown', onKey, true);
|
|
380
|
+
menu.remove();
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const focusedInRoot = () => root instanceof ShadowRoot ? root.activeElement : document.activeElement;
|
|
384
|
+
/** Commit pending raw-view typing into the document (debounce flush). */
|
|
385
|
+
function commitRawEdit() {
|
|
386
|
+
if (rawEditTimer) {
|
|
387
|
+
clearTimeout(rawEditTimer);
|
|
388
|
+
rawEditTimer = null;
|
|
389
|
+
}
|
|
390
|
+
if (rawTextarea && rawTextarea.parentNode === body) {
|
|
391
|
+
controller.dispatch({ type: 'replace-document', text: rawTextarea.value });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function ensureRawTextarea() {
|
|
395
|
+
if (rawTextarea)
|
|
396
|
+
return rawTextarea;
|
|
397
|
+
const ta = el('textarea', 'omni-csv__raw omni-csv__raw-edit');
|
|
398
|
+
ta.spellcheck = false;
|
|
399
|
+
ta.setAttribute('aria-label', t('csv.rawView'));
|
|
400
|
+
// Keep typing out of the sort/undo shortcuts; the browser's own
|
|
401
|
+
// undo/redo applies inside the textarea.
|
|
402
|
+
ta.addEventListener('keydown', (e) => e.stopPropagation());
|
|
403
|
+
ta.addEventListener('input', () => {
|
|
404
|
+
if (rawEditTimer)
|
|
405
|
+
clearTimeout(rawEditTimer);
|
|
406
|
+
rawEditTimer = setTimeout(commitRawEdit, RAW_EDIT_DEBOUNCE_MS);
|
|
407
|
+
});
|
|
408
|
+
// Leaving the editor (e.g. clicking Save or Table) commits first, so
|
|
409
|
+
// the action that took the focus sees the edited document.
|
|
410
|
+
ta.addEventListener('blur', () => {
|
|
411
|
+
if (rawEditTimer)
|
|
412
|
+
commitRawEdit();
|
|
413
|
+
});
|
|
414
|
+
rawTextarea = ta;
|
|
415
|
+
return ta;
|
|
416
|
+
}
|
|
417
|
+
/** Swap a cell's content for a text input; Enter/blur commits, Esc cancels. */
|
|
418
|
+
function startInlineEdit(cell, current, commit) {
|
|
419
|
+
const inputEl = document.createElement('input');
|
|
420
|
+
inputEl.type = 'text';
|
|
421
|
+
inputEl.className = 'omni-csv__cell-input';
|
|
422
|
+
inputEl.value = current;
|
|
423
|
+
let done = false;
|
|
424
|
+
const finish = (commitValue) => {
|
|
425
|
+
if (done)
|
|
426
|
+
return;
|
|
427
|
+
done = true;
|
|
428
|
+
if (commitValue && inputEl.value !== current) {
|
|
429
|
+
commit(inputEl.value); // dispatch triggers a re-render
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
render(controller.state); // restore the original cell
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
inputEl.addEventListener('keydown', (e) => {
|
|
436
|
+
// Keep typing out of the sort/undo shortcuts.
|
|
437
|
+
e.stopPropagation();
|
|
438
|
+
if (e.key === 'Enter') {
|
|
439
|
+
e.preventDefault();
|
|
440
|
+
finish(true);
|
|
441
|
+
}
|
|
442
|
+
else if (e.key === 'Escape') {
|
|
443
|
+
e.preventDefault();
|
|
444
|
+
finish(false);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
inputEl.addEventListener('blur', () => finish(true));
|
|
448
|
+
// Editing input must not bubble a click back into sort/edit handlers.
|
|
449
|
+
inputEl.addEventListener('click', (e) => e.stopPropagation());
|
|
450
|
+
cell.replaceChildren(inputEl);
|
|
451
|
+
inputEl.focus();
|
|
452
|
+
if (typeof inputEl.select === 'function')
|
|
453
|
+
inputEl.select();
|
|
454
|
+
}
|
|
455
|
+
// --- state-driven rendering --------------------------------------------
|
|
456
|
+
function render(state) {
|
|
457
|
+
delimiterSelect.value = state.delimiter;
|
|
458
|
+
// Delimiter switching reparses and would discard edits (docs §6).
|
|
459
|
+
delimiterSelect.disabled = state.dirty;
|
|
460
|
+
if (searchInput.value !== state.search)
|
|
461
|
+
searchInput.value = state.search;
|
|
462
|
+
pageSizeSelect.value = String(state.pageSize);
|
|
463
|
+
rowNumToggle.setAttribute('aria-pressed', String(state.showRowNumbers));
|
|
464
|
+
// Editing controls surface once editing has started (docs §6) — pure
|
|
465
|
+
// readers never see them. "Add row" also shows for empty documents.
|
|
466
|
+
// "Add column" is the fallback while there is no data column target
|
|
467
|
+
// from which the column context menu could be opened.
|
|
468
|
+
const editingActive = state.dirty || state.canUndo || state.canRedo;
|
|
469
|
+
for (const btn of [undoBtn, redoBtn, saveBtn]) {
|
|
470
|
+
btn.style.display = editingActive ? '' : 'none';
|
|
471
|
+
}
|
|
472
|
+
addRowBtn.style.display =
|
|
473
|
+
editingActive || (state.rowCount === 0 && state.status === 'ok') ? '' : 'none';
|
|
474
|
+
addColumnBtn.style.display =
|
|
475
|
+
state.columnCount === 0 && state.status === 'ok' ? '' : 'none';
|
|
476
|
+
undoBtn.disabled = !state.canUndo;
|
|
477
|
+
redoBtn.disabled = !state.canRedo;
|
|
478
|
+
if (ctx.writeback)
|
|
479
|
+
saveBtn.disabled = !state.dirty;
|
|
480
|
+
dirtyBadge.textContent = state.dirty ? t('common.unsaved') : '';
|
|
481
|
+
viewToggle.textContent =
|
|
482
|
+
state.viewMode === 'table' ? t('csv.rawView') : t('csv.tableView');
|
|
483
|
+
viewToggle.setAttribute('aria-pressed', String(state.viewMode === 'raw'));
|
|
484
|
+
statsToggle.setAttribute('aria-pressed', String(state.statsVisible));
|
|
485
|
+
const hasActiveSearch = state.search.length >= CSV_MIN_SEARCH_LENGTH;
|
|
486
|
+
const rowSummary = hasActiveSearch
|
|
487
|
+
? t('common.rowsMatched', {
|
|
488
|
+
matched: state.matchedRowCount,
|
|
489
|
+
total: state.rowCount
|
|
490
|
+
})
|
|
491
|
+
: t('common.rows', { count: state.rowCount });
|
|
492
|
+
meta.textContent = `${rowSummary} · ${t('common.columns', {
|
|
493
|
+
count: state.columnCount
|
|
494
|
+
})}`;
|
|
495
|
+
diagnosticsBar.replaceChildren();
|
|
496
|
+
const hasNotices = state.diagnostics.length > 0 || state.failure !== null;
|
|
497
|
+
diagnosticsBar.style.display = hasNotices ? '' : 'none';
|
|
498
|
+
if (state.failure) {
|
|
499
|
+
// Failed parses must surface their reason — never a bare empty
|
|
500
|
+
// table (docs/viewers/csv.md §2).
|
|
501
|
+
diagnosticsBar.appendChild(el('div', 'omni-csv__diag-error', t(state.failure.messageKey, state.failure.args)));
|
|
502
|
+
}
|
|
503
|
+
for (const diag of state.diagnostics) {
|
|
504
|
+
diagnosticsBar.appendChild(el('div', undefined, t(diag.messageKey, diag.args)));
|
|
505
|
+
}
|
|
506
|
+
const rawEditable = state.viewMode === 'raw' && state.status === 'ok';
|
|
507
|
+
if (rawEditable && rawTextarea && rawTextarea.parentNode === body) {
|
|
508
|
+
// The raw editor is already mounted — this render came from its own
|
|
509
|
+
// debounced commit or a toolbar action. Keep the node so focus and
|
|
510
|
+
// caret survive; refresh its content only when the change came from
|
|
511
|
+
// elsewhere (undo/redo, table edits), never mid-typing.
|
|
512
|
+
if (focusedInRoot() !== rawTextarea) {
|
|
513
|
+
rawTextarea.value = controller.toRawText();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
body.replaceChildren();
|
|
518
|
+
if (state.viewMode === 'raw') {
|
|
519
|
+
if (rawEditable) {
|
|
520
|
+
const ta = ensureRawTextarea();
|
|
521
|
+
ta.value = controller.toRawText();
|
|
522
|
+
body.appendChild(ta);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
// partial/failed documents stay a read-only source view
|
|
526
|
+
// (docs §6 — a truncated document must not be editable).
|
|
527
|
+
body.appendChild(el('pre', 'omni-csv__raw', controller.rawText));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else if (state.columnCount === 0) {
|
|
531
|
+
body.appendChild(el('div', 'omni-csv__empty', t('csv.empty')));
|
|
532
|
+
}
|
|
533
|
+
else if (state.rowCount > 0 &&
|
|
534
|
+
hasActiveSearch &&
|
|
535
|
+
state.matchedRowCount === 0) {
|
|
536
|
+
body.appendChild(el('div', 'omni-csv__empty', t('csv.noMatches')));
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
const table = renderTable(state);
|
|
540
|
+
body.appendChild(table);
|
|
541
|
+
// A header-only document is still editable: keep the headers visible
|
|
542
|
+
// so rename and column context-menu actions remain reachable.
|
|
543
|
+
if (state.rowCount === 0) {
|
|
544
|
+
body.appendChild(el('div', 'omni-csv__empty', t('csv.empty')));
|
|
545
|
+
}
|
|
546
|
+
if (state.statsVisible) {
|
|
547
|
+
// The stats row sticks right below the header row; its offset
|
|
548
|
+
// is the measured header height (0 in JSDOM — harmless).
|
|
549
|
+
const headRow = table.querySelector('thead tr');
|
|
550
|
+
const headHeight = headRow?.offsetHeight ?? 0;
|
|
551
|
+
table.style.setProperty('--omni-csv-head-h', `${headHeight}px`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const showPager = state.pageCount > 1 && state.viewMode === 'table';
|
|
556
|
+
footer.style.display = showPager ? '' : 'none';
|
|
557
|
+
if (showPager) {
|
|
558
|
+
pageInfo.textContent = t('common.page', {
|
|
559
|
+
page: state.page + 1,
|
|
560
|
+
pages: state.pageCount
|
|
561
|
+
});
|
|
562
|
+
prevBtn.disabled = state.page === 0;
|
|
563
|
+
nextBtn.disabled = state.page >= state.pageCount - 1;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function renderTable(state) {
|
|
567
|
+
const table = el('table', 'omni-csv__table');
|
|
568
|
+
// Session column widths survive re-renders; a column-count change
|
|
569
|
+
// (delimiter switch) invalidates them.
|
|
570
|
+
if (columnWidths && columnWidths.length !== state.columnCount) {
|
|
571
|
+
columnWidths = null;
|
|
572
|
+
}
|
|
573
|
+
const editable = state.status === 'ok';
|
|
574
|
+
const colgroup = el('colgroup');
|
|
575
|
+
if (state.showRowNumbers)
|
|
576
|
+
colgroup.appendChild(el('col'));
|
|
577
|
+
const dataCols = [];
|
|
578
|
+
for (let c = 0; c < state.columnCount; c++) {
|
|
579
|
+
const col = el('col');
|
|
580
|
+
const width = columnWidths?.[c];
|
|
581
|
+
if (width != null)
|
|
582
|
+
col.style.width = `${width}px`;
|
|
583
|
+
dataCols.push(col);
|
|
584
|
+
colgroup.appendChild(col);
|
|
585
|
+
}
|
|
586
|
+
table.appendChild(colgroup);
|
|
587
|
+
if (columnWidths)
|
|
588
|
+
applyFixedLayout();
|
|
589
|
+
const dataThs = [];
|
|
590
|
+
/** Keep the table exactly as wide as its frozen data columns. The
|
|
591
|
+
* resize interaction then transfers width between two adjacent
|
|
592
|
+
* columns while the total table width stays constant. */
|
|
593
|
+
function applyFixedLayout() {
|
|
594
|
+
if (!columnWidths)
|
|
595
|
+
return;
|
|
596
|
+
const width = columnWidths.reduce((total, columnWidth) => total + (columnWidth ?? 0), 0);
|
|
597
|
+
table.style.tableLayout = 'fixed';
|
|
598
|
+
table.style.width = `${width}px`;
|
|
599
|
+
table.style.minWidth = '0';
|
|
600
|
+
}
|
|
601
|
+
function freezeWidths() {
|
|
602
|
+
if (columnWidths)
|
|
603
|
+
return;
|
|
604
|
+
columnWidths = dataThs.map((th) => Math.max(th.offsetWidth || FALLBACK_COLUMN_WIDTH, MIN_COLUMN_WIDTH));
|
|
605
|
+
columnWidths.forEach((w, i) => {
|
|
606
|
+
const col = dataCols[i];
|
|
607
|
+
if (col && w != null)
|
|
608
|
+
col.style.width = `${w}px`;
|
|
609
|
+
});
|
|
610
|
+
applyFixedLayout();
|
|
611
|
+
}
|
|
612
|
+
function startResize(event, columnIndex) {
|
|
613
|
+
event.preventDefault();
|
|
614
|
+
event.stopPropagation();
|
|
615
|
+
activeResizeCleanup?.();
|
|
616
|
+
freezeWidths();
|
|
617
|
+
const adjacentColumnIndex = columnIndex + 1;
|
|
618
|
+
// A handle sits on the right edge of its header, so it divides
|
|
619
|
+
// this column and the next one. There is no adjacent column on
|
|
620
|
+
// the far right edge to trade width with.
|
|
621
|
+
if (!columnWidths || adjacentColumnIndex >= columnWidths.length)
|
|
622
|
+
return;
|
|
623
|
+
const frozenWidths = columnWidths;
|
|
624
|
+
const startX = event.clientX;
|
|
625
|
+
const startWidth = frozenWidths[columnIndex] ?? FALLBACK_COLUMN_WIDTH;
|
|
626
|
+
const startAdjacentWidth = frozenWidths[adjacentColumnIndex] ?? FALLBACK_COLUMN_WIDTH;
|
|
627
|
+
const onMove = (ev) => {
|
|
628
|
+
const requestedDelta = Math.round(ev.clientX - startX);
|
|
629
|
+
const delta = Math.min(Math.max(requestedDelta, MIN_COLUMN_WIDTH - startWidth), startAdjacentWidth - MIN_COLUMN_WIDTH);
|
|
630
|
+
frozenWidths[columnIndex] = startWidth + delta;
|
|
631
|
+
frozenWidths[adjacentColumnIndex] = startAdjacentWidth - delta;
|
|
632
|
+
const col = dataCols[columnIndex];
|
|
633
|
+
const adjacentCol = dataCols[adjacentColumnIndex];
|
|
634
|
+
if (col)
|
|
635
|
+
col.style.width = `${frozenWidths[columnIndex]}px`;
|
|
636
|
+
if (adjacentCol) {
|
|
637
|
+
adjacentCol.style.width = `${frozenWidths[adjacentColumnIndex]}px`;
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
const onUp = () => activeResizeCleanup?.();
|
|
641
|
+
window.addEventListener('pointermove', onMove);
|
|
642
|
+
window.addEventListener('pointerup', onUp);
|
|
643
|
+
activeResizeCleanup = () => {
|
|
644
|
+
window.removeEventListener('pointermove', onMove);
|
|
645
|
+
window.removeEventListener('pointerup', onUp);
|
|
646
|
+
activeResizeCleanup = null;
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
const thead = el('thead');
|
|
650
|
+
const headRow = el('tr');
|
|
651
|
+
if (state.showRowNumbers) {
|
|
652
|
+
// Current document row numbers (1-based) — not sortable.
|
|
653
|
+
const numTh = el('th', 'omni-csv__rownum', '#');
|
|
654
|
+
numTh.setAttribute('aria-label', t('csv.rowNumbers'));
|
|
655
|
+
headRow.appendChild(numTh);
|
|
656
|
+
}
|
|
657
|
+
state.headers.forEach((header, columnIndex) => {
|
|
658
|
+
const th = el('th');
|
|
659
|
+
th.appendChild(el('span', undefined, header));
|
|
660
|
+
const hint = editable ? t('csv.header.hint') : t('csv.sort.hint');
|
|
661
|
+
th.title =
|
|
662
|
+
header.length > CELL_TOOLTIP_MIN_CHARS ? `${header}\n${hint}` : hint;
|
|
663
|
+
th.setAttribute('role', 'columnheader');
|
|
664
|
+
dataThs.push(th);
|
|
665
|
+
const handle = el('span', 'omni-csv__resize-handle');
|
|
666
|
+
handle.title = t('csv.resizeColumn');
|
|
667
|
+
handle.addEventListener('pointerdown', (e) => startResize(e, columnIndex));
|
|
668
|
+
// A click that follows a resize drag must not toggle the sort.
|
|
669
|
+
handle.addEventListener('click', (e) => e.stopPropagation());
|
|
670
|
+
handle.addEventListener('dblclick', (e) => {
|
|
671
|
+
e.stopPropagation();
|
|
672
|
+
if (!columnWidths)
|
|
673
|
+
return;
|
|
674
|
+
columnWidths[columnIndex] = null;
|
|
675
|
+
if (columnWidths.every((w) => w == null))
|
|
676
|
+
columnWidths = null;
|
|
677
|
+
render(controller.state);
|
|
678
|
+
});
|
|
679
|
+
th.appendChild(handle);
|
|
680
|
+
if (state.sort.columnIndex === columnIndex && state.sort.direction) {
|
|
681
|
+
th.setAttribute('aria-sort', state.sort.direction === 'asc' ? 'ascending' : 'descending');
|
|
682
|
+
th.appendChild(el('span', 'omni-csv__sort-indicator', state.sort.direction === 'asc' ? '▲' : '▼'));
|
|
683
|
+
}
|
|
684
|
+
// Per-render elements: listeners are collected with the nodes.
|
|
685
|
+
// Single click sorts (deferred so a double-click can win); double
|
|
686
|
+
// click renames the header (docs §6).
|
|
687
|
+
th.addEventListener('click', () => {
|
|
688
|
+
if (th.querySelector('input'))
|
|
689
|
+
return;
|
|
690
|
+
if (pendingSortTimer)
|
|
691
|
+
clearTimeout(pendingSortTimer);
|
|
692
|
+
pendingSortTimer = setTimeout(() => {
|
|
693
|
+
pendingSortTimer = null;
|
|
694
|
+
controller.dispatch({ type: 'sort-column', columnIndex });
|
|
695
|
+
}, SORT_CLICK_DELAY_MS);
|
|
696
|
+
});
|
|
697
|
+
th.addEventListener('dblclick', () => {
|
|
698
|
+
if (pendingSortTimer) {
|
|
699
|
+
clearTimeout(pendingSortTimer);
|
|
700
|
+
pendingSortTimer = null;
|
|
701
|
+
}
|
|
702
|
+
if (th.querySelector('input'))
|
|
703
|
+
return;
|
|
704
|
+
if (!editable) {
|
|
705
|
+
showToast(t('csv.editUnavailable'));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
startInlineEdit(th, controller.state.headers[columnIndex] ?? '', (value) => controller.dispatch({ type: 'edit-header', columnIndex, value }));
|
|
709
|
+
});
|
|
710
|
+
if (editable) {
|
|
711
|
+
th.addEventListener('contextmenu', (e) => {
|
|
712
|
+
e.preventDefault();
|
|
713
|
+
openCellContextMenu(e, null, columnIndex);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
headRow.appendChild(th);
|
|
717
|
+
});
|
|
718
|
+
thead.appendChild(headRow);
|
|
719
|
+
if (state.statsVisible) {
|
|
720
|
+
thead.appendChild(renderStatsRow(state));
|
|
721
|
+
}
|
|
722
|
+
table.appendChild(thead);
|
|
723
|
+
const tbody = el('tbody');
|
|
724
|
+
for (const rowId of state.visibleRowIds) {
|
|
725
|
+
const tr = el('tr');
|
|
726
|
+
if (editable) {
|
|
727
|
+
// Row and column ops live in a right-click menu (docs §6) —
|
|
728
|
+
// no permanent ops column cluttering the table.
|
|
729
|
+
tr.addEventListener('contextmenu', (e) => {
|
|
730
|
+
e.preventDefault();
|
|
731
|
+
const cell = e.target?.closest?.('td');
|
|
732
|
+
// Hitting the row-number gutter (or the tr itself in
|
|
733
|
+
// synthetic events) offers row ops only.
|
|
734
|
+
const columnIndex = cell && !cell.classList.contains('omni-csv__rownum')
|
|
735
|
+
? cell.cellIndex - (state.showRowNumbers ? 1 : 0)
|
|
736
|
+
: null;
|
|
737
|
+
openCellContextMenu(e, rowId, columnIndex);
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
if (state.showRowNumbers) {
|
|
741
|
+
tr.appendChild(el('td', 'omni-csv__rownum', String(controller.getRowNumber(rowId))));
|
|
742
|
+
}
|
|
743
|
+
const cells = controller.getRowById(rowId);
|
|
744
|
+
for (let c = 0; c < state.columnCount; c++) {
|
|
745
|
+
const columnIndex = c;
|
|
746
|
+
const text = cells[c] ?? '';
|
|
747
|
+
const td = el('td', undefined, text);
|
|
748
|
+
// Truncated long cells reveal their full content on hover.
|
|
749
|
+
if (text.length > CELL_TOOLTIP_MIN_CHARS || text.includes('\n')) {
|
|
750
|
+
td.title = text;
|
|
751
|
+
}
|
|
752
|
+
td.addEventListener('dblclick', () => {
|
|
753
|
+
if (td.querySelector('input'))
|
|
754
|
+
return;
|
|
755
|
+
if (!editable) {
|
|
756
|
+
showToast(t('csv.editUnavailable'));
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
startInlineEdit(td, controller.getRowById(rowId)[columnIndex] ?? '', (value) => controller.dispatch({
|
|
760
|
+
type: 'edit-cell',
|
|
761
|
+
rowId,
|
|
762
|
+
columnIndex,
|
|
763
|
+
value
|
|
764
|
+
}));
|
|
765
|
+
});
|
|
766
|
+
tr.appendChild(td);
|
|
767
|
+
}
|
|
768
|
+
tbody.appendChild(tr);
|
|
769
|
+
}
|
|
770
|
+
table.appendChild(tbody);
|
|
771
|
+
return table;
|
|
772
|
+
}
|
|
773
|
+
/** Second sticky thead row: per-column compact stats, details in the
|
|
774
|
+
* tooltip. Stats cover the whole document regardless of the search
|
|
775
|
+
* filter (docs §2). */
|
|
776
|
+
function renderStatsRow(state) {
|
|
777
|
+
const stats = controller.getStatistics();
|
|
778
|
+
const row = el('tr', 'omni-csv__stats-row');
|
|
779
|
+
if (state.showRowNumbers) {
|
|
780
|
+
row.appendChild(el('th', 'omni-csv__rownum', ''));
|
|
781
|
+
}
|
|
782
|
+
for (let c = 0; c < state.columnCount; c++) {
|
|
783
|
+
const stat = stats[c];
|
|
784
|
+
const cell = el('th');
|
|
785
|
+
if (!stat) {
|
|
786
|
+
row.appendChild(cell);
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const parts = [t('csv.stats.compact.count', { count: stat.count })];
|
|
790
|
+
if (stat.nullCount > 0) {
|
|
791
|
+
parts.push(t('csv.stats.compact.nulls', {
|
|
792
|
+
percent: formatPercent(stat.nullPercent)
|
|
793
|
+
}));
|
|
794
|
+
}
|
|
795
|
+
if (stat.numeric) {
|
|
796
|
+
parts.push(t('csv.stats.compact.numeric', {
|
|
797
|
+
mean: formatStatNumber(stat.numeric.mean),
|
|
798
|
+
min: formatStatNumber(stat.numeric.min),
|
|
799
|
+
max: formatStatNumber(stat.numeric.max)
|
|
800
|
+
}));
|
|
801
|
+
}
|
|
802
|
+
cell.textContent = parts.join(' · ');
|
|
803
|
+
const details = [
|
|
804
|
+
`${t('csv.stats.count')} ${stat.count} / ${stat.total}`,
|
|
805
|
+
`${t('csv.stats.nulls')} ${stat.nullCount} (${formatPercent(stat.nullPercent)})`
|
|
806
|
+
];
|
|
807
|
+
if (stat.numeric) {
|
|
808
|
+
details.push(`${t('csv.stats.mean')} ${formatStatNumber(stat.numeric.mean)}`, `${t('csv.stats.min')} ${formatStatNumber(stat.numeric.min)}`, `${t('csv.stats.max')} ${formatStatNumber(stat.numeric.max)}`);
|
|
809
|
+
}
|
|
810
|
+
cell.title = details.join(' · ');
|
|
811
|
+
row.appendChild(cell);
|
|
812
|
+
}
|
|
813
|
+
return row;
|
|
814
|
+
}
|
|
815
|
+
const unsubscribe = controller.subscribe(render);
|
|
816
|
+
render(controller.state);
|
|
817
|
+
if (options.signal?.aborted) {
|
|
818
|
+
cleanup();
|
|
819
|
+
throw new MountAbortedError();
|
|
820
|
+
}
|
|
821
|
+
function cleanup() {
|
|
822
|
+
unsubscribe();
|
|
823
|
+
for (const dispose of disposers)
|
|
824
|
+
dispose();
|
|
825
|
+
disposers.length = 0;
|
|
826
|
+
activeResizeCleanup?.();
|
|
827
|
+
closeContextMenu();
|
|
828
|
+
if (pendingSortTimer)
|
|
829
|
+
clearTimeout(pendingSortTimer);
|
|
830
|
+
if (rawEditTimer)
|
|
831
|
+
clearTimeout(rawEditTimer);
|
|
832
|
+
if (toastTimer)
|
|
833
|
+
clearTimeout(toastTimer);
|
|
834
|
+
if (root instanceof ShadowRoot) {
|
|
835
|
+
root.replaceChildren();
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
root.replaceChildren();
|
|
839
|
+
root.classList.remove(VIEWER_ROOT_CLASS, 'omni-viewer--csv');
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
dispose: cleanup,
|
|
844
|
+
isDirty: () => controller.state.dirty
|
|
845
|
+
};
|
|
846
|
+
}
|