├── docs ├── references.bib ├── assets │ ├── images │ │ ├── logo.png │ │ ├── icon.svg │ │ └── logo.svg │ └── stylesheets │ │ ├── extra.css │ │ └── full-width-mkdocs.css ├── components │ ├── table.md │ ├── annotated-image.md │ ├── annotated-text.md │ └── index.md ├── demos │ ├── quaero-explorer.md │ └── index.md ├── tutorials │ ├── index.md │ ├── customize-explorer.md │ ├── new-project.md │ └── run-quaero-explorer.md ├── installation.md └── index.md ├── metanno ├── recipes │ └── __init__.py ├── version.py └── __init__.py ├── client ├── index.ts ├── components │ ├── index.ts │ ├── BooleanInput │ │ ├── style.css │ │ └── index.tsx │ ├── DraggableHeaderRenderer │ │ └── index.tsx │ ├── Table │ │ └── style.css │ ├── AnnotatedText │ │ ├── style.css │ │ ├── tokenize.tsx │ │ └── index.tsx │ ├── InputSuggest │ │ ├── style.css │ │ └── index.tsx │ └── AnnotatedImage │ │ └── index.tsx ├── dist-globals.ts ├── utils │ ├── replaceObject.ts │ ├── arrayEquals.ts │ ├── keyboard.ts │ ├── index.ts │ ├── currentEvent.ts │ ├── hooks.ts │ ├── memoize.ts │ ├── textRange.ts │ └── reconcile.ts ├── plugin.ts ├── icon.svg ├── style.css └── types.ts ├── .gitmodules ├── pret └── ui │ └── metanno │ └── __init__.py ├── install.json ├── tsconfig.json ├── .github └── workflows │ ├── delete-preview-docs.yml │ ├── docs.yml │ ├── release.yml │ └── tests.yml ├── playwright.config.ts ├── CITATION.cff ├── galata_config.py ├── tests └── jupyter │ ├── todo_html │ ├── TodoHTML.ipynb │ ├── todoapp.py │ └── run.spec.ts │ ├── quaero_app │ ├── Quaero.ipynb │ └── run.spec.ts │ └── run.sh ├── webpack.jupyter.js ├── LICENSE ├── .pre-commit-config.yaml ├── package.json ├── .prettierignore ├── .gitignore ├── mkdocs.yml ├── pyproject.toml ├── CHANGELOG.md ├── README.md └── examples └── quaero.py /docs/references.bib: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /metanno/recipes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /metanno/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.0-beta.5" 2 | -------------------------------------------------------------------------------- /client/index.ts: -------------------------------------------------------------------------------- 1 | import '@pret-globals'; 2 | 3 | import "./style.css"; 4 | -------------------------------------------------------------------------------- /docs/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percevalw/metanno/HEAD/docs/assets/images/logo.png -------------------------------------------------------------------------------- /metanno/__init__.py: -------------------------------------------------------------------------------- 1 | from .ui import AnnotatedText, AnnotatedImage, Table # noqa: F401,F403 2 | from .version import __version__ # noqa: F401 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/react-data-grid"] 2 | path = submodules/react-data-grid 3 | url = https://github.com/percevalw/react-data-grid.git 4 | -------------------------------------------------------------------------------- /client/components/index.ts: -------------------------------------------------------------------------------- 1 | export { AnnotatedText } from './AnnotatedText'; 2 | export { Table } from './Table'; 3 | export { AnnotatedImage } from './AnnotatedImage'; 4 | -------------------------------------------------------------------------------- /pret/ui/metanno/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Backward compatibility shim for the previous `pret.ui.metanno` import path. 3 | """ 4 | from metanno.ui import * # noqa: F401,F403 5 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "metanno", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package metanno" 5 | } 6 | -------------------------------------------------------------------------------- /client/dist-globals.ts: -------------------------------------------------------------------------------- 1 | import { AnnotatedText as i_0, Table as i_1, AnnotatedImage as i_2 } from './components'; 2 | 3 | (window as any).Metanno = {'AnnotatedText': i_0, 'Table': i_1, 'AnnotatedImage': i_2}; 4 | -------------------------------------------------------------------------------- /client/utils/replaceObject.ts: -------------------------------------------------------------------------------- 1 | 2 | export const replaceObject = (obj: object, new_obj: object) => { 3 | Object.keys(obj).forEach(key => { 4 | delete obj[key]; 5 | }) 6 | Object.assign(obj, new_obj); 7 | } 8 | -------------------------------------------------------------------------------- /docs/components/table.md: -------------------------------------------------------------------------------- 1 | # Table {: #metanno.ui.Table } 2 | 3 | ::: metanno.ui.Table 4 | options: 5 | heading_level: 2 6 | show_bases: false 7 | show_source: false 8 | only_class_level: true 9 | -------------------------------------------------------------------------------- /docs/demos/quaero-explorer.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ```python { .render-with-pret .full-width } 4 | from examples.quaero import app 5 | 6 | 7 | app(deduplicate=True)[0] 8 | ``` 9 | -------------------------------------------------------------------------------- /docs/components/annotated-image.md: -------------------------------------------------------------------------------- 1 | # AnnotatedImage {: #metanno.ui.AnnotatedImage } 2 | 3 | ::: metanno.ui.AnnotatedImage 4 | options: 5 | heading_level: 2 6 | show_bases: false 7 | show_source: false 8 | only_class_level: true 9 | -------------------------------------------------------------------------------- /docs/components/annotated-text.md: -------------------------------------------------------------------------------- 1 | # AnnotatedText {: #metanno.ui.AnnotatedText } 2 | 3 | ::: metanno.ui.AnnotatedText 4 | options: 5 | heading_level: 2 6 | show_bases: false 7 | show_source: false 8 | only_class_level: true 9 | -------------------------------------------------------------------------------- /client/plugin.ts: -------------------------------------------------------------------------------- 1 | // noinspection ES6UnusedImports 2 | import '@pret-globals'; 3 | 4 | import "./style.css"; 5 | 6 | const plugin = { 7 | id: "metanno:plugin", // app 8 | activate: () => null, 9 | autoStart: true, 10 | }; 11 | 12 | export default plugin; 13 | -------------------------------------------------------------------------------- /client/components/BooleanInput/style.css: -------------------------------------------------------------------------------- 1 | .rdg-checkbox-container { 2 | display: flex; 3 | height: 100%; 4 | align-items: center; 5 | justify-content: center; 6 | } 7 | 8 | .rdg-checkbox-container > .rdg-checkbox-label { 9 | position: relative; 10 | } 11 | -------------------------------------------------------------------------------- /client/utils/arrayEquals.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const arrayEquals = (a1: any[], a2: any[]) => { 4 | // https://stackoverflow.com/questions/7837456/how-to-compare-arrays-in-javascript/7837725#7837725 5 | let i = a1.length; 6 | while (i--) { 7 | if (a1[i] !== a2[i]) return false; 8 | } 9 | return true 10 | }; 11 | -------------------------------------------------------------------------------- /docs/demos/index.md: -------------------------------------------------------------------------------- 1 | # Demos 2 | 3 | Here are some examples of applications you can build with Metanno. 4 | 5 | 6 | 7 | === card {: href=/demos/quaero-explorer/ } 8 | 9 | :fontawesome-solid-compass: 10 | **Quaero Explorer** 11 | 12 | --- 13 | Explore and edit the Quaero dataset. 14 | 15 | 16 | -------------------------------------------------------------------------------- /client/utils/keyboard.ts: -------------------------------------------------------------------------------- 1 | 2 | export const makeModKeys = (event: React.KeyboardEvent | React.MouseEvent | React.TouchEvent): string[] => { 3 | const modkeys = []; 4 | if (event.shiftKey) 5 | modkeys.push("Shift"); 6 | if (event.metaKey) 7 | modkeys.push("Meta"); 8 | if (event.ctrlKey) 9 | modkeys.push("Control"); 10 | return modkeys; 11 | }; 12 | -------------------------------------------------------------------------------- /client/utils/index.ts: -------------------------------------------------------------------------------- 1 | export {memoize} from "./memoize"; 2 | export {replaceObject} from "./replaceObject"; 3 | export {cachedReconcile} from "./reconcile"; 4 | export {makeModKeys} from "./keyboard"; 5 | export {getDocumentSelectedRanges} from "./textRange"; 6 | export {arrayEquals} from "./arrayEquals"; 7 | export {getCurrentEvent} from "./currentEvent"; 8 | export {useEventCallback, useCachedReconcile} from "./hooks"; 9 | -------------------------------------------------------------------------------- /client/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/assets/images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/utils/currentEvent.ts: -------------------------------------------------------------------------------- 1 | const currentEvent: {current: Event} = { 2 | current: null, 3 | }; 4 | 5 | document.addEventListener("click", setCurrentEvent, {capture: true}); 6 | document.addEventListener("mousedown", setCurrentEvent, {capture: true}); 7 | document.addEventListener("mouseup", setCurrentEvent, {capture: true}); 8 | 9 | function setCurrentEvent(event: Event) { 10 | currentEvent.current = event; 11 | } 12 | 13 | export function getCurrentEvent() { 14 | return currentEvent.current; 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["client"], 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "es5", 6 | "lib": [ 7 | "es2020", 8 | "dom", 9 | "es2021" 10 | ], 11 | "moduleResolution": "node", 12 | "sourceMap": true, 13 | "rootDirs": ["client"], 14 | "allowJs": true, 15 | "outDir": "lib", 16 | "strict": false, 17 | "allowSyntheticDefaultImports": true, 18 | "jsx": "react-jsx", 19 | "skipLibCheck": true, 20 | "downlevelIteration": true, 21 | "paths": { 22 | "react": [ "./node_modules/@types/react" ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/delete-preview-docs.yml: -------------------------------------------------------------------------------- 1 | name: Delete preview docs 2 | 3 | on: 4 | workflow_dispatch: 5 | delete: 6 | 7 | jobs: 8 | delete: 9 | name: Delete Vercel Project 10 | if: github.event.ref_type == 'branch' 11 | runs-on: ubuntu-latest 12 | steps: 13 | - run: | 14 | # Set up Vercel 15 | npm install --global vercel@latest 16 | # Pull Vercel environment 17 | vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} 18 | # Delete vercel project linked to this branch 19 | vercel remove metanno-${{ github.event.ref }} --yes --token=${{ secrets.VERCEL_TOKEN }} 20 | -------------------------------------------------------------------------------- /client/style.css: -------------------------------------------------------------------------------- 1 | /* Light Theme, feel free to override */ 2 | :root[data-theme="light"] { 3 | --metanno-color: #000; 4 | --metanno-background-color: #fff; 5 | } 6 | 7 | /* Dark Theme, feel free to override */ 8 | :root[data-theme="dark"] { 9 | --metanno-color: #fff; 10 | --metanno-background-color: #1f1d1d; 11 | } 12 | 13 | :root, .metanno-table > .rdg { 14 | /* --metanno-font-size: 1rem; */ 15 | --metanno-font-family: Verdana, --apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 16 | --metanno-box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.24); 17 | } 18 | 19 | .jp-OutputArea { 20 | --metanno-font-size: var(--jp-code-font-size); 21 | } 22 | -------------------------------------------------------------------------------- /docs/components/index.md: -------------------------------------------------------------------------------- 1 | # Building Blocks of Metanno 2 | 3 | This page provides an overview of the base components available in Metanno. 4 | 5 | 6 | 7 | | Component | Description | 8 | |---------------------------------------------|------------------------------------------------------------| 9 | | [AnnotatedText][metanno.ui.AnnotatedText] | View and interact with a text with optional annotations. | 10 | | [AnnotatedImage][metanno.ui.AnnotatedImage] | View and interact with an image with optional annotations. | 11 | | [Table][metanno.ui.Table] | View and interact with a table | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | import "@jupyterlab/galata/lib/playwright-config"; 3 | 4 | export default defineConfig<{ 5 | python: string; 6 | }>({ 7 | use: { 8 | // jupyter url 9 | baseURL: "http://localhost:8889", 10 | }, 11 | timeout: 60_000, 12 | reporter: [ 13 | [ 14 | "blob", 15 | { outputFile: `./blob-report/report-${process.env.PYTHON_VERSION}.zip` }, 16 | ], 17 | ], 18 | projects: [ 19 | { 20 | name: `chromium-${process.env.PYTHON_VERSION}`, 21 | use: { ...devices["Desktop Chrome"] }, 22 | }, 23 | { 24 | name: `firefox-${process.env.PYTHON_VERSION}`, 25 | use: { ...devices["Desktop Firefox"] }, 26 | }, 27 | { 28 | name: `webkit-${process.env.PYTHON_VERSION}`, 29 | use: { ...devices["Desktop Safari"] }, 30 | }, 31 | ], 32 | }); 33 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # This CITATION.cff file was generated with cffinit. 2 | # Visit https://bit.ly/cffinit to generate yours today! 3 | 4 | cff-version: 1.2.0 5 | title: >- 6 | Metanno: a modular annotator building framework 7 | message: If you use Metanno, please cite us as below. 8 | type: software 9 | authors: 10 | - given-names: Perceval 11 | family-names: Wajsbürt 12 | repository-code: "https://github.com/percevalw/metanno" 13 | abstract: >- 14 | Metanno is a Python package designed for creating custom annotation interfaces in JupyterLab. 15 | Key features include: 16 | - modularity: configurable number of views of the data 17 | - customization: easy customization of the software behavior in Python 18 | - interactivity: live access to the annotations from Python 19 | keywords: 20 | - nlp 21 | - framework 22 | - modular 23 | - jupyter 24 | - annotation 25 | - ner 26 | - tabular 27 | license: MIT 28 | year: 2023 29 | -------------------------------------------------------------------------------- /galata_config.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: F821 2 | import getpass 3 | import os 4 | from pathlib import Path 5 | from tempfile import mkdtemp 6 | 7 | import jupyterlab 8 | 9 | # Test if we are running in a docker 10 | if getpass.getuser() == "jovyan": 11 | c.ServerApp.ip = "0.0.0.0" # noqa S104 12 | 13 | 14 | c.ServerApp.port = 8889 15 | c.ServerApp.port_retries = 0 16 | c.ServerApp.open_browser = False 17 | c.LabServerApp.extra_labextensions_path = str( 18 | Path(jupyterlab.__file__).parent / "galata" 19 | ) # noqa: E501 20 | 21 | c.LabApp.workspaces_dir = mkdtemp(prefix="galata-workspaces-") 22 | 23 | c.ServerApp.root_dir = os.environ.get( 24 | "JUPYTERLAB_GALATA_ROOT_DIR", mkdtemp(prefix="galata-test-") 25 | ) 26 | c.IdentityProvider.token = "" 27 | c.ServerApp.token = "" # For python 3.7, otherwise IdentityProvider.token is used 28 | c.ServerApp.password = "" 29 | c.ServerApp.disable_check_xsrf = True 30 | c.LabApp.expose_app_in_browser = True 31 | -------------------------------------------------------------------------------- /tests/jupyter/todo_html/TodoHTML.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "metadata": {}, 5 | "cell_type": "code", 6 | "outputs": [], 7 | "execution_count": null, 8 | "source": [ 9 | "from todoapp import TodoApp, state # noqa\n", 10 | "\n", 11 | "TodoApp()" 12 | ], 13 | "id": "56761f9f6fd7676c" 14 | } 15 | ], 16 | "metadata": { 17 | "kernelspec": { 18 | "display_name": "Python 3", 19 | "language": "python", 20 | "name": "python3" 21 | }, 22 | "language_info": { 23 | "codemirror_mode": { 24 | "name": "ipython", 25 | "version": 3 26 | }, 27 | "file_extension": ".py", 28 | "mimetype": "text/x-python", 29 | "name": "python", 30 | "nbconvert_exporter": "python", 31 | "pygments_lexer": "ipython3", 32 | "version": "3.8.20" 33 | }, 34 | "widgets": { 35 | "application/vnd.jupyter.widget-state+json": { 36 | "state": {}, 37 | "version_major": 2, 38 | "version_minor": 0 39 | } 40 | } 41 | }, 42 | "nbformat": 4, 43 | "nbformat_minor": 5 44 | } 45 | -------------------------------------------------------------------------------- /webpack.jupyter.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | // mode: 'development', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.[jt]sx?$/, 9 | use: 'ts-loader', 10 | exclude: /node_modules|submodules/, 11 | }, 12 | { 13 | test: /\.py$/, 14 | type: 'asset/inline', 15 | generator: { 16 | dataUrl: content => content.toString(), 17 | }, 18 | }, 19 | { 20 | test: /\.m?js/, 21 | resolve: { 22 | fullySpecified: false 23 | } 24 | }, 25 | ], 26 | }, 27 | resolve: { 28 | extensions: ['.tsx', '.ts', '.js', '.py', '.css'], 29 | alias: {'@pret-globals': path.resolve('client/dist-globals.ts')} 30 | }, 31 | optimization: { 32 | usedExports: true, 33 | }, 34 | cache: { 35 | type: 'filesystem', 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Perceval Wajsburt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: no-commit-to-branch 9 | - id: end-of-file-fixer 10 | - id: check-yaml 11 | args: ["--unsafe"] 12 | - id: check-toml 13 | - id: check-json 14 | - id: check-symlinks 15 | - id: check-docstring-first 16 | - id: check-added-large-files 17 | - id: detect-private-key 18 | # ruff 19 | - repo: https://github.com/charliermarsh/ruff-pre-commit 20 | # Ruff version. 21 | rev: 'v0.9.6' 22 | hooks: 23 | - id: ruff 24 | args: ['--config', 'pyproject.toml', '--fix', '--show-fixes'] 25 | - id: ruff-format 26 | args: ['--config', 'pyproject.toml', '--diff'] 27 | - id: ruff-format 28 | args: ['--config', 'pyproject.toml'] 29 | - repo: https://github.com/asottile/blacken-docs 30 | rev: 1.19.1 31 | hooks: 32 | - id: blacken-docs 33 | additional_dependencies: [black==22.12.0] 34 | exclude: notebooks/ 35 | -------------------------------------------------------------------------------- /docs/tutorials/index.md: -------------------------------------------------------------------------------- 1 | # Tutorials 2 | 3 | Here is a list of tutorials to help you learn how to use Metanno. 4 | 5 | For a deeper understanding of the framework, check out the [Pret tutorials](https://percevalw.github.io/pret/main/tutorials/). 6 | 7 | 8 | 9 | === card {: href=/tutorials/new-project/ } 10 | 11 | :fontawesome-solid-plus: 12 | **Set up a new project** 13 | 14 | --- 15 | Learn how to install Metanno in a new project or add it to an existing one. 16 | 17 | === card {: href=/tutorials/run-quaero-explorer/ } 18 | 19 | :fontawesome-solid-gear: 20 | **Run the Data Explorer demo** 21 | 22 | --- 23 | Run the Quaero Explorer demo app and discover its features: persisting annotations, collaborating, and more. 24 | 25 | === card {: href=/tutorials/customize-explorer/ } 26 | 27 | :fontawesome-solid-robot: 28 | **Customize the Data Explorer** 29 | 30 | --- 31 | Learn how to pre-annotate entities using EDS-NLP and annotate higher-level structures in the Data Explorer. 32 | 33 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/jupyter/quaero_app/Quaero.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "b9561b94-fec8-4efe-8959-963cd6881733", 7 | "metadata": { 8 | "tags": [] 9 | }, 10 | "outputs": [], 11 | "source": [ 12 | "from examples.quaero import app" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "id": "49af05e6-1b87-4c54-ad66-369be09ff908", 19 | "metadata": { 20 | "tags": [] 21 | }, 22 | "outputs": [], 23 | "source": [ 24 | "view, handles = app(deduplicate=True)" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "id": "14bee694-f684-4d26-a3f7-5d901da11c23", 31 | "metadata": { 32 | "tags": [] 33 | }, 34 | "outputs": [], 35 | "source": [ 36 | "view" 37 | ] 38 | } 39 | ], 40 | "metadata": { 41 | "kernelspec": { 42 | "display_name": "py310", 43 | "language": "python", 44 | "name": "py310" 45 | }, 46 | "language_info": { 47 | "codemirror_mode": { 48 | "name": "ipython", 49 | "version": 3 50 | }, 51 | "file_extension": ".py", 52 | "mimetype": "text/x-python", 53 | "name": "python", 54 | "nbconvert_exporter": "python", 55 | "pygments_lexer": "ipython3", 56 | "version": "3.10.16" 57 | } 58 | }, 59 | "nbformat": 4, 60 | "nbformat_minor": 5 61 | } 62 | -------------------------------------------------------------------------------- /client/utils/hooks.ts: -------------------------------------------------------------------------------- 1 | import {useCallback, useEffect, useRef} from 'react'; 2 | import { internalReconcile } from "./reconcile"; 3 | 4 | export function useEventCallback(callback) { 5 | const callbackRef = useRef(callback); 6 | useEffect(() => { 7 | callbackRef.current = callback; 8 | }); 9 | return useCallback((...args) => callbackRef.current(...args), []); 10 | } 11 | 12 | 13 | /** 14 | * React-hook version of `cachedReconcile`. 15 | * 16 | * ```ts 17 | * const select = useCachedReconcile( 18 | * (value: number) => ({ a: { subkey: 3 }, b: value }) 19 | * ); 20 | * 21 | * const res4 = select(4); 22 | * const res5 = select(5); 23 | * // res4 !== res5 (object identity changes) 24 | * // res4.a === res5.a (un-changed slice is preserved) 25 | * ``` 26 | * 27 | * @param fn — pure selector whose output you want to reconcile 28 | * @returns — memoised selector that maximises referential equality 29 | */ 30 | export function useCachedReconcile( 31 | fn: (...args: Inputs) => Output 32 | ): (...args: Inputs) => Output { 33 | const cacheRef = useRef(null); 34 | const reconciledSelector = useCallback( 35 | (...args: Inputs) => { 36 | const next = fn(...args); 37 | 38 | const didReconcile = internalReconcile(next, cacheRef.current); 39 | 40 | if (!didReconcile) cacheRef.current = next; 41 | 42 | return cacheRef.current as Output; 43 | }, 44 | [fn] 45 | ); 46 | 47 | return reconciledSelector; 48 | } 49 | -------------------------------------------------------------------------------- /tests/jupyter/todo_html/todoapp.py: -------------------------------------------------------------------------------- 1 | from pret import component, create_store, use_state, use_store_snapshot 2 | 3 | # Pending deprecation, prefer pret.react 4 | from pret.react import div, input, label, p 5 | 6 | state = create_store( 7 | { 8 | "faire à manger": True, 9 | "faire la vaisselle": False, 10 | }, 11 | sync=True, 12 | ) 13 | 14 | 15 | @component 16 | def TodoApp(): 17 | todos = use_store_snapshot(state) 18 | typed, set_typed = use_state("") 19 | num_remaining = sum(not ok for ok in todos.values()) 20 | plural = "s" if num_remaining > 1 else "" 21 | 22 | def on_key_down(event): 23 | if event.key == "Enter": 24 | state[typed] = False 25 | set_typed("") 26 | 27 | return div( 28 | *( 29 | div( 30 | input( 31 | checked=ok, 32 | type="checkbox", 33 | id=todo.replace(" ", "-"), 34 | on_change=lambda e, t=todo: state.update({t: e.target.checked}), 35 | ), 36 | label(todo, **{"for": todo.replace(" ", "-")}), 37 | ) 38 | for todo, ok in todos.items() 39 | ), 40 | input( 41 | value=typed, 42 | on_change=lambda event: set_typed(event.target.value), 43 | on_key_down=on_key_down, 44 | placeholder="Add a todo", 45 | ), 46 | p(f"Number of unfinished todo{plural}: {num_remaining}", level="body-md"), 47 | ) 48 | -------------------------------------------------------------------------------- /client/utils/memoize.ts: -------------------------------------------------------------------------------- 1 | import isEqual from "react-fast-compare"; 2 | 3 | export {isEqual}; 4 | 5 | export const shallowCompare = (obj1: object, obj2: object) => 6 | obj1 === obj2 || 7 | (typeof obj1 === 'object' && typeof obj2 == 'object' && 8 | obj1 !== null && obj2 !== null && 9 | Object.keys(obj1).length === Object.keys(obj2).length && 10 | Object.keys(obj1).every(key => obj2.hasOwnProperty(key) && obj1[key] === obj2[key])); 11 | 12 | 13 | export const memoize = ( 14 | factory: (...rest: Inputs) => Output, 15 | checkDeps: (...rest: Inputs) => any = ((...rest) => rest.length > 0 ? rest[0] : null), 16 | shallow: boolean = false, 17 | post: boolean = false, 18 | ): (...rest: Inputs) => Output => { 19 | let last = null; 20 | let cache: Output = null; 21 | return (...args: Inputs): Output => { 22 | if (post) { 23 | const new_state = factory(...args); 24 | if (!(shallow && shallowCompare(new_state, cache) || !shallow && isEqual(new_state, cache))) { 25 | cache = new_state; 26 | } 27 | return cache; 28 | } 29 | else { 30 | const state = checkDeps(...args); 31 | if (!(shallow && shallowCompare(last, state) && last !== null || !shallow && isEqual(last, state) && last !== null)) { 32 | last = state; 33 | cache = factory(...args); 34 | } 35 | return cache; 36 | } 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build documentation 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main, master ] 7 | env: 8 | BRANCH_NAME: ${{ github.head_ref || github.ref_name }} 9 | 10 | jobs: 11 | docs: 12 | name: Build documentation 13 | runs-on: ubuntu-latest 14 | 15 | env: 16 | PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | submodules: 'recursive' 23 | 24 | - name: 'Set up Node' 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: "20.x" 28 | 29 | - name: 'Install uv' 30 | uses: astral-sh/setup-uv@v6 31 | with: 32 | python-version: "3.10" 33 | enable-cache: true 34 | activate-environment: true 35 | cache-dependency-glob: | 36 | pyproject.toml 37 | uv.lock 38 | 39 | - name: 'Set up Git' 40 | run: | 41 | git config user.name ${{ github.actor }} 42 | git config user.email ${{ github.actor }}@users.noreply.github.com 43 | echo Current branch: $BRANCH_NAME 44 | 45 | - name: 'Install' 46 | run: | 47 | yarn install 48 | # install setuptools < 81 to fix issue with pkg_resources 49 | uv pip install -e . --group docs "setuptools<81" 50 | 51 | - name: 'Build docs' 52 | run: | 53 | git fetch origin gh-pages 54 | mike delete $BRANCH_NAME 55 | mike deploy --push $BRANCH_NAME 56 | -------------------------------------------------------------------------------- /tests/jupyter/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PID_PATH=/tmp/pret-jupyter-galata-app.pid 4 | # Check node >= 20 (i'm not sure this is 5 | # strictly necessary, but I think it will avoid a few headaches) 6 | NODE_VERSION=$(node --version) 7 | if [ "$(echo "${NODE_VERSION}\nv20" | sort -V|tail -1)" != "${NODE_VERSION}" ]; then 8 | echo "Node >= 20 is required" 9 | exit 1 10 | fi 11 | echo "Node version: $NODE_VERSION" 12 | 13 | # Used to put playwrigth reports in a separate folder 14 | PYTHON_VERSION=$(python --version | cut -d' ' -f2) 15 | echo "Python version: $PYTHON_VERSION" 16 | export PYTHON_VERSION 17 | 18 | # Start Jupyter Lab, save the PID to stop it later, and store logs 19 | export JUPYTERLAB_GALATA_ROOT_DIR=tests/jupyter 20 | jupyter --version 21 | jupyter labextension list 22 | echo "Pret installed package" 23 | pip show -f pret 24 | 25 | JUPYTERLAB_VERSION=$(pip show jupyterlab -V | grep Version | cut -d' ' -f2) 26 | if [ -z "$JUPYTERLAB_VERSION" ]; then 27 | echo "Jupyter Lab is not installed" 28 | exit 1 29 | fi 30 | echo "Jupyter Lab version: $JUPYTERLAB_VERSION" 31 | # Will be read in *.spec.ts files 32 | export JUPYTERLAB_VERSION 33 | 34 | echo "Starting Jupyter Lab" 35 | jupyter lab --config galata_config.py > /tmp/jupyter.log 2>&1 & 36 | echo $! > $PID_PATH 37 | 38 | # Wait for Jupyter Lab to start (check for "is running at:" in the logs) 39 | while ! grep -q "is running at:" /tmp/jupyter.log; do 40 | # Check the app is running 41 | if ! kill -0 $(cat $PID_PATH) 2>/dev/null; then 42 | echo "Jupyter Lab failed to start" 43 | cat /tmp/jupyter.log 44 | exit 1 45 | fi 46 | sleep 1 47 | done 48 | 49 | # Run the tests 50 | echo "Running playwright tests" 51 | yarn playwright test tests/jupyter "$@" 52 | 53 | # Store return code 54 | RET_CODE=$? 55 | 56 | # Stop Jupyter Lab 57 | kill $(cat $PID_PATH) 58 | 59 | # Return the return code 60 | exit $RET_CODE 61 | -------------------------------------------------------------------------------- /client/utils/textRange.ts: -------------------------------------------------------------------------------- 1 | import {TextRange} from "../types"; 2 | 3 | export function getDocumentSelectedRanges(): TextRange[] { 4 | const ranges: TextRange[] = []; 5 | let range = null; 6 | const get_span_begin = (range) => { 7 | return (range?.getAttribute?.("span_begin") 8 | || range.parentElement.getAttribute("span_begin") 9 | || range.parentElement.parentElement.getAttribute("span_begin")) 10 | } 11 | if (window.getSelection) { 12 | const selection = window.getSelection(); 13 | let begin = null, end = null; 14 | for (let i = 0; i < selection.rangeCount; i++) { 15 | range = selection.getRangeAt(i); 16 | const startContainerBegin = parseInt( 17 | // @ts-ignore 18 | get_span_begin(range.startContainer), 19 | 10 20 | ); 21 | const endContainerBegin = parseInt( 22 | // @ts-ignore 23 | get_span_begin(range.endContainer), 24 | 10 25 | ); 26 | if (!isNaN(startContainerBegin)) { 27 | if (!isNaN(begin) && begin !== null) { 28 | begin = Math.min(begin, range.startOffset + startContainerBegin); 29 | } else { 30 | begin = range.startOffset + startContainerBegin; 31 | } 32 | } 33 | if (!isNaN(endContainerBegin)) { 34 | if (!isNaN(begin) && begin !== null) { 35 | end = Math.max(end, range.endOffset + endContainerBegin); 36 | } else { 37 | end = range.endOffset + endContainerBegin; 38 | } 39 | } 40 | } 41 | if (!isNaN(begin) && begin !== null && !isNaN(end) && end !== null && begin !== end) { 42 | ranges.push({ 43 | begin: begin, 44 | end: end, 45 | }); 46 | } 47 | return ranges; 48 | } else { // @ts-ignore 49 | if (document.selection && document.selection.type !== "Control") { 50 | } 51 | } 52 | return ranges 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | publish: 10 | name: Build and Publish Package 11 | runs-on: ubuntu-latest 12 | permissions: 13 | # Trusted Publishing to PyPI 14 | id-token: write 15 | steps: 16 | - name: 'Checkout' 17 | uses: actions/checkout@v4 18 | with: 19 | submodules: 'recursive' 20 | 21 | - name: 'Set up Node' 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: "20.x" 25 | 26 | - name: Set up Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: "3.10" 30 | 31 | - name: 'Install hatch' 32 | run: pip install hatch 33 | 34 | - name: "Build wheel distribution" 35 | run: | 36 | hatch build -t wheel 37 | 38 | - name: Publish package 39 | uses: pypa/gh-action-pypi-publish@release/v1 40 | 41 | docs: 42 | name: Documentation 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v4 47 | with: 48 | submodules: 'recursive' 49 | 50 | - name: 'Set up Node' 51 | uses: actions/setup-node@v4 52 | with: 53 | node-version: "20.x" 54 | 55 | - name: 'Install uv' 56 | uses: astral-sh/setup-uv@v6 57 | with: 58 | python-version: "3.10" 59 | enable-cache: true 60 | activate-environment: true 61 | cache-dependency-glob: | 62 | pyproject.toml 63 | uv.lock 64 | 65 | - name: 'Set up Git' 66 | run: | 67 | git config user.name ${{ github.actor }} 68 | git config user.email ${{ github.actor }}@users.noreply.github.com 69 | echo Current branch: $BRANCH_NAME 70 | 71 | - name: 'Install' 72 | run: | 73 | yarn install 74 | # install setuptools < 81 to fix issue with pkg_resources 75 | uv pip install -e . --group docs "setuptools<81" 76 | 77 | - name: Build documentation 78 | run: | 79 | git fetch origin gh-pages 80 | mike deploy --push --alias-type=copy --update-aliases $GITHUB_REF_NAME latest 81 | -------------------------------------------------------------------------------- /client/utils/reconcile.ts: -------------------------------------------------------------------------------- 1 | 2 | export function internalReconcile(a, b) { 3 | // 7.1. All identical values are equivalent, as determined by ===. 4 | if (a === b) { 5 | return true; 6 | } 7 | 8 | const typeA = typeof a; 9 | const typeB = typeof b; 10 | 11 | if (typeA !== typeB) { 12 | return false; 13 | } 14 | 15 | // 7.3. Other pairs that do not both pass typeof value == 'object', equivalence is determined by ==. 16 | if (!a || !b || (typeA !== "object" && typeB !== "object")) { 17 | return a == b; // eslint-disable-line eqeqeq 18 | } 19 | if (Object.isFrozen(a)) { 20 | return false; 21 | } 22 | 23 | let i, key; 24 | 25 | if ((a === null) || (b === null)) { 26 | return false; 27 | } 28 | 29 | const ka = Object.keys(a); 30 | const kb = Object.keys(b); 31 | let has_diff = ka.length !== kb.length; 32 | 33 | for (i = kb.length - 1; i >= 0; i--) { 34 | key = kb[i]; 35 | if (internalReconcile(a[key], b[key])) { 36 | a[key] = b[key]; 37 | } else { 38 | has_diff = true; 39 | } 40 | } 41 | return !has_diff; 42 | } 43 | 44 | export function reconcile(a, b) { 45 | const reconciled = internalReconcile(a, b); 46 | return reconciled ? b : a; 47 | } 48 | 49 | /** 50 | * Reconciles the previous and the new output of a function call 51 | * to maximize referential equality (===) between any item of the output object 52 | * This allows React to quickly detect parts of the state that haven't changed 53 | * when the state selectors are monolithic blocks, as in our case. 54 | * 55 | * For instance 56 | * ```es6 57 | * func = cachedReconcile((value) => {a: {subkey: 3}, b: value}) 58 | * res4 = func(4) 59 | * res5 = func(5) 60 | * res4 !== res5 (the object has changed) 61 | * res4['a'] === res5['a'] (but the 'a' entry has not) 62 | * ``` 63 | * @param fn: Function whose output we want to cache 64 | */ 65 | export function cachedReconcile(fn: (...args: Inputs) => Output): (...args: Inputs) => Output { 66 | let cache = null; 67 | return ((...args) => { 68 | const a = fn(...args); 69 | const reconciled = internalReconcile(a, cache); 70 | cache = reconciled ? cache : a; 71 | return cache; 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metanno", 3 | "version": "1.0.0-beta.5", 4 | "description": "Modular annotator building framework in Python", 5 | "main": "client/index.ts", 6 | "repository": "https://github.com/percevalw/metanno", 7 | "author": "Perceval Wajsbürt ", 8 | "scripts": { 9 | "build:react-data-grid": "cd submodules/react-data-grid && jlpm run prepublishOnly; cd ../..", 10 | "build": "jlpm clean && jlpm build:react-data-grid && jlpm build:jupyter", 11 | "build:dev": "jlpm clean && jlpm build:react-data-grid && jlpm build:jupyter:dev", 12 | "build:jupyter": "jupyter labextension build .", 13 | "build:jupyter:dev": "jupyter labextension build --development True .", 14 | "clean": "rm -rf metanno/js-extension" 15 | }, 16 | "license": "Apache-2.0", 17 | "peerDependencies": { 18 | "react": "^17.0.0", 19 | "react-dom": "^17.0.0" 20 | }, 21 | "dependencies": { 22 | "color": "^4.2.3", 23 | "konva": "^9.3.18", 24 | "react-autosuggest": "^10.1.0", 25 | "react-data-grid": "link:./submodules/react-data-grid", 26 | "react-dnd": "^16.0.1", 27 | "react-dnd-html5-backend": "^16.0.1", 28 | "react-fast-compare": "^3.2.2", 29 | "react-konva": "^17.0.1-3", 30 | "use-image": "^1.1.4" 31 | }, 32 | "devDependencies": { 33 | "@jupyterlab/builder": "^3.6.5", 34 | "@jupyterlab/galata": "5", 35 | "@types/react": "^17.0.0", 36 | "@types/react-dom": "^17.0.0", 37 | "file-loader": "^6.2.0", 38 | "husky": ">=6", 39 | "lint-staged": ">=10", 40 | "old-galata": "npm:@jupyterlab/galata@4.5.8", 41 | "prettier": "^2.8.8", 42 | "react": "^17.0.2", 43 | "react-dom": "^17.0.2", 44 | "ts-loader": "^9.4.2", 45 | "typescript": "^5.8.2" 46 | }, 47 | "jupyterlab": { 48 | "extension": "client/plugin.ts", 49 | "schemaDir": "client/schema", 50 | "outputDir": "metanno/js-extension", 51 | "webpackConfig": "./webpack.jupyter.js" 52 | }, 53 | "prettier": {}, 54 | "browserslist": { 55 | "production": [ 56 | ">0.2%", 57 | "not dead", 58 | "not op_mini all" 59 | ], 60 | "development": [ 61 | "last 1 chrome version", 62 | "last 1 firefox version", 63 | "last 1 safari version" 64 | ] 65 | }, 66 | "lint-staged": { 67 | "*.{js,css,md}": "prettier --write" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## As a user 4 | 5 | A simple pip installation should be enough to install Metanno *both* as a standalone web app framework and as a JupyterLab extension: 6 | 7 | ```bash { data-md-color-scheme="slate" } 8 | pip install metanno==1.0.0-beta.5 9 | ``` 10 | 11 | To use it with Jupyter, if you install the library in a custom environment (conda, venv, or other), 12 | you will likely need to tell Jupyter where to find the front-end files. 13 | You can do this by running the following command (only once), and restart your Jupyterlab server: 14 | 15 | ```bash { data-md-color-scheme="slate" } 16 | pret update-jupyter-config --apply 17 | ``` 18 | 19 | ## As a contributor 20 | 21 | If you want to contribute to Metanno, you should have a programming environment: 22 | 23 | - Python 3.7 or later, with pip and [hatch](https://hatch.pypa.io/latest/) installed 24 | - Node.js 20 or later (you can use [nvm](https://github.com/nvm-sh/nvm) to easily install and manage Node.js versions) 25 | - JupyterLab 3 (the built extension will be compatible with JupyterLab 4) 26 | - Various web browsers for testing (e.g., Chrome, Firefox, Safari) 27 | - A Git client to clone the repository and manage your changes 28 | 29 | ```bash { data-md-color-scheme="slate" } 30 | git clone https://github.com/percevalw/metanno.git 31 | cd metanno 32 | ``` 33 | 34 | Then, create a new branch for your changes: 35 | 36 | ```bash { data-md-color-scheme="slate" } 37 | git checkout -b my-feature-branch 38 | ``` 39 | 40 | Create (optional) virtual env and install all development deps. 41 | Install the package in editable mode with development dependencies: 42 | 43 | ```bash { data-md-color-scheme="slate" } 44 | yarn install 45 | pip install -e . --group dev #(1)! 46 | yarn playwright install --with-deps # browsers for UI tests 47 | ``` 48 | 49 | 1. or `uv pip install -e . --group dev` with uv 50 | 51 | ### Running the UI tests 52 | 53 | Metanno uses [playwright](https://playwright.dev/) to test the JupyterLab extension (which should cover most of the app features). 54 | You can run the tests to ensure everything is working correctly. 55 | 56 | ```bash { data-md-color-scheme="slate" } 57 | sh tests/jupyter/run.sh #(1)! 58 | ``` 59 | 60 | 1. or `uv run sh tests/jupyter/run.sh` with uv 61 | 62 | ### Building the documentation 63 | 64 | The documentation is built with [MkDocs](https://www.mkdocs.org/) and [MkDocs Material](https://squidfunk.github.io/mkdocs-material/) theme, along with quite a few customizations. 65 | To build the documentation, you can use the following command: 66 | 67 | ```bash { data-md-color-scheme="slate" } 68 | pip install -e . --group docs #(1)! 69 | mkdocs serve #(2)! 70 | ``` 71 | 72 | 1. or `uv pip install -e . --group docs` with uv 73 | 2. or `uv run mkdocs serve` with uv 74 | -------------------------------------------------------------------------------- /client/components/DraggableHeaderRenderer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useLayoutEffect, useRef} from 'react'; 2 | import {useDrag, useDrop} from 'react-dnd'; 3 | 4 | import {HeaderRendererProps} from 'react-data-grid'; 5 | 6 | function useCombinedRefs(...refs) { 7 | return useCallback(handle => { 8 | for (const ref of refs) { 9 | if (typeof ref === 'function') { 10 | ref(handle); 11 | } else if (ref !== null && 'current' in ref) { 12 | ref.current = handle; 13 | } 14 | } 15 | }, refs); 16 | } 17 | 18 | 19 | export function useFocusRef(isSelected: boolean) { 20 | const ref = useRef(null); 21 | 22 | useLayoutEffect(() => { 23 | if (!isSelected) return; 24 | ref.current?.focus({preventScroll: true}); 25 | }, [isSelected]); 26 | 27 | return { 28 | ref, 29 | tabIndex: isSelected ? 0 : -1 30 | }; 31 | } 32 | 33 | 34 | function HeaderRenderer({ 35 | isCellSelected, 36 | column, 37 | children, 38 | onColumnsReorder, 39 | }: HeaderRendererProps & { 40 | onColumnsReorder: (source: string, target: string) => void, 41 | children?: (args: { 42 | ref: React.RefObject; 43 | tabIndex: number; 44 | }) => React.ReactElement; 45 | }) { 46 | const {ref, tabIndex} = useFocusRef(isCellSelected); 47 | 48 | 49 | const [{isDragging}, drag] = useDrag({ 50 | type: 'METANNO_COLUMN_DRAG', 51 | item: {key: column.key}, 52 | collect: monitor => ({ 53 | isDragging: !!monitor.isDragging() 54 | }) 55 | }); 56 | 57 | const [{isOver}, drop] = useDrop({ 58 | accept: 'METANNO_COLUMN_DRAG', 59 | // @ts-ignore 60 | drop({key}) { 61 | onColumnsReorder(key, column.key); 62 | }, 63 | collect: monitor => ({ 64 | isOver: !!monitor.isOver(), 65 | canDrop: !!monitor.canDrop() 66 | }) 67 | }); 68 | 69 | return ( 70 |
77 |
{column.name}
78 | {children ?
{children({ref, tabIndex})}
: null} 79 |
80 | ); 81 | } 82 | 83 | export default HeaderRenderer; 84 | -------------------------------------------------------------------------------- /client/components/BooleanInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useLayoutEffect as useLayoutEffect$1, useRef} from 'react'; 2 | import './style.css'; 3 | 4 | 5 | const checkboxLabel = "c1w6d5eo700-beta7"; 6 | const checkboxLabelClassname = `rdg-checkbox-label ${checkboxLabel}`; 7 | const checkboxInput = "c1h7iz8d700-beta7"; 8 | const checkboxInputClassname = `rdg-checkbox-input ${checkboxInput}`; 9 | const checkbox = "cc79ydj700-beta7"; 10 | const checkboxClassname = `rdg-checkbox ${checkbox}`; 11 | const checkboxLabelDisabled = "c1e5jt0b700-beta7"; 12 | const checkboxLabelDisabledClassname = `rdg-checkbox-label-disabled ${checkboxLabelDisabled}`; 13 | 14 | const useLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect$1; 15 | 16 | function useFocusRef(isSelected) { 17 | const ref = useRef(null); 18 | useLayoutEffect(() => { 19 | var _ref$current; 20 | 21 | if (!isSelected) return; 22 | (_ref$current = ref.current) == null ? void 0 : _ref$current.focus({ 23 | preventScroll: true 24 | }); 25 | }, [isSelected]); 26 | return { 27 | ref, 28 | tabIndex: isSelected ? 0 : -1 29 | }; 30 | } 31 | 32 | type SharedInputProps = Pick, 'disabled' | 'onClick' | 'aria-label' | 'aria-labelledby'>; 33 | 34 | declare interface SelectCellFormatterProps extends SharedInputProps { 35 | isCellSelected: boolean; 36 | value: boolean; 37 | onChange: (value: boolean, isShiftClick: boolean) => void; 38 | } 39 | 40 | 41 | export default function BooleanInput({ 42 | value, 43 | isCellSelected, 44 | disabled, 45 | onClick, 46 | onChange, 47 | 'aria-label': ariaLabel, 48 | 'aria-labelledby': ariaLabelledBy 49 | }: SelectCellFormatterProps): JSX.Element { 50 | const { 51 | ref, 52 | tabIndex 53 | } = useFocusRef(isCellSelected); 54 | 55 | function handleChange(e) { 56 | onChange(e.target.checked, e.nativeEvent.shiftKey); 57 | } 58 | 59 | return ( 60 |
61 |