├── .yarnrc.yml
├── style
├── index.js
├── index.css
├── icons
│ ├── mdi-page-layout-header.svg
│ ├── mdi-table-column-plus-after.svg
│ ├── mdi-table-row-plus-after.svg
│ ├── mdi-format-list-bulleted-type.svg
│ ├── mdi-table-column-width.svg
│ ├── mdi-format-list-numbered.svg
│ ├── mdi-table-column-remove.svg
│ ├── mdi-table-row-remove.svg
│ ├── mdi-snowflake.svg
│ ├── mdi-snowflake-off.svg
│ └── LICENSE
└── base.css
├── setup.py
├── .binder
├── postBuild
└── environment.yml
├── src
├── typings.d.ts
├── icons.ts
├── statusbar.tsx
├── documentwidget.ts
├── searchprovider.ts
├── index.ts
└── widget.ts
├── screenshots
├── launcher.png
├── setosa-demo.gif
├── formula-support.gif
└── freeze-support.gif
├── .prettierignore
├── install.json
├── .github
└── workflows
│ ├── enforce-label.yml
│ ├── check-release.yml
│ ├── binder-on-pr.yml
│ ├── publish.yml
│ ├── prep-release.yml
│ ├── publish-release.yml
│ ├── build.yml
│ └── codeql-analysis.yml
├── .copier-answers.yml
├── tsconfig.json
├── jupyterlab_spreadsheet_editor
└── __init__.py
├── LICENSE
├── .gitignore
├── schema
└── plugin.json
├── CHANGELOG.md
├── pyproject.toml
├── RELEASE.md
├── README.md
└── package.json
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/style/index.js:
--------------------------------------------------------------------------------
1 | import './base.css';
2 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | __import__("setuptools").setup()
2 |
--------------------------------------------------------------------------------
/style/index.css:
--------------------------------------------------------------------------------
1 | @import url('base.css');
2 |
--------------------------------------------------------------------------------
/.binder/postBuild:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 |
4 | python -m pip install .
5 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | const script: string;
3 | export default script;
4 | }
5 |
--------------------------------------------------------------------------------
/screenshots/launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor/HEAD/screenshots/launcher.png
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | **/node_modules
3 | **/lib
4 | **/package.json
5 | !/package.json
6 | jupyterlab_spreadsheet_editor
7 |
--------------------------------------------------------------------------------
/screenshots/setosa-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor/HEAD/screenshots/setosa-demo.gif
--------------------------------------------------------------------------------
/screenshots/formula-support.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor/HEAD/screenshots/formula-support.gif
--------------------------------------------------------------------------------
/screenshots/freeze-support.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor/HEAD/screenshots/freeze-support.gif
--------------------------------------------------------------------------------
/.binder/environment.yml:
--------------------------------------------------------------------------------
1 | name: jupyterlab-spreadsheet-editor
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - python=3.11
6 | - jupyterlab >=4,<5.0.0a0
7 | - nodejs=20
8 | - pip
9 |
--------------------------------------------------------------------------------
/install.json:
--------------------------------------------------------------------------------
1 | {
2 | "packageManager": "python",
3 | "packageName": "jupyterlab-spreadsheet-editor",
4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab-spreadsheet-editor"
5 | }
6 |
--------------------------------------------------------------------------------
/style/icons/mdi-page-layout-header.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/style/icons/mdi-table-column-plus-after.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/style/icons/mdi-table-row-plus-after.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.github/workflows/enforce-label.yml:
--------------------------------------------------------------------------------
1 | name: Enforce PR label
2 |
3 | on:
4 | pull_request:
5 | types: [labeled, unlabeled, opened, edited, synchronize]
6 | jobs:
7 | enforce-label:
8 | runs-on: ubuntu-latest
9 | permissions:
10 | pull-requests: write
11 | steps:
12 | - name: enforce-triage-label
13 | uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1
14 |
--------------------------------------------------------------------------------
/style/icons/mdi-format-list-bulleted-type.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/style/icons/mdi-table-column-width.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/style/icons/mdi-format-list-numbered.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/style/icons/mdi-table-column-remove.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/style/icons/mdi-table-row-remove.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.copier-answers.yml:
--------------------------------------------------------------------------------
1 | # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2 | _commit: v4.2.4
3 | _src_path: https://github.com/jupyterlab/extension-template
4 | author_email: ''
5 | author_name: Michał Krassowski
6 | has_binder: false
7 | has_settings: true
8 | kind: frontend
9 | labextension_name: spreadsheet-editor
10 | project_short_description: JupyterLab spreadsheet (csv/tsv) editor
11 | python_name: jupyterlab_spreadsheet_editor
12 | repository: https://github.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor.git
13 | test: false
14 |
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "composite": true,
5 | "declaration": true,
6 | "esModuleInterop": true,
7 | "incremental": true,
8 | "jsx": "react",
9 | "module": "esnext",
10 | "moduleResolution": "node",
11 | "noEmitOnError": true,
12 | "noImplicitAny": true,
13 | "noUnusedLocals": true,
14 | "preserveWatchOutput": true,
15 | "resolveJsonModule": true,
16 | "outDir": "lib",
17 | "rootDir": "src",
18 | "strict": true,
19 | "strictNullChecks": true,
20 | "target": "ES2018"
21 | },
22 | "include": ["src/*"]
23 | }
24 |
--------------------------------------------------------------------------------
/jupyterlab_spreadsheet_editor/__init__.py:
--------------------------------------------------------------------------------
1 | try:
2 | from ._version import __version__
3 | except ImportError:
4 | # Fallback when using the package in dev mode without installing
5 | # in editable mode with pip. It is highly recommended to install
6 | # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs
7 | import warnings
8 | warnings.warn("Importing 'jupyterlab_spreadsheet_editor' outside a proper installation.")
9 | __version__ = "dev"
10 |
11 |
12 | def _jupyter_labextension_paths():
13 | return [{
14 | "src": "labextension",
15 | "dest": "spreadsheet-editor"
16 | }]
17 |
--------------------------------------------------------------------------------
/style/icons/mdi-snowflake.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/style/icons/mdi-snowflake-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.github/workflows/check-release.yml:
--------------------------------------------------------------------------------
1 | name: Check Release
2 | on:
3 | push:
4 | branches: ["main"]
5 | pull_request:
6 | branches: ["*"]
7 |
8 | jobs:
9 | check_release:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v3
14 | - name: Base Setup
15 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
16 | - name: Check Release
17 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2
18 | with:
19 |
20 | token: ${{ secrets.GITHUB_TOKEN }}
21 |
22 | - name: Upload Distributions
23 | uses: actions/upload-artifact@v3
24 | with:
25 | name: jupyterlab_spreadsheet_editor-releaser-dist-${{ github.run_number }}
26 | path: .jupyter_releaser_checkout/dist
27 |
--------------------------------------------------------------------------------
/.github/workflows/binder-on-pr.yml:
--------------------------------------------------------------------------------
1 | # Copyright (c) fcollonval
2 | # Distributed under the terms of the Modified BSD License.
3 | # Reference https://mybinder.readthedocs.io/en/latest/howto/gh-actions-badges.html
4 | name: Binder Badge
5 | on:
6 | pull_request_target:
7 | types: [opened]
8 |
9 | jobs:
10 | binder:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: comment on PR with Binder link
14 | uses: actions/github-script@v1
15 | with:
16 | github-token: ${{secrets.GITHUB_TOKEN}}
17 | script: |
18 | var PR_HEAD_USERREPO = process.env.PR_HEAD_USERREPO;
19 | var PR_HEAD_REF = process.env.PR_HEAD_REF;
20 | github.issues.createComment({
21 | issue_number: context.issue.number,
22 | owner: context.repo.owner,
23 | repo: context.repo.repo,
24 | body: `[](https://mybinder.org/v2/gh/${PR_HEAD_USERREPO}/${PR_HEAD_REF}?urlpath=lab) :point_left: Launch a binder notebook on branch _${PR_HEAD_USERREPO}/${PR_HEAD_REF}_`
25 | })
26 | env:
27 | PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
28 | PR_HEAD_USERREPO: ${{ github.event.pull_request.head.repo.full_name }}
29 |
30 |
--------------------------------------------------------------------------------
/src/icons.ts:
--------------------------------------------------------------------------------
1 | import { LabIcon } from '@jupyterlab/ui-components';
2 |
3 | import removeRowSvg from '../style/icons/mdi-table-row-remove.svg';
4 | import addColumnSvg from '../style/icons/mdi-table-column-plus-after.svg';
5 | import removeColumnSvg from '../style/icons/mdi-table-column-remove.svg';
6 | import freezeColumnSvg from '../style/icons/mdi-snowflake.svg';
7 | import unfreezeColumnSvg from '../style/icons/mdi-snowflake-off.svg';
8 | import addRowSvg from '../style/icons/mdi-table-row-plus-after.svg';
9 |
10 | export const freezeColumnIcon = new LabIcon({
11 | name: 'spreadsheet:freeze-columns',
12 | svgstr: freezeColumnSvg
13 | });
14 |
15 | export const unfreezeColumnIcon = new LabIcon({
16 | name: 'spreadsheet:unfreeze-columns',
17 | svgstr: unfreezeColumnSvg
18 | });
19 |
20 | export const removeColumnIcon = new LabIcon({
21 | name: 'spreadsheet:remove-column',
22 | svgstr: removeColumnSvg
23 | });
24 |
25 | export const addColumnIcon = new LabIcon({
26 | name: 'spreadsheet:add-column',
27 | svgstr: addColumnSvg
28 | });
29 |
30 | export const removeRowIcon = new LabIcon({
31 | name: 'spreadsheet:remove-row',
32 | svgstr: removeRowSvg
33 | });
34 |
35 | export const addRowIcon = new LabIcon({
36 | name: 'spreadsheet:add-row',
37 | svgstr: addRowSvg
38 | });
39 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Package
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: Install node
14 | uses: actions/setup-node@v1
15 | with:
16 | node-version: '14.x'
17 | registry-url: 'https://registry.npmjs.org'
18 | - name: Install Python
19 | uses: actions/setup-python@v2
20 | with:
21 | python-version: '3.x'
22 | - name: Install dependencies
23 | run: |
24 | python -m pip install --upgrade pip build twine jupyterlab
25 | - name: Build the federated extension in a separated build environment
26 | run: |
27 | python -m build
28 | - name: Publish the Python package
29 | env:
30 | TWINE_USERNAME: __token__
31 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
32 | run: |
33 | twine upload dist/*
34 | - name: Build the source extension
35 | run: |
36 | jlpm
37 | jlpm build
38 | - name: Publish the NPM package
39 | run: |
40 | echo $PRE_RELEASE
41 | if [[ $PRE_RELEASE == "true" ]]; then export TAG="next"; else export TAG="latest"; fi
42 | npm publish --tag ${TAG} --access public
43 | env:
44 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
45 | PRE_RELEASE: ${{ github.event.release.prerelease }}
46 |
47 |
--------------------------------------------------------------------------------
/.github/workflows/prep-release.yml:
--------------------------------------------------------------------------------
1 | name: "Step 1: Prep Release"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | version_spec:
6 | description: "New Version Specifier"
7 | default: "next"
8 | required: false
9 | branch:
10 | description: "The branch to target"
11 | required: false
12 | post_version_spec:
13 | description: "Post Version Specifier"
14 | required: false
15 | since:
16 | description: "Use PRs with activity since this date or git reference"
17 | required: false
18 | since_last_stable:
19 | description: "Use PRs with activity since the last stable git tag"
20 | required: false
21 | type: boolean
22 | jobs:
23 | prep_release:
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
27 |
28 | - name: Prep Release
29 | id: prep-release
30 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2
31 | with:
32 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }}
33 | version_spec: ${{ github.event.inputs.version_spec }}
34 | post_version_spec: ${{ github.event.inputs.post_version_spec }}
35 | branch: ${{ github.event.inputs.branch }}
36 | since: ${{ github.event.inputs.since }}
37 | since_last_stable: ${{ github.event.inputs.since_last_stable }}
38 |
39 | - name: "** Next Step **"
40 | run: |
41 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}"
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2020, Michał Krassowski All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | * Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | * Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | * Neither the name of the copyright holder nor the names of its
16 | contributors may be used to endorse or promote products derived from
17 | this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/.github/workflows/publish-release.yml:
--------------------------------------------------------------------------------
1 | name: "Step 2: Publish Release"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | branch:
6 | description: "The target branch"
7 | required: false
8 | release_url:
9 | description: "The URL of the draft GitHub release"
10 | required: false
11 | steps_to_skip:
12 | description: "Comma separated list of steps to skip"
13 | required: false
14 |
15 | jobs:
16 | publish_release:
17 | runs-on: ubuntu-latest
18 | permissions:
19 | # This is useful if you want to use PyPI trusted publisher
20 | # and NPM provenance
21 | id-token: write
22 | steps:
23 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
24 |
25 | - name: Populate Release
26 | id: populate-release
27 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2
28 | with:
29 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }}
30 | branch: ${{ github.event.inputs.branch }}
31 | release_url: ${{ github.event.inputs.release_url }}
32 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }}
33 |
34 | - name: Finalize Release
35 | id: finalize-release
36 | env:
37 | # The following are needed if you use legacy PyPI set up
38 | # PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
39 | # PYPI_TOKEN_MAP: ${{ secrets.PYPI_TOKEN_MAP }}
40 | # TWINE_USERNAME: __token__
41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
42 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2
43 | with:
44 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }}
45 | release_url: ${{ steps.populate-release.outputs.release_url }}
46 |
47 | - name: "** Next Step **"
48 | if: ${{ success() }}
49 | run: |
50 | echo "Verify the final release"
51 | echo ${{ steps.finalize-release.outputs.release_url }}
52 |
53 | - name: "** Failure Message **"
54 | if: ${{ failure() }}
55 | run: |
56 | echo "Failed to Publish the Draft Release Url:"
57 | echo ${{ steps.populate-release.outputs.release_url }}
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.bundle.*
2 | lib/
3 | node_modules/
4 | *.log
5 | .eslintcache
6 | .stylelintcache
7 | *.egg-info/
8 | .ipynb_checkpoints
9 | *.tsbuildinfo
10 | jupyterlab_spreadsheet_editor/labextension
11 | # Version file is handled by hatchling
12 | jupyterlab_spreadsheet_editor/_version.py
13 |
14 | # Created by https://www.gitignore.io/api/python
15 | # Edit at https://www.gitignore.io/?templates=python
16 |
17 | ### Python ###
18 | # Byte-compiled / optimized / DLL files
19 | __pycache__/
20 | *.py[cod]
21 | *$py.class
22 |
23 | # C extensions
24 | *.so
25 |
26 | # Distribution / packaging
27 | .Python
28 | build/
29 | develop-eggs/
30 | dist/
31 | downloads/
32 | eggs/
33 | .eggs/
34 | lib/
35 | lib64/
36 | parts/
37 | sdist/
38 | var/
39 | wheels/
40 | pip-wheel-metadata/
41 | share/python-wheels/
42 | .installed.cfg
43 | *.egg
44 | MANIFEST
45 |
46 | # PyInstaller
47 | # Usually these files are written by a python script from a template
48 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
49 | *.manifest
50 | *.spec
51 |
52 | # Installer logs
53 | pip-log.txt
54 | pip-delete-this-directory.txt
55 |
56 | # Unit test / coverage reports
57 | htmlcov/
58 | .tox/
59 | .nox/
60 | .coverage
61 | .coverage.*
62 | .cache
63 | nosetests.xml
64 | coverage/
65 | coverage.xml
66 | *.cover
67 | .hypothesis/
68 | .pytest_cache/
69 |
70 | # Translations
71 | *.mo
72 | *.pot
73 |
74 | # Scrapy stuff:
75 | .scrapy
76 |
77 | # Sphinx documentation
78 | docs/_build/
79 |
80 | # PyBuilder
81 | target/
82 |
83 | # pyenv
84 | .python-version
85 |
86 | # celery beat schedule file
87 | celerybeat-schedule
88 |
89 | # SageMath parsed files
90 | *.sage.py
91 |
92 | # Spyder project settings
93 | .spyderproject
94 | .spyproject
95 |
96 | # Rope project settings
97 | .ropeproject
98 |
99 | # Mr Developer
100 | .mr.developer.cfg
101 | .project
102 | .pydevproject
103 |
104 | # mkdocs documentation
105 | /site
106 |
107 | # mypy
108 | .mypy_cache/
109 | .dmypy.json
110 | dmypy.json
111 |
112 | # Pyre type checker
113 | .pyre/
114 |
115 | # End of https://www.gitignore.io/api/python
116 |
117 | # OSX files
118 | .DS_Store
119 |
120 | # Yarn cache
121 | .yarn/
122 |
--------------------------------------------------------------------------------
/schema/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "jupyter.lab.shortcuts": [],
3 | "title": "JupyterLab Spreadsheet Editor",
4 | "description": "JupyterLab spreadsheet editor settings.",
5 | "jupyter.lab.menus": {
6 | "context": [
7 | {
8 | "command": "spreadsheet-editor:copy",
9 | "selector": ".se-area-container .jexcel",
10 | "rank": 0
11 | },
12 | {
13 | "command": "spreadsheet-editor:paste",
14 | "selector": ".se-area-container .jexcel",
15 | "rank": 1
16 | },
17 | {
18 | "type": "separator",
19 | "selector": ".se-area-container .jexcel",
20 | "rank": 2
21 | },
22 | {
23 | "command": "spreadsheet-editor:undo",
24 | "selector": ".se-area-container",
25 | "rank": 3
26 | },
27 | {
28 | "command": "spreadsheet-editor:redo",
29 | "selector": ".se-area-container",
30 | "rank": 4
31 | },
32 | {
33 | "type": "separator",
34 | "selector": ".se-area-container .jexcel td",
35 | "rank": 5
36 | },
37 | {
38 | "command": "spreadsheet-editor:insert-row-above",
39 | "selector": ".se-area-container .jexcel td[data-y]",
40 | "rank": 6
41 | },
42 | {
43 | "command": "spreadsheet-editor:insert-row-below",
44 | "selector": ".se-area-container .jexcel td[data-y]",
45 | "rank": 7
46 | },
47 | {
48 | "command": "spreadsheet-editor:insert-column-left",
49 | "selector": ".se-area-container .jexcel td[data-x]",
50 | "rank": 8
51 | },
52 | {
53 | "command": "spreadsheet-editor:insert-column-right",
54 | "selector": ".se-area-container .jexcel td[data-x]",
55 | "rank": 9
56 | },
57 | {
58 | "type": "separator",
59 | "selector": ".se-area-container .jexcel td",
60 | "rank": 10
61 | },
62 | {
63 | "command": "spreadsheet-editor:remove-column",
64 | "selector": ".se-area-container .jexcel td[data-x]",
65 | "rank": 11
66 | },
67 | {
68 | "command": "spreadsheet-editor:remove-row",
69 | "selector": ".se-area-container .jexcel td[data-y]",
70 | "rank": 12
71 | }
72 | ]
73 | },
74 | "type": "object",
75 | "properties": {},
76 | "additionalProperties": false
77 | }
78 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: main
6 | pull_request:
7 | branches: '*'
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v3
16 |
17 | - name: Base Setup
18 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
19 |
20 | - name: Install dependencies
21 | run: python -m pip install -U "jupyterlab>=4.0.0,<5"
22 |
23 | - name: Lint the extension
24 | run: |
25 | set -eux
26 | jlpm
27 | jlpm run lint:check
28 |
29 | - name: Build the extension
30 | run: |
31 | set -eux
32 | python -m pip install .[test]
33 |
34 | jupyter labextension list
35 | jupyter labextension list 2>&1 | grep -ie "spreadsheet-editor.*OK"
36 | python -m jupyterlab.browser_check
37 |
38 | - name: Package the extension
39 | run: |
40 | set -eux
41 |
42 | pip install build
43 | python -m build
44 | pip uninstall -y "jupyterlab_spreadsheet_editor" jupyterlab
45 |
46 | - name: Upload extension packages
47 | uses: actions/upload-artifact@v3
48 | with:
49 | name: extension-artifacts
50 | path: dist/jupyterlab_spreadsheet_editor*
51 | if-no-files-found: error
52 |
53 | test_isolated:
54 | needs: build
55 | runs-on: ubuntu-latest
56 |
57 | steps:
58 | - name: Install Python
59 | uses: actions/setup-python@v4
60 | with:
61 | python-version: '3.9'
62 | architecture: 'x64'
63 | - uses: actions/download-artifact@v3
64 | with:
65 | name: extension-artifacts
66 | - name: Install and Test
67 | run: |
68 | set -eux
69 | # Remove NodeJS, twice to take care of system and locally installed node versions.
70 | sudo rm -rf $(which node)
71 | sudo rm -rf $(which node)
72 |
73 | pip install "jupyterlab>=4.0.0,<5" jupyterlab_spreadsheet_editor*.whl
74 |
75 |
76 | jupyter labextension list
77 | jupyter labextension list 2>&1 | grep -ie "spreadsheet-editor.*OK"
78 | python -m jupyterlab.browser_check --no-browser-test
79 |
80 |
81 | check_links:
82 | name: Check Links
83 | runs-on: ubuntu-latest
84 | timeout-minutes: 15
85 | steps:
86 | - uses: actions/checkout@v3
87 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
88 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1
89 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 |
4 |
5 | ## 0.7.2
6 |
7 | ([Full Changelog](https://github.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor/compare/v0.7.1...5743dc620db86b1caa7edaabc3823dd0150f7120))
8 |
9 | ### Bugs fixed
10 |
11 | - Disable conflicting JSpreadsheet export [#71](https://github.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor/pull/71) ([@renan-r-santos](https://github.com/renan-r-santos))
12 |
13 | ### Maintenance and upkeep improvements
14 |
15 | - Update binder environment.yml [#67](https://github.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor/pull/67) ([@manics](https://github.com/manics))
16 |
17 | ### Contributors to this release
18 |
19 | ([GitHub contributors page for this release](https://github.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor/graphs/contributors?from=2024-03-16&to=2024-07-22&type=c))
20 |
21 | [@github-actions](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-spreadsheet-editor+involves%3Agithub-actions+updated%3A2024-03-16..2024-07-22&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-spreadsheet-editor+involves%3Amanics+updated%3A2024-03-16..2024-07-22&type=Issues) | [@renan-r-santos](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-spreadsheet-editor+involves%3Arenan-r-santos+updated%3A2024-03-16..2024-07-22&type=Issues)
22 |
23 |
24 |
25 | ## 0.7.1
26 |
27 | ([Full Changelog](https://github.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor/compare/v0.6.1...06a2d2b0772ecf4308b3dc48fb13a7710e6de924))
28 |
29 | ### Maintenance and upkeep improvements
30 |
31 | - Port to JupyterLab 4 [#64](https://github.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor/pull/64) ([@krassowski](https://github.com/krassowski))
32 |
33 | ### Other merged PRs
34 |
35 | - Update name of the default branch in readme and workflows [#55](https://github.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor/pull/55) ([@krassowski](https://github.com/krassowski))
36 |
37 | ### Contributors to this release
38 |
39 | ([GitHub contributors page for this release](https://github.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor/graphs/contributors?from=2021-09-25&to=2024-03-16&type=c))
40 |
41 | [@github-actions](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-spreadsheet-editor+involves%3Agithub-actions+updated%3A2021-09-25..2024-03-16&type=Issues) | [@krassowski](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-spreadsheet-editor+involves%3Akrassowski+updated%3A2021-09-25..2024-03-16&type=Issues)
42 |
--------------------------------------------------------------------------------
/src/statusbar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { VDomModel, VDomRenderer } from '@jupyterlab/apputils';
4 | import {
5 | ITranslator,
6 | nullTranslator,
7 | TranslationBundle
8 | } from '@jupyterlab/translation';
9 | import { TextItem } from '@jupyterlab/statusbar';
10 | import { ISelection, SpreadsheetWidget } from './widget';
11 |
12 | /**
13 | * StatusBar item to display selection span.
14 | */
15 | export class SelectionStatus extends VDomRenderer {
16 | constructor(translator?: ITranslator) {
17 | super(new SelectionStatus.Model());
18 | this.translator = translator || nullTranslator;
19 | this._trans = this.translator.load('spreadsheet-editor');
20 | }
21 |
22 | render() {
23 | if (!this.model) {
24 | return null;
25 | }
26 | const selection = this.model.selection;
27 | if (!selection) {
28 | return ;
29 | }
30 | // if only one cell (or zero cells) is selected, do not show anything
31 | if (selection.rows <= 1 && selection.columns <= 1) {
32 | return ;
33 | }
34 | this.node.title =
35 | this._trans._n('Selected %1 row', 'Selected %1 rows', selection.rows) +
36 | this._trans._n(' and %1 column', ' and %1 columns', selection.columns);
37 |
38 | const text =
39 | this._trans._n('%1 row', '%1 rows', selection.rows) +
40 | this._trans._n(', %1 column', ', %1 columns', selection.columns);
41 |
42 | return ;
43 | }
44 |
45 | protected translator: ITranslator;
46 | private _trans: TranslationBundle;
47 | }
48 |
49 | export namespace SelectionStatus {
50 | export class Model extends VDomModel {
51 | private _spreadsheetWidget: SpreadsheetWidget | null = null;
52 |
53 | get selection(): ISelection | undefined {
54 | return this.spreadsheetWidget?.selection;
55 | }
56 |
57 | set spreadsheetWidget(widget: SpreadsheetWidget | null) {
58 | if (this._spreadsheetWidget) {
59 | this._spreadsheetWidget.selectionChanged.disconnect(
60 | this._triggerChange,
61 | this
62 | );
63 | }
64 | this._spreadsheetWidget = widget;
65 | if (this._spreadsheetWidget) {
66 | this._spreadsheetWidget.selectionChanged.connect(
67 | this._triggerChange,
68 | this
69 | );
70 | }
71 | this._triggerChange();
72 | }
73 |
74 | get spreadsheetWidget(): SpreadsheetWidget | null {
75 | return this._spreadsheetWidget;
76 | }
77 |
78 | private _triggerChange(): void {
79 | this.stateChanged.emit(void 0);
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/style/base.css:
--------------------------------------------------------------------------------
1 | @import url('~jspreadsheet-ce/dist/jspreadsheet.css');
2 | @import url('~jspreadsheet-ce/dist/jspreadsheet.theme.css');
3 |
4 | table.jexcel {
5 | border-top: 0;
6 | border-left: 0;
7 | }
8 |
9 | .jexcel_container {
10 | overflow: visible;
11 | padding: 0;
12 | }
13 |
14 | .jexcel_contextmenu {
15 | display: none;
16 | }
17 |
18 | .jexcel_content {
19 | padding: 0;
20 | }
21 |
22 | .se-area-container {
23 | overflow: auto;
24 | width: 100%;
25 | height: 100%;
26 | }
27 |
28 | .se-column-types {
29 | width: max-content;
30 | height: var(--jp-private-toolbar-height);
31 | }
32 |
33 | .se-column-types select {
34 | display: inline-block;
35 | }
36 |
37 | .se-hidden {
38 | display: none;
39 | }
40 |
41 | svg[data-icon='spreadsheet:column-types'] {
42 | transform: rotate(90deg);
43 | }
44 |
45 | /**
46 | Dark theme
47 | */
48 | body[data-jp-theme-light='false'] .jexcel_container {
49 | /* use https://bossanova.uk/jexcel/v4/cases/themes tool to adjust */
50 | --jexcel_header_color: #ccc;
51 | --jexcel_header_color_highlighted: #ddd;
52 | --jexcel_header_background: #313131;
53 | --jexcel_header_background_highlighted: #777;
54 | --jexcel_content_color: #fff;
55 | --jexcel_content_color_highlighted: #333;
56 | --jexcel_content_background: #111;
57 | --jexcel_content_background_highlighted: #333;
58 | --jexcel_menu_background: #7e7e7e;
59 | --jexcel_menu_background_highlighted: #ebebeb;
60 | --jexcel_menu_color: #ddd;
61 | --jexcel_menu_color_highlighted: #222;
62 | --jexcel_menu_box_shadow: unset;
63 | --jexcel_border_color: #5f5f5f;
64 | --jexcel_border_color_highlighted: #999;
65 | --active_color: #eee;
66 | }
67 |
68 | body[data-jp-theme-light='false'] .se-backlight {
69 | background: rgb(85 45 0 / 75%);
70 | }
71 |
72 | /**
73 | Light theme
74 | */
75 | body[data-jp-theme-light='true'] .jexcel_container {
76 | --jexcel_header_color: #000;
77 | --jexcel_header_color_highlighted: #000;
78 | --jexcel_header_background: #f3f3f3;
79 | --jexcel_header_background_highlighted: #dcdcdc;
80 | --jexcel_content_color: #000;
81 | --jexcel_content_color_highlighted: #000;
82 | --jexcel_content_background: #fff;
83 | --jexcel_content_background_highlighted: rgb(0 0 0 / 5%);
84 | --jexcel_menu_background: #fff;
85 | --jexcel_menu_background_highlighted: #ebebeb;
86 | --jexcel_menu_color: #555;
87 | --jexcel_menu_color_highlighted: #555;
88 | --jexcel_menu_box_shadow: 2px 2px 2px 0px rgb(143 144 145 / 100%);
89 | --jexcel_border_color: #ccc;
90 | --jexcel_border_color_highlighted: #000;
91 | --active_color: #007aff;
92 | }
93 |
94 | body[data-jp-theme-light='true'] .se-backlight {
95 | background: rgb(255 255 0 / 75%);
96 | }
97 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '44 0 * * 6'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'javascript', 'python' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37 | # Learn more:
38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
39 |
40 | steps:
41 | - name: Checkout repository
42 | uses: actions/checkout@v2
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version>=0.3.2"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "jupyterlab-spreadsheet-editor"
7 | readme = "README.md"
8 | license = { file = "LICENSE" }
9 | requires-python = ">=3.8"
10 | classifiers = [
11 | "Framework :: Jupyter",
12 | "Framework :: Jupyter :: JupyterLab",
13 | "Framework :: Jupyter :: JupyterLab :: 4",
14 | "Framework :: Jupyter :: JupyterLab :: Extensions",
15 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt",
16 | "License :: OSI Approved :: BSD License",
17 | "Programming Language :: Python",
18 | "Programming Language :: Python :: 3",
19 | "Programming Language :: Python :: 3.8",
20 | "Programming Language :: Python :: 3.9",
21 | "Programming Language :: Python :: 3.10",
22 | "Programming Language :: Python :: 3.11",
23 | "Programming Language :: Python :: 3.12",
24 | ]
25 | dependencies = [
26 | ]
27 | dynamic = ["version", "description", "authors", "urls", "keywords"]
28 |
29 | [tool.hatch.version]
30 | source = "nodejs"
31 |
32 | [tool.hatch.metadata.hooks.nodejs]
33 | fields = ["description", "authors", "urls"]
34 |
35 | [tool.hatch.build.targets.sdist]
36 | artifacts = ["jupyterlab_spreadsheet_editor/labextension"]
37 | exclude = [".github", "binder"]
38 |
39 | [tool.hatch.build.targets.wheel.shared-data]
40 | "jupyterlab_spreadsheet_editor/labextension" = "share/jupyter/labextensions/spreadsheet-editor"
41 | "install.json" = "share/jupyter/labextensions/spreadsheet-editor/install.json"
42 |
43 | [tool.hatch.build.hooks.version]
44 | path = "jupyterlab_spreadsheet_editor/_version.py"
45 |
46 | [tool.hatch.build.hooks.jupyter-builder]
47 | dependencies = ["hatch-jupyter-builder>=0.5"]
48 | build-function = "hatch_jupyter_builder.npm_builder"
49 | ensured-targets = [
50 | "jupyterlab_spreadsheet_editor/labextension/static/style.js",
51 | "jupyterlab_spreadsheet_editor/labextension/package.json",
52 | ]
53 | skip-if-exists = ["jupyterlab_spreadsheet_editor/labextension/static/style.js"]
54 |
55 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs]
56 | build_cmd = "build:prod"
57 | npm = ["jlpm"]
58 |
59 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs]
60 | build_cmd = "install:extension"
61 | npm = ["jlpm"]
62 | source_dir = "src"
63 | build_dir = "jupyterlab_spreadsheet_editor/labextension"
64 |
65 | [tool.jupyter-releaser.options]
66 | version_cmd = "hatch version"
67 |
68 | [tool.jupyter-releaser.hooks]
69 | before-build-npm = [
70 | "python -m pip install 'jupyterlab>=4.0.0,<5'",
71 | "jlpm",
72 | "jlpm build:prod"
73 | ]
74 | before-build-python = ["jlpm clean:all"]
75 |
76 | [tool.check-wheel-contents]
77 | ignore = ["W002"]
78 |
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 | # Making a new release of jupyterlab_spreadsheet_editor
2 |
3 | The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser).
4 |
5 | ## Manual release
6 |
7 | ### Python package
8 |
9 | This extension can be distributed as Python packages. All of the Python
10 | packaging instructions are in the `pyproject.toml` file to wrap your extension in a
11 | Python package. Before generating a package, you first need to install some tools:
12 |
13 | ```bash
14 | pip install build twine hatch
15 | ```
16 |
17 | Bump the version using `hatch`. By default this will create a tag.
18 | See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details.
19 |
20 | ```bash
21 | hatch version
22 | ```
23 |
24 | Make sure to clean up all the development files before building the package:
25 |
26 | ```bash
27 | jlpm clean:all
28 | ```
29 |
30 | You could also clean up the local git repository:
31 |
32 | ```bash
33 | git clean -dfX
34 | ```
35 |
36 | To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do:
37 |
38 | ```bash
39 | python -m build
40 | ```
41 |
42 | > `python setup.py sdist bdist_wheel` is deprecated and will not work for this package.
43 |
44 | Then to upload the package to PyPI, do:
45 |
46 | ```bash
47 | twine upload dist/*
48 | ```
49 |
50 | ### NPM package
51 |
52 | To publish the frontend part of the extension as a NPM package, do:
53 |
54 | ```bash
55 | npm login
56 | npm publish --access public
57 | ```
58 |
59 | ## Automated releases with the Jupyter Releaser
60 |
61 | The extension repository should already be compatible with the Jupyter Releaser.
62 |
63 | Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information.
64 |
65 | Here is a summary of the steps to cut a new release:
66 |
67 | - Add tokens to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository:
68 | - `ADMIN_GITHUB_TOKEN` (with "public_repo" and "repo:status" permissions); see the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token)
69 | - `NPM_TOKEN` (with "automation" permission); see the [documentation](https://docs.npmjs.com/creating-and-viewing-access-tokens)
70 | - Set up PyPI
71 |
72 | Using PyPI trusted publisher (modern way)
73 |
74 | - Set up your PyPI project by [adding a trusted publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/)
75 | - The _workflow name_ is `publish-release.yml` and the _environment_ should be left blank.
76 | - Ensure the publish release job as `permissions`: `id-token : write` (see the [documentation](https://docs.pypi.org/trusted-publishers/using-a-publisher/))
77 |
78 |
79 |
80 | Using PyPI token (legacy way)
81 |
82 | - If the repo generates PyPI release(s), create a scoped PyPI [token](https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/#saving-credentials-on-github). We recommend using a scoped token for security reasons.
83 |
84 | - You can store the token as `PYPI_TOKEN` in your fork's `Secrets`.
85 |
86 | - Advanced usage: if you are releasing multiple repos, you can create a secret named `PYPI_TOKEN_MAP` instead of `PYPI_TOKEN` that is formatted as follows:
87 |
88 | ```text
89 | owner1/repo1,token1
90 | owner2/repo2,token2
91 | ```
92 |
93 | If you have multiple Python packages in the same repository, you can point to them as follows:
94 |
95 | ```text
96 | owner1/repo1/path/to/package1,token1
97 | owner1/repo1/path/to/package2,token2
98 | ```
99 |
100 |
101 |
102 | - Go to the Actions panel
103 | - Run the "Step 1: Prep Release" workflow
104 | - Check the draft changelog
105 | - Run the "Step 2: Publish Release" workflow
106 |
107 | ## Publishing to `conda-forge`
108 |
109 | If the package is not on conda forge yet, check the documentation to learn how to add it: https://conda-forge.org/docs/maintainer/adding_pkgs.html
110 |
111 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically.
112 |
--------------------------------------------------------------------------------
/style/icons/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014, Austin Andrews (http://materialdesignicons.com/),
2 | with Reserved Font Name Material Design Icons.
3 | Copyright (c) 2014, Google (http://www.google.com/design/)
4 | uses the license at https://github.com/google/material-design-icons/blob/master/LICENSE
5 |
6 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
7 | This license is copied below, and is also available with a FAQ at:
8 | http://scripts.sil.org/OFL
9 |
10 |
11 | -----------------------------------------------------------
12 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
13 | -----------------------------------------------------------
14 |
15 | PREAMBLE
16 | The goals of the Open Font License (OFL) are to stimulate worldwide
17 | development of collaborative font projects, to support the font creation
18 | efforts of academic and linguistic communities, and to provide a free and
19 | open framework in which fonts may be shared and improved in partnership
20 | with others.
21 |
22 | The OFL allows the licensed fonts to be used, studied, modified and
23 | redistributed freely as long as they are not sold by themselves. The
24 | fonts, including any derivative works, can be bundled, embedded,
25 | redistributed and/or sold with any software provided that any reserved
26 | names are not used by derivative works. The fonts and derivatives,
27 | however, cannot be released under any other type of license. The
28 | requirement for fonts to remain under this license does not apply
29 | to any document created using the fonts or their derivatives.
30 |
31 | DEFINITIONS
32 | "Font Software" refers to the set of files released by the Copyright
33 | Holder(s) under this license and clearly marked as such. This may
34 | include source files, build scripts and documentation.
35 |
36 | "Reserved Font Name" refers to any names specified as such after the
37 | copyright statement(s).
38 |
39 | "Original Version" refers to the collection of Font Software components as
40 | distributed by the Copyright Holder(s).
41 |
42 | "Modified Version" refers to any derivative made by adding to, deleting,
43 | or substituting -- in part or in whole -- any of the components of the
44 | Original Version, by changing formats or by porting the Font Software to a
45 | new environment.
46 |
47 | "Author" refers to any designer, engineer, programmer, technical
48 | writer or other person who contributed to the Font Software.
49 |
50 | PERMISSION & CONDITIONS
51 | Permission is hereby granted, free of charge, to any person obtaining
52 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
53 | redistribute, and sell modified and unmodified copies of the Font
54 | Software, subject to the following conditions:
55 |
56 | 1) Neither the Font Software nor any of its individual components,
57 | in Original or Modified Versions, may be sold by itself.
58 |
59 | 2) Original or Modified Versions of the Font Software may be bundled,
60 | redistributed and/or sold with any software, provided that each copy
61 | contains the above copyright notice and this license. These can be
62 | included either as stand-alone text files, human-readable headers or
63 | in the appropriate machine-readable metadata fields within text or
64 | binary files as long as those fields can be easily viewed by the user.
65 |
66 | 3) No Modified Version of the Font Software may use the Reserved Font
67 | Name(s) unless explicit written permission is granted by the corresponding
68 | Copyright Holder. This restriction only applies to the primary font name as
69 | presented to the users.
70 |
71 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
72 | Software shall not be used to promote, endorse or advertise any
73 | Modified Version, except to acknowledge the contribution(s) of the
74 | Copyright Holder(s) and the Author(s) or with their explicit written
75 | permission.
76 |
77 | 5) The Font Software, modified or unmodified, in part or in whole,
78 | must be distributed entirely under this license, and must not be
79 | distributed under any other license. The requirement for fonts to
80 | remain under this license does not apply to any document created
81 | using the Font Software.
82 |
83 | TERMINATION
84 | This license becomes null and void if any of the above conditions are
85 | not met.
86 |
87 | DISCLAIMER
88 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
89 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
90 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
91 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
92 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
93 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
94 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
95 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
96 | OTHER DEALINGS IN THE FONT SOFTWARE.
97 |
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JupyterLab Spreadsheet Editor
2 |
3 | [](https://jupyterlab-contrib.github.io/)
4 | [](https://github.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor/actions/workflows/build.yml)
5 | [](https://mybinder.org/v2/gh/jupyterlab-contrib/jupyterlab-spreadsheet-editor/main?urlpath=lab)
6 | [](https://python.org/pypi/jupyterlab-spreadsheet-editor)
7 |
8 | JupyterLab spreadsheet editor enables interactive editing of comma/tab separated value spreadsheets.
9 | It support formulas, sorting, column/row rearrangements and more!
10 |
11 | > Note: you might be interested to checkout [tabular-data-editor](https://github.com/jupytercalpoly/jupyterlab-tabular-data-editor) as well
12 |
13 | ## Showcase
14 |
15 | **Fully featured integration**
16 |
17 | - row/column operations, column width adjustment
18 | - search and replace
19 |
20 | 
21 |
22 | **Formula support**
23 | basic formula calculation (rendering) - as implemented by jExcel.
24 |
25 | 
26 |
27 | **Column freezing**
28 | for exploration of wide datasets with many covariates
29 |
30 | 
31 |
32 | **Launcher items**:
33 | create CSV/TSV files easily from the launcher or the palette.
34 |
35 | 
36 |
37 | **Lightweight and reliable dependencies**:
38 | the spreadsheet interface is built with the [jexcel](https://github.com/paulhodel/jexcel), while [Papa Parse](https://github.com/mholt/PapaParse) provides very fast, [RFC 4180](https://tools.ietf.org/html/rfc4180) compatible CSV parsing (both have no third-party dependencies).
39 |
40 | ## Requirements
41 |
42 | - JupyterLab >= 3.0
43 |
44 | ## Install
45 |
46 | ```bash
47 | pip install jupyterlab-spreadsheet-editor
48 | ```
49 |
50 | ## Contributing
51 |
52 | ### Development install
53 |
54 | Note: You will need NodeJS to build the extension package.
55 |
56 | The `jlpm` command is JupyterLab's pinned version of
57 | [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use
58 | `yarn` or `npm` in lieu of `jlpm` below.
59 |
60 | ```bash
61 | # Clone the repo to your local environment
62 | # Change directory to the jupyterlab-spreadsheet-editor directory
63 | # Install package in development mode
64 | pip install -e .
65 | # Link your development version of the extension with JupyterLab
66 | jupyter labextension develop . --overwrite
67 | # Rebuild extension Typescript source after making changes
68 | jlpm run build
69 | ```
70 |
71 | You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.
72 |
73 | ```bash
74 | # Watch the source directory in one terminal, automatically rebuilding when needed
75 | jlpm run watch
76 | # Run JupyterLab in another terminal
77 | jupyter lab
78 | ```
79 |
80 | With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt).
81 |
82 | By default, the `jlpm run build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command:
83 |
84 | ```bash
85 | jupyter lab build --minimize=False
86 | ```
87 |
88 | ### Uninstall
89 |
90 | ```bash
91 | pip uninstall jupyterlab-spreadsheet-editor
92 | ```
93 |
94 | ## Related extensions
95 |
96 | Spreadsheet editors:
97 |
98 | - [jupyterlab-tabular-data-editor](https://github.com/jupytercalpoly/jupyterlab-tabular-data-editor)
99 |
100 | Spreadsheet viewers:
101 |
102 | - [jupyterlab-spreadsheet](https://github.com/quigleyj97/jupyterlab-spreadsheet) implements Excel spreadsheet viewer
103 | - the built-in [csvviewer](https://github.com/jupyterlab/jupyterlab/tree/master/packages/csvviewer) ([extension](https://github.com/jupyterlab/jupyterlab/tree/master/packages/csvviewer-extension)) allows to display CSV/TSV files
104 |
105 | In-notebook spreadsheet widgets:
106 |
107 | - [ipysheet](https://github.com/QuantStack/ipysheet) - programmable sheet creation, exploration and modification
108 | - [qgrid](https://github.com/quantopian/qgrid) - interactive DataFrame exploration and modification
109 |
--------------------------------------------------------------------------------
/src/documentwidget.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Jupyter Development Team.
2 | // Distributed under the terms of the Modified BSD License.
3 |
4 | import { DocumentWidget } from '@jupyterlab/docregistry';
5 | import { SpreadsheetWidget } from './widget';
6 | import { ToolbarButton } from '@jupyterlab/apputils';
7 | import { LabIcon } from '@jupyterlab/ui-components';
8 | import numberedSvg from '../style/icons/mdi-format-list-numbered.svg';
9 | import fitColumnWidthSvg from '../style/icons/mdi-table-column-width.svg';
10 | import listTypeSvg from '../style/icons/mdi-format-list-bulleted-type.svg';
11 | import topHeaderSvg from '../style/icons/mdi-page-layout-header.svg';
12 | import {
13 | freezeColumnIcon,
14 | unfreezeColumnIcon,
15 | removeColumnIcon,
16 | addColumnIcon,
17 | removeRowIcon,
18 | addRowIcon
19 | } from './icons';
20 | import { nullTranslator, TranslationBundle } from '@jupyterlab/translation';
21 |
22 | export class SpreadsheetEditorDocumentWidget extends DocumentWidget {
23 | protected _trans: TranslationBundle;
24 |
25 | constructor(options: DocumentWidget.IOptions) {
26 | super(options);
27 | const translator = options.translator || nullTranslator;
28 | this._trans = translator.load('spreadsheet-editor');
29 |
30 | const addRowButton = new ToolbarButton({
31 | icon: addRowIcon,
32 | onClick: () => {
33 | this.content.jexcel!.insertRow();
34 | this.content.updateModel();
35 | },
36 | tooltip: this._trans.__('Insert a row at the end')
37 | });
38 | this.toolbar.addItem('spreadsheet:insert-row', addRowButton);
39 |
40 | const removeRowButton = new ToolbarButton({
41 | icon: removeRowIcon,
42 | onClick: () => {
43 | this.content.jexcel!.deleteRow(this.content.jexcel!.rows.length - 1, 1);
44 | this.content.updateModel();
45 | },
46 | tooltip: this._trans.__('Remove the last row')
47 | });
48 | this.toolbar.addItem('spreadsheet:remove-row', removeRowButton);
49 |
50 | const addColumnButton = new ToolbarButton({
51 | icon: addColumnIcon,
52 | onClick: () => {
53 | this.content.jexcel!.insertColumn();
54 | this.content.updateModel();
55 | },
56 | tooltip: this._trans.__('Insert a column at the end')
57 | });
58 | this.toolbar.addItem('spreadsheet:insert-column', addColumnButton);
59 |
60 | const removeColumnButton = new ToolbarButton({
61 | icon: removeColumnIcon,
62 | onClick: () => {
63 | this.content.jexcel!.deleteColumn(this.content.columnsNumber);
64 | this.content.updateModel();
65 | },
66 | tooltip: this._trans.__('Remove the last column')
67 | });
68 | this.toolbar.addItem('spreadsheet:remove-column', removeColumnButton);
69 |
70 | let indexVisible = true;
71 | const showHideIndex = new ToolbarButton({
72 | icon: new LabIcon({
73 | name: 'spreadsheet:show-hide-index',
74 | svgstr: numberedSvg
75 | }),
76 | onClick: () => {
77 | if (indexVisible) {
78 | this.content.jexcel!.hideIndex();
79 | } else {
80 | this.content.jexcel!.showIndex();
81 | }
82 | indexVisible = !indexVisible;
83 | },
84 | tooltip: this._trans.__('Show/hide row numbers')
85 | });
86 | this.toolbar.addItem('spreadsheet:show-hide-index', showHideIndex);
87 |
88 | const fitColumnWidthButton = new ToolbarButton({
89 | icon: new LabIcon({
90 | name: 'spreadsheet:fit-columns',
91 | svgstr: fitColumnWidthSvg
92 | }),
93 | onClick: () => {
94 | switch (this.content.fitMode) {
95 | case 'fit-cells':
96 | this.content.fitMode = 'all-equal-fit';
97 | break;
98 | case 'all-equal-fit':
99 | this.content.fitMode = 'all-equal-default';
100 | break;
101 | case 'all-equal-default':
102 | this.content.fitMode = 'fit-cells';
103 | }
104 | this.content.relayout();
105 | },
106 | tooltip: this._trans.__('Fit columns width')
107 | });
108 | this.toolbar.addItem('spreadsheet:fit-columns', fitColumnWidthButton);
109 |
110 | const freezeColumnButton = new ToolbarButton({
111 | icon: freezeColumnIcon,
112 | onClick: () => {
113 | this.content.freezeSelectedColumns();
114 | },
115 | tooltip: this._trans.__(
116 | 'Freeze the initial columns (up to the selected column)'
117 | )
118 | });
119 | this.toolbar.addItem('spreadsheet:freeze-columns', freezeColumnButton);
120 |
121 | const unfreezeColumnButton = new ToolbarButton({
122 | icon: unfreezeColumnIcon,
123 | onClick: () => {
124 | this.content.unfreezeColumns();
125 | },
126 | tooltip: this._trans.__('Unfreeze frozen column')
127 | });
128 | this.toolbar.addItem('spreadsheet:unfreeze-columns', unfreezeColumnButton);
129 |
130 | const switchHeadersButton = new ToolbarButton({
131 | icon: new LabIcon({
132 | name: 'spreadsheet:switch-headers',
133 | svgstr: topHeaderSvg
134 | }),
135 | onClick: () => {
136 | this.content.switchHeaders();
137 | },
138 | tooltip: this._trans.__('First row as a header')
139 | });
140 | this.toolbar.addItem('spreadsheet:switch-headers', switchHeadersButton);
141 |
142 | const columnTypeButton = new ToolbarButton({
143 | icon: new LabIcon({
144 | name: 'spreadsheet:column-types',
145 | svgstr: listTypeSvg
146 | }),
147 | onClick: () => {
148 | this.content.switchTypesBar();
149 | },
150 | tooltip: this._trans.__('Show/hide column types bar')
151 | });
152 | this.toolbar.addItem(
153 | 'spreadsheet:switch-column-types-bar',
154 | columnTypeButton
155 | );
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spreadsheet-editor",
3 | "version": "0.7.2",
4 | "description": "JupyterLab spreadsheet (csv/tsv) editor",
5 | "keywords": [
6 | "jupyter",
7 | "jupyterlab",
8 | "jupyterlab-extension",
9 | "csv",
10 | "tsv",
11 | "spreadsheeet",
12 | "excel"
13 | ],
14 | "homepage": "https://github.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor",
15 | "bugs": {
16 | "url": "https://github.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor/issues"
17 | },
18 | "license": "BSD-3-Clause",
19 | "author": "Michał Krassowski",
20 | "files": [
21 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
22 | "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
23 | "style/icons/**/*.{svg}",
24 | "style/index.js",
25 | "schema/*.json"
26 | ],
27 | "main": "lib/index.js",
28 | "types": "lib/index.d.ts",
29 | "style": "style/index.css",
30 | "repository": {
31 | "type": "git",
32 | "url": "https://github.com/jupyterlab-contrib/jupyterlab-spreadsheet-editor.git"
33 | },
34 | "scripts": {
35 | "build": "jlpm build:lib && jlpm build:labextension:dev",
36 | "build:labextension": "jupyter labextension build .",
37 | "build:labextension:dev": "jupyter labextension build --development True .",
38 | "build:lib": "tsc --sourceMap",
39 | "build:lib:prod": "tsc",
40 | "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
41 | "clean": "jlpm clean:lib",
42 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
43 | "clean:labextension": "rimraf jupyterlab_spreadsheet_editor/labextension jupyterlab_spreadsheet_editor/_version.py",
44 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
45 | "clean:lintcache": "rimraf .eslintcache .stylelintcache",
46 | "eslint": "jlpm eslint:check --fix",
47 | "eslint:check": "eslint . --cache --ext .ts,.tsx",
48 | "install:extension": "jlpm build",
49 | "lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
50 | "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
51 | "prettier": "jlpm prettier:base --write --list-different",
52 | "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
53 | "prettier:check": "jlpm prettier:base --check",
54 | "stylelint": "jlpm stylelint:check --fix",
55 | "stylelint:check": "stylelint --cache \"style/**/*.css\"",
56 | "watch": "run-p watch:src watch:labextension",
57 | "watch:labextension": "jupyter labextension watch .",
58 | "watch:src": "tsc -w --sourceMap"
59 | },
60 | "dependencies": {
61 | "@jupyterlab/application": "^4.0.11",
62 | "@jupyterlab/apputils": "^4.1.11",
63 | "@jupyterlab/coreutils": "^6.0.11",
64 | "@jupyterlab/docregistry": "^4.0.11",
65 | "@jupyterlab/documentsearch": "^4.0.11",
66 | "@jupyterlab/filebrowser": "^4.0.11",
67 | "@jupyterlab/fileeditor": "^4.0.11",
68 | "@jupyterlab/launcher": "^4.0.11",
69 | "@jupyterlab/mainmenu": "^4.0.11",
70 | "@jupyterlab/statusbar": "^4.0.11",
71 | "@jupyterlab/translation": "^4.0.11",
72 | "@jupyterlab/ui-components": "^4.0.11",
73 | "@lumino/commands": "^2.0.1",
74 | "@lumino/coreutils": "^2.0.0",
75 | "@lumino/messaging": "^2.0.0",
76 | "@lumino/signaling": "^2.0.0",
77 | "@lumino/widgets": "^2.0.1",
78 | "@types/papaparse": "^5.3.14",
79 | "jspreadsheet-ce": "^4.13.4",
80 | "papaparse": "^5.4.1"
81 | },
82 | "devDependencies": {
83 | "@jupyterlab/builder": "^4.0.0",
84 | "@types/json-schema": "^7.0.11",
85 | "@types/react": "^18.0.26",
86 | "@types/react-addons-linked-state-mixin": "^0.14.22",
87 | "@typescript-eslint/eslint-plugin": "^6.1.0",
88 | "@typescript-eslint/parser": "^6.1.0",
89 | "css-loader": "^6.7.1",
90 | "eslint": "^8.36.0",
91 | "eslint-config-prettier": "^8.8.0",
92 | "eslint-plugin-prettier": "^5.0.0",
93 | "eslint-plugin-react": "^7.19.0",
94 | "npm-run-all": "^4.1.5",
95 | "prettier": "^3.0.0",
96 | "rimraf": "^5.0.1",
97 | "source-map-loader": "^1.0.2",
98 | "style-loader": "^3.3.1",
99 | "stylelint": "^15.10.1",
100 | "stylelint-config-recommended": "^13.0.0",
101 | "stylelint-config-standard": "^34.0.0",
102 | "stylelint-csstree-validator": "^3.0.0",
103 | "stylelint-prettier": "^4.0.0",
104 | "typescript": "~5.0.2",
105 | "yjs": "^13.5.40"
106 | },
107 | "sideEffects": [
108 | "style/*.css",
109 | "style/index.js"
110 | ],
111 | "jupyterlab": {
112 | "extension": true,
113 | "outputDir": "jupyterlab_spreadsheet_editor/labextension",
114 | "schemaDir": "schema"
115 | },
116 | "styleModule": "style/index.js",
117 | "eslintConfig": {
118 | "extends": [
119 | "eslint:recommended",
120 | "plugin:@typescript-eslint/eslint-recommended",
121 | "plugin:@typescript-eslint/recommended",
122 | "plugin:prettier/recommended"
123 | ],
124 | "parser": "@typescript-eslint/parser",
125 | "parserOptions": {
126 | "project": "tsconfig.json",
127 | "sourceType": "module"
128 | },
129 | "plugins": [
130 | "@typescript-eslint"
131 | ],
132 | "rules": {
133 | "@typescript-eslint/naming-convention": [
134 | "error",
135 | {
136 | "selector": "interface",
137 | "format": [
138 | "PascalCase"
139 | ],
140 | "custom": {
141 | "regex": "^I[A-Z]",
142 | "match": true
143 | }
144 | }
145 | ],
146 | "@typescript-eslint/no-unused-vars": [
147 | "warn",
148 | {
149 | "args": "none"
150 | }
151 | ],
152 | "@typescript-eslint/no-explicit-any": "off",
153 | "@typescript-eslint/no-namespace": "off",
154 | "@typescript-eslint/no-use-before-define": "off",
155 | "@typescript-eslint/quotes": [
156 | "error",
157 | "single",
158 | {
159 | "avoidEscape": true,
160 | "allowTemplateLiterals": false
161 | }
162 | ],
163 | "curly": [
164 | "error",
165 | "all"
166 | ],
167 | "eqeqeq": "error",
168 | "prefer-arrow-callback": "error"
169 | }
170 | },
171 | "eslintIgnore": [
172 | "node_modules",
173 | "dist",
174 | "coverage",
175 | "**/*.d.ts"
176 | ],
177 | "prettier": {
178 | "singleQuote": true,
179 | "trailingComma": "none",
180 | "arrowParens": "avoid",
181 | "endOfLine": "auto",
182 | "overrides": [
183 | {
184 | "files": "package.json",
185 | "options": {
186 | "tabWidth": 4
187 | }
188 | }
189 | ]
190 | },
191 | "stylelint": {
192 | "extends": [
193 | "stylelint-config-recommended",
194 | "stylelint-config-standard",
195 | "stylelint-prettier/recommended"
196 | ],
197 | "plugins": [
198 | "stylelint-csstree-validator"
199 | ],
200 | "rules": {
201 | "csstree/validator": true,
202 | "property-no-vendor-prefix": null,
203 | "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
204 | "selector-no-vendor-prefix": null,
205 | "value-no-vendor-prefix": null,
206 | "custom-property-pattern": null
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/searchprovider.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Jupyter Development Team.
2 | // Distributed under the terms of the Modified BSD License.
3 |
4 | import {
5 | ISearchMatch,
6 | ISearchProvider,
7 | ISearchProviderFactory,
8 | SearchProvider,
9 | IFilters
10 | } from '@jupyterlab/documentsearch';
11 | import { SpreadsheetEditorDocumentWidget } from './documentwidget';
12 | import { Widget } from '@lumino/widgets';
13 | import { DocumentWidget } from '@jupyterlab/docregistry';
14 | import { SpreadsheetWidget } from './widget';
15 | import { ISignal, Signal } from '@lumino/signaling';
16 | import { JspreadsheetInstance } from 'jspreadsheet-ce';
17 |
18 | export interface ICellCoordinates {
19 | column: number;
20 | row: number;
21 | }
22 |
23 | interface ICellSearchMatch extends ISearchMatch, ICellCoordinates {
24 | // no-op
25 | }
26 |
27 | class CoordinatesSet {
28 | protected _set: Set;
29 |
30 | constructor() {
31 | this._set = new Set();
32 | }
33 |
34 | protected _valueToString(value: ICellCoordinates): string {
35 | if (typeof value === 'undefined') {
36 | return value;
37 | }
38 | return value.column + '|' + value.row;
39 | }
40 |
41 | protected _stringToValue(value: string): ICellCoordinates {
42 | if (typeof value === 'undefined') {
43 | return value;
44 | }
45 | const parts = value.split('|');
46 | if (parts.length !== 2) {
47 | console.warn('A problem with stringToValue input detected!');
48 | }
49 | return {
50 | column: parseInt(parts[0], 10),
51 | row: parseInt(parts[1], 10)
52 | };
53 | }
54 |
55 | add(value: ICellCoordinates): this {
56 | this._set.add(this._valueToString(value));
57 | return this;
58 | }
59 |
60 | delete(value: ICellCoordinates): boolean {
61 | return this._set.delete(this._valueToString(value));
62 | }
63 |
64 | has(value: ICellCoordinates): boolean {
65 | return this._set.has(this._valueToString(value));
66 | }
67 |
68 | values(): IterableIterator {
69 | const iterator = this._set.values();
70 | const stringToValue = this._stringToValue;
71 | return {
72 | [Symbol.iterator](): IterableIterator {
73 | return this;
74 | },
75 | return(value: any): IteratorResult {
76 | const returned = iterator.return!(value);
77 | return {
78 | done: returned.done,
79 | value: stringToValue(returned.value)
80 | };
81 | },
82 | next() {
83 | const next = iterator.next();
84 | return {
85 | done: next.done,
86 | value: stringToValue(next.value)
87 | };
88 | },
89 | throw() {
90 | const thrown = iterator.throw!();
91 | return {
92 | done: thrown.done,
93 | value: stringToValue(thrown.value)
94 | };
95 | }
96 | };
97 | }
98 |
99 | clear() {
100 | return this._set.clear();
101 | }
102 | }
103 |
104 | export class SpreadsheetSearchProviderFactory
105 | implements ISearchProviderFactory
106 | {
107 | createNew(
108 | widget: Widget
109 | //translator?: ITranslator
110 | ): ISearchProvider {
111 | return new SpreadsheetSearchProvider(
112 | widget as SpreadsheetEditorDocumentWidget
113 | );
114 | }
115 |
116 | /**
117 | * Report whether or not this provider has the ability to search on the given object
118 | */
119 | isApplicable(domain: Widget): domain is SpreadsheetEditorDocumentWidget {
120 | // check to see if the SpreadsheetSearchProvider can search on the
121 | // first cell, false indicates another editor is present
122 | return (
123 | domain instanceof DocumentWidget &&
124 | domain.content instanceof SpreadsheetWidget
125 | );
126 | }
127 | }
128 |
129 | export class SpreadsheetSearchProvider extends SearchProvider {
130 | constructor(widget: SpreadsheetEditorDocumentWidget) {
131 | super(widget);
132 | this._sheet = widget.content;
133 | this._target = this._sheet.jexcel!;
134 | this.backlitMatches = new CoordinatesSet();
135 | }
136 |
137 | private mostRecentSelectedCell: any;
138 |
139 | get changed(): ISignal {
140 | return this._changed;
141 | }
142 |
143 | get currentMatchIndex(): number | null {
144 | return this._currentMatchIndex;
145 | }
146 |
147 | readonly isReadOnly: boolean = false;
148 |
149 | get matches(): ICellSearchMatch[] {
150 | return this._matches;
151 | }
152 |
153 | get matchesCount(): number | null {
154 | return this._matches.length;
155 | }
156 |
157 | endQuery(): Promise {
158 | this.backlightOff();
159 | this._currentMatchIndex = 0;
160 | this._matches = [];
161 | this._sheet.changed.disconnect(this._onSheetChanged, this);
162 | return Promise.resolve(undefined);
163 | }
164 |
165 | /**
166 | * Clear currently highlighted match.
167 | */
168 | async clearHighlight(): Promise {
169 | this.backlightOff();
170 | }
171 |
172 | private backlightOff() {
173 | for (const matchCoords of this.backlitMatches.values()) {
174 | const cell: HTMLElement = this._target.getCellFromCoords(
175 | matchCoords.column,
176 | matchCoords.row
177 | );
178 | cell.classList.remove('se-backlight');
179 | }
180 | this.backlitMatches.clear();
181 | }
182 |
183 | async endSearch(): Promise {
184 | // restore the selection
185 | // eslint-disable-next-line eqeqeq
186 | if (this._target.selectedCell == null && this.mostRecentSelectedCell) {
187 | this._target.selectedCell = this.mostRecentSelectedCell;
188 | }
189 | return this.endQuery();
190 | }
191 |
192 | private getSelectedCellCoordinates(): ICellCoordinates | null {
193 | const target = this._target;
194 | const columns = target.getSelectedColumns();
195 | const rows = target.getSelectedRows(true) as number[];
196 | if (rows.length === 1 && columns.length === 1) {
197 | return {
198 | column: columns[0],
199 | row: rows[0]
200 | };
201 | }
202 | return null;
203 | }
204 |
205 | private _initialQueryCoodrs: ICellCoordinates | null = null;
206 |
207 | getInitialQuery(): string {
208 | const coords = this.getSelectedCellCoordinates();
209 | this._initialQueryCoodrs = coords;
210 | if (coords) {
211 | const value = this._target.getValueFromCoords(
212 | coords.column,
213 | coords.row,
214 | false
215 | );
216 | if (value) {
217 | return value.toString();
218 | }
219 | }
220 | // Close the editor to avoid overwriting contents of last edited cell
221 | // as users starts typing into the search box after pressing ctrl + f
222 | // (but only do that after the initial value was taken)
223 | this._target.resetSelection(true);
224 | return '';
225 | }
226 |
227 | async highlightNext(): Promise {
228 | if (this._currentMatchIndex + 1 < this.matches.length) {
229 | this._currentMatchIndex += 1;
230 | } else {
231 | this._currentMatchIndex = 0;
232 | }
233 | const match = this.matches[this._currentMatchIndex];
234 | if (!match) {
235 | return;
236 | }
237 | this.highlight(match);
238 | return match;
239 | }
240 |
241 | async highlightPrevious(): Promise {
242 | if (this._currentMatchIndex > 0) {
243 | this._currentMatchIndex -= 1;
244 | } else {
245 | this._currentMatchIndex = this.matches.length - 1;
246 | }
247 | const match = this.matches[this._currentMatchIndex];
248 | if (!match) {
249 | return;
250 | }
251 | this.highlight(match);
252 | return match;
253 | }
254 |
255 | highlight(match: ICellSearchMatch) {
256 | this.backlightMatches();
257 | // select the matched cell, which leads to a loss of "focus" (or rather jexcel eagerly intercepting events)
258 | this._target.updateSelectionFromCoords(
259 | match.column,
260 | match.row,
261 | match.column,
262 | match.row,
263 | null
264 | );
265 | // "regain" focus by erasing selection information (but keeping all the CSS) - this is a workaround (best avoided)
266 | this.mostRecentSelectedCell = this._target.selectedCell;
267 | this._target.selectedCell = null;
268 | this._sheet.scrollCellIntoView({ row: match.row, column: match.column });
269 | }
270 |
271 | async replaceAllMatches(newText: string): Promise {
272 | for (let i = 0; i < this.matches.length; i++) {
273 | this._currentMatchIndex = i;
274 | await this.replaceCurrentMatch(newText, true);
275 | }
276 | this._matches = this.findMatches();
277 | this.backlightMatches();
278 | return true;
279 | }
280 |
281 | async replaceCurrentMatch(
282 | newText: string,
283 | isReplaceAll = false
284 | ): Promise {
285 | let replaceOccurred = false;
286 | const match = this.matches[this._currentMatchIndex];
287 | const cell = this._target.getValueFromCoords(
288 | match.column,
289 | match.row,
290 | false
291 | );
292 | let index = -1;
293 | let matchesInCell = 0;
294 |
295 | const newValue = String(cell).replace(this._query!, substring => {
296 | index += 1;
297 | matchesInCell += 1;
298 | if (index === match.position) {
299 | replaceOccurred = true;
300 | return newText;
301 | }
302 |
303 | return substring;
304 | });
305 | let subsequentIndex = this._currentMatchIndex + 1;
306 | while (subsequentIndex < this.matches.length) {
307 | const subsequent = this.matches[subsequentIndex];
308 | if (subsequent.column === match.column && subsequent.row === match.row) {
309 | subsequent.position -= 1;
310 | } else {
311 | break;
312 | }
313 | subsequentIndex += 1;
314 | }
315 |
316 | this._target.setValueFromCoords(match.column, match.row, newValue, false);
317 |
318 | if (!isReplaceAll && matchesInCell === 1) {
319 | const matchCoords = { column: match.column, row: match.row };
320 | const cell: HTMLElement = this._target.getCellFromCoords(
321 | match.column,
322 | match.row
323 | );
324 | cell.classList.remove('se-backlight');
325 | this.backlitMatches.delete(matchCoords);
326 | }
327 |
328 | if (!isReplaceAll) {
329 | await this.highlightNext();
330 | }
331 | return replaceOccurred;
332 | }
333 |
334 | private _onSheetChanged() {
335 | // matches may need updating
336 | this._matches = this.findMatches(false);
337 | // update backlight
338 | this.backlightOff();
339 | this.backlightMatches();
340 | this._changed.emit(undefined);
341 | }
342 |
343 | protected backlitMatches: CoordinatesSet;
344 |
345 | /**
346 | * Highlight n=1000 matches around the current match.
347 | * The number of highlights is limited to prevent negative impact on the UX in huge notebooks.
348 | */
349 | protected backlightMatches(n = 1000): void {
350 | for (
351 | let i = Math.max(0, this._currentMatchIndex - n / 2);
352 | i < Math.min(this._currentMatchIndex + n / 2, this.matches.length);
353 | i++
354 | ) {
355 | const match = this.matches[i];
356 | const matchCoord = {
357 | column: match.column,
358 | row: match.row
359 | };
360 |
361 | if (!this.backlitMatches.has(matchCoord)) {
362 | const cell: HTMLElement = this._target.getCellFromCoords(
363 | match.column,
364 | match.row
365 | );
366 | cell.classList.add('se-backlight');
367 | this.backlitMatches.add(matchCoord);
368 | }
369 | }
370 | }
371 |
372 | protected findMatches(highlightFirst = true): ICellSearchMatch[] {
373 | const currentCellCoordinates = this._initialQueryCoodrs;
374 | this._initialQueryCoodrs = null;
375 | let currentMatchIndex = 0;
376 | const query = this._query;
377 | if (!query) {
378 | return [];
379 | }
380 |
381 | const matches: ICellSearchMatch[] = [];
382 | const data = this._target.getData();
383 | let rowNumber = 0;
384 | let columnNumber = -1;
385 | let index = 0;
386 | let totalMatchIndex = 0;
387 | for (const row of data) {
388 | for (const cell of row) {
389 | columnNumber += 1;
390 | if (!cell) {
391 | continue;
392 | }
393 | const matched = String(cell).match(query!);
394 | if (!matched) {
395 | continue;
396 | }
397 | index = 0;
398 | if (
399 | currentCellCoordinates !== null &&
400 | currentCellCoordinates.row === rowNumber &&
401 | currentCellCoordinates.column === columnNumber
402 | ) {
403 | currentMatchIndex = totalMatchIndex;
404 | }
405 | for (const match of matched) {
406 | matches.push({
407 | row: rowNumber,
408 | column: columnNumber,
409 | position: index,
410 | text: match
411 | });
412 | index += 1;
413 | totalMatchIndex += 1;
414 | }
415 | }
416 | columnNumber = -1;
417 | rowNumber += 1;
418 | }
419 | this._currentMatchIndex = currentMatchIndex;
420 | this._matches = matches;
421 |
422 | if (matches.length && highlightFirst) {
423 | this.highlight(matches[this._currentMatchIndex]);
424 | }
425 |
426 | return matches;
427 | }
428 | async startQuery(query: RegExp, filters: IFilters): Promise {
429 | const searchTarget = this.widget;
430 | this._sheet = searchTarget.content;
431 | this._query = query;
432 |
433 | this._sheet.changed.connect(this._onSheetChanged, this);
434 |
435 | this.findMatches();
436 | }
437 |
438 | private _changed = new Signal(this);
439 |
440 | private _target: JspreadsheetInstance;
441 | private _sheet: SpreadsheetWidget;
442 | private _query: RegExp | null = null;
443 | private _matches: ICellSearchMatch[] = [];
444 | private _currentMatchIndex: number = 0;
445 | }
446 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Jupyter Development Team.
2 | // Distributed under the terms of the Modified BSD License.
3 |
4 | import {
5 | ILayoutRestorer,
6 | JupyterFrontEnd,
7 | JupyterFrontEndPlugin
8 | } from '@jupyterlab/application';
9 | import { IMainMenu } from '@jupyterlab/mainmenu';
10 | import {
11 | ABCWidgetFactory,
12 | DocumentRegistry,
13 | IDocumentWidget
14 | } from '@jupyterlab/docregistry';
15 | import { Widget } from '@lumino/widgets';
16 | import { DocumentWidget } from '@jupyterlab/docregistry';
17 | import { ICommandPalette, WidgetTracker } from '@jupyterlab/apputils';
18 | import {
19 | redoIcon,
20 | undoIcon,
21 | addAboveIcon,
22 | addBelowIcon,
23 | addIcon,
24 | copyIcon,
25 | pasteIcon
26 | } from '@jupyterlab/ui-components';
27 | import { ISearchProviderRegistry } from '@jupyterlab/documentsearch';
28 | import { CommandRegistry } from '@lumino/commands';
29 |
30 | import '../style/index.css';
31 | import { SpreadsheetWidget } from './widget';
32 | import { SpreadsheetEditorDocumentWidget } from './documentwidget';
33 | import { SpreadsheetSearchProviderFactory } from './searchprovider';
34 | import { ILauncher } from '@jupyterlab/launcher';
35 | import { spreadsheetIcon } from '@jupyterlab/ui-components';
36 | import {
37 | IFileBrowserFactory,
38 | IDefaultFileBrowser
39 | } from '@jupyterlab/filebrowser';
40 | import { IStatusBar } from '@jupyterlab/statusbar';
41 | import { SelectionStatus } from './statusbar';
42 | import {
43 | ITranslator,
44 | nullTranslator,
45 | TranslationBundle
46 | } from '@jupyterlab/translation';
47 | import { removeColumnIcon, removeRowIcon } from './icons';
48 |
49 | const paletteCategory = 'Spreadsheet Editor';
50 |
51 | const FACTORY = 'Spreadsheet Editor';
52 |
53 | /**
54 | * A widget factory for editors.
55 | */
56 | export class SpreadsheetEditorFactory extends ABCWidgetFactory<
57 | IDocumentWidget,
58 | DocumentRegistry.ICodeModel
59 | > {
60 | /**
61 | * Create a new widget given a context.
62 | */
63 | protected createNewWidget(
64 | context: DocumentRegistry.CodeContext
65 | ): IDocumentWidget {
66 | const content = new SpreadsheetWidget(context);
67 | return new SpreadsheetEditorDocumentWidget({
68 | content,
69 | context,
70 | translator: this.translator
71 | });
72 | }
73 | }
74 |
75 | /**
76 | * Add File Editor undo and redo widgets to the Edit menu
77 | */
78 | export function addUndoRedoToEditMenu(menu: IMainMenu) {
79 | const isEnabled = (widget: Widget): boolean => {
80 | return (
81 | widget instanceof DocumentWidget &&
82 | widget.content instanceof SpreadsheetWidget
83 | );
84 | };
85 | menu.editMenu.undoers.undo.add({
86 | id: CommandIDs.undo,
87 | isEnabled
88 | });
89 |
90 | menu.editMenu.undoers.redo.add({
91 | id: CommandIDs.redo,
92 | isEnabled
93 | });
94 | }
95 |
96 | /**
97 | * Function to create a new untitled text file, given the current working directory.
98 | */
99 | function createNew(commands: CommandRegistry, cwd: string, ext = 'tsv') {
100 | return commands
101 | .execute('docmanager:new-untitled', {
102 | path: cwd,
103 | type: 'file',
104 | ext
105 | })
106 | .then(model => {
107 | return commands.execute('docmanager:open', {
108 | path: model.path,
109 | factory: FACTORY
110 | });
111 | });
112 | }
113 |
114 | /**
115 | * The command IDs used by the spreadsheet editor plugin.
116 | */
117 | export namespace CommandIDs {
118 | export const createNewCSV = 'spreadsheet-editor:create-new-csv-file';
119 |
120 | export const createNewTSV = 'spreadsheet-editor:create-new-tsv-file';
121 |
122 | export const undo = 'spreadsheet-editor:undo';
123 |
124 | export const redo = 'spreadsheet-editor:redo';
125 |
126 | export const copy = 'spreadsheet-editor:copy';
127 |
128 | export const paste = 'spreadsheet-editor:paste';
129 |
130 | export const insertRowBelow = 'spreadsheet-editor:insert-row-below';
131 |
132 | export const insertRowAbove = 'spreadsheet-editor:insert-row-above';
133 |
134 | export const insertColumnLeft = 'spreadsheet-editor:insert-column-left';
135 |
136 | export const insertColumnRight = 'spreadsheet-editor:insert-column-right';
137 |
138 | export const removeColumn = 'spreadsheet-editor:remove-column';
139 |
140 | export const removeRow = 'spreadsheet-editor:remove-row';
141 | }
142 |
143 | /**
144 | * Add Create New DSV File to the Launcher
145 | */
146 | export function addCreateNewToLauncher(
147 | launcher: ILauncher,
148 | trans: TranslationBundle
149 | ) {
150 | launcher.add({
151 | command: CommandIDs.createNewCSV,
152 | category: trans.__('Other'),
153 | rank: 3
154 | });
155 | launcher.add({
156 | command: CommandIDs.createNewTSV,
157 | category: trans.__('Other'),
158 | rank: 3
159 | });
160 | }
161 |
162 | /**
163 | * Add the New File command
164 | */
165 | export function addCreateNewCommands(
166 | commands: CommandRegistry,
167 | contextMenuHitTest: (
168 | fn: (node: HTMLElement) => boolean
169 | ) => HTMLElement | undefined,
170 | tracker: WidgetTracker>,
171 | browserFactory: IFileBrowserFactory,
172 | defaultBrowser: IDefaultFileBrowser,
173 | trans: TranslationBundle
174 | ) {
175 | const getJexcel = () => {
176 | return tracker.currentWidget?.content?.jexcel;
177 | };
178 |
179 | commands.addCommand(CommandIDs.undo, {
180 | label: trans.__('Undo'),
181 | icon: undoIcon.bindprops({ stylesheet: 'menuItem' }),
182 | execute: () => {
183 | const jexcel = getJexcel();
184 | if (!jexcel) {
185 | return;
186 | }
187 | jexcel.undo();
188 | },
189 | isEnabled: () => {
190 | const jexcel = getJexcel();
191 | if (!jexcel) {
192 | return false;
193 | }
194 | return jexcel.history.length !== 0 && jexcel.historyIndex !== 0;
195 | }
196 | });
197 |
198 | commands.addCommand(CommandIDs.redo, {
199 | label: trans.__('Redo'),
200 | icon: redoIcon.bindprops({ stylesheet: 'menuItem' }),
201 | execute: () => {
202 | const jexcel = getJexcel();
203 | if (!jexcel) {
204 | return;
205 | }
206 | jexcel.redo();
207 | },
208 | isEnabled: () => {
209 | const jexcel = getJexcel();
210 | if (!jexcel) {
211 | return false;
212 | }
213 | return (
214 | jexcel.history.length !== 0 &&
215 | jexcel.historyIndex !== jexcel.history.length - 1
216 | );
217 | }
218 | });
219 |
220 | commands.addCommand(CommandIDs.copy, {
221 | label: trans.__('Copy'),
222 | icon: copyIcon.bindprops({ stylesheet: 'menuItem' }),
223 | execute: () => {
224 | const jexcel = getJexcel();
225 | if (!jexcel) {
226 | return;
227 | }
228 | jexcel.copy(true);
229 | },
230 | isEnabled: () => !!getJexcel()
231 | });
232 |
233 | commands.addCommand(CommandIDs.paste, {
234 | label: trans.__('Paste'),
235 | icon: pasteIcon.bindprops({ stylesheet: 'menuItem' }),
236 | execute: async () => {
237 | const jexcel = getJexcel();
238 | if (!jexcel) {
239 | return;
240 | }
241 | const selection = jexcel.selectedCell;
242 | if (!selection) {
243 | return;
244 | }
245 | const text = await navigator.clipboard.readText();
246 | if (text) {
247 | jexcel.paste(selection[0] as number, selection[1] as number, text);
248 | }
249 | },
250 | isEnabled: () => !!getJexcel()
251 | });
252 |
253 | commands.addCommand(CommandIDs.insertRowBelow, {
254 | label: trans.__('Insert Row Below'),
255 | icon: addBelowIcon.bindprops({ stylesheet: 'menuItem' }),
256 | execute: () => {
257 | const jexcel = getJexcel();
258 | if (!jexcel) {
259 | return;
260 | }
261 | const cell = contextMenuHitTest(node => node.matches('td[data-y]'));
262 | const rowNumber = cell?.dataset.y
263 | ? parseInt(cell.dataset.y, 10)
264 | : undefined;
265 | jexcel.insertRow(1, rowNumber);
266 | },
267 | isEnabled: () => !!getJexcel()
268 | });
269 |
270 | commands.addCommand(CommandIDs.insertRowAbove, {
271 | label: trans.__('Insert Row Above'),
272 | icon: addAboveIcon.bindprops({ stylesheet: 'menuItem' }),
273 | execute: () => {
274 | const jexcel = getJexcel();
275 | if (!jexcel) {
276 | return;
277 | }
278 | const cell = contextMenuHitTest(node => node.matches('td[data-y]'));
279 | const rowNumber = cell?.dataset.y
280 | ? parseInt(cell.dataset.y, 10)
281 | : undefined;
282 | // @ts-expect-error (wrong typing for insertBefore as `number`, should be `boolean`)
283 | jexcel.insertRow(1, rowNumber, true);
284 | },
285 | isEnabled: () => !!getJexcel()
286 | });
287 |
288 | commands.addCommand(CommandIDs.insertColumnLeft, {
289 | label: trans.__('Insert Column To The Left'),
290 | icon: addIcon.bindprops({ stylesheet: 'menuItem' }),
291 | execute: () => {
292 | const jexcel = getJexcel();
293 | if (!jexcel) {
294 | return;
295 | }
296 | const cell = contextMenuHitTest(node => node.matches('td[data-x]'));
297 | const columnNumber = cell?.dataset.x
298 | ? parseInt(cell.dataset.x, 10)
299 | : undefined;
300 | jexcel.insertColumn(1, columnNumber, true);
301 | },
302 | isEnabled: () => !!getJexcel()
303 | });
304 |
305 | commands.addCommand(CommandIDs.insertColumnRight, {
306 | label: trans.__('Insert Column To The Right'),
307 | icon: addIcon.bindprops({ stylesheet: 'menuItem' }),
308 | execute: () => {
309 | const jexcel = getJexcel();
310 | if (!jexcel) {
311 | return;
312 | }
313 | const cell = contextMenuHitTest(node => node.matches('td[data-x]'));
314 | const columnNumber = cell?.dataset.x
315 | ? parseInt(cell.dataset.x, 10)
316 | : undefined;
317 | jexcel.insertColumn(1, columnNumber, false);
318 | },
319 | isEnabled: () => !!getJexcel()
320 | });
321 |
322 | commands.addCommand(CommandIDs.removeColumn, {
323 | label: trans.__('Delete Column'),
324 | icon: removeColumnIcon.bindprops({ stylesheet: 'menuItem' }),
325 | execute: () => {
326 | const jexcel = getJexcel();
327 | if (!jexcel) {
328 | return;
329 | }
330 | const cell = contextMenuHitTest(node => node.matches('td[data-x]'));
331 | if (!cell) {
332 | return;
333 | }
334 | const columnNumber = parseInt(cell.dataset.x!, 10);
335 | jexcel.deleteColumn(columnNumber);
336 | },
337 | isEnabled: () => !!getJexcel()
338 | });
339 |
340 | commands.addCommand(CommandIDs.removeRow, {
341 | label: trans.__('Delete Row'),
342 | icon: removeRowIcon.bindprops({ stylesheet: 'menuItem' }),
343 | execute: () => {
344 | const jexcel = getJexcel();
345 | if (!jexcel) {
346 | return;
347 | }
348 | const cell = contextMenuHitTest(node => node.matches('td[data-y]'));
349 | if (!cell) {
350 | return;
351 | }
352 | const rowNumber = parseInt(cell.dataset.y!, 10);
353 | jexcel.deleteRow(rowNumber);
354 | },
355 | isEnabled: () => !!getJexcel()
356 | });
357 |
358 | commands.addCommand(CommandIDs.createNewCSV, {
359 | label: args =>
360 | args['isPalette'] ? trans.__('New CSV File') : trans.__('CSV File'),
361 | caption: trans.__('Create a new CSV file'),
362 | icon: args => (args['isPalette'] ? undefined : spreadsheetIcon),
363 | execute: args => {
364 | const currentBrowser =
365 | browserFactory?.tracker.currentWidget ?? defaultBrowser;
366 | const cwd = args['cwd'] || currentBrowser.model.path;
367 | return createNew(commands, cwd as string, 'csv');
368 | }
369 | });
370 |
371 | commands.addCommand(CommandIDs.createNewTSV, {
372 | label: args =>
373 | args['isPalette'] ? trans.__('New TSV File') : trans.__('TSV File'),
374 | caption: trans.__('Create a new TSV file'),
375 | icon: args => (args['isPalette'] ? undefined : spreadsheetIcon),
376 | execute: args => {
377 | const currentBrowser =
378 | browserFactory?.tracker.currentWidget ?? defaultBrowser;
379 | const cwd = args['cwd'] || currentBrowser.model.path;
380 | return createNew(commands, cwd as string, 'tsv');
381 | }
382 | });
383 | }
384 |
385 | const PLUGIN_ID = 'spreadsheet-editor:plugin';
386 |
387 | /**
388 | * Initialization data for the spreadsheet-editor extension.
389 | */
390 | const extension: JupyterFrontEndPlugin = {
391 | id: PLUGIN_ID,
392 | autoStart: true,
393 | requires: [IFileBrowserFactory, IDefaultFileBrowser],
394 | optional: [
395 | ICommandPalette,
396 | ILauncher,
397 | IMainMenu,
398 | ILayoutRestorer,
399 | ISearchProviderRegistry,
400 | IStatusBar,
401 | ITranslator
402 | ],
403 | activate: (
404 | app: JupyterFrontEnd,
405 | browserFactory: IFileBrowserFactory,
406 | defaultBrowser: IDefaultFileBrowser,
407 | palette: ICommandPalette | null,
408 | launcher: ILauncher | null,
409 | menu: IMainMenu | null,
410 | restorer: ILayoutRestorer | null,
411 | searchregistry: ISearchProviderRegistry | null,
412 | statusBar: IStatusBar | null,
413 | translator: ITranslator | null
414 | ) => {
415 | translator = translator || nullTranslator;
416 | const trans = translator.load(PLUGIN_ID);
417 |
418 | const factory = new SpreadsheetEditorFactory({
419 | name: FACTORY,
420 | fileTypes: ['csv', 'tsv', '*'],
421 | defaultFor: ['csv', 'tsv'],
422 | translator: translator
423 | });
424 |
425 | const tracker = new WidgetTracker>({
426 | namespace: PLUGIN_ID
427 | });
428 |
429 | if (restorer) {
430 | void restorer.restore(tracker, {
431 | command: 'docmanager:open',
432 | args: widget => ({ path: widget.context.path, factory: FACTORY }),
433 | name: widget => widget.context.path
434 | });
435 | }
436 |
437 | app.docRegistry.addWidgetFactory(factory);
438 | const ft = app.docRegistry.getFileType('csv');
439 |
440 | factory.widgetCreated.connect((sender, widget) => {
441 | // Track the widget.
442 | void tracker.add(widget);
443 | // Notify the widget tracker if restore data needs to update.
444 | widget.context.pathChanged.connect(() => {
445 | void tracker.save(widget);
446 | });
447 |
448 | if (ft) {
449 | widget.title.icon = ft.icon!;
450 | widget.title.iconClass = ft.iconClass!;
451 | widget.title.iconLabel = ft.iconLabel!;
452 | }
453 | });
454 |
455 | if (searchregistry) {
456 | searchregistry.add(PLUGIN_ID, new SpreadsheetSearchProviderFactory());
457 | }
458 |
459 | addCreateNewCommands(
460 | app.commands,
461 | app.contextMenuHitTest.bind(app),
462 | tracker,
463 | browserFactory,
464 | defaultBrowser,
465 | trans
466 | );
467 |
468 | if (palette) {
469 | palette.addItem({
470 | command: CommandIDs.createNewCSV,
471 | args: { isPalette: true },
472 | category: paletteCategory
473 | });
474 | palette.addItem({
475 | command: CommandIDs.createNewTSV,
476 | args: { isPalette: true },
477 | category: paletteCategory
478 | });
479 | }
480 |
481 | if (launcher) {
482 | addCreateNewToLauncher(launcher, trans);
483 | }
484 |
485 | if (menu) {
486 | addUndoRedoToEditMenu(menu);
487 | }
488 |
489 | if (statusBar) {
490 | const item = new SelectionStatus(translator);
491 | // Keep the status item up-to-date with the current spreadsheet editor.
492 | tracker.currentChanged.connect(() => {
493 | const current = tracker.currentWidget;
494 | item.model.spreadsheetWidget = current?.content ?? null;
495 | });
496 |
497 | statusBar.registerStatusItem(PLUGIN_ID, {
498 | item,
499 | align: 'right',
500 | rank: 4,
501 | isActive: () =>
502 | !!app.shell.currentWidget &&
503 | !!tracker.currentWidget &&
504 | app.shell.currentWidget === tracker.currentWidget
505 | });
506 | }
507 | }
508 | };
509 |
510 | export default extension;
511 |
--------------------------------------------------------------------------------
/src/widget.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Jupyter Development Team.
2 | // Distributed under the terms of the Modified BSD License.
3 |
4 | import { Widget } from '@lumino/widgets';
5 | import { DocumentRegistry } from '@jupyterlab/docregistry';
6 | import { PromiseDelegate, UUID } from '@lumino/coreutils';
7 | import { PathExt } from '@jupyterlab/coreutils';
8 | import Papa from 'papaparse';
9 | import { Message } from '@lumino/messaging';
10 | import jspreadsheet from 'jspreadsheet-ce';
11 | import { Signal } from '@lumino/signaling';
12 | import { ICellCoordinates } from './searchprovider';
13 |
14 | type columnTypeId =
15 | | 'autocomplete'
16 | | 'calendar'
17 | | 'checkbox'
18 | | 'color'
19 | | 'dropdown'
20 | | 'hidden'
21 | | 'html'
22 | | 'image'
23 | | 'numeric'
24 | | 'radio'
25 | | 'text';
26 |
27 | export interface ISelection {
28 | rows: number;
29 | columns: number;
30 | }
31 |
32 | /**
33 | * An spreadsheet widget.
34 | */
35 | export class SpreadsheetWidget extends Widget {
36 | public jexcel: jspreadsheet.JspreadsheetInstance | null = null;
37 | protected separator: string;
38 | protected linebreak: string = '\n';
39 | public fitMode: 'all-equal-default' | 'all-equal-fit' | 'fit-cells';
40 | public changed: Signal;
41 | protected hasFrozenColumns: boolean;
42 | private editor: HTMLDivElement;
43 | private container: HTMLDivElement;
44 | private columnTypesBar: HTMLDivElement;
45 | private selectAllElement: HTMLElement | null = null;
46 |
47 | protected firstRowAsHeader: boolean;
48 | private header: Array | undefined = undefined;
49 | private columnTypes: Array = [];
50 | public selectionChanged: Signal;
51 |
52 | /**
53 | * Construct a new Spreadsheet widget.
54 | */
55 | constructor(context: DocumentRegistry.CodeContext) {
56 | super();
57 | this.id = UUID.uuid4();
58 | this.title.label = PathExt.basename(context.localPath);
59 | this.title.closable = true;
60 | this.context = context;
61 | this.separator = ''; // Papa auto detect
62 | this.fitMode = 'all-equal-default';
63 | if (context.localPath.endsWith('tsv')) {
64 | this.separator = '\t';
65 | }
66 | if (context.localPath.endsWith('csv')) {
67 | this.separator = ',';
68 | }
69 | this.hasFrozenColumns = false;
70 | this.firstRowAsHeader = false;
71 |
72 | context.ready
73 | .then(() => {
74 | this._onContextReady();
75 | })
76 | .catch(console.warn);
77 | this.changed = new Signal(this);
78 | this._selection = {
79 | rows: 0,
80 | columns: 0
81 | };
82 | this.selectionChanged = new Signal(this);
83 |
84 | const container = document.createElement('div');
85 | container.className = 'se-area-container';
86 | this.node.appendChild(container);
87 |
88 | // TODO: move to a separate class/widget
89 | this.columnTypesBar = document.createElement('div');
90 | this.columnTypesBar.classList.add('se-column-types');
91 | this.columnTypesBar.classList.add('se-hidden');
92 | this.columnTypeSelectors = new Map();
93 | container.appendChild(this.columnTypesBar);
94 |
95 | this.editor = document.createElement('div');
96 | container.appendChild(this.editor);
97 | this.container = container;
98 | }
99 |
100 | protected parseValue(content: string): jspreadsheet.CellValue[][] {
101 | const parsed = Papa.parse(content, { delimiter: this.separator });
102 | if (!this.separator) {
103 | this.separator = parsed.meta.delimiter;
104 | }
105 | this.linebreak = parsed.meta.linebreak;
106 | if (parsed.errors.length) {
107 | console.warn('Parsing errors encountered', parsed.errors);
108 | }
109 | const columnsNumber = this.extractColumnNumber(parsed.data);
110 | // TODO: read the actual type from a file?
111 | // TODO only redefine if reading for the first time?
112 | // TODO add/remove when column added removed?
113 | if (typeof this.columnTypes === 'undefined') {
114 | this.columnTypes = [
115 | ...Array(columnsNumber).map(() => 'text' as columnTypeId)
116 | ];
117 | }
118 |
119 | if (this.firstRowAsHeader) {
120 | this.header = parsed.data.shift();
121 | } else {
122 | this.header = undefined;
123 | }
124 |
125 | return parsed.data;
126 | }
127 |
128 | extractColumnNumber(data: jspreadsheet.CellValue[][]): number {
129 | return data.length ? data[0].length : 0;
130 | }
131 |
132 | columns(columnsNumber: number) {
133 | const columns: Array = [];
134 |
135 | for (let i = 0; i < columnsNumber; i++) {
136 | columns.push({
137 | title: this.header ? this.header[i] : undefined,
138 | type: this.columnTypes[i]
139 | });
140 | }
141 | return columns;
142 | }
143 |
144 | private onChange(): void {
145 | this.context.model.sharedModel.setSource(this.getValue());
146 | this.changed.emit();
147 | }
148 |
149 | public get selection(): ISelection {
150 | return this._selection;
151 | }
152 |
153 | private _selection: ISelection;
154 |
155 | private _onContextReady(): void {
156 | if (this.isDisposed) {
157 | return;
158 | }
159 | const contextModel = this.context.model;
160 |
161 | // Set the editor model value.
162 | const content = contextModel.toString();
163 | const data = this.parseValue(content);
164 |
165 | const options: jspreadsheet.JSpreadsheetOptions = {
166 | data: data,
167 | // Disable export since its shortcut conflicts with JupyterLab's default save shortcut
168 | allowExport: false,
169 | minDimensions: [1, 1],
170 | // minSpareCols: 1,
171 | // minSpareRows: 1,
172 | csvFileName: this.title.label,
173 | // @ts-expect-error (boolean missing in typing, but documented in repo)
174 | contextMenu: false,
175 | columnDrag: true,
176 | onchange: () => {
177 | this.onChange();
178 | },
179 | // insert
180 | oninsertrow: () => {
181 | this.onChange();
182 | },
183 | oninsertcolumn: () => {
184 | this.onChange();
185 | this.populateColumnTypesBar();
186 | this.onResize();
187 | },
188 | // move
189 | onmoverow: () => {
190 | this.onChange();
191 | },
192 | onmovecolumn: () => {
193 | this.onChange();
194 | },
195 | // delete
196 | ondeleterow: () => {
197 | this.onChange();
198 | },
199 | ondeletecolumn: () => {
200 | this.onChange();
201 | this.populateColumnTypesBar();
202 | this.onResize();
203 | },
204 | // resize
205 | onresizecolumn: () => {
206 | this.adjustColumnTypesWidth();
207 | },
208 | onselection: (
209 | el: HTMLElement,
210 | borderLeft: number,
211 | borderTop: number,
212 | borderRight: number,
213 | borderBottom: number,
214 | origin: any
215 | ) => {
216 | this._selection = {
217 | rows: borderBottom - borderTop + 1,
218 | columns: borderRight - borderLeft + 1
219 | };
220 | this.selectionChanged.emit(this._selection);
221 | // TODO: support all corners of selection
222 | this.scrollCellIntoView({ column: borderLeft, row: borderTop });
223 | },
224 | columns: this.columns(this.extractColumnNumber(data))
225 | };
226 |
227 | this.createEditor(options);
228 |
229 | // Wire signal connections.
230 | contextModel.contentChanged.connect(this._onContentChanged, this);
231 |
232 | this.populateColumnTypesBar();
233 |
234 | // If the sheet is not too big, use the more user-friendly columns width adjustment model
235 | if (data.length && data[0].length * data.length < 100 * 100) {
236 | this.fitMode = 'fit-cells';
237 | this.relayout();
238 | }
239 |
240 | // Resolve the ready promise.
241 | this._ready.resolve(undefined);
242 | }
243 |
244 | reloadEditor(options: jspreadsheet.JSpreadsheetOptions) {
245 | const config = this.jexcel!.getConfig();
246 | this.jexcel!.destroy();
247 | this.createEditor({
248 | ...config,
249 | ...options
250 | });
251 | this.relayout();
252 | }
253 |
254 | switchHeaders() {
255 | const value = this.getValue();
256 | this.firstRowAsHeader = !this.firstRowAsHeader;
257 | const data = this.parseValue(value);
258 | this.reloadEditor({
259 | data: data,
260 | columns: this.columns(this.extractColumnNumber(data))
261 | });
262 | }
263 |
264 | switchTypesBar() {
265 | this.columnTypesBar.classList.toggle('se-hidden');
266 | }
267 |
268 | columnTypeSelectors: Map;
269 |
270 | protected populateColumnTypesBar() {
271 | // TODO: interface with name, id, options callback?
272 | const options = [
273 | 'text',
274 | 'numeric',
275 | 'hidden',
276 | 'dropdown',
277 | 'autocomplete',
278 | 'checkbox',
279 | 'radio',
280 | 'calendar',
281 | 'image',
282 | 'color',
283 | 'html'
284 | ];
285 | this.columnTypesBar.innerHTML = '';
286 | this.columnTypeSelectors.clear();
287 | for (let columnId = 0; columnId < this.columnsNumber; columnId++) {
288 | // TODO react widget
289 | const columnTypeSelector = document.createElement('select');
290 | for (const option of options) {
291 | const optionElement = document.createElement('option');
292 | optionElement.value = option;
293 | optionElement.text = option;
294 | columnTypeSelector.appendChild(optionElement);
295 | }
296 | columnTypeSelector.onchange = ev => {
297 | const select = ev.target as HTMLSelectElement;
298 | this.columnTypes[columnId] = select.options[select.selectedIndex]
299 | .value as columnTypeId;
300 | this.reloadEditor({ columns: this.columns(this.columnTypes.length) });
301 | };
302 | this.columnTypeSelectors.set(columnId, columnTypeSelector);
303 | this.columnTypesBar.appendChild(columnTypeSelector);
304 | }
305 | }
306 |
307 | protected adjustColumnTypesWidth() {
308 | if (!this.selectAllElement || this.columnTypeSelectors.size === 0) {
309 | return;
310 | }
311 | this.columnTypesBar.style.marginLeft =
312 | this.selectAllElement.offsetWidth + 'px';
313 | const widths = this.jexcel!.getWidth(undefined);
314 | for (let columnId = 0; columnId < this.columnsNumber; columnId++) {
315 | this.columnTypeSelectors.get(columnId)!.style.width =
316 | widths[columnId] + 'px';
317 | }
318 | }
319 |
320 | protected createEditor(options: jspreadsheet.JSpreadsheetOptions) {
321 | this.jexcel = jspreadsheet(this.editor, options);
322 | this.selectAllElement =
323 | this.jexcel.headerContainer.querySelector('.jexcel_selectall');
324 | }
325 |
326 | protected onAfterShow(msg: Message) {
327 | super.onAfterShow(msg);
328 | this.relayout();
329 | }
330 |
331 | get ready(): Promise {
332 | return this._ready.promise;
333 | }
334 |
335 | getValue(): string {
336 | const data = this.jexcel!.getData();
337 | if (this.firstRowAsHeader) {
338 | data.unshift(this.jexcel!.getHeaders(true) as string[]);
339 | }
340 | return Papa.unparse(data, {
341 | delimiter: this.separator,
342 | newline: this.linebreak
343 | });
344 | }
345 |
346 | setValue(value: string) {
347 | const parsed = this.parseValue(value);
348 | this.jexcel!.setData(parsed);
349 | }
350 |
351 | private _onContentChanged(): void {
352 | const oldValue = this.getValue();
353 | const newValue = this.context.model.toString();
354 |
355 | if (oldValue !== newValue) {
356 | this.setValue(newValue);
357 | }
358 | }
359 |
360 | get wrapper() {
361 | if (this.hasFrozenColumns) {
362 | return this.jexcel!.content;
363 | }
364 | return this.container;
365 | }
366 |
367 | onResize() {
368 | if (!this.jexcel) {
369 | return;
370 | }
371 | if (this.fitMode === 'all-equal-fit') {
372 | this.relayout();
373 | }
374 | if (this.hasFrozenColumns) {
375 | this.jexcel.content.style.width = this.node.offsetWidth + 'px';
376 | this.jexcel.content.style.height =
377 | (this.node.querySelector('.jexcel_content') as HTMLElement)
378 | .offsetHeight + 'px';
379 | }
380 | this.adjustColumnTypesWidth();
381 | }
382 |
383 | protected onActivateRequest(msg: Message): void {
384 | // ensure focus
385 | // TODO
386 | // this.jexcel.el.focus();
387 | this.editor.focus();
388 | }
389 |
390 | dispose(): void {
391 | if (this.jexcel) {
392 | this.jexcel.destroy();
393 | }
394 | super.dispose();
395 | }
396 |
397 | updateModel() {
398 | this.context.model.sharedModel.setSource(this.getValue());
399 | }
400 |
401 | freezeSelectedColumns() {
402 | const columns = this.jexcel!.getSelectedColumns();
403 | this.reloadEditor({
404 | freezeColumns: Math.max(...columns) + 1,
405 | tableOverflow: true,
406 | tableWidth: this.node.offsetWidth + 'px',
407 | tableHeight: this.node.offsetHeight + 'px'
408 | });
409 | this.hasFrozenColumns = true;
410 | }
411 |
412 | unfreezeColumns() {
413 | this.reloadEditor({
414 | freezeColumns: undefined,
415 | tableOverflow: false,
416 | tableWidth: undefined,
417 | tableHeight: undefined
418 | });
419 | this.hasFrozenColumns = false;
420 | }
421 |
422 | get columnsNumber(): number {
423 | const data = this.jexcel!.getData();
424 | if (!data.length) {
425 | return 0;
426 | }
427 | return data[0].length;
428 | }
429 |
430 | getHeaderElements() {
431 | const headers = [];
432 | for (const element of this.jexcel!.headerContainer.children) {
433 | // TODO use data attribute?
434 | if (element.className !== 'jexcel_selectall') {
435 | headers.push(element);
436 | }
437 | }
438 | return headers;
439 | }
440 |
441 | relayout() {
442 | if (!this.jexcel) {
443 | return;
444 | }
445 | const columns = this.columnsNumber;
446 |
447 | if (!columns) {
448 | return;
449 | }
450 |
451 | switch (this.fitMode) {
452 | case 'all-equal-default': {
453 | const options = this.jexcel.getConfig();
454 | for (let i = 0; i < columns; i++) {
455 | this.jexcel.setWidth(i, options.defaultColWidth, undefined);
456 | }
457 | break;
458 | }
459 | case 'all-equal-fit': {
460 | const indexColumn = this.node.querySelector(
461 | '.jexcel_selectall'
462 | ) as HTMLElement;
463 | const availableWidth = this.node.clientWidth - indexColumn.offsetWidth;
464 | const widthPerColumn = availableWidth / columns;
465 | for (let i = 0; i < columns; i++) {
466 | this.jexcel.setWidth(i, widthPerColumn, undefined);
467 | }
468 | break;
469 | }
470 | case 'fit-cells': {
471 | const data = this.jexcel.getData();
472 | const headers = this.getHeaderElements();
473 | for (let i = 0; i < columns; i++) {
474 | let maxColumnWidth = Math.max(25, headers[i].scrollWidth);
475 | for (let j = 0; j < data.length; j++) {
476 | const cell = this.jexcel.getCellFromCoords(i, j) as HTMLElement;
477 | maxColumnWidth = Math.max(maxColumnWidth, cell.scrollWidth);
478 | }
479 | this.jexcel.setWidth(i, maxColumnWidth, undefined);
480 | }
481 | break;
482 | }
483 | }
484 | this.adjustColumnTypesWidth();
485 | }
486 |
487 | context: DocumentRegistry.CodeContext;
488 | private _ready = new PromiseDelegate();
489 |
490 | scrollCellIntoView(match: ICellCoordinates) {
491 | const cell = this.jexcel!.getCellFromCoords(match.column, match.row);
492 | const cellRect = cell.getBoundingClientRect();
493 | const wrapperRect = this.wrapper.getBoundingClientRect();
494 | let alignToTop = false;
495 | const softMargin = 3;
496 |
497 | if (cellRect.right > wrapperRect.right) {
498 | this.wrapper.scrollBy(cellRect.right - wrapperRect.right, 0);
499 | } else if (cellRect.left < wrapperRect.left) {
500 | this.wrapper.scrollBy(cellRect.left - wrapperRect.left, 0);
501 | }
502 |
503 | if (cellRect.top - softMargin < wrapperRect.top) {
504 | alignToTop = true;
505 | }
506 | if (alignToTop || cellRect.bottom + softMargin > wrapperRect.bottom) {
507 | cell.scrollIntoView(alignToTop);
508 | }
509 | }
510 | }
511 |
--------------------------------------------------------------------------------