mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Fix Chart and Signature evaluating JS strings (#3226)
This commit is contained in:
1 parent
01857d234a
commit
6c1d546583
8 files changed
+157
-133
No files matched your search
@@ -77,9 +77,12 @@ Two known snags:
|
|||||||
- **Astro wraps a `define:vars` script in an IIFE.** Anything the markup calls (`onclick="setPageListItems(this)"`) must be assigned to `window` explicitly — see `shared/components/demo/AdvancedTable.astro`. If several instances define the same global, make it stateless and resolve the instance from the clicked element.
|
- **Astro wraps a `define:vars` script in an IIFE.** Anything the markup calls (`onclick="setPageListItems(this)"`) must be assigned to `window` explicitly — see `shared/components/demo/AdvancedTable.astro`. If several instances define the same global, make it stateless and resolve the instance from the clicked element.
|
||||||
- The type checker does not see `define:vars` bindings used inside template literals. `shared/components/marketing/hero/Side.astro` carries a `// @ts-nocheck` for exactly that; copy the comment with its explanation rather than restructuring the script.
|
- The type checker does not see `define:vars` bindings used inside template literals. `shared/components/marketing/hero/Side.astro` carries a `// @ts-nocheck` for exactly that; copy the comment with its explanation rather than restructuring the script.
|
||||||
|
|
||||||
## 5. When the script has to be a string
|
## 5. No caller-supplied JS
|
||||||
|
|
||||||
If caller-supplied JS must land *inside* a function body, `define:vars` cannot do it — it injects variables, not code. Then build the script as a template string in the frontmatter and render it with `<Fragment set:html={script} />` inside `<CaptureScript>` (see `shared/ui/Signature.astro`). Serialise every interpolated value with `JSON.stringify` and keep raw-JS props documented as an escape hatch.
|
Never accept JavaScript as a string — no raw-JS props, no expressions in data files turned into code with `new Function` or `set:html`. They are `eval` sinks and block a strict CSP. Instead:
|
||||||
|
|
||||||
|
- Page-specific behaviour goes in the page's own `<CaptureScript>` and reaches the plugin instance through its `window.tabler_*` registry (see `preview/pages/signatures.astro`).
|
||||||
|
- Behaviour picked from data uses a whitelist of names mapped to real functions inside the script (see the chart `x-formatter` / `y-formatter` in `shared/components/demo/Chart.astro`).
|
||||||
|
|
||||||
## 6. Instance registries
|
## 6. Instance registries
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@tabler/preview": patch
|
||||||
|
"@tabler/docs": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Removed JS string evaluation from `Chart` axis formatters in `charts.json` and the `extraJs` prop from `Signature`.
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
---
|
---
|
||||||
// Raw JS passed as Signature `extraJs` (appended inside the SignaturePad init).
|
|
||||||
import CardSubtitle from '@ui/CardSubtitle.astro'
|
import CardSubtitle from '@ui/CardSubtitle.astro'
|
||||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||||
import Signature from '@ui/Signature.astro'
|
import Signature from '@ui/Signature.astro'
|
||||||
@@ -11,51 +10,10 @@ import CardTitle from '@ui/CardTitle.astro'
|
|||||||
import FormGroup from '@ui/FormGroup.astro'
|
import FormGroup from '@ui/FormGroup.astro'
|
||||||
import CaptureModal from '@shared/components/CaptureModal.astro'
|
import CaptureModal from '@shared/components/CaptureModal.astro'
|
||||||
import DocsLink from '@ui/DocsLink.astro'
|
import DocsLink from '@ui/DocsLink.astro'
|
||||||
|
import CaptureScript from '@shared/components/CaptureScript.astro'
|
||||||
|
|
||||||
const simpleJs = `signaturePad.fromDataURL("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA1OTIgMjAwIj4KICA8ZyBjbGlwLXBhdGg9InVybCgjYSkiPgogICAgPHBhdGggZmlsbD0iIzAwMCIgZD0iTTEzMi4wOTIgNzkuMDY1YzAgLjMwNC0uMzU1IDEuNDAzLTEuMDYzIDMuMjk4LS42OTQgMS44OTQtMS4xNDIgMy4wMjItMS4zNDUgMy4zODQtMi4zNDIgMy44NDctNi43NiA3LjE4LTEzLjI1MiAxMC4wMDEtNS4zNjUgMi4yMjctMTMuNDU1IDUuNTktMjQuMjcgMTAuMDg4YTE0OC4xOSAxNDguMTkgMCAwIDAtMTAuMzQ3IDMuMTI0Yy0yLjY5Ljg4Mi02LjU4NiAyLjk1LTExLjY5IDYuMjA0YTE4Ny42NiAxODcuNjYgMCAwIDAtMS4wNjMgMTAuMTk2IDQwNy40OSA0MDcuNDkgMCAwIDEtMS40MzIgMTUuMzM4Yy0uODgyIDEuMTg2LTEuOTg4IDEuNzc5LTMuMzE4IDEuNzc5LjExNSAwIC4xNzMtLjAxNS4xNzMtLjA0NC0uMjAyLS4xMDEtLjg5Ni0uMjgyLTIuMDgyLS41NDJ2LTIuODg1YzAtMi43MDUuMjMxLTUuNDYuNjk0LTguMjY2IDAtLjA1Ny4yNTMtLjc3My43Ni0yLjE0Ny41MDUtMS4zMzEuNzU4LTIuMTQxLjc1OC0yLjQzIDAtMS42MDUtLjI5Ni0zLjI0LS44ODktNC45MDMtLjM5IDEuMTI4LTEuMTEzIDEuODU5LTIuMTY5IDIuMTkxLTEuMjQ0LS43MjMtMS44NjUtMS45ODgtMS44NjUtMy43OTYgMC0uMjAzIDEuMDQ4LTEuNzI5IDMuMTQ1LTQuNTc4IDIuMTQtMi45NjUgMy4zODMtNS4wMDQgMy43My02LjExNy4yNzUtLjgxLjc2Ny0zLjQ1IDEuNDc1LTcuOTE5IDEuMDQxLTYuNjEgMS45Ni0xMS40NCAyLjc1NS0xNC40OTEgMS42OTItNi42MzkgMy42OTQtMTAuODYyIDYuMDA4LTEyLjY3bDIuNDMgMS44MjN2MS44NjVjLTEuNTA1IDEuMTU3LTIuNTY3IDMuMjYyLTMuMTkgNi4zMTMtLjE4Ny45NC0uNjM2IDMuMzQxLTEuMzQ0IDcuMjAzLS4xNDUuNzA4LS43MjMgMi41NDUtMS43MzUgNS41MS0uOTExIDIuNjAzLTEuMzY3IDQuMTg3LTEuMzY3IDQuNzUxdjUuNDI0Yy4yNi4yNDUuNS4zNjguNzE2LjM2OCAxLjIxNSAwIDMuMTA5LS41MDYgNS42ODMtMS41MTggMy4wOC0xLjE1NyA1LjAxNy0xLjg1MSA1LjgxMy0yLjA4MyAxMy4wNzEtMy4zODQgMjMuMTUtNy4yMzggMzAuMjM1LTExLjU2MyA2LjIxOC0zLjc0NiA5Ljk5Mi02LjI5OCAxMS4zMjItNy42NTggMS41NzYtMS41NjIgMi4zNjQtMy42NjYgMi4zNjQtNi4zMTMgMC00LjY3MS0yLjEwNC04LjU2OS02LjMxMi0xMS42OTMtMy44Ni0yLjkwNy04LjE5MS00LjM2LTEyLjk5Mi00LjM2LTMuNzg4IDAtOC4yNzEuNDYyLTEzLjQ0NyAxLjM4OC02LjY2NiAxLjIxNS0xMi45MiAyLjkwNy0xOC43NjIgNS4wNzYtOC44NjQgMy4yODQtMTMuNzM2IDYuODM0LTE0LjYxOCAxMC42NTJsLTEuMDg1LjgwM0M1OC44MzkgNzguNDIyIDU4IDc3LjA2MiA1OCA3NS43OWMwLTEuODA4IDEuODgtNC4wMTQgNS42NC02LjYxNyAzLjA4LTIuMTQgNS44Mi0zLjU2NSA4LjIyLTQuMjc0IDE1Ljk0OS00LjkxNyAyNy44NjQtNy4zNzYgMzUuNzQ0LTcuMzc2IDcuNDYxIDAgMTMuNDEyIDEuOTUzIDE3Ljg1MSA1Ljg1NyA0LjQyNCAzLjg2MiA2LjYzNyA5LjA5IDYuNjM3IDE1LjY4NVptMzIuMjk1IDU1LjQwN3YxLjkwOWMtLjU5Mi44MjQtMS4yNDMgMS4yMzYtMS45NTIgMS4yMzYtLjQ0OCAwLTEuOTczLTEuMzU5LTQuNTc2LTQuMDc4LTIuNjAzLTIuNzMzLTQuMzM4LTQuMS01LjIwNi00LjEgMCAuMDU4LTMuMDggMS4yNDQtOS4yMzkgMy41NTgtNi4zMDUgMi4zNDMtMTAuMzE3IDMuNTE0LTEyLjAzOCAzLjUxNC0xLjQxNyAwLTIuOTg2LS42OTQtNC43MDctMi4wODMtMS43Mi0xLjM3NC0yLjU4MS0yLjY2OC0yLjU4MS0zLjg4MyAwLTMuMTM4IDMuNDM0LTYuNzk3IDEwLjMwMy0xMC45NzcgNi40MzQtMy45NjMgMTEuMzQzLTUuOTQ0IDE0LjcyNy01Ljk0NCAxLjQxNyAwIDIuNjkuODEgMy44MTcgMi40M3YxLjgyMmMtLjU2NC44MS0xLjIxNCAxLjIxNS0xLjk1MiAxLjIxNS0uMTMgMC0uNjE0LS4xODEtMS40NTMtLjU0My0uODM5LS4zNzYtMS4zMzctLjU2NC0xLjQ5Ni0uNTY0LTEuMTI4IDAtMy43MTcgMS4xNjUtNy43NjUgMy40OTMtMy44MzIgMi4xNjktNi4zOTkgMy43ODktNy43IDQuODYtMS4zMDIgMS4xMjgtMi4xMzMgMi40MTUtMi40OTQgMy44NjEuOTgzLjkyNiAyLjE1NCAxLjM4OCAzLjUxMyAxLjM4OC43MDkgMCA0LjEwNy0xLjE3MSAxMC4xOTQtMy41MTQgNi4wNzMtMi4zNzIgOS43NDYtMy41NTggMTEuMDE5LTMuNTU4IDEuODY1IDAgMy4yMS40NyA0LjAzNCAxLjQxLjE4OC4yMTcuOTA0IDEuNDkgMi4xNDcgMy44MThhMjEuOTAyIDIxLjkwMiAwIDAgMCAzLjQwNSA0LjczWm02OS44ODQtMTkuNzQydjEuOTUzYy0yLjk5MyAyLjQ1OC01Ljg5OSA1LjIxMy04LjcxOSA4LjI2NS04Ljc2MiA5LjQ1OS0xNC42NjIgMTQuMTg4LTE3LjY5OSAxNC4xODgtMi40NTggMC01LjE5OC0yLjIyLTguMjItNi42NmwtNi41MjgtOS42NTRjLTEuMjQ0LjIwMy0zLjQ5MiAyLjQ2Ni02Ljc0NiA2Ljc5LTMuMDUxIDQuMTIyLTUuODQyIDYuMTgzLTguMzcyIDYuMTgzbC0xLjE5My0uNjI5Yy0uODgyLTEuNjItMi41MzgtNS4xNy00Ljk2Ny0xMC42NTItMi42NDYtNS45MjktMy45NjktOS4zMDYtMy45NjktMTAuMTMxIDAtMS40MzIuODQ2LTIuNjE4IDIuNTM4LTMuNTU4IDEuNjkxIDIuMDQgMy4zNTQgNC44NjcgNC45ODggOC40ODMgMi40NTggNS4zMzYgMy44NzUgOC4zMyA0LjI1MSA4Ljk4MWgyLjA2MWMuNTY0LTEuMDQxIDEuODk0LTIuNjU0IDMuOTkxLTQuODM4LjczNy0xLjEyOCAxLjgyMi0yLjY5IDMuMjUzLTQuNjg2IDEuNjc3LTIuMzcyIDMuMDk1LTMuNTU3IDQuMjUxLTMuNTU3IDEuMDcgMCAxLjk3NC41OTIgMi43MTEgMS43NzggMS4wNzEgMS42OTMgMi45ODYgNC43MyA1Ljc0OCA5LjExMiAzLjA1MSA0Ljc0NCA1LjMgNy4xMTYgNi43NDYgNy4xMTYgMS4wNyAwIDUuMDgyLTMuMDgxIDEyLjAzNy05LjI0MiA2Ljk3LTYuMTYxIDEwLjc5NS05LjI0MiAxMS40NzQtOS4yNDJoMi4zNjRabTQ0LjU3MyAxOS41MjVjLTEuMDQyIDEuMjQ0LTIuNDAxIDEuODY2LTQuMDc4IDEuODY2aC0xNC45ODhjLTMuNDcgMC03LjMzOC0uNTIxLTExLjYwNC0xLjU2Mi01LjU5NS0xLjM2LTguNzYyLTMuMDY3LTkuNS01LjEyLS44MDktMi4zMTQtMS4yMTQtNC41MzQtMS4yMTQtNi42NiAwLTQuMjY3IDEuODM2LTcuNTQzIDUuNTA5LTkuODI4IDMuMDk0LTEuOTgxIDYuODc2LTIuOTcyIDExLjM0NC0yLjk3MiAzLjg0NiAwIDYuODk3LjgzMiA5LjE1MyAyLjQ5NSAyLjI1NSAxLjY0OSAzLjM4MyAzLjg5OCAzLjM4MyA2Ljc0NyAwIDEuNTkxLTIuNzExIDMuMzI2LTguMTMzIDUuMjA2LTQuNDgzIDEuNjItNy43NDQgMi40My05Ljc4MiAyLjQzYTQuNDkgNC40OSAwIDAgMS0uNjczLS4wNDNjMCAuMzktLjMxMS44NjgtLjkzMiAxLjQzMi4zMzIuNTY0IDEuOTgxIDEuMjE0IDQuOTQ1IDEuOTUyIDIuNjYuNjggNC41NTUgMS4wMiA1LjY4MiAxLjAyaDEzLjQ3YzAgLjAyOSAxLjY5OS4yODIgNS4wOTcuNzU5IDEuMDcuNjggMS44NDMgMS40MzkgMi4zMjEgMi4yNzhabS0xNy40NjEtMTYuOWMtMS41NzYtMS42MDUtNC4xMTMtMi40MDgtNy42MTMtMi40MDgtMi40NTggMC00LjczNS40NzctNi44MzIgMS40MzItMi43MDQgMS4yNDQtNC4wNTYgMi45MjktNC4wNTYgNS4wNTUgMCAuNzUyLjI1MyAxLjI4Ny43NTkgMS42MDVoMLine truncated
|
const signatureImage =
|
||||||
|
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA1OTIgMjAwIj4KICA8ZyBjbGlwLXBhdGg9InVybCgjYSkiPgogICAgPHBhdGggZmlsbD0iIzAwMCIgZD0iTTEzMi4wOTIgNzkuMDY1YzAgLjMwNC0uMzU1IDEuNDAzLTEuMDYzIDMuMjk4LS42OTQgMS44OTQtMS4xNDIgMy4wMjItMS4zNDUgMy4zODQtMi4zNDIgMy44NDctNi43NiA3LjE4LTEzLjI1MiAxMC4wMDEtNS4zNjUgMi4yMjctMTMuNDU1IDUuNTktMjQuMjcgMTAuMDg4YTE0OC4xOSAxNDguMTkgMCAwIDAtMTAuMzQ3IDMuMTI0Yy0yLjY5Ljg4Mi02LjU4NiAyLjk1LTExLjY5IDYuMjA0YTE4Ny42NiAxODcuNjYgMCAwIDAtMS4wNjMgMTAuMTk2IDQwNy40OSA0MDcuNDkgMCAwIDEtMS40MzIgMTUuMzM4Yy0uODgyIDEuMTg2LTEuOTg4IDEuNzc5LTMuMzE4IDEuNzc5LjExNSAwIC4xNzMtLjAxNS4xNzMtLjA0NC0uMjAyLS4xMDEtLjg5Ni0uMjgyLTIuMDgyLS41NDJ2LTIuODg1YzAtMi43MDUuMjMxLTUuNDYuNjk0LTguMjY2IDAtLjA1Ny4yNTMtLjc3My43Ni0yLjE0Ny41MDUtMS4zMzEuNzU4LTIuMTQxLjc1OC0yLjQzIDAtMS42MDUtLjI5Ni0zLjI0LS44ODktNC45MDMtLjM5IDEuMTI4LTEuMTEzIDEuODU5LTIuMTY5IDIuMTkxLTEuMjQ0LS43MjMtMS44NjUtMS45ODgtMS44NjUtMy43OTYgMC0uMjAzIDEuMDQ4LTEuNzI5IDMuMTQ1LTQuNTc4IDIuMTQtMi45NjUgMy4zODMtNS4wMDQgMy43My02LjExNy4yNzUtLjgxLjc2Ny0zLjQ1IDEuNDc1LTcuOTE5IDEuMDQxLTYuNjEgMS45Ni0xMS40NCAyLjc1NS0xNC40OTEgMS42OTItNi42MzkgMy42OTQtMTAuODYyIDYuMDA4LTEyLjY3bDIuNDMgMS44MjN2MS44NjVjLTEuNTA1IDEuMTU3LTIuNTY3IDMuMjYyLTMuMTkgNi4zMTMtLjE4Ny45NC0uNjM2IDMuMzQxLTEuMzQ0IDcuMjAzLS4xNDUuNzA4LS43MjMgMi41NDUtMS43MzUgNS41MS0uOTExIDIuNjAzLTEuMzY3IDQuMTg3LTEuMzY3IDQuNzUxdjUuNDI0Yy4yNi4yNDUuNS4zNjguNzE2LjM2OCAxLjIxNSAwIDMuMTA5LS41MDYgNS42ODMtMS41MTggMy4wOC0xLjE1NyA1LjAxNy0xLjg1MSA1LjgxMy0yLjA4MyAxMy4wNzEtMy4zODQgMjMuMTUtNy4yMzggMzAuMjM1LTExLjU2MyA2LjIxOC0zLjc0NiA5Ljk5Mi02LjI5OCAxMS4zMjItNy42NTggMS41NzYtMS41NjIgMi4zNjQtMy42NjYgMi4zNjQtNi4zMTMgMC00LjY3MS0yLjEwNC04LjU2OS02LjMxMi0xMS42OTMtMy44Ni0yLjkwNy04LjE5MS00LjM2LTEyLjk5Mi00LjM2LTMuNzg4IDAtOC4yNzEuNDYyLTEzLjQ0NyAxLjM4OC02LjY2NiAxLjIxNS0xMi45MiAyLjkwNy0xOC43NjIgNS4wNzYtOC44NjQgMy4yODQtMTMuNzM2IDYuODM0LTE0LjYxOCAxMC42NTJsLTEuMDg1LjgwM0M1OC44MzkgNzguNDIyIDU4IDc3LjA2MiA1OCA3NS43OWMwLTEuODA4IDEuODgtNC4wMTQgNS42NC02LjYxNyAzLjA4LTIuMTQgNS44Mi0zLjU2NSA4LjIyLTQuMjc0IDE1Ljk0OS00LjkxNyAyNy44NjQtNy4zNzYgMzUuNzQ0LTcuMzc2IDcuNDYxIDAgMTMuNDEyIDEuOTUzIDE3Ljg1MSA1Ljg1NyA0LjQyNCAzLjg2MiA2LjYzNyA5LjA5IDYuNjM3IDE1LjY4NVptMzIuMjk1IDU1LjQwN3YxLjkwOWMtLjU5Mi44MjQtMS4yNDMgMS4yMzYtMS45NTIgMS4yMzYtLjQ0OCAwLTEuOTczLTEuMzU5LTQuNTc2LTQuMDc4LTIuNjAzLTIuNzMzLTQuMzM4LTQuMS01LjIwNi00LjEgMCAuMDU4LTMuMDggMS4yNDQtOS4yMzkgMy41NTgtNi4zMDUgMi4zNDMtMTAuMzE3IDMuNTE0LTEyLjAzOCAzLjUxNC0xLjQxNyAwLTIuOTg2LS42OTQtNC43MDctMi4wODMtMS43Mi0xLjM3NC0yLjU4MS0yLjY2OC0yLjU4MS0zLjg4MyAwLTMuMTM4IDMuNDM0LTYuNzk3IDEwLjMwMy0xMC45NzcgNi40MzQtMy45NjMgMTEuMzQzLTUuOTQ0IDE0LjcyNy01Ljk0NCAxLjQxNyAwIDIuNjkuODEgMy44MTcgMi40M3YxLjgyMmMtLjU2NC44MS0xLjIxNCAxLjIxNS0xLjk1MiAxLjIxNS0uMTMgMC0uNjE0LS4xODEtMS40NTMtLjU0My0uODM5LS4zNzYtMS4zMzctLjU2NC0xLjQ5Ni0uNTY0LTEuMTI4IDAtMy43MTcgMS4xNjUtNy43NjUgMy40OTMtMy44MzIgMi4xNjktNi4zOTkgMy43ODktNy43IDQuODYtMS4zMDIgMS4xMjgtMi4xMzMgMi40MTUtMi40OTQgMy44NjEuOTgzLjkyNiAyLjE1NCAxLjM4OCAzLjUxMyAxLjM4OC43MDkgMCA0LjEwNy0xLjE3MSAxMC4xOTQtMy41MTQgNi4wNzMtMi4zNzIgOS43NDYtMy41NTggMTEuMDE5LTMuNTU4IDEuODY1IDAgMy4yMS40NyA0LjAzNCAxLjQxLjE4OC4yMTcuOTA0IDEuNDkgMi4xNDcgMy44MThhMjEuOTAyIDIxLjkwMiAwIDAgMCAzLjQwNSA0LjczWm02OS44ODQtMTkuNzQydjEuOTUzYy0yLjk5MyAyLjQ1OC01Ljg5OSA1LjIxMy04LjcxOSA4LjI2NS04Ljc2MiA5LjQ1OS0xNC42NjIgMTQuMTg4LTE3LjY5OSAxNC4xODgtMi40NTggMC01LjE5OC0yLjIyLTguMjItNi42NmwtNi41MjgtOS42NTRjLTEuMjQ0LjIwMy0zLjQ5MiAyLjQ2Ni02Ljc0NiA2Ljc5LTMuMDUxIDQuMTIyLTUuODQyIDYuMTgzLTguMzcyIDYuMTgzbC0xLjE5My0uNjI5Yy0uODgyLTEuNjItMi41MzgtNS4xNy00Ljk2Ny0xMC42NTItMi42NDYtNS45MjktMy45NjktOS4zMDYtMy45NjktMTAuMTMxIDAtMS40MzIuODQ2LTIuNjE4IDIuNTM4LTMuNTU4IDEuNjkxIDIuMDQgMy4zNTQgNC44NjcgNC45ODggOC40ODMgMi40NTggNS4zMzYgMy44NzUgOC4zMyA0LjI1MSA4Ljk4MWgyLjA2MWMuNTY0LTEuMDQxIDEuODk0LTIuNjU0IDMuOTkxLTQuODM4LjczNy0xLjEyOCAxLjgyMi0yLjY5IDMuMjUzLTQuNjg2IDEuNjc3LTIuMzcyIDMuMDk1LTMuNTU3IDQuMjUxLTMuNTU3IDEuMDcgMCAxLjk3NC41OTIgMi43MTEgMS43NzggMS4wNzEgMS42OTMgMi45ODYgNC43MyA1Ljc0OCA5LjExMiAzLjA1MSA0Ljc0NCA1LjMgNy4xMTYgNi43NDYgNy4xMTYgMS4wNyAwIDUuMDgyLTMuMDgxIDEyLjAzNy05LjI0MiA2Ljk3LTYuMTYxIDEwLjc5NS05LjI0MiAxMS40NzQtOS4yNDJoMi4zNjRabTQ0LjU3MyAxOS41MjVjLTEuMDQyIDEuMjQ0LTIuNDAxIDEuODY2LTQuMDc4IDEuODY2aC0xNC45ODhjLTMuNDcgMC03LjMzOC0uNTIxLTExLjYwNC0xLjU2Mi01LjU5NS0xLjM2LTguNzYyLTMuMDY3LTkuNS01LjEyLS44MDktMi4zMTQtMS4yMTQtNC41MzQtMS4yMTQtNi42NiAwLTQuMjY3IDEuODM2LTcuNTQzIDUuNTA5LTkuODI4IDMuMDk0LTEuOTgxIDYuODc2LTIuOTcyIDExLjM0NC0yLjk3MiAzLjg0NiAwIDYuODk3LjgzMiA5LjE1MyAyLjQ5NSAyLjI1NSAxLjY0OSAzLjM4MyAzLjg5OCAzLjM4MyA2Ljc0NyAwIDEuNTkxLTIuNzExIDMuMzI2LTguMTMzIDUuMjA2LTQuNDgzIDEuNjItNy43NDQgMi40My05Ljc4MiAyLjQzYTQuNDkgNC40OSAwIDAgMS0uNjczLS4wNDNjMCAuMzktLjMxMS44NjgtLjkzMiAxLjQzMi4zMzIuNTY0IDEuOTgxIDEuMjE0IDQuOTQ1IDEuOTUyIDIuNjYuNjggNC41NTUgMS4wMiA1LjY4MiAxLjAyaDEzLjQ3YzAgLjAyOSAxLjY5OS4yODIgNS4wOTcuNzU5IDEuMDcuNjggMS44NDMgMS40MzkgMi4zMjEgMi4yNzhabS0xNy40NjEtMTYuOWMtMS41NzYtMS42MDUtNC4xMTMtMi40MDgtNy42MTMtMi40MDgtMi40NTggMC00LjczNS40NzctNi44MzIgMS40MzItMi43MDQgMS4yNDQtNC4wNTYgMi45MjktNC4wNTYgNS4wNTUgMCAuNzUyLjI1MyAxLjI4Ny43NTkgMS42MDVoMi43MTFjLjk1NSAwIDMuNTUtLjgwMyA3Ljc4Ny0yLjLine truncated
|
||||||
const advancedJs = `function download(dataURL, filename) {
|
|
||||||
const blob = dataURLToBlob(dataURL);
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.style = "display: none";
|
|
||||||
a.href = url;
|
|
||||||
a.download = filename;
|
|
||||||
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dataURLToBlob(dataURL) {
|
|
||||||
const parts = dataURL.split(';base64,');
|
|
||||||
const contentType = parts[0].split(":")[1];
|
|
||||||
const raw = window.atob(parts[1]);
|
|
||||||
const rawLength = raw.length;
|
|
||||||
const uInt8Array = new Uint8Array(rawLength);
|
|
||||||
|
|
||||||
for (let i = 0; i < rawLength; ++i) {
|
|
||||||
uInt8Array[i] = raw.charCodeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Blob([uInt8Array], { type: contentType });
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelector("#signature-advanced-color").addEventListener("input", function (e) {
|
|
||||||
signaturePad.penColor = e.target.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector("#signature-advanced-svg").addEventListener("click", function () {
|
|
||||||
const dataURL = signaturePad.toDataURL('image/svg+xml');
|
|
||||||
download(dataURL, "signature.svg");
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector("#signature-advanced-png").addEventListener("click", function () {
|
|
||||||
const dataURL = signaturePad.toDataURL();
|
|
||||||
download(dataURL, "signature.png");
|
|
||||||
});`
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<DefaultLayout title="Signatures" pageMenu="extra.signatures" pageLibs={['signature_pad']}>
|
<DefaultLayout title="Signatures" pageMenu="extra.signatures" pageLibs={['signature_pad']}>
|
||||||
@@ -74,7 +32,7 @@ document.querySelector("#signature-advanced-png").addEventListener("click", func
|
|||||||
<input type="text" class="form-control" id="signature-last-name" name="last_name" />
|
<input type="text" class="form-control" id="signature-last-name" name="last_name" />
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup label="Signature" required associate={false}>
|
<FormGroup label="Signature" required associate={false}>
|
||||||
<Signature id="default" clear extraJs={simpleJs} />
|
<Signature id="default" clear />
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</form>
|
</form>
|
||||||
<div class="text-secondary fs-5">I agree that the signature and initials will be the electronic representation of my signature and initials for all purposes when I (or my agent) use them on documents, including legally binding contracts - just the same as a pen-and-paper signature or initial.</div>
|
<div class="text-secondary fs-5">I agree that the signature and initials will be the electronic representation of my signature and initials for all purposes when I (or my agent) use them on documents, including legally binding contracts - just the same as a pen-and-paper signature or initial.</div>
|
||||||
@@ -96,7 +54,7 @@ document.querySelector("#signature-advanced-png").addEventListener("click", func
|
|||||||
<CardBody>
|
<CardBody>
|
||||||
<CardTitle>Advanced demo</CardTitle>
|
<CardTitle>Advanced demo</CardTitle>
|
||||||
|
|
||||||
<Signature id="advanced" clear extraJs={advancedJs} />
|
<Signature id="advanced" clear />
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -126,6 +84,49 @@ document.querySelector("#signature-advanced-png").addEventListener("click", func
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CaptureScript>
|
||||||
|
<!-- BEGIN SIGNATURE DEMO -->
|
||||||
|
<script is:inline define:vars={{ signatureImage }}>
|
||||||
|
function download(dataURL, filename) {
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.style.display = 'none'
|
||||||
|
a.href = dataURL
|
||||||
|
a.download = filename
|
||||||
|
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
a.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSignatureDemo() {
|
||||||
|
const signatures = window.tabler_signature ?? {}
|
||||||
|
|
||||||
|
signatures.default?.fromDataURL(signatureImage)
|
||||||
|
|
||||||
|
const advanced = signatures.advanced
|
||||||
|
|
||||||
|
if (!advanced) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('signature-advanced-color')?.addEventListener('input', function (e) {
|
||||||
|
advanced.penColor = e.target.value
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById('signature-advanced-svg')?.addEventListener('click', function () {
|
||||||
|
download(advanced.toDataURL('image/svg+xml'), 'signature.svg')
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById('signature-advanced-png')?.addEventListener('click', function () {
|
||||||
|
download(advanced.toDataURL(), 'signature.png')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
document.readyState !== 'loading' ? initSignatureDemo() : document.addEventListener('DOMContentLoaded', initSignatureDemo, { once: true })
|
||||||
|
</script>
|
||||||
|
<!-- END SIGNATURE DEMO -->
|
||||||
|
</CaptureScript>
|
||||||
|
|
||||||
<CaptureModal>
|
<CaptureModal>
|
||||||
<div class="modal" tabindex="-1" id="modal-signature" role="dialog" aria-modal="true" aria-labelledby="modal-signature-save-title">
|
<div class="modal" tabindex="-1" id="modal-signature" role="dialog" aria-modal="true" aria-labelledby="modal-signature-save-title">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ if (size === 'sm') {
|
|||||||
// The :root custom-property block stays a plain string rendered via set:html — it's
|
// The :root custom-property block stays a plain string rendered via set:html — it's
|
||||||
// inert CSS text (no injection surface), not executable code.
|
// inert CSS text (no injection surface), not executable code.
|
||||||
const style = data ? chartStyle({ id, data }) : '';
|
const style = data ? chartStyle({ id, data }) : '';
|
||||||
const { config, xFormatterExpr, yFormatterExpr } = data ? chartConfig({ id, data, height }) : { config: undefined, xFormatterExpr: undefined, yFormatterExpr: undefined };
|
const { config, xFormatter, yFormatter } = data ? chartConfig({ id, data, height }) : { config: undefined, xFormatter: undefined, yFormatter: undefined };
|
||||||
const chartKey = `chart-${id}`;
|
const chartKey = `chart-${id}`;
|
||||||
const elementId = `chart-${id}`;
|
const elementId = `chart-${id}`;
|
||||||
---
|
---
|
||||||
@@ -54,9 +54,14 @@ const elementId = `chart-${id}`;
|
|||||||
<Fragment set:html={style} />
|
<Fragment set:html={style} />
|
||||||
<CaptureScript>
|
<CaptureScript>
|
||||||
<!-- BEGIN CHART -->
|
<!-- BEGIN CHART -->
|
||||||
<script is:inline define:vars={{ chartKey, elementId, config, xFormatterExpr, yFormatterExpr }}>
|
<script is:inline define:vars={{ chartKey, elementId, config, xFormatter, yFormatter }}>
|
||||||
window.tabler_chart ??= {};
|
window.tabler_chart ??= {};
|
||||||
|
|
||||||
|
const formatters = {
|
||||||
|
thousands: (val) => val.toLocaleString('en-US'),
|
||||||
|
'suffix-k': (val) => val + 'K',
|
||||||
|
};
|
||||||
|
|
||||||
// Heatmap and treemap shade each cell by computing off the literal base color in JS
|
// Heatmap and treemap shade each cell by computing off the literal base color in JS
|
||||||
// (light -> full color-mix by value), unlike other chart types where the
|
// (light -> full color-mix by value), unlike other chart types where the
|
||||||
// SVG `fill` attribute just holds `var(--chart-...)` and the browser paints
|
// SVG `fill` attribute just holds `var(--chart-...)` and the browser paints
|
||||||
@@ -85,12 +90,12 @@ const elementId = `chart-${id}`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initChart() {
|
function initChart() {
|
||||||
if (xFormatterExpr && config.xaxis?.labels) {
|
if (Object.hasOwn(formatters, xFormatter) && config.xaxis?.labels) {
|
||||||
config.xaxis.labels.formatter = new Function('val', `return (${xFormatterExpr})`);
|
config.xaxis.labels.formatter = formatters[xFormatter];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (yFormatterExpr && config.yaxis?.labels) {
|
if (Object.hasOwn(formatters, yFormatter) && config.yaxis?.labels) {
|
||||||
config.yaxis.labels.formatter = new Function('val', `return (${yFormatterExpr})`);
|
config.yaxis.labels.formatter = formatters[yFormatter];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((config.chart?.type === 'heatmap' || config.chart?.type === 'treemap') && Array.isArray(config.colors)) {
|
if ((config.chart?.type === 'heatmap' || config.chart?.type === 'treemap') && Array.isArray(config.colors)) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
"height": 18,
|
"height": 18,
|
||||||
"datetime": true,
|
"datetime": true,
|
||||||
"stroke-curve": "straight",
|
"stroke-curve": "straight",
|
||||||
"y-formatter": "val.toLocaleString(\"en-US\")",
|
"y-formatter": "thousands",
|
||||||
"series": [
|
"series": [
|
||||||
{
|
{
|
||||||
"name": "Mobile",
|
"name": "Mobile",
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"horizontal": true,
|
"horizontal": true,
|
||||||
"stacked": true,
|
"stacked": true,
|
||||||
"categories": [2019, 2020, 2021, 2022, 2023, 2024, 2025],
|
"categories": [2019, 2020, 2021, 2022, 2023, 2024, 2025],
|
||||||
"x-formatter": "val + \"K\"",
|
"x-formatter": "suffix-k",
|
||||||
"series": [
|
"series": [
|
||||||
{
|
{
|
||||||
"name": "North America",
|
"name": "North America",
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
"type": "line",
|
"type": "line",
|
||||||
"datetime": true,
|
"datetime": true,
|
||||||
"stroke-curve": "straight",
|
"stroke-curve": "straight",
|
||||||
"y-formatter": "val.toLocaleString(\"en-US\")",
|
"y-formatter": "thousands",
|
||||||
"series": [
|
"series": [
|
||||||
{
|
{
|
||||||
"name": "Total visits",
|
"name": "Total visits",
|
||||||
@@ -302,7 +302,7 @@
|
|||||||
"stacked": true,
|
"stacked": true,
|
||||||
"legend": true,
|
"legend": true,
|
||||||
"name": "Semantic colors",
|
"name": "Semantic colors",
|
||||||
"y-formatter": "val.toLocaleString(\"en-US\")",
|
"y-formatter": "thousands",
|
||||||
"categories": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
"categories": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
||||||
"series": [
|
"series": [
|
||||||
{ "name": "Hit", "color": "success", "data": [18000, 19200, 21000, 20300, 22000, 16600, 15000] },
|
{ "name": "Hit", "color": "success", "data": [18000, 19200, 21000, 20300, 22000, 16600, 15000] },
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { chartStyle, chartConfig, type ChartData } from './chart-script'
|
import charts from '../data/charts.json'
|
||||||
|
import { chartStyle, chartConfig, chartFormatters, type ChartData } from './chart-script'
|
||||||
|
|
||||||
describe('chartStyle', () => {
|
describe('chartStyle', () => {
|
||||||
it('emits an id-scoped color custom property per serie', () => {
|
it('emits an id-scoped color custom property per serie', () => {
|
||||||
@@ -131,16 +132,21 @@ describe('chartConfig', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns the x-formatter as a separate expression instead of embedding it', () => {
|
it('returns the formatter names separately instead of embedding them', () => {
|
||||||
const { config, xFormatterExpr } = chartConfig({ id: 'demo', data: { 'series': [], 'x-formatter': 'val + "K"' }, height: 10 })
|
const { config, xFormatter, yFormatter } = chartConfig({ id: 'demo', data: { 'series': [], 'x-formatter': 'suffix-k', 'y-formatter': 'thousands' }, height: 10 })
|
||||||
expect(xFormatterExpr).toBe('val + "K"')
|
expect(xFormatter).toBe('suffix-k')
|
||||||
expect(JSON.stringify(config)).not.toContain('val + "K"')
|
expect(yFormatter).toBe('thousands')
|
||||||
|
expect(JSON.stringify(config)).not.toMatch(/suffix-k|thousands/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns the y-formatter as a separate expression instead of embedding it', () => {
|
it('only uses known formatter names in charts.json', () => {
|
||||||
const { config, yFormatterExpr } = chartConfig({ id: 'demo', data: { 'series': [], 'y-formatter': 'val.toLocaleString("en-US")' }, height: 10 })
|
const used = Object.values(charts as Record<string, Record<string, unknown>>)
|
||||||
expect(yFormatterExpr).toBe('val.toLocaleString("en-US")')
|
.flatMap((chart) => [chart['x-formatter'], chart['y-formatter']])
|
||||||
expect(JSON.stringify(config)).not.toContain('toLocaleString')
|
.filter((name) => name !== undefined)
|
||||||
|
expect(used.length).toBeGreaterThan(0)
|
||||||
|
for (const name of used) {
|
||||||
|
expect(chartFormatters).toContain(name)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses the ApexCharts 7 legend marker options', () => {
|
it('uses the ApexCharts 7 legend marker options', () => {
|
||||||
|
|||||||
+11
-11
@@ -2,6 +2,10 @@
|
|||||||
// inert, not executable). chartConfig() returns a JSON-serializable config object,
|
// inert, not executable). chartConfig() returns a JSON-serializable config object,
|
||||||
// rendered by Chart.astro via <script define:vars>.
|
// rendered by Chart.astro via <script define:vars>.
|
||||||
|
|
||||||
|
/** Named axis label formatters, implemented in Chart.astro. */
|
||||||
|
export const chartFormatters = ['thousands', 'suffix-k'] as const
|
||||||
|
export type ChartFormatter = (typeof chartFormatters)[number]
|
||||||
|
|
||||||
type Serie = {
|
type Serie = {
|
||||||
'name'?: string
|
'name'?: string
|
||||||
/** overrides the chart type for this one serie — how a combination chart is built */
|
/** overrides the chart type for this one serie — how a combination chart is built */
|
||||||
@@ -69,8 +73,8 @@ export type ChartData = {
|
|||||||
'stroke-curve'?: string
|
'stroke-curve'?: string
|
||||||
'hide-grid'?: boolean
|
'hide-grid'?: boolean
|
||||||
'show-x'?: boolean
|
'show-x'?: boolean
|
||||||
'x-formatter'?: string
|
'x-formatter'?: ChartFormatter
|
||||||
'y-formatter'?: string
|
'y-formatter'?: ChartFormatter
|
||||||
'y-max'?: number
|
'y-max'?: number
|
||||||
'y-title'?: string
|
'y-title'?: string
|
||||||
'y-tooltip'?: boolean
|
'y-tooltip'?: boolean
|
||||||
@@ -173,15 +177,11 @@ export function chartStyle(opts: { id: string; data: ChartData }): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The ApexCharts config, as a real JSON-serializable object (rendered via
|
* The ApexCharts config, as a real JSON-serializable object (rendered via
|
||||||
* <script define:vars> — no string-built script). The one exception is
|
* <script define:vars> — no string-built script). Axis label formatters can't be
|
||||||
* `x-formatter` / `y-formatter`: a handful of charts.json entries carry a raw JS
|
* represented as data, so `x-formatter` / `y-formatter` name one of `chartFormatters`
|
||||||
* expression for an axis label formatter (e.g. `val + "K"`), which can't be represented
|
* and come back as `xFormatter` / `yFormatter`, for Chart.astro to map to a real function.
|
||||||
* as data. It comes back as `xFormatterExpr` / `yFormatterExpr` instead of being embedded in `config`,
|
|
||||||
* for the caller to turn into a real function with `new Function` — see
|
|
||||||
* Chart.astro. This is data-driven (from our own charts.json, not user input),
|
|
||||||
* same trust level as everything else here.
|
|
||||||
*/
|
*/
|
||||||
export function chartConfig(opts: { id: string; data: ChartData; height: number }): { config: Record<string, unknown>; xFormatterExpr?: string | undefined; yFormatterExpr?: string | undefined } {
|
export function chartConfig(opts: { id: string; data: ChartData; height: number }): { config: Record<string, unknown>; xFormatter?: ChartFormatter | undefined; yFormatter?: ChartFormatter | undefined } {
|
||||||
const { id, data, height } = opts
|
const { id, data, height } = opts
|
||||||
const type = data.type ?? 'bar'
|
const type = data.type ?? 'bar'
|
||||||
const series = data.series ?? []
|
const series = data.series ?? []
|
||||||
@@ -468,5 +468,5 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
|
|||||||
config.markers = { size: 2 }
|
config.markers = { size: 2 }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { config, xFormatterExpr: data['x-formatter'], yFormatterExpr: data['y-formatter'] }
|
return { config, xFormatter: data['x-formatter'], yFormatter: data['y-formatter'] }
|
||||||
}
|
}
|
||||||
+57
-54
@@ -9,75 +9,78 @@ interface Props {
|
|||||||
width?: number
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
event?: string
|
event?: string
|
||||||
/** raw JS appended inside the init callback */
|
|
||||||
extraJs?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, class: className, clear, width = 400, height = 400, event = 'DOMContentLoaded', extraJs } = Astro.props
|
const { id, class: className, clear = false, width = 400, height = 400, event = 'DOMContentLoaded' } = Astro.props
|
||||||
|
|
||||||
const signatureClass = ['signature position-relative', className]
|
const signatureClass = ['signature position-relative', className]
|
||||||
|
const signatureKey = id
|
||||||
const clearSnippet = clear
|
const canvasId = `signature-${id}`
|
||||||
? ` document.querySelector(${JSON.stringify(`#signature-${id}-clear`)}).addEventListener("click", function () {
|
const clearId = `signature-${id}-clear`
|
||||||
signaturePad.clear();
|
|
||||||
});
|
|
||||||
`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
// extraJs is a documented escape hatch for raw JS (see Props) — not user data, so it
|
|
||||||
// stays interpolated verbatim rather than JSON-serialized.
|
|
||||||
const extraSnippet = extraJs
|
|
||||||
? `${extraJs}
|
|
||||||
`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
// set:html (below) is genuinely required here: extraJs is a documented raw-JS escape
|
|
||||||
// hatch that must land *inside* the init callback body (it references canvas /
|
|
||||||
// signaturePad), so the whole script has to be assembled as text — <script define:vars>
|
|
||||||
// can only inject variables, not splice caller-supplied code into the function body.
|
|
||||||
const script = `<!-- BEGIN SIGNATURE PAD -->
|
|
||||||
<script>
|
|
||||||
document.addEventListener(${JSON.stringify(event)}, function () {
|
|
||||||
const canvas = document.getElementById(${JSON.stringify(`signature-${id}`)});
|
|
||||||
|
|
||||||
if (canvas) {
|
|
||||||
const signaturePad = new SignaturePad(canvas, {
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
penColor: getComputedStyle(canvas).color
|
|
||||||
});
|
|
||||||
|
|
||||||
${clearSnippet}
|
|
||||||
function resizeCanvas() {
|
|
||||||
const ratio = Math.max(window.devicePixelRatio || 1, 1);
|
|
||||||
|
|
||||||
canvas.width = canvas.offsetWidth * ratio;
|
|
||||||
canvas.height = canvas.offsetHeight * ratio;
|
|
||||||
canvas.getContext("2d").scale(ratio, ratio);
|
|
||||||
signaturePad.fromData(signaturePad.toData());
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("resize", resizeCanvas);
|
|
||||||
resizeCanvas();
|
|
||||||
|
|
||||||
${extraSnippet}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<!-- END SIGNATURE PAD -->`
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class:list={signatureClass}>
|
<div class:list={signatureClass}>
|
||||||
{
|
{
|
||||||
clear && (
|
clear && (
|
||||||
<div class="position-absolute top-0 end-0 p-2">
|
<div class="position-absolute top-0 end-0 p-2">
|
||||||
<button type="button" class="btn btn-icon" id={`signature-${id}-clear`} title="Clear signature" aria-label="Clear signature" data-bs-toggle="tooltip">
|
<button type="button" class="btn btn-icon" id={clearId} title="Clear signature" aria-label="Clear signature" data-bs-toggle="tooltip">
|
||||||
<Icon name="trash" />{' '}
|
<Icon name="trash" />{' '}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<canvas id={`signature-${id}`} width={width} height={height} class="signature-canvas"></canvas>
|
<canvas id={canvasId} width={width} height={height} class="signature-canvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<CaptureScript>
|
<CaptureScript>
|
||||||
<Fragment set:html={script} />
|
<!-- BEGIN SIGNATURE PAD -->
|
||||||
|
<script is:inline define:vars={{ signatureKey, canvasId, clearId, clear, event }}>
|
||||||
|
window.tabler_signature ??= {}
|
||||||
|
|
||||||
|
function resizeCanvas(canvas, signaturePad) {
|
||||||
|
const ratio = Math.max(window.devicePixelRatio || 1, 1)
|
||||||
|
|
||||||
|
canvas.width = canvas.offsetWidth * ratio
|
||||||
|
canvas.height = canvas.offsetHeight * ratio
|
||||||
|
canvas.getContext('2d').scale(ratio, ratio)
|
||||||
|
signaturePad.fromData(signaturePad.toData())
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSignature() {
|
||||||
|
const canvas = document.getElementById(canvasId)
|
||||||
|
|
||||||
|
if (!canvas) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let signaturePad = window.tabler_signature[signatureKey]
|
||||||
|
|
||||||
|
if (!signaturePad) {
|
||||||
|
signaturePad = new SignaturePad(canvas, {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
penColor: getComputedStyle(canvas).color,
|
||||||
|
})
|
||||||
|
|
||||||
|
window.tabler_signature[signatureKey] = signaturePad
|
||||||
|
|
||||||
|
if (clear) {
|
||||||
|
document.getElementById(clearId)?.addEventListener('click', function () {
|
||||||
|
signaturePad.clear()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', function () {
|
||||||
|
resizeCanvas(canvas, signaturePad)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeCanvas(canvas, signaturePad)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === 'DOMContentLoaded') {
|
||||||
|
document.readyState !== 'loading' ? initSignature() : document.addEventListener('DOMContentLoaded', initSignature, { once: true })
|
||||||
|
} else {
|
||||||
|
document.addEventListener(event, initSignature)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<!-- END SIGNATURE PAD -->
|
||||||
</CaptureScript>
|
</CaptureScript>
|
||||||
Reference in new issue
Block a user