@svgrid/enterprise 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +97 -0
- package/README.md +37 -0
- package/package.json +72 -0
- package/src/ai.test.ts +522 -0
- package/src/ai.ts +782 -0
- package/src/export.ts +512 -0
- package/src/import.test.ts +415 -0
- package/src/import.ts +648 -0
- package/src/index.ts +88 -0
- package/src/install.ts +114 -0
- package/src/license.ts +90 -0
- package/src/pdfmake-shims.d.ts +22 -0
- package/src/pivot.test.ts +308 -0
- package/src/pivot.ts +549 -0
- package/src/print.ts +127 -0
- package/src/revoked.ts +49 -0
- package/src/smart-shim.ts +105 -0
- package/src/smart.export.d.ts +5 -0
- package/src/smart.export.js +7 -0
- package/src/staged-editing.ts +0 -0
- package/src/upgrade-prompt.test.ts +71 -0
- package/src/upgrade-prompt.ts +148 -0
- package/src/watermark.test.ts +97 -0
- package/src/watermark.ts +156 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the unlicensed soft-gate watermark + console nudge.
|
|
3
|
+
* - The watermark reads "www.svgrid.com" and sits in the bottom-right of the
|
|
4
|
+
* grid root.
|
|
5
|
+
* - It fades out and is removed after 5 seconds.
|
|
6
|
+
* - The console nudge names the commercial license + sales contact.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
10
|
+
import { emitUnlicensedNudge, dismissUnlicensedNudge } from './watermark'
|
|
11
|
+
|
|
12
|
+
const WATERMARK_ATTR = 'data-@svgrid/enterprise-watermark'
|
|
13
|
+
|
|
14
|
+
function makeGridRoot(): HTMLElement {
|
|
15
|
+
const el = document.createElement('div')
|
|
16
|
+
el.className = 'sv-grid-root'
|
|
17
|
+
document.body.appendChild(el)
|
|
18
|
+
return el
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function flushMicrotasks() {
|
|
22
|
+
return Promise.resolve()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('unlicensed watermark', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.useFakeTimers()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
dismissUnlicensedNudge()
|
|
32
|
+
vi.useRealTimers()
|
|
33
|
+
document.body.innerHTML = ''
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('attaches a "www.svgrid.com" watermark to the grid bottom-right', async () => {
|
|
37
|
+
const grid = makeGridRoot()
|
|
38
|
+
emitUnlicensedNudge()
|
|
39
|
+
await flushMicrotasks()
|
|
40
|
+
|
|
41
|
+
const mark = grid.querySelector(`[${WATERMARK_ATTR}]`) as HTMLAnchorElement | null
|
|
42
|
+
expect(mark).not.toBeNull()
|
|
43
|
+
expect(mark!.textContent).toBe('www.svgrid.com')
|
|
44
|
+
expect(mark!.href).toContain('svgrid.com')
|
|
45
|
+
expect(mark!.style.position).toBe('absolute')
|
|
46
|
+
expect(mark!.style.bottom).toBe('6px')
|
|
47
|
+
expect(mark!.style.right).toBe('8px')
|
|
48
|
+
// The grid gets a positioning context so the absolute watermark anchors.
|
|
49
|
+
expect(grid.style.position).toBe('relative')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('fades out and removes the watermark after ~5 seconds', async () => {
|
|
53
|
+
const grid = makeGridRoot()
|
|
54
|
+
emitUnlicensedNudge()
|
|
55
|
+
await flushMicrotasks()
|
|
56
|
+
|
|
57
|
+
const mark = grid.querySelector(`[${WATERMARK_ATTR}]`) as HTMLElement
|
|
58
|
+
expect(mark.style.opacity).toBe('1')
|
|
59
|
+
|
|
60
|
+
// After 5s the fade starts...
|
|
61
|
+
vi.advanceTimersByTime(5000)
|
|
62
|
+
expect(mark.style.opacity).toBe('0')
|
|
63
|
+
|
|
64
|
+
// ...and after the fade transition the node is gone.
|
|
65
|
+
vi.advanceTimersByTime(700)
|
|
66
|
+
expect(grid.querySelector(`[${WATERMARK_ATTR}]`)).toBeNull()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('does not re-add the watermark after it has faded out', async () => {
|
|
70
|
+
const grid = makeGridRoot()
|
|
71
|
+
emitUnlicensedNudge()
|
|
72
|
+
await flushMicrotasks()
|
|
73
|
+
vi.advanceTimersByTime(5700)
|
|
74
|
+
expect(grid.querySelector(`[${WATERMARK_ATTR}]`)).toBeNull()
|
|
75
|
+
|
|
76
|
+
// A later DOM mutation must not bring it back (grid is marked "nudged").
|
|
77
|
+
document.body.appendChild(document.createElement('span'))
|
|
78
|
+
await flushMicrotasks()
|
|
79
|
+
expect(grid.querySelector(`[${WATERMARK_ATTR}]`)).toBeNull()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('logs a one-time commercial-license console nudge', async () => {
|
|
83
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
84
|
+
try {
|
|
85
|
+
emitUnlicensedNudge()
|
|
86
|
+
const msg = spy.mock.calls.map((c) => String(c[0])).join('\n')
|
|
87
|
+
expect(msg).toContain('Commercial version which requires a license')
|
|
88
|
+
expect(msg).toContain('sales@jqwidgets.com')
|
|
89
|
+
// One-time: a second call does not log again.
|
|
90
|
+
const callsAfterFirst = spy.mock.calls.length
|
|
91
|
+
emitUnlicensedNudge()
|
|
92
|
+
expect(spy.mock.calls.length).toBe(callsAfterFirst)
|
|
93
|
+
} finally {
|
|
94
|
+
spy.mockRestore()
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
})
|
package/src/watermark.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Soft-gate UX for unlicensed @svgrid/enterprise use. Shows a small clickable
|
|
2
|
+
// "www.svgrid.com" watermark in the bottom-right corner of every SvGrid on
|
|
3
|
+
// the page, which fades out after 5 seconds, and emits a one-time console
|
|
4
|
+
// nudge. Hard errors (revoked / bad-prefix keys) still throw via license.ts -
|
|
5
|
+
// this file is only for the "no key set" case where the feature continues to
|
|
6
|
+
// work but the user gets nudged.
|
|
7
|
+
|
|
8
|
+
const WATERMARK_ATTR = 'data-@svgrid/enterprise-watermark'
|
|
9
|
+
// Marks a grid that has already shown (and faded) its watermark, so the
|
|
10
|
+
// MutationObserver doesn't re-add it once it fades out and is removed.
|
|
11
|
+
const NUDGED_ATTR = 'data-@svgrid/enterprise-nudged'
|
|
12
|
+
const HOMEPAGE_URL = 'https://www.svgrid.com'
|
|
13
|
+
const LICENSE_URL = 'https://www.svgrid.com/pricing'
|
|
14
|
+
const WATERMARK_LABEL = 'www.svgrid.com'
|
|
15
|
+
|
|
16
|
+
// How long the watermark stays fully visible before it starts fading, and how
|
|
17
|
+
// long the fade itself takes.
|
|
18
|
+
const VISIBLE_MS = 5000
|
|
19
|
+
const FADE_MS = 700
|
|
20
|
+
|
|
21
|
+
let consoleNudgeShown = false
|
|
22
|
+
let observer: MutationObserver | null = null
|
|
23
|
+
|
|
24
|
+
export function emitUnlicensedNudge(): void {
|
|
25
|
+
showConsoleOnce()
|
|
26
|
+
if (typeof document === 'undefined' || typeof window === 'undefined') return
|
|
27
|
+
// Defer one tick: installPro may run inside onApiReady before the grid's
|
|
28
|
+
// root element is in the DOM. The microtask gives Svelte time to flush.
|
|
29
|
+
queueMicrotask(() => attachToAllGrids())
|
|
30
|
+
// Watch for grids that mount later (route changes, dynamic loads).
|
|
31
|
+
if (!observer) {
|
|
32
|
+
observer = new MutationObserver(() => attachToAllGrids())
|
|
33
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function dismissUnlicensedNudge(): void {
|
|
38
|
+
if (observer) {
|
|
39
|
+
observer.disconnect()
|
|
40
|
+
observer = null
|
|
41
|
+
}
|
|
42
|
+
if (typeof document === 'undefined') return
|
|
43
|
+
document.querySelectorAll(`[${WATERMARK_ATTR}]`).forEach((el) => el.remove())
|
|
44
|
+
document
|
|
45
|
+
.querySelectorAll(`[${NUDGED_ATTR}]`)
|
|
46
|
+
.forEach((el) => el.removeAttribute(NUDGED_ATTR))
|
|
47
|
+
consoleNudgeShown = false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function showConsoleOnce(): void {
|
|
51
|
+
if (consoleNudgeShown) return
|
|
52
|
+
consoleNudgeShown = true
|
|
53
|
+
if (typeof console === 'undefined') return
|
|
54
|
+
// Per spec we use console.log (not warn/error) - this is a nudge, not an
|
|
55
|
+
// error condition. Styled with %c so it stands out in the devtools.
|
|
56
|
+
console.log(
|
|
57
|
+
'%c @svgrid/enterprise %c User is using a Commercial version which requires a license.\n' +
|
|
58
|
+
'Contact sales@jqwidgets.com for details or visit the license page: ' +
|
|
59
|
+
LICENSE_URL,
|
|
60
|
+
'background:#4f46e5;color:#fff;padding:2px 6px;border-radius:3px;font-weight:600',
|
|
61
|
+
'color:inherit',
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function attachToAllGrids(): void {
|
|
66
|
+
if (typeof document === 'undefined') return
|
|
67
|
+
// Primary selector targets SvGrid's root container class. Fall back to
|
|
68
|
+
// ARIA grid role for headless renderers that re-style the wrapper.
|
|
69
|
+
const grids = document.querySelectorAll<HTMLElement>(
|
|
70
|
+
'.sv-grid-root, [role="grid"]',
|
|
71
|
+
)
|
|
72
|
+
if (grids.length === 0) {
|
|
73
|
+
ensureFixedFallback()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
// A grid root was found - remove any fixed fallback we may have installed
|
|
77
|
+
// earlier before the grid mounted.
|
|
78
|
+
document
|
|
79
|
+
.querySelectorAll(`[${WATERMARK_ATTR}="fixed"]`)
|
|
80
|
+
.forEach((el) => el.remove())
|
|
81
|
+
grids.forEach(attachWatermarkTo)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function attachWatermarkTo(grid: HTMLElement): void {
|
|
85
|
+
// Show the watermark once per grid. The marker survives the fade-out so the
|
|
86
|
+
// MutationObserver doesn't keep re-adding it on every DOM change.
|
|
87
|
+
if (grid.hasAttribute(NUDGED_ATTR)) return
|
|
88
|
+
grid.setAttribute(NUDGED_ATTR, '1')
|
|
89
|
+
const cs = getComputedStyle(grid)
|
|
90
|
+
if (cs.position === 'static') {
|
|
91
|
+
// Need a positioning context for absolute positioning. Only override when
|
|
92
|
+
// 'static' (the browser default) - don't clobber an explicit
|
|
93
|
+
// relative/absolute the consumer chose.
|
|
94
|
+
grid.style.position = 'relative'
|
|
95
|
+
}
|
|
96
|
+
const a = buildWatermark('grid')
|
|
97
|
+
applyWatermarkStyles(a, /* fixed */ false)
|
|
98
|
+
grid.appendChild(a)
|
|
99
|
+
scheduleFadeOut(a)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ensureFixedFallback(): void {
|
|
103
|
+
if (typeof document === 'undefined') return
|
|
104
|
+
if (document.querySelector(`[${WATERMARK_ATTR}="fixed"]`)) return
|
|
105
|
+
const a = buildWatermark('fixed')
|
|
106
|
+
applyWatermarkStyles(a, /* fixed */ true)
|
|
107
|
+
document.body.appendChild(a)
|
|
108
|
+
scheduleFadeOut(a)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildWatermark(kind: 'grid' | 'fixed'): HTMLAnchorElement {
|
|
112
|
+
const a = document.createElement('a')
|
|
113
|
+
a.setAttribute(WATERMARK_ATTR, kind)
|
|
114
|
+
a.href = HOMEPAGE_URL
|
|
115
|
+
a.target = '_blank'
|
|
116
|
+
a.rel = 'noopener noreferrer'
|
|
117
|
+
a.textContent = WATERMARK_LABEL
|
|
118
|
+
a.title =
|
|
119
|
+
'Unlicensed @svgrid/enterprise. Contact sales@jqwidgets.com or visit www.svgrid.com.'
|
|
120
|
+
return a
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Keep the watermark visible for VISIBLE_MS, then fade it out and remove it. */
|
|
124
|
+
function scheduleFadeOut(el: HTMLElement): void {
|
|
125
|
+
if (typeof window === 'undefined') {
|
|
126
|
+
el.remove()
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
window.setTimeout(() => {
|
|
130
|
+
el.style.opacity = '0'
|
|
131
|
+
window.setTimeout(() => el.remove(), FADE_MS)
|
|
132
|
+
}, VISIBLE_MS)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function applyWatermarkStyles(el: HTMLAnchorElement, fixed: boolean): void {
|
|
136
|
+
Object.assign(el.style, {
|
|
137
|
+
position: fixed ? 'fixed' : 'absolute',
|
|
138
|
+
bottom: fixed ? '12px' : '6px',
|
|
139
|
+
right: fixed ? '12px' : '8px',
|
|
140
|
+
zIndex: '2147483647',
|
|
141
|
+
background: 'rgba(15,23,42,0.85)',
|
|
142
|
+
color: '#fff',
|
|
143
|
+
padding: fixed ? '6px 10px' : '4px 8px',
|
|
144
|
+
borderRadius: fixed ? '4px' : '3px',
|
|
145
|
+
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
|
|
146
|
+
fontSize: fixed ? '11px' : '10px',
|
|
147
|
+
lineHeight: '1.2',
|
|
148
|
+
textDecoration: 'none',
|
|
149
|
+
boxShadow: fixed ? '0 2px 12px rgba(0,0,0,0.35)' : '0 1px 4px rgba(0,0,0,0.3)',
|
|
150
|
+
border: '1px solid rgba(255,255,255,0.18)',
|
|
151
|
+
pointerEvents: 'auto',
|
|
152
|
+
letterSpacing: '0.02em',
|
|
153
|
+
opacity: '1',
|
|
154
|
+
transition: `opacity ${FADE_MS}ms ease`,
|
|
155
|
+
} as Partial<CSSStyleDeclaration> as CSSStyleDeclaration)
|
|
156
|
+
}
|