diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..50391af --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,58 @@ +name: Publish Python Package + +on: + release: + types: [created] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - uses: actions/cache@v2 + name: Configure pip caching + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Install dependencies + run: | + pip install -e '.[test]' + - name: Run tests + run: | + pytest + deploy: + runs-on: ubuntu-latest + needs: [test] + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.10" + - uses: actions/cache@v2 + name: Configure pip caching + with: + path: ~/.cache/pip + key: ${{ runner.os }}-publish-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-publish-pip- + - name: Install dependencies + run: | + pip install setuptools wheel twine build + - name: Publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: | + python -m build + twine upload dist/* + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2ba2afc --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +name: Test + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - uses: actions/cache@v2 + name: Configure pip caching + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Install dependencies + run: | + pip install -e '.[test]' + - name: Run tests + run: | + pytest + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e61ce1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.venv +__pycache__/ +*.py[cod] +*$py.class +venv +.eggs +.pytest_cache +*.egg-info +.DS_Store +.vscode +dist +build diff --git a/README.md b/README.md new file mode 100644 index 0000000..0de9aae --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# datasette-hovercards + +[![PyPI](https://img.shields.io/pypi/v/datasette-hovercards.svg)](https://pypi.org/project/datasette-hovercards/) +[![Changelog](https://img.shields.io/github/v/release/simonw/datasette-hovercards?include_prereleases&label=changelog)](https://github.com/simonw/datasette-hovercards/releases) +[![Tests](https://github.com/simonw/datasette-hovercards/workflows/Test/badge.svg)](https://github.com/simonw/datasette-hovercards/actions?query=workflow%3ATest) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-hovercards/blob/main/LICENSE) + +Add preview hovercards to links in Datasette + +## Installation + +Install this plugin in the same environment as Datasette. + + $ datasette install datasette-hovercards + +## Usage + +Usage instructions go here. + +## Development + +To set up this plugin locally, first checkout the code. Then create a new virtual environment: + + cd datasette-hovercards + python3 -mvenv venv + source venv/bin/activate + +Or if you are using `pipenv`: + + pipenv shell + +Now install the dependencies and test dependencies: + + pip install -e '.[test]' + +To run the tests: + + pytest diff --git a/datasette_hovercards/__init__.py b/datasette_hovercards/__init__.py new file mode 100644 index 0000000..901c0d0 --- /dev/null +++ b/datasette_hovercards/__init__.py @@ -0,0 +1,11 @@ +from datasette import hookimpl + + +@hookimpl +def extra_js_urls(datasette, table): + if table: + return [ + datasette.urls.static_plugins( + "datasette-hovercards", "datasette-hovercards.js" + ) + ] diff --git a/datasette_hovercards/static/datasette-hovercards.js b/datasette_hovercards/static/datasette-hovercards.js new file mode 100644 index 0000000..96840d7 --- /dev/null +++ b/datasette_hovercards/static/datasette-hovercards.js @@ -0,0 +1,89 @@ +let hovercardOuterAnimation = null; +let hovercardInnerAnimation = null; +let hovercard = document.createElement("div"); +hovercard.setAttribute("id", "datasette-hovercard") +hovercard.style.width = '300px'; +hovercard.style.height = '200px'; +hovercard.style.overflow = 'auto'; +hovercard.style.backgroundColor = 'white'; +hovercard.style.border = '1px solid #ccc'; +hovercard.style.padding = '10px'; +hovercard.style.position = 'absolute'; +hovercard.style.display = 'none'; +hovercard.style.boxShadow = '1px 2px 8px 2px rgba(0,0,0,0.08)'; + +const hovercardEscape = (s) => { + return (s || "").toString() + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +}; + +document.addEventListener("mouseover", async (ev) => { + const a = ev.target; + if (a.nodeName != 'A') { + return; + } + // TODO: Respect base_url and suchlike + if (a.pathname.split("/").length != 4) { + return; // Definitely not a row + } + // OK, it might be a row! Try a fetch + let row; + if (a.hovercardRowData) { + row = a.hovercardRowData; + } else { + const response = await fetch(a.pathname + ".json?_shape=array"); + if (response.status == 200) { + const data = await response.json(); + row = data[0]; + a.hovercardRowData = row; + } + } + if (row) { + // Cancel any existing animations + if (hovercardOuterAnimation) { + clearTimeout(hovercardOuterAnimation); + } + if (hovercardInnerAnimation) { + clearTimeout(hovercardInnerAnimation); + } + hovercard.style.top = (ev.pageY + 5) + 'px'; + hovercard.style.left = (ev.pageX - 15) + 'px'; + let html = ['