├── .eslintignore ├── .github └── workflows │ └── autofix.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── bun.lock ├── docs ├── .vitepress │ ├── cache │ │ └── deps │ │ │ ├── _metadata.json │ │ │ ├── chunk-H6CAFZOV.js │ │ │ ├── chunk-H6CAFZOV.js.map │ │ │ ├── chunk-LW4I4DCF.js │ │ │ ├── chunk-LW4I4DCF.js.map │ │ │ ├── package.json │ │ │ ├── vitepress___@vue_devtools-api.js │ │ │ ├── vitepress___@vue_devtools-api.js.map │ │ │ ├── vitepress___@vueuse_core.js │ │ │ ├── vitepress___@vueuse_core.js.map │ │ │ ├── vitepress___@vueuse_integrations_useFocusTrap.js │ │ │ ├── vitepress___@vueuse_integrations_useFocusTrap.js.map │ │ │ ├── vitepress___mark__js_src_vanilla__js.js │ │ │ ├── vitepress___mark__js_src_vanilla__js.js.map │ │ │ ├── vitepress___minisearch.js │ │ │ ├── vitepress___minisearch.js.map │ │ │ ├── vue.js │ │ │ └── vue.js.map │ ├── config.ts │ └── theme │ │ ├── index.ts │ │ └── style.css ├── boilerplate.md ├── cli.md ├── component-api.md ├── configuration.md ├── embed.md ├── hot-reload.md ├── index.md ├── install.md ├── introduction.md ├── public │ ├── favicon.ico │ └── svelte-standalone.png ├── shadcn.md ├── shared.md ├── svelte-notifications.md ├── tailwind.md └── web-components.md ├── eslint.config.mjs ├── lib ├── cli │ ├── cli-build.ts │ ├── cli-create.ts │ ├── cli.ts │ ├── methods │ │ ├── build.ts │ │ ├── create.ts │ │ └── index.ts │ └── utils │ │ ├── getPath.ts │ │ ├── hashmaps.ts │ │ └── isDependency.ts ├── dir.ts ├── embed.ts ├── generate-plop.ts ├── index.ts ├── plop-templates │ ├── config │ │ ├── no-typescript.hbs │ │ ├── types-auto.hbs │ │ ├── types-multiple.hbs │ │ └── types.hbs │ ├── embed.hbs │ ├── route │ │ ├── layout.hbs │ │ ├── route-auto-start.hbs │ │ ├── route-callable.hbs │ │ ├── route-multiple.hbs │ │ ├── route-with-class.hbs │ │ └── route-with-target.hbs │ ├── story │ │ ├── story-no-config.hbs │ │ └── story-with-config.hbs │ └── structure │ │ └── component.hbs ├── plopfile-with-js.ts └── plopfile.ts ├── package.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | 15 | # Ignore rollup builds 16 | static/dist -------------------------------------------------------------------------------- /.github/workflows/autofix.yml: -------------------------------------------------------------------------------- 1 | name: autofix.ci 2 | on: 3 | pull_request: 4 | push: 5 | branches: [ "main" ] 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | autofix: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: oven-sh/setup-bun@v2 15 | with: 16 | bun-version: 1.2.0 17 | 18 | - run: bun install --frozen-lockfile 19 | - run: bunx prettier --write . 20 | 21 | - uses: autofix-ci/action@dd55f44df8f7cdb7a6bf74c78677eb8acd40cd0a 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /dist 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | docs/.vitepress/dist -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore files for PNPM, NPM and YARN 2 | pnpm-lock.yaml 3 | package-lock.json 4 | yarn.lock 5 | 6 | # Ignore rollup builds 7 | static/dist 8 | 9 | # Ignore handlebard 10 | *.hbs 11 | .github/* 12 | 13 | dist 14 | docs/.vitepress/cache -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Breno Lira 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Svelte Standalone 2 | 3 | **Svelte Standalone** is a CLI tool that simplifies bundling Svelte components into standalone JavaScript files. It has _opt-in_ support for **Tailwind**, **TypeScript**, and **Storybook**, making it simple to integrate to your workflow. 4 | 5 | [Start Bundling Svelte Right Now!](https://standalone.brenoliradev.com/) 6 | 7 | ![image](https://github.com/user-attachments/assets/88123f50-28a1-4ee9-8db9-4633dff6abbe) 8 | 9 | --- 10 | 11 | ## License 12 | 13 | MIT © 2024 Breno Lira 14 | 15 | # standalone-test 16 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash": "7e639472", 3 | "configHash": "b66c32e7", 4 | "lockfileHash": "e3b0c442", 5 | "browserHash": "d86e20e5", 6 | "optimized": { 7 | "vue": { 8 | "src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js", 9 | "file": "vue.js", 10 | "fileHash": "6b0d315b", 11 | "needsInterop": false 12 | }, 13 | "vitepress > @vue/devtools-api": { 14 | "src": "../../../../node_modules/@vue/devtools-api/dist/index.js", 15 | "file": "vitepress___@vue_devtools-api.js", 16 | "fileHash": "fda30008", 17 | "needsInterop": false 18 | }, 19 | "vitepress > @vueuse/core": { 20 | "src": "../../../../node_modules/@vueuse/core/index.mjs", 21 | "file": "vitepress___@vueuse_core.js", 22 | "fileHash": "d9fc7e48", 23 | "needsInterop": false 24 | }, 25 | "vitepress > @vueuse/integrations/useFocusTrap": { 26 | "src": "../../../../node_modules/@vueuse/integrations/useFocusTrap.mjs", 27 | "file": "vitepress___@vueuse_integrations_useFocusTrap.js", 28 | "fileHash": "98f578e1", 29 | "needsInterop": false 30 | }, 31 | "vitepress > mark.js/src/vanilla.js": { 32 | "src": "../../../../node_modules/mark.js/src/vanilla.js", 33 | "file": "vitepress___mark__js_src_vanilla__js.js", 34 | "fileHash": "dcf5ff4f", 35 | "needsInterop": false 36 | }, 37 | "vitepress > minisearch": { 38 | "src": "../../../../node_modules/minisearch/dist/es/index.js", 39 | "file": "vitepress___minisearch.js", 40 | "fileHash": "e9011790", 41 | "needsInterop": false 42 | } 43 | }, 44 | "chunks": { 45 | "chunk-H6CAFZOV": { 46 | "file": "chunk-H6CAFZOV.js" 47 | }, 48 | "chunk-LW4I4DCF": { 49 | "file": "chunk-LW4I4DCF.js" 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vitepress___@vueuse_core.js: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultMagicKeysAliasMap, 3 | StorageSerializers, 4 | TransitionPresets, 5 | assert, 6 | breakpointsAntDesign, 7 | breakpointsBootstrapV5, 8 | breakpointsElement, 9 | breakpointsMasterCss, 10 | breakpointsPrimeFlex, 11 | breakpointsQuasar, 12 | breakpointsSematic, 13 | breakpointsTailwind, 14 | breakpointsVuetify, 15 | breakpointsVuetifyV2, 16 | breakpointsVuetifyV3, 17 | bypassFilter, 18 | camelize, 19 | clamp, 20 | cloneFnJSON, 21 | computedAsync, 22 | computedEager, 23 | computedInject, 24 | computedWithControl, 25 | containsProp, 26 | controlledRef, 27 | createEventHook, 28 | createFetch, 29 | createFilterWrapper, 30 | createGlobalState, 31 | createInjectionState, 32 | createReusableTemplate, 33 | createSharedComposable, 34 | createSingletonPromise, 35 | createTemplatePromise, 36 | createUnrefFn, 37 | customStorageEventName, 38 | debounceFilter, 39 | defaultDocument, 40 | defaultLocation, 41 | defaultNavigator, 42 | defaultWindow, 43 | executeTransition, 44 | extendRef, 45 | formatDate, 46 | formatTimeAgo, 47 | get, 48 | getLifeCycleTarget, 49 | getSSRHandler, 50 | hasOwn, 51 | hyphenate, 52 | identity, 53 | increaseWithUnit, 54 | injectLocal, 55 | invoke, 56 | isClient, 57 | isDef, 58 | isDefined, 59 | isIOS, 60 | isObject, 61 | isWorker, 62 | makeDestructurable, 63 | mapGamepadToXbox360Controller, 64 | noop, 65 | normalizeDate, 66 | notNullish, 67 | now, 68 | objectEntries, 69 | objectOmit, 70 | objectPick, 71 | onClickOutside, 72 | onElementRemoval, 73 | onKeyDown, 74 | onKeyPressed, 75 | onKeyStroke, 76 | onKeyUp, 77 | onLongPress, 78 | onStartTyping, 79 | pausableFilter, 80 | promiseTimeout, 81 | provideLocal, 82 | provideSSRWidth, 83 | pxValue, 84 | rand, 85 | reactify, 86 | reactifyObject, 87 | reactiveComputed, 88 | reactiveOmit, 89 | reactivePick, 90 | refAutoReset, 91 | refDebounced, 92 | refDefault, 93 | refThrottled, 94 | refWithControl, 95 | resolveRef, 96 | resolveUnref, 97 | set, 98 | setSSRHandler, 99 | syncRef, 100 | syncRefs, 101 | templateRef, 102 | throttleFilter, 103 | timestamp, 104 | toArray, 105 | toReactive, 106 | toRef, 107 | toRefs, 108 | toValue, 109 | tryOnBeforeMount, 110 | tryOnBeforeUnmount, 111 | tryOnMounted, 112 | tryOnScopeDispose, 113 | tryOnUnmounted, 114 | unrefElement, 115 | until, 116 | useActiveElement, 117 | useAnimate, 118 | useArrayDifference, 119 | useArrayEvery, 120 | useArrayFilter, 121 | useArrayFind, 122 | useArrayFindIndex, 123 | useArrayFindLast, 124 | useArrayIncludes, 125 | useArrayJoin, 126 | useArrayMap, 127 | useArrayReduce, 128 | useArraySome, 129 | useArrayUnique, 130 | useAsyncQueue, 131 | useAsyncState, 132 | useBase64, 133 | useBattery, 134 | useBluetooth, 135 | useBreakpoints, 136 | useBroadcastChannel, 137 | useBrowserLocation, 138 | useCached, 139 | useClipboard, 140 | useClipboardItems, 141 | useCloned, 142 | useColorMode, 143 | useConfirmDialog, 144 | useCountdown, 145 | useCounter, 146 | useCssVar, 147 | useCurrentElement, 148 | useCycleList, 149 | useDark, 150 | useDateFormat, 151 | useDebounceFn, 152 | useDebouncedRefHistory, 153 | useDeviceMotion, 154 | useDeviceOrientation, 155 | useDevicePixelRatio, 156 | useDevicesList, 157 | useDisplayMedia, 158 | useDocumentVisibility, 159 | useDraggable, 160 | useDropZone, 161 | useElementBounding, 162 | useElementByPoint, 163 | useElementHover, 164 | useElementSize, 165 | useElementVisibility, 166 | useEventBus, 167 | useEventListener, 168 | useEventSource, 169 | useEyeDropper, 170 | useFavicon, 171 | useFetch, 172 | useFileDialog, 173 | useFileSystemAccess, 174 | useFocus, 175 | useFocusWithin, 176 | useFps, 177 | useFullscreen, 178 | useGamepad, 179 | useGeolocation, 180 | useIdle, 181 | useImage, 182 | useInfiniteScroll, 183 | useIntersectionObserver, 184 | useInterval, 185 | useIntervalFn, 186 | useKeyModifier, 187 | useLastChanged, 188 | useLocalStorage, 189 | useMagicKeys, 190 | useManualRefHistory, 191 | useMediaControls, 192 | useMediaQuery, 193 | useMemoize, 194 | useMemory, 195 | useMounted, 196 | useMouse, 197 | useMouseInElement, 198 | useMousePressed, 199 | useMutationObserver, 200 | useNavigatorLanguage, 201 | useNetwork, 202 | useNow, 203 | useObjectUrl, 204 | useOffsetPagination, 205 | useOnline, 206 | usePageLeave, 207 | useParallax, 208 | useParentElement, 209 | usePerformanceObserver, 210 | usePermission, 211 | usePointer, 212 | usePointerLock, 213 | usePointerSwipe, 214 | usePreferredColorScheme, 215 | usePreferredContrast, 216 | usePreferredDark, 217 | usePreferredLanguages, 218 | usePreferredReducedMotion, 219 | usePreferredReducedTransparency, 220 | usePrevious, 221 | useRafFn, 222 | useRefHistory, 223 | useResizeObserver, 224 | useSSRWidth, 225 | useScreenOrientation, 226 | useScreenSafeArea, 227 | useScriptTag, 228 | useScroll, 229 | useScrollLock, 230 | useSessionStorage, 231 | useShare, 232 | useSorted, 233 | useSpeechRecognition, 234 | useSpeechSynthesis, 235 | useStepper, 236 | useStorage, 237 | useStorageAsync, 238 | useStyleTag, 239 | useSupported, 240 | useSwipe, 241 | useTemplateRefsList, 242 | useTextDirection, 243 | useTextSelection, 244 | useTextareaAutosize, 245 | useThrottleFn, 246 | useThrottledRefHistory, 247 | useTimeAgo, 248 | useTimeout, 249 | useTimeoutFn, 250 | useTimeoutPoll, 251 | useTimestamp, 252 | useTitle, 253 | useToNumber, 254 | useToString, 255 | useToggle, 256 | useTransition, 257 | useUrlSearchParams, 258 | useUserMedia, 259 | useVModel, 260 | useVModels, 261 | useVibrate, 262 | useVirtualList, 263 | useWakeLock, 264 | useWebNotification, 265 | useWebSocket, 266 | useWebWorker, 267 | useWebWorkerFn, 268 | useWindowFocus, 269 | useWindowScroll, 270 | useWindowSize, 271 | watchArray, 272 | watchAtMost, 273 | watchDebounced, 274 | watchDeep, 275 | watchIgnorable, 276 | watchImmediate, 277 | watchOnce, 278 | watchPausable, 279 | watchThrottled, 280 | watchTriggerable, 281 | watchWithFilter, 282 | whenever 283 | } from "./chunk-H6CAFZOV.js"; 284 | import "./chunk-LW4I4DCF.js"; 285 | export { 286 | DefaultMagicKeysAliasMap, 287 | StorageSerializers, 288 | TransitionPresets, 289 | assert, 290 | computedAsync as asyncComputed, 291 | refAutoReset as autoResetRef, 292 | breakpointsAntDesign, 293 | breakpointsBootstrapV5, 294 | breakpointsElement, 295 | breakpointsMasterCss, 296 | breakpointsPrimeFlex, 297 | breakpointsQuasar, 298 | breakpointsSematic, 299 | breakpointsTailwind, 300 | breakpointsVuetify, 301 | breakpointsVuetifyV2, 302 | breakpointsVuetifyV3, 303 | bypassFilter, 304 | camelize, 305 | clamp, 306 | cloneFnJSON, 307 | computedAsync, 308 | computedEager, 309 | computedInject, 310 | computedWithControl, 311 | containsProp, 312 | computedWithControl as controlledComputed, 313 | controlledRef, 314 | createEventHook, 315 | createFetch, 316 | createFilterWrapper, 317 | createGlobalState, 318 | createInjectionState, 319 | reactify as createReactiveFn, 320 | createReusableTemplate, 321 | createSharedComposable, 322 | createSingletonPromise, 323 | createTemplatePromise, 324 | createUnrefFn, 325 | customStorageEventName, 326 | debounceFilter, 327 | refDebounced as debouncedRef, 328 | watchDebounced as debouncedWatch, 329 | defaultDocument, 330 | defaultLocation, 331 | defaultNavigator, 332 | defaultWindow, 333 | computedEager as eagerComputed, 334 | executeTransition, 335 | extendRef, 336 | formatDate, 337 | formatTimeAgo, 338 | get, 339 | getLifeCycleTarget, 340 | getSSRHandler, 341 | hasOwn, 342 | hyphenate, 343 | identity, 344 | watchIgnorable as ignorableWatch, 345 | increaseWithUnit, 346 | injectLocal, 347 | invoke, 348 | isClient, 349 | isDef, 350 | isDefined, 351 | isIOS, 352 | isObject, 353 | isWorker, 354 | makeDestructurable, 355 | mapGamepadToXbox360Controller, 356 | noop, 357 | normalizeDate, 358 | notNullish, 359 | now, 360 | objectEntries, 361 | objectOmit, 362 | objectPick, 363 | onClickOutside, 364 | onElementRemoval, 365 | onKeyDown, 366 | onKeyPressed, 367 | onKeyStroke, 368 | onKeyUp, 369 | onLongPress, 370 | onStartTyping, 371 | pausableFilter, 372 | watchPausable as pausableWatch, 373 | promiseTimeout, 374 | provideLocal, 375 | provideSSRWidth, 376 | pxValue, 377 | rand, 378 | reactify, 379 | reactifyObject, 380 | reactiveComputed, 381 | reactiveOmit, 382 | reactivePick, 383 | refAutoReset, 384 | refDebounced, 385 | refDefault, 386 | refThrottled, 387 | refWithControl, 388 | resolveRef, 389 | resolveUnref, 390 | set, 391 | setSSRHandler, 392 | syncRef, 393 | syncRefs, 394 | templateRef, 395 | throttleFilter, 396 | refThrottled as throttledRef, 397 | watchThrottled as throttledWatch, 398 | timestamp, 399 | toArray, 400 | toReactive, 401 | toRef, 402 | toRefs, 403 | toValue, 404 | tryOnBeforeMount, 405 | tryOnBeforeUnmount, 406 | tryOnMounted, 407 | tryOnScopeDispose, 408 | tryOnUnmounted, 409 | unrefElement, 410 | until, 411 | useActiveElement, 412 | useAnimate, 413 | useArrayDifference, 414 | useArrayEvery, 415 | useArrayFilter, 416 | useArrayFind, 417 | useArrayFindIndex, 418 | useArrayFindLast, 419 | useArrayIncludes, 420 | useArrayJoin, 421 | useArrayMap, 422 | useArrayReduce, 423 | useArraySome, 424 | useArrayUnique, 425 | useAsyncQueue, 426 | useAsyncState, 427 | useBase64, 428 | useBattery, 429 | useBluetooth, 430 | useBreakpoints, 431 | useBroadcastChannel, 432 | useBrowserLocation, 433 | useCached, 434 | useClipboard, 435 | useClipboardItems, 436 | useCloned, 437 | useColorMode, 438 | useConfirmDialog, 439 | useCountdown, 440 | useCounter, 441 | useCssVar, 442 | useCurrentElement, 443 | useCycleList, 444 | useDark, 445 | useDateFormat, 446 | refDebounced as useDebounce, 447 | useDebounceFn, 448 | useDebouncedRefHistory, 449 | useDeviceMotion, 450 | useDeviceOrientation, 451 | useDevicePixelRatio, 452 | useDevicesList, 453 | useDisplayMedia, 454 | useDocumentVisibility, 455 | useDraggable, 456 | useDropZone, 457 | useElementBounding, 458 | useElementByPoint, 459 | useElementHover, 460 | useElementSize, 461 | useElementVisibility, 462 | useEventBus, 463 | useEventListener, 464 | useEventSource, 465 | useEyeDropper, 466 | useFavicon, 467 | useFetch, 468 | useFileDialog, 469 | useFileSystemAccess, 470 | useFocus, 471 | useFocusWithin, 472 | useFps, 473 | useFullscreen, 474 | useGamepad, 475 | useGeolocation, 476 | useIdle, 477 | useImage, 478 | useInfiniteScroll, 479 | useIntersectionObserver, 480 | useInterval, 481 | useIntervalFn, 482 | useKeyModifier, 483 | useLastChanged, 484 | useLocalStorage, 485 | useMagicKeys, 486 | useManualRefHistory, 487 | useMediaControls, 488 | useMediaQuery, 489 | useMemoize, 490 | useMemory, 491 | useMounted, 492 | useMouse, 493 | useMouseInElement, 494 | useMousePressed, 495 | useMutationObserver, 496 | useNavigatorLanguage, 497 | useNetwork, 498 | useNow, 499 | useObjectUrl, 500 | useOffsetPagination, 501 | useOnline, 502 | usePageLeave, 503 | useParallax, 504 | useParentElement, 505 | usePerformanceObserver, 506 | usePermission, 507 | usePointer, 508 | usePointerLock, 509 | usePointerSwipe, 510 | usePreferredColorScheme, 511 | usePreferredContrast, 512 | usePreferredDark, 513 | usePreferredLanguages, 514 | usePreferredReducedMotion, 515 | usePreferredReducedTransparency, 516 | usePrevious, 517 | useRafFn, 518 | useRefHistory, 519 | useResizeObserver, 520 | useSSRWidth, 521 | useScreenOrientation, 522 | useScreenSafeArea, 523 | useScriptTag, 524 | useScroll, 525 | useScrollLock, 526 | useSessionStorage, 527 | useShare, 528 | useSorted, 529 | useSpeechRecognition, 530 | useSpeechSynthesis, 531 | useStepper, 532 | useStorage, 533 | useStorageAsync, 534 | useStyleTag, 535 | useSupported, 536 | useSwipe, 537 | useTemplateRefsList, 538 | useTextDirection, 539 | useTextSelection, 540 | useTextareaAutosize, 541 | refThrottled as useThrottle, 542 | useThrottleFn, 543 | useThrottledRefHistory, 544 | useTimeAgo, 545 | useTimeout, 546 | useTimeoutFn, 547 | useTimeoutPoll, 548 | useTimestamp, 549 | useTitle, 550 | useToNumber, 551 | useToString, 552 | useToggle, 553 | useTransition, 554 | useUrlSearchParams, 555 | useUserMedia, 556 | useVModel, 557 | useVModels, 558 | useVibrate, 559 | useVirtualList, 560 | useWakeLock, 561 | useWebNotification, 562 | useWebSocket, 563 | useWebWorker, 564 | useWebWorkerFn, 565 | useWindowFocus, 566 | useWindowScroll, 567 | useWindowSize, 568 | watchArray, 569 | watchAtMost, 570 | watchDebounced, 571 | watchDeep, 572 | watchIgnorable, 573 | watchImmediate, 574 | watchOnce, 575 | watchPausable, 576 | watchThrottled, 577 | watchTriggerable, 578 | watchWithFilter, 579 | whenever 580 | }; 581 | //# sourceMappingURL=vitepress___@vueuse_core.js.map 582 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": [], 4 | "sourcesContent": [], 5 | "mappings": "", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js: -------------------------------------------------------------------------------- 1 | import { 2 | notNullish, 3 | toArray, 4 | tryOnScopeDispose, 5 | unrefElement 6 | } from "./chunk-H6CAFZOV.js"; 7 | import { 8 | computed, 9 | ref, 10 | toValue, 11 | watch 12 | } from "./chunk-LW4I4DCF.js"; 13 | 14 | // node_modules/tabbable/dist/index.esm.js 15 | var candidateSelectors = ["input:not([inert])", "select:not([inert])", "textarea:not([inert])", "a[href]:not([inert])", "button:not([inert])", "[tabindex]:not(slot):not([inert])", "audio[controls]:not([inert])", "video[controls]:not([inert])", '[contenteditable]:not([contenteditable="false"]):not([inert])', "details>summary:first-of-type:not([inert])", "details:not([inert])"]; 16 | var candidateSelector = candidateSelectors.join(","); 17 | var NoElement = typeof Element === "undefined"; 18 | var matches = NoElement ? function() { 19 | } : Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; 20 | var getRootNode = !NoElement && Element.prototype.getRootNode ? function(element) { 21 | var _element$getRootNode; 22 | return element === null || element === void 0 ? void 0 : (_element$getRootNode = element.getRootNode) === null || _element$getRootNode === void 0 ? void 0 : _element$getRootNode.call(element); 23 | } : function(element) { 24 | return element === null || element === void 0 ? void 0 : element.ownerDocument; 25 | }; 26 | var isInert = function isInert2(node, lookUp) { 27 | var _node$getAttribute; 28 | if (lookUp === void 0) { 29 | lookUp = true; 30 | } 31 | var inertAtt = node === null || node === void 0 ? void 0 : (_node$getAttribute = node.getAttribute) === null || _node$getAttribute === void 0 ? void 0 : _node$getAttribute.call(node, "inert"); 32 | var inert = inertAtt === "" || inertAtt === "true"; 33 | var result = inert || lookUp && node && isInert2(node.parentNode); 34 | return result; 35 | }; 36 | var isContentEditable = function isContentEditable2(node) { 37 | var _node$getAttribute2; 38 | var attValue = node === null || node === void 0 ? void 0 : (_node$getAttribute2 = node.getAttribute) === null || _node$getAttribute2 === void 0 ? void 0 : _node$getAttribute2.call(node, "contenteditable"); 39 | return attValue === "" || attValue === "true"; 40 | }; 41 | var getCandidates = function getCandidates2(el, includeContainer, filter) { 42 | if (isInert(el)) { 43 | return []; 44 | } 45 | var candidates = Array.prototype.slice.apply(el.querySelectorAll(candidateSelector)); 46 | if (includeContainer && matches.call(el, candidateSelector)) { 47 | candidates.unshift(el); 48 | } 49 | candidates = candidates.filter(filter); 50 | return candidates; 51 | }; 52 | var getCandidatesIteratively = function getCandidatesIteratively2(elements, includeContainer, options) { 53 | var candidates = []; 54 | var elementsToCheck = Array.from(elements); 55 | while (elementsToCheck.length) { 56 | var element = elementsToCheck.shift(); 57 | if (isInert(element, false)) { 58 | continue; 59 | } 60 | if (element.tagName === "SLOT") { 61 | var assigned = element.assignedElements(); 62 | var content = assigned.length ? assigned : element.children; 63 | var nestedCandidates = getCandidatesIteratively2(content, true, options); 64 | if (options.flatten) { 65 | candidates.push.apply(candidates, nestedCandidates); 66 | } else { 67 | candidates.push({ 68 | scopeParent: element, 69 | candidates: nestedCandidates 70 | }); 71 | } 72 | } else { 73 | var validCandidate = matches.call(element, candidateSelector); 74 | if (validCandidate && options.filter(element) && (includeContainer || !elements.includes(element))) { 75 | candidates.push(element); 76 | } 77 | var shadowRoot = element.shadowRoot || // check for an undisclosed shadow 78 | typeof options.getShadowRoot === "function" && options.getShadowRoot(element); 79 | var validShadowRoot = !isInert(shadowRoot, false) && (!options.shadowRootFilter || options.shadowRootFilter(element)); 80 | if (shadowRoot && validShadowRoot) { 81 | var _nestedCandidates = getCandidatesIteratively2(shadowRoot === true ? element.children : shadowRoot.children, true, options); 82 | if (options.flatten) { 83 | candidates.push.apply(candidates, _nestedCandidates); 84 | } else { 85 | candidates.push({ 86 | scopeParent: element, 87 | candidates: _nestedCandidates 88 | }); 89 | } 90 | } else { 91 | elementsToCheck.unshift.apply(elementsToCheck, element.children); 92 | } 93 | } 94 | } 95 | return candidates; 96 | }; 97 | var hasTabIndex = function hasTabIndex2(node) { 98 | return !isNaN(parseInt(node.getAttribute("tabindex"), 10)); 99 | }; 100 | var getTabIndex = function getTabIndex2(node) { 101 | if (!node) { 102 | throw new Error("No node provided"); 103 | } 104 | if (node.tabIndex < 0) { 105 | if ((/^(AUDIO|VIDEO|DETAILS)$/.test(node.tagName) || isContentEditable(node)) && !hasTabIndex(node)) { 106 | return 0; 107 | } 108 | } 109 | return node.tabIndex; 110 | }; 111 | var getSortOrderTabIndex = function getSortOrderTabIndex2(node, isScope) { 112 | var tabIndex = getTabIndex(node); 113 | if (tabIndex < 0 && isScope && !hasTabIndex(node)) { 114 | return 0; 115 | } 116 | return tabIndex; 117 | }; 118 | var sortOrderedTabbables = function sortOrderedTabbables2(a, b) { 119 | return a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex; 120 | }; 121 | var isInput = function isInput2(node) { 122 | return node.tagName === "INPUT"; 123 | }; 124 | var isHiddenInput = function isHiddenInput2(node) { 125 | return isInput(node) && node.type === "hidden"; 126 | }; 127 | var isDetailsWithSummary = function isDetailsWithSummary2(node) { 128 | var r = node.tagName === "DETAILS" && Array.prototype.slice.apply(node.children).some(function(child) { 129 | return child.tagName === "SUMMARY"; 130 | }); 131 | return r; 132 | }; 133 | var getCheckedRadio = function getCheckedRadio2(nodes, form) { 134 | for (var i = 0; i < nodes.length; i++) { 135 | if (nodes[i].checked && nodes[i].form === form) { 136 | return nodes[i]; 137 | } 138 | } 139 | }; 140 | var isTabbableRadio = function isTabbableRadio2(node) { 141 | if (!node.name) { 142 | return true; 143 | } 144 | var radioScope = node.form || getRootNode(node); 145 | var queryRadios = function queryRadios2(name) { 146 | return radioScope.querySelectorAll('input[type="radio"][name="' + name + '"]'); 147 | }; 148 | var radioSet; 149 | if (typeof window !== "undefined" && typeof window.CSS !== "undefined" && typeof window.CSS.escape === "function") { 150 | radioSet = queryRadios(window.CSS.escape(node.name)); 151 | } else { 152 | try { 153 | radioSet = queryRadios(node.name); 154 | } catch (err) { 155 | console.error("Looks like you have a radio button with a name attribute containing invalid CSS selector characters and need the CSS.escape polyfill: %s", err.message); 156 | return false; 157 | } 158 | } 159 | var checked = getCheckedRadio(radioSet, node.form); 160 | return !checked || checked === node; 161 | }; 162 | var isRadio = function isRadio2(node) { 163 | return isInput(node) && node.type === "radio"; 164 | }; 165 | var isNonTabbableRadio = function isNonTabbableRadio2(node) { 166 | return isRadio(node) && !isTabbableRadio(node); 167 | }; 168 | var isNodeAttached = function isNodeAttached2(node) { 169 | var _nodeRoot; 170 | var nodeRoot = node && getRootNode(node); 171 | var nodeRootHost = (_nodeRoot = nodeRoot) === null || _nodeRoot === void 0 ? void 0 : _nodeRoot.host; 172 | var attached = false; 173 | if (nodeRoot && nodeRoot !== node) { 174 | var _nodeRootHost, _nodeRootHost$ownerDo, _node$ownerDocument; 175 | attached = !!((_nodeRootHost = nodeRootHost) !== null && _nodeRootHost !== void 0 && (_nodeRootHost$ownerDo = _nodeRootHost.ownerDocument) !== null && _nodeRootHost$ownerDo !== void 0 && _nodeRootHost$ownerDo.contains(nodeRootHost) || node !== null && node !== void 0 && (_node$ownerDocument = node.ownerDocument) !== null && _node$ownerDocument !== void 0 && _node$ownerDocument.contains(node)); 176 | while (!attached && nodeRootHost) { 177 | var _nodeRoot2, _nodeRootHost2, _nodeRootHost2$ownerD; 178 | nodeRoot = getRootNode(nodeRootHost); 179 | nodeRootHost = (_nodeRoot2 = nodeRoot) === null || _nodeRoot2 === void 0 ? void 0 : _nodeRoot2.host; 180 | attached = !!((_nodeRootHost2 = nodeRootHost) !== null && _nodeRootHost2 !== void 0 && (_nodeRootHost2$ownerD = _nodeRootHost2.ownerDocument) !== null && _nodeRootHost2$ownerD !== void 0 && _nodeRootHost2$ownerD.contains(nodeRootHost)); 181 | } 182 | } 183 | return attached; 184 | }; 185 | var isZeroArea = function isZeroArea2(node) { 186 | var _node$getBoundingClie = node.getBoundingClientRect(), width = _node$getBoundingClie.width, height = _node$getBoundingClie.height; 187 | return width === 0 && height === 0; 188 | }; 189 | var isHidden = function isHidden2(node, _ref) { 190 | var displayCheck = _ref.displayCheck, getShadowRoot = _ref.getShadowRoot; 191 | if (getComputedStyle(node).visibility === "hidden") { 192 | return true; 193 | } 194 | var isDirectSummary = matches.call(node, "details>summary:first-of-type"); 195 | var nodeUnderDetails = isDirectSummary ? node.parentElement : node; 196 | if (matches.call(nodeUnderDetails, "details:not([open]) *")) { 197 | return true; 198 | } 199 | if (!displayCheck || displayCheck === "full" || displayCheck === "legacy-full") { 200 | if (typeof getShadowRoot === "function") { 201 | var originalNode = node; 202 | while (node) { 203 | var parentElement = node.parentElement; 204 | var rootNode = getRootNode(node); 205 | if (parentElement && !parentElement.shadowRoot && getShadowRoot(parentElement) === true) { 206 | return isZeroArea(node); 207 | } else if (node.assignedSlot) { 208 | node = node.assignedSlot; 209 | } else if (!parentElement && rootNode !== node.ownerDocument) { 210 | node = rootNode.host; 211 | } else { 212 | node = parentElement; 213 | } 214 | } 215 | node = originalNode; 216 | } 217 | if (isNodeAttached(node)) { 218 | return !node.getClientRects().length; 219 | } 220 | if (displayCheck !== "legacy-full") { 221 | return true; 222 | } 223 | } else if (displayCheck === "non-zero-area") { 224 | return isZeroArea(node); 225 | } 226 | return false; 227 | }; 228 | var isDisabledFromFieldset = function isDisabledFromFieldset2(node) { 229 | if (/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(node.tagName)) { 230 | var parentNode = node.parentElement; 231 | while (parentNode) { 232 | if (parentNode.tagName === "FIELDSET" && parentNode.disabled) { 233 | for (var i = 0; i < parentNode.children.length; i++) { 234 | var child = parentNode.children.item(i); 235 | if (child.tagName === "LEGEND") { 236 | return matches.call(parentNode, "fieldset[disabled] *") ? true : !child.contains(node); 237 | } 238 | } 239 | return true; 240 | } 241 | parentNode = parentNode.parentElement; 242 | } 243 | } 244 | return false; 245 | }; 246 | var isNodeMatchingSelectorFocusable = function isNodeMatchingSelectorFocusable2(options, node) { 247 | if (node.disabled || // we must do an inert look up to filter out any elements inside an inert ancestor 248 | // because we're limited in the type of selectors we can use in JSDom (see related 249 | // note related to `candidateSelectors`) 250 | isInert(node) || isHiddenInput(node) || isHidden(node, options) || // For a details element with a summary, the summary element gets the focus 251 | isDetailsWithSummary(node) || isDisabledFromFieldset(node)) { 252 | return false; 253 | } 254 | return true; 255 | }; 256 | var isNodeMatchingSelectorTabbable = function isNodeMatchingSelectorTabbable2(options, node) { 257 | if (isNonTabbableRadio(node) || getTabIndex(node) < 0 || !isNodeMatchingSelectorFocusable(options, node)) { 258 | return false; 259 | } 260 | return true; 261 | }; 262 | var isValidShadowRootTabbable = function isValidShadowRootTabbable2(shadowHostNode) { 263 | var tabIndex = parseInt(shadowHostNode.getAttribute("tabindex"), 10); 264 | if (isNaN(tabIndex) || tabIndex >= 0) { 265 | return true; 266 | } 267 | return false; 268 | }; 269 | var sortByOrder = function sortByOrder2(candidates) { 270 | var regularTabbables = []; 271 | var orderedTabbables = []; 272 | candidates.forEach(function(item, i) { 273 | var isScope = !!item.scopeParent; 274 | var element = isScope ? item.scopeParent : item; 275 | var candidateTabindex = getSortOrderTabIndex(element, isScope); 276 | var elements = isScope ? sortByOrder2(item.candidates) : element; 277 | if (candidateTabindex === 0) { 278 | isScope ? regularTabbables.push.apply(regularTabbables, elements) : regularTabbables.push(element); 279 | } else { 280 | orderedTabbables.push({ 281 | documentOrder: i, 282 | tabIndex: candidateTabindex, 283 | item, 284 | isScope, 285 | content: elements 286 | }); 287 | } 288 | }); 289 | return orderedTabbables.sort(sortOrderedTabbables).reduce(function(acc, sortable) { 290 | sortable.isScope ? acc.push.apply(acc, sortable.content) : acc.push(sortable.content); 291 | return acc; 292 | }, []).concat(regularTabbables); 293 | }; 294 | var tabbable = function tabbable2(container, options) { 295 | options = options || {}; 296 | var candidates; 297 | if (options.getShadowRoot) { 298 | candidates = getCandidatesIteratively([container], options.includeContainer, { 299 | filter: isNodeMatchingSelectorTabbable.bind(null, options), 300 | flatten: false, 301 | getShadowRoot: options.getShadowRoot, 302 | shadowRootFilter: isValidShadowRootTabbable 303 | }); 304 | } else { 305 | candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorTabbable.bind(null, options)); 306 | } 307 | return sortByOrder(candidates); 308 | }; 309 | var focusable = function focusable2(container, options) { 310 | options = options || {}; 311 | var candidates; 312 | if (options.getShadowRoot) { 313 | candidates = getCandidatesIteratively([container], options.includeContainer, { 314 | filter: isNodeMatchingSelectorFocusable.bind(null, options), 315 | flatten: true, 316 | getShadowRoot: options.getShadowRoot 317 | }); 318 | } else { 319 | candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorFocusable.bind(null, options)); 320 | } 321 | return candidates; 322 | }; 323 | var isTabbable = function isTabbable2(node, options) { 324 | options = options || {}; 325 | if (!node) { 326 | throw new Error("No node provided"); 327 | } 328 | if (matches.call(node, candidateSelector) === false) { 329 | return false; 330 | } 331 | return isNodeMatchingSelectorTabbable(options, node); 332 | }; 333 | var focusableCandidateSelector = candidateSelectors.concat("iframe").join(","); 334 | var isFocusable = function isFocusable2(node, options) { 335 | options = options || {}; 336 | if (!node) { 337 | throw new Error("No node provided"); 338 | } 339 | if (matches.call(node, focusableCandidateSelector) === false) { 340 | return false; 341 | } 342 | return isNodeMatchingSelectorFocusable(options, node); 343 | }; 344 | 345 | // node_modules/focus-trap/dist/focus-trap.esm.js 346 | function _arrayLikeToArray(r, a) { 347 | (null == a || a > r.length) && (a = r.length); 348 | for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; 349 | return n; 350 | } 351 | function _arrayWithoutHoles(r) { 352 | if (Array.isArray(r)) return _arrayLikeToArray(r); 353 | } 354 | function _defineProperty(e, r, t) { 355 | return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { 356 | value: t, 357 | enumerable: true, 358 | configurable: true, 359 | writable: true 360 | }) : e[r] = t, e; 361 | } 362 | function _iterableToArray(r) { 363 | if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); 364 | } 365 | function _nonIterableSpread() { 366 | throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); 367 | } 368 | function ownKeys(e, r) { 369 | var t = Object.keys(e); 370 | if (Object.getOwnPropertySymbols) { 371 | var o = Object.getOwnPropertySymbols(e); 372 | r && (o = o.filter(function(r2) { 373 | return Object.getOwnPropertyDescriptor(e, r2).enumerable; 374 | })), t.push.apply(t, o); 375 | } 376 | return t; 377 | } 378 | function _objectSpread2(e) { 379 | for (var r = 1; r < arguments.length; r++) { 380 | var t = null != arguments[r] ? arguments[r] : {}; 381 | r % 2 ? ownKeys(Object(t), true).forEach(function(r2) { 382 | _defineProperty(e, r2, t[r2]); 383 | }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function(r2) { 384 | Object.defineProperty(e, r2, Object.getOwnPropertyDescriptor(t, r2)); 385 | }); 386 | } 387 | return e; 388 | } 389 | function _toConsumableArray(r) { 390 | return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); 391 | } 392 | function _toPrimitive(t, r) { 393 | if ("object" != typeof t || !t) return t; 394 | var e = t[Symbol.toPrimitive]; 395 | if (void 0 !== e) { 396 | var i = e.call(t, r || "default"); 397 | if ("object" != typeof i) return i; 398 | throw new TypeError("@@toPrimitive must return a primitive value."); 399 | } 400 | return ("string" === r ? String : Number)(t); 401 | } 402 | function _toPropertyKey(t) { 403 | var i = _toPrimitive(t, "string"); 404 | return "symbol" == typeof i ? i : i + ""; 405 | } 406 | function _unsupportedIterableToArray(r, a) { 407 | if (r) { 408 | if ("string" == typeof r) return _arrayLikeToArray(r, a); 409 | var t = {}.toString.call(r).slice(8, -1); 410 | return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; 411 | } 412 | } 413 | var activeFocusTraps = { 414 | activateTrap: function activateTrap(trapStack, trap) { 415 | if (trapStack.length > 0) { 416 | var activeTrap = trapStack[trapStack.length - 1]; 417 | if (activeTrap !== trap) { 418 | activeTrap._setPausedState(true); 419 | } 420 | } 421 | var trapIndex = trapStack.indexOf(trap); 422 | if (trapIndex === -1) { 423 | trapStack.push(trap); 424 | } else { 425 | trapStack.splice(trapIndex, 1); 426 | trapStack.push(trap); 427 | } 428 | }, 429 | deactivateTrap: function deactivateTrap(trapStack, trap) { 430 | var trapIndex = trapStack.indexOf(trap); 431 | if (trapIndex !== -1) { 432 | trapStack.splice(trapIndex, 1); 433 | } 434 | if (trapStack.length > 0 && !trapStack[trapStack.length - 1]._isManuallyPaused()) { 435 | trapStack[trapStack.length - 1]._setPausedState(false); 436 | } 437 | } 438 | }; 439 | var isSelectableInput = function isSelectableInput2(node) { 440 | return node.tagName && node.tagName.toLowerCase() === "input" && typeof node.select === "function"; 441 | }; 442 | var isEscapeEvent = function isEscapeEvent2(e) { 443 | return (e === null || e === void 0 ? void 0 : e.key) === "Escape" || (e === null || e === void 0 ? void 0 : e.key) === "Esc" || (e === null || e === void 0 ? void 0 : e.keyCode) === 27; 444 | }; 445 | var isTabEvent = function isTabEvent2(e) { 446 | return (e === null || e === void 0 ? void 0 : e.key) === "Tab" || (e === null || e === void 0 ? void 0 : e.keyCode) === 9; 447 | }; 448 | var isKeyForward = function isKeyForward2(e) { 449 | return isTabEvent(e) && !e.shiftKey; 450 | }; 451 | var isKeyBackward = function isKeyBackward2(e) { 452 | return isTabEvent(e) && e.shiftKey; 453 | }; 454 | var delay = function delay2(fn) { 455 | return setTimeout(fn, 0); 456 | }; 457 | var valueOrHandler = function valueOrHandler2(value) { 458 | for (var _len = arguments.length, params = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 459 | params[_key - 1] = arguments[_key]; 460 | } 461 | return typeof value === "function" ? value.apply(void 0, params) : value; 462 | }; 463 | var getActualTarget = function getActualTarget2(event) { 464 | return event.target.shadowRoot && typeof event.composedPath === "function" ? event.composedPath()[0] : event.target; 465 | }; 466 | var internalTrapStack = []; 467 | var createFocusTrap = function createFocusTrap2(elements, userOptions) { 468 | var doc = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.document) || document; 469 | var trapStack = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.trapStack) || internalTrapStack; 470 | var config = _objectSpread2({ 471 | returnFocusOnDeactivate: true, 472 | escapeDeactivates: true, 473 | delayInitialFocus: true, 474 | isKeyForward, 475 | isKeyBackward 476 | }, userOptions); 477 | var state = { 478 | // containers given to createFocusTrap() 479 | // @type {Array} 480 | containers: [], 481 | // list of objects identifying tabbable nodes in `containers` in the trap 482 | // NOTE: it's possible that a group has no tabbable nodes if nodes get removed while the trap 483 | // is active, but the trap should never get to a state where there isn't at least one group 484 | // with at least one tabbable node in it (that would lead to an error condition that would 485 | // result in an error being thrown) 486 | // @type {Array<{ 487 | // container: HTMLElement, 488 | // tabbableNodes: Array, // empty if none 489 | // focusableNodes: Array, // empty if none 490 | // posTabIndexesFound: boolean, 491 | // firstTabbableNode: HTMLElement|undefined, 492 | // lastTabbableNode: HTMLElement|undefined, 493 | // firstDomTabbableNode: HTMLElement|undefined, 494 | // lastDomTabbableNode: HTMLElement|undefined, 495 | // nextTabbableNode: (node: HTMLElement, forward: boolean) => HTMLElement|undefined 496 | // }>} 497 | containerGroups: [], 498 | // same order/length as `containers` list 499 | // references to objects in `containerGroups`, but only those that actually have 500 | // tabbable nodes in them 501 | // NOTE: same order as `containers` and `containerGroups`, but __not necessarily__ 502 | // the same length 503 | tabbableGroups: [], 504 | nodeFocusedBeforeActivation: null, 505 | mostRecentlyFocusedNode: null, 506 | active: false, 507 | paused: false, 508 | manuallyPaused: false, 509 | // timer ID for when delayInitialFocus is true and initial focus in this trap 510 | // has been delayed during activation 511 | delayInitialFocusTimer: void 0, 512 | // the most recent KeyboardEvent for the configured nav key (typically [SHIFT+]TAB), if any 513 | recentNavEvent: void 0 514 | }; 515 | var trap; 516 | var getOption = function getOption2(configOverrideOptions, optionName, configOptionName) { 517 | return configOverrideOptions && configOverrideOptions[optionName] !== void 0 ? configOverrideOptions[optionName] : config[configOptionName || optionName]; 518 | }; 519 | var findContainerIndex = function findContainerIndex2(element, event) { 520 | var composedPath = typeof (event === null || event === void 0 ? void 0 : event.composedPath) === "function" ? event.composedPath() : void 0; 521 | return state.containerGroups.findIndex(function(_ref) { 522 | var container = _ref.container, tabbableNodes = _ref.tabbableNodes; 523 | return container.contains(element) || // fall back to explicit tabbable search which will take into consideration any 524 | // web components if the `tabbableOptions.getShadowRoot` option was used for 525 | // the trap, enabling shadow DOM support in tabbable (`Node.contains()` doesn't 526 | // look inside web components even if open) 527 | (composedPath === null || composedPath === void 0 ? void 0 : composedPath.includes(container)) || tabbableNodes.find(function(node) { 528 | return node === element; 529 | }); 530 | }); 531 | }; 532 | var getNodeForOption = function getNodeForOption2(optionName) { 533 | var _ref2 = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {}, _ref2$hasFallback = _ref2.hasFallback, hasFallback = _ref2$hasFallback === void 0 ? false : _ref2$hasFallback, _ref2$params = _ref2.params, params = _ref2$params === void 0 ? [] : _ref2$params; 534 | var optionValue = config[optionName]; 535 | if (typeof optionValue === "function") { 536 | optionValue = optionValue.apply(void 0, _toConsumableArray(params)); 537 | } 538 | if (optionValue === true) { 539 | optionValue = void 0; 540 | } 541 | if (!optionValue) { 542 | if (optionValue === void 0 || optionValue === false) { 543 | return optionValue; 544 | } 545 | throw new Error("`".concat(optionName, "` was specified but was not a node, or did not return a node")); 546 | } 547 | var node = optionValue; 548 | if (typeof optionValue === "string") { 549 | try { 550 | node = doc.querySelector(optionValue); 551 | } catch (err) { 552 | throw new Error("`".concat(optionName, '` appears to be an invalid selector; error="').concat(err.message, '"')); 553 | } 554 | if (!node) { 555 | if (!hasFallback) { 556 | throw new Error("`".concat(optionName, "` as selector refers to no known node")); 557 | } 558 | } 559 | } 560 | return node; 561 | }; 562 | var getInitialFocusNode = function getInitialFocusNode2() { 563 | var node = getNodeForOption("initialFocus", { 564 | hasFallback: true 565 | }); 566 | if (node === false) { 567 | return false; 568 | } 569 | if (node === void 0 || node && !isFocusable(node, config.tabbableOptions)) { 570 | if (findContainerIndex(doc.activeElement) >= 0) { 571 | node = doc.activeElement; 572 | } else { 573 | var firstTabbableGroup = state.tabbableGroups[0]; 574 | var firstTabbableNode = firstTabbableGroup && firstTabbableGroup.firstTabbableNode; 575 | node = firstTabbableNode || getNodeForOption("fallbackFocus"); 576 | } 577 | } else if (node === null) { 578 | node = getNodeForOption("fallbackFocus"); 579 | } 580 | if (!node) { 581 | throw new Error("Your focus-trap needs to have at least one focusable element"); 582 | } 583 | return node; 584 | }; 585 | var updateTabbableNodes = function updateTabbableNodes2() { 586 | state.containerGroups = state.containers.map(function(container) { 587 | var tabbableNodes = tabbable(container, config.tabbableOptions); 588 | var focusableNodes = focusable(container, config.tabbableOptions); 589 | var firstTabbableNode = tabbableNodes.length > 0 ? tabbableNodes[0] : void 0; 590 | var lastTabbableNode = tabbableNodes.length > 0 ? tabbableNodes[tabbableNodes.length - 1] : void 0; 591 | var firstDomTabbableNode = focusableNodes.find(function(node) { 592 | return isTabbable(node); 593 | }); 594 | var lastDomTabbableNode = focusableNodes.slice().reverse().find(function(node) { 595 | return isTabbable(node); 596 | }); 597 | var posTabIndexesFound = !!tabbableNodes.find(function(node) { 598 | return getTabIndex(node) > 0; 599 | }); 600 | return { 601 | container, 602 | tabbableNodes, 603 | focusableNodes, 604 | /** True if at least one node with positive `tabindex` was found in this container. */ 605 | posTabIndexesFound, 606 | /** First tabbable node in container, __tabindex__ order; `undefined` if none. */ 607 | firstTabbableNode, 608 | /** Last tabbable node in container, __tabindex__ order; `undefined` if none. */ 609 | lastTabbableNode, 610 | // NOTE: DOM order is NOT NECESSARILY "document position" order, but figuring that out 611 | // would require more than just https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition 612 | // because that API doesn't work with Shadow DOM as well as it should (@see 613 | // https://github.com/whatwg/dom/issues/320) and since this first/last is only needed, so far, 614 | // to address an edge case related to positive tabindex support, this seems like a much easier, 615 | // "close enough most of the time" alternative for positive tabindexes which should generally 616 | // be avoided anyway... 617 | /** First tabbable node in container, __DOM__ order; `undefined` if none. */ 618 | firstDomTabbableNode, 619 | /** Last tabbable node in container, __DOM__ order; `undefined` if none. */ 620 | lastDomTabbableNode, 621 | /** 622 | * Finds the __tabbable__ node that follows the given node in the specified direction, 623 | * in this container, if any. 624 | * @param {HTMLElement} node 625 | * @param {boolean} [forward] True if going in forward tab order; false if going 626 | * in reverse. 627 | * @returns {HTMLElement|undefined} The next tabbable node, if any. 628 | */ 629 | nextTabbableNode: function nextTabbableNode(node) { 630 | var forward = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : true; 631 | var nodeIdx = tabbableNodes.indexOf(node); 632 | if (nodeIdx < 0) { 633 | if (forward) { 634 | return focusableNodes.slice(focusableNodes.indexOf(node) + 1).find(function(el) { 635 | return isTabbable(el); 636 | }); 637 | } 638 | return focusableNodes.slice(0, focusableNodes.indexOf(node)).reverse().find(function(el) { 639 | return isTabbable(el); 640 | }); 641 | } 642 | return tabbableNodes[nodeIdx + (forward ? 1 : -1)]; 643 | } 644 | }; 645 | }); 646 | state.tabbableGroups = state.containerGroups.filter(function(group) { 647 | return group.tabbableNodes.length > 0; 648 | }); 649 | if (state.tabbableGroups.length <= 0 && !getNodeForOption("fallbackFocus")) { 650 | throw new Error("Your focus-trap must have at least one container with at least one tabbable node in it at all times"); 651 | } 652 | if (state.containerGroups.find(function(g) { 653 | return g.posTabIndexesFound; 654 | }) && state.containerGroups.length > 1) { 655 | throw new Error("At least one node with a positive tabindex was found in one of your focus-trap's multiple containers. Positive tabindexes are only supported in single-container focus-traps."); 656 | } 657 | }; 658 | var _getActiveElement = function getActiveElement(el) { 659 | var activeElement = el.activeElement; 660 | if (!activeElement) { 661 | return; 662 | } 663 | if (activeElement.shadowRoot && activeElement.shadowRoot.activeElement !== null) { 664 | return _getActiveElement(activeElement.shadowRoot); 665 | } 666 | return activeElement; 667 | }; 668 | var _tryFocus = function tryFocus(node) { 669 | if (node === false) { 670 | return; 671 | } 672 | if (node === _getActiveElement(document)) { 673 | return; 674 | } 675 | if (!node || !node.focus) { 676 | _tryFocus(getInitialFocusNode()); 677 | return; 678 | } 679 | node.focus({ 680 | preventScroll: !!config.preventScroll 681 | }); 682 | state.mostRecentlyFocusedNode = node; 683 | if (isSelectableInput(node)) { 684 | node.select(); 685 | } 686 | }; 687 | var getReturnFocusNode = function getReturnFocusNode2(previousActiveElement) { 688 | var node = getNodeForOption("setReturnFocus", { 689 | params: [previousActiveElement] 690 | }); 691 | return node ? node : node === false ? false : previousActiveElement; 692 | }; 693 | var findNextNavNode = function findNextNavNode2(_ref3) { 694 | var target = _ref3.target, event = _ref3.event, _ref3$isBackward = _ref3.isBackward, isBackward = _ref3$isBackward === void 0 ? false : _ref3$isBackward; 695 | target = target || getActualTarget(event); 696 | updateTabbableNodes(); 697 | var destinationNode = null; 698 | if (state.tabbableGroups.length > 0) { 699 | var containerIndex = findContainerIndex(target, event); 700 | var containerGroup = containerIndex >= 0 ? state.containerGroups[containerIndex] : void 0; 701 | if (containerIndex < 0) { 702 | if (isBackward) { 703 | destinationNode = state.tabbableGroups[state.tabbableGroups.length - 1].lastTabbableNode; 704 | } else { 705 | destinationNode = state.tabbableGroups[0].firstTabbableNode; 706 | } 707 | } else if (isBackward) { 708 | var startOfGroupIndex = state.tabbableGroups.findIndex(function(_ref4) { 709 | var firstTabbableNode = _ref4.firstTabbableNode; 710 | return target === firstTabbableNode; 711 | }); 712 | if (startOfGroupIndex < 0 && (containerGroup.container === target || isFocusable(target, config.tabbableOptions) && !isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target, false))) { 713 | startOfGroupIndex = containerIndex; 714 | } 715 | if (startOfGroupIndex >= 0) { 716 | var destinationGroupIndex = startOfGroupIndex === 0 ? state.tabbableGroups.length - 1 : startOfGroupIndex - 1; 717 | var destinationGroup = state.tabbableGroups[destinationGroupIndex]; 718 | destinationNode = getTabIndex(target) >= 0 ? destinationGroup.lastTabbableNode : destinationGroup.lastDomTabbableNode; 719 | } else if (!isTabEvent(event)) { 720 | destinationNode = containerGroup.nextTabbableNode(target, false); 721 | } 722 | } else { 723 | var lastOfGroupIndex = state.tabbableGroups.findIndex(function(_ref5) { 724 | var lastTabbableNode = _ref5.lastTabbableNode; 725 | return target === lastTabbableNode; 726 | }); 727 | if (lastOfGroupIndex < 0 && (containerGroup.container === target || isFocusable(target, config.tabbableOptions) && !isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target))) { 728 | lastOfGroupIndex = containerIndex; 729 | } 730 | if (lastOfGroupIndex >= 0) { 731 | var _destinationGroupIndex = lastOfGroupIndex === state.tabbableGroups.length - 1 ? 0 : lastOfGroupIndex + 1; 732 | var _destinationGroup = state.tabbableGroups[_destinationGroupIndex]; 733 | destinationNode = getTabIndex(target) >= 0 ? _destinationGroup.firstTabbableNode : _destinationGroup.firstDomTabbableNode; 734 | } else if (!isTabEvent(event)) { 735 | destinationNode = containerGroup.nextTabbableNode(target); 736 | } 737 | } 738 | } else { 739 | destinationNode = getNodeForOption("fallbackFocus"); 740 | } 741 | return destinationNode; 742 | }; 743 | var checkPointerDown = function checkPointerDown2(e) { 744 | var target = getActualTarget(e); 745 | if (findContainerIndex(target, e) >= 0) { 746 | return; 747 | } 748 | if (valueOrHandler(config.clickOutsideDeactivates, e)) { 749 | trap.deactivate({ 750 | // NOTE: by setting `returnFocus: false`, deactivate() will do nothing, 751 | // which will result in the outside click setting focus to the node 752 | // that was clicked (and if not focusable, to "nothing"); by setting 753 | // `returnFocus: true`, we'll attempt to re-focus the node originally-focused 754 | // on activation (or the configured `setReturnFocus` node), whether the 755 | // outside click was on a focusable node or not 756 | returnFocus: config.returnFocusOnDeactivate 757 | }); 758 | return; 759 | } 760 | if (valueOrHandler(config.allowOutsideClick, e)) { 761 | return; 762 | } 763 | e.preventDefault(); 764 | }; 765 | var checkFocusIn = function checkFocusIn2(event) { 766 | var target = getActualTarget(event); 767 | var targetContained = findContainerIndex(target, event) >= 0; 768 | if (targetContained || target instanceof Document) { 769 | if (targetContained) { 770 | state.mostRecentlyFocusedNode = target; 771 | } 772 | } else { 773 | event.stopImmediatePropagation(); 774 | var nextNode; 775 | var navAcrossContainers = true; 776 | if (state.mostRecentlyFocusedNode) { 777 | if (getTabIndex(state.mostRecentlyFocusedNode) > 0) { 778 | var mruContainerIdx = findContainerIndex(state.mostRecentlyFocusedNode); 779 | var tabbableNodes = state.containerGroups[mruContainerIdx].tabbableNodes; 780 | if (tabbableNodes.length > 0) { 781 | var mruTabIdx = tabbableNodes.findIndex(function(node) { 782 | return node === state.mostRecentlyFocusedNode; 783 | }); 784 | if (mruTabIdx >= 0) { 785 | if (config.isKeyForward(state.recentNavEvent)) { 786 | if (mruTabIdx + 1 < tabbableNodes.length) { 787 | nextNode = tabbableNodes[mruTabIdx + 1]; 788 | navAcrossContainers = false; 789 | } 790 | } else { 791 | if (mruTabIdx - 1 >= 0) { 792 | nextNode = tabbableNodes[mruTabIdx - 1]; 793 | navAcrossContainers = false; 794 | } 795 | } 796 | } 797 | } 798 | } else { 799 | if (!state.containerGroups.some(function(g) { 800 | return g.tabbableNodes.some(function(n) { 801 | return getTabIndex(n) > 0; 802 | }); 803 | })) { 804 | navAcrossContainers = false; 805 | } 806 | } 807 | } else { 808 | navAcrossContainers = false; 809 | } 810 | if (navAcrossContainers) { 811 | nextNode = findNextNavNode({ 812 | // move FROM the MRU node, not event-related node (which will be the node that is 813 | // outside the trap causing the focus escape we're trying to fix) 814 | target: state.mostRecentlyFocusedNode, 815 | isBackward: config.isKeyBackward(state.recentNavEvent) 816 | }); 817 | } 818 | if (nextNode) { 819 | _tryFocus(nextNode); 820 | } else { 821 | _tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode()); 822 | } 823 | } 824 | state.recentNavEvent = void 0; 825 | }; 826 | var checkKeyNav = function checkKeyNav2(event) { 827 | var isBackward = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false; 828 | state.recentNavEvent = event; 829 | var destinationNode = findNextNavNode({ 830 | event, 831 | isBackward 832 | }); 833 | if (destinationNode) { 834 | if (isTabEvent(event)) { 835 | event.preventDefault(); 836 | } 837 | _tryFocus(destinationNode); 838 | } 839 | }; 840 | var checkTabKey = function checkTabKey2(event) { 841 | if (config.isKeyForward(event) || config.isKeyBackward(event)) { 842 | checkKeyNav(event, config.isKeyBackward(event)); 843 | } 844 | }; 845 | var checkEscapeKey = function checkEscapeKey2(event) { 846 | if (isEscapeEvent(event) && valueOrHandler(config.escapeDeactivates, event) !== false) { 847 | event.preventDefault(); 848 | trap.deactivate(); 849 | } 850 | }; 851 | var checkClick = function checkClick2(e) { 852 | var target = getActualTarget(e); 853 | if (findContainerIndex(target, e) >= 0) { 854 | return; 855 | } 856 | if (valueOrHandler(config.clickOutsideDeactivates, e)) { 857 | return; 858 | } 859 | if (valueOrHandler(config.allowOutsideClick, e)) { 860 | return; 861 | } 862 | e.preventDefault(); 863 | e.stopImmediatePropagation(); 864 | }; 865 | var addListeners = function addListeners2() { 866 | if (!state.active) { 867 | return; 868 | } 869 | activeFocusTraps.activateTrap(trapStack, trap); 870 | state.delayInitialFocusTimer = config.delayInitialFocus ? delay(function() { 871 | _tryFocus(getInitialFocusNode()); 872 | }) : _tryFocus(getInitialFocusNode()); 873 | doc.addEventListener("focusin", checkFocusIn, true); 874 | doc.addEventListener("mousedown", checkPointerDown, { 875 | capture: true, 876 | passive: false 877 | }); 878 | doc.addEventListener("touchstart", checkPointerDown, { 879 | capture: true, 880 | passive: false 881 | }); 882 | doc.addEventListener("click", checkClick, { 883 | capture: true, 884 | passive: false 885 | }); 886 | doc.addEventListener("keydown", checkTabKey, { 887 | capture: true, 888 | passive: false 889 | }); 890 | doc.addEventListener("keydown", checkEscapeKey); 891 | return trap; 892 | }; 893 | var removeListeners = function removeListeners2() { 894 | if (!state.active) { 895 | return; 896 | } 897 | doc.removeEventListener("focusin", checkFocusIn, true); 898 | doc.removeEventListener("mousedown", checkPointerDown, true); 899 | doc.removeEventListener("touchstart", checkPointerDown, true); 900 | doc.removeEventListener("click", checkClick, true); 901 | doc.removeEventListener("keydown", checkTabKey, true); 902 | doc.removeEventListener("keydown", checkEscapeKey); 903 | return trap; 904 | }; 905 | var checkDomRemoval = function checkDomRemoval2(mutations) { 906 | var isFocusedNodeRemoved = mutations.some(function(mutation) { 907 | var removedNodes = Array.from(mutation.removedNodes); 908 | return removedNodes.some(function(node) { 909 | return node === state.mostRecentlyFocusedNode; 910 | }); 911 | }); 912 | if (isFocusedNodeRemoved) { 913 | _tryFocus(getInitialFocusNode()); 914 | } 915 | }; 916 | var mutationObserver = typeof window !== "undefined" && "MutationObserver" in window ? new MutationObserver(checkDomRemoval) : void 0; 917 | var updateObservedNodes = function updateObservedNodes2() { 918 | if (!mutationObserver) { 919 | return; 920 | } 921 | mutationObserver.disconnect(); 922 | if (state.active && !state.paused) { 923 | state.containers.map(function(container) { 924 | mutationObserver.observe(container, { 925 | subtree: true, 926 | childList: true 927 | }); 928 | }); 929 | } 930 | }; 931 | trap = { 932 | get active() { 933 | return state.active; 934 | }, 935 | get paused() { 936 | return state.paused; 937 | }, 938 | activate: function activate(activateOptions) { 939 | if (state.active) { 940 | return this; 941 | } 942 | var onActivate = getOption(activateOptions, "onActivate"); 943 | var onPostActivate = getOption(activateOptions, "onPostActivate"); 944 | var checkCanFocusTrap = getOption(activateOptions, "checkCanFocusTrap"); 945 | if (!checkCanFocusTrap) { 946 | updateTabbableNodes(); 947 | } 948 | state.active = true; 949 | state.paused = false; 950 | state.nodeFocusedBeforeActivation = doc.activeElement; 951 | onActivate === null || onActivate === void 0 || onActivate(); 952 | var finishActivation = function finishActivation2() { 953 | if (checkCanFocusTrap) { 954 | updateTabbableNodes(); 955 | } 956 | addListeners(); 957 | updateObservedNodes(); 958 | onPostActivate === null || onPostActivate === void 0 || onPostActivate(); 959 | }; 960 | if (checkCanFocusTrap) { 961 | checkCanFocusTrap(state.containers.concat()).then(finishActivation, finishActivation); 962 | return this; 963 | } 964 | finishActivation(); 965 | return this; 966 | }, 967 | deactivate: function deactivate(deactivateOptions) { 968 | if (!state.active) { 969 | return this; 970 | } 971 | var options = _objectSpread2({ 972 | onDeactivate: config.onDeactivate, 973 | onPostDeactivate: config.onPostDeactivate, 974 | checkCanReturnFocus: config.checkCanReturnFocus 975 | }, deactivateOptions); 976 | clearTimeout(state.delayInitialFocusTimer); 977 | state.delayInitialFocusTimer = void 0; 978 | removeListeners(); 979 | state.active = false; 980 | state.paused = false; 981 | updateObservedNodes(); 982 | activeFocusTraps.deactivateTrap(trapStack, trap); 983 | var onDeactivate = getOption(options, "onDeactivate"); 984 | var onPostDeactivate = getOption(options, "onPostDeactivate"); 985 | var checkCanReturnFocus = getOption(options, "checkCanReturnFocus"); 986 | var returnFocus = getOption(options, "returnFocus", "returnFocusOnDeactivate"); 987 | onDeactivate === null || onDeactivate === void 0 || onDeactivate(); 988 | var finishDeactivation = function finishDeactivation2() { 989 | delay(function() { 990 | if (returnFocus) { 991 | _tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)); 992 | } 993 | onPostDeactivate === null || onPostDeactivate === void 0 || onPostDeactivate(); 994 | }); 995 | }; 996 | if (returnFocus && checkCanReturnFocus) { 997 | checkCanReturnFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)).then(finishDeactivation, finishDeactivation); 998 | return this; 999 | } 1000 | finishDeactivation(); 1001 | return this; 1002 | }, 1003 | pause: function pause(pauseOptions) { 1004 | if (!state.active) { 1005 | return this; 1006 | } 1007 | state.manuallyPaused = true; 1008 | return this._setPausedState(true, pauseOptions); 1009 | }, 1010 | unpause: function unpause(unpauseOptions) { 1011 | if (!state.active) { 1012 | return this; 1013 | } 1014 | state.manuallyPaused = false; 1015 | if (trapStack[trapStack.length - 1] !== this) { 1016 | return this; 1017 | } 1018 | return this._setPausedState(false, unpauseOptions); 1019 | }, 1020 | updateContainerElements: function updateContainerElements(containerElements) { 1021 | var elementsAsArray = [].concat(containerElements).filter(Boolean); 1022 | state.containers = elementsAsArray.map(function(element) { 1023 | return typeof element === "string" ? doc.querySelector(element) : element; 1024 | }); 1025 | if (state.active) { 1026 | updateTabbableNodes(); 1027 | } 1028 | updateObservedNodes(); 1029 | return this; 1030 | } 1031 | }; 1032 | Object.defineProperties(trap, { 1033 | _isManuallyPaused: { 1034 | value: function value() { 1035 | return state.manuallyPaused; 1036 | } 1037 | }, 1038 | _setPausedState: { 1039 | value: function value(paused, options) { 1040 | if (state.paused === paused) { 1041 | return this; 1042 | } 1043 | state.paused = paused; 1044 | if (paused) { 1045 | var onPause = getOption(options, "onPause"); 1046 | var onPostPause = getOption(options, "onPostPause"); 1047 | onPause === null || onPause === void 0 || onPause(); 1048 | removeListeners(); 1049 | updateObservedNodes(); 1050 | onPostPause === null || onPostPause === void 0 || onPostPause(); 1051 | } else { 1052 | var onUnpause = getOption(options, "onUnpause"); 1053 | var onPostUnpause = getOption(options, "onPostUnpause"); 1054 | onUnpause === null || onUnpause === void 0 || onUnpause(); 1055 | updateTabbableNodes(); 1056 | addListeners(); 1057 | updateObservedNodes(); 1058 | onPostUnpause === null || onPostUnpause === void 0 || onPostUnpause(); 1059 | } 1060 | return this; 1061 | } 1062 | } 1063 | }); 1064 | trap.updateContainerElements(elements); 1065 | return trap; 1066 | }; 1067 | 1068 | // node_modules/@vueuse/integrations/useFocusTrap.mjs 1069 | function useFocusTrap(target, options = {}) { 1070 | let trap; 1071 | const { immediate, ...focusTrapOptions } = options; 1072 | const hasFocus = ref(false); 1073 | const isPaused = ref(false); 1074 | const activate = (opts) => trap && trap.activate(opts); 1075 | const deactivate = (opts) => trap && trap.deactivate(opts); 1076 | const pause = () => { 1077 | if (trap) { 1078 | trap.pause(); 1079 | isPaused.value = true; 1080 | } 1081 | }; 1082 | const unpause = () => { 1083 | if (trap) { 1084 | trap.unpause(); 1085 | isPaused.value = false; 1086 | } 1087 | }; 1088 | const targets = computed(() => { 1089 | const _targets = toValue(target); 1090 | return toArray(_targets).map((el) => { 1091 | const _el = toValue(el); 1092 | return typeof _el === "string" ? _el : unrefElement(_el); 1093 | }).filter(notNullish); 1094 | }); 1095 | watch( 1096 | targets, 1097 | (els) => { 1098 | if (!els.length) 1099 | return; 1100 | trap = createFocusTrap(els, { 1101 | ...focusTrapOptions, 1102 | onActivate() { 1103 | hasFocus.value = true; 1104 | if (options.onActivate) 1105 | options.onActivate(); 1106 | }, 1107 | onDeactivate() { 1108 | hasFocus.value = false; 1109 | if (options.onDeactivate) 1110 | options.onDeactivate(); 1111 | } 1112 | }); 1113 | if (immediate) 1114 | activate(); 1115 | }, 1116 | { flush: "post" } 1117 | ); 1118 | tryOnScopeDispose(() => deactivate()); 1119 | return { 1120 | hasFocus, 1121 | isPaused, 1122 | activate, 1123 | deactivate, 1124 | pause, 1125 | unpause 1126 | }; 1127 | } 1128 | export { 1129 | useFocusTrap 1130 | }; 1131 | /*! Bundled license information: 1132 | 1133 | tabbable/dist/index.esm.js: 1134 | (*! 1135 | * tabbable 6.2.0 1136 | * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE 1137 | *) 1138 | 1139 | focus-trap/dist/focus-trap.esm.js: 1140 | (*! 1141 | * focus-trap 7.6.4 1142 | * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE 1143 | *) 1144 | */ 1145 | //# sourceMappingURL=vitepress___@vueuse_integrations_useFocusTrap.js.map 1146 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js: -------------------------------------------------------------------------------- 1 | // node_modules/mark.js/src/lib/domiterator.js 2 | var DOMIterator = class _DOMIterator { 3 | /** 4 | * @param {HTMLElement|HTMLElement[]|NodeList|string} ctx - The context DOM 5 | * element, an array of DOM elements, a NodeList or a selector 6 | * @param {boolean} [iframes=true] - A boolean indicating if iframes should 7 | * be handled 8 | * @param {string[]} [exclude=[]] - An array containing exclusion selectors 9 | * for iframes 10 | * @param {number} [iframesTimeout=5000] - A number indicating the ms to 11 | * wait before an iframe should be skipped, in case the load event isn't 12 | * fired. This also applies if the user is offline and the resource of the 13 | * iframe is online (either by the browsers "offline" mode or because 14 | * there's no internet connection) 15 | */ 16 | constructor(ctx, iframes = true, exclude = [], iframesTimeout = 5e3) { 17 | this.ctx = ctx; 18 | this.iframes = iframes; 19 | this.exclude = exclude; 20 | this.iframesTimeout = iframesTimeout; 21 | } 22 | /** 23 | * Checks if the specified DOM element matches the selector 24 | * @param {HTMLElement} element - The DOM element 25 | * @param {string|string[]} selector - The selector or an array with 26 | * selectors 27 | * @return {boolean} 28 | * @access public 29 | */ 30 | static matches(element, selector) { 31 | const selectors = typeof selector === "string" ? [selector] : selector, fn = element.matches || element.matchesSelector || element.msMatchesSelector || element.mozMatchesSelector || element.oMatchesSelector || element.webkitMatchesSelector; 32 | if (fn) { 33 | let match = false; 34 | selectors.every((sel) => { 35 | if (fn.call(element, sel)) { 36 | match = true; 37 | return false; 38 | } 39 | return true; 40 | }); 41 | return match; 42 | } else { 43 | return false; 44 | } 45 | } 46 | /** 47 | * Returns all contexts filtered by duplicates (even nested) 48 | * @return {HTMLElement[]} - An array containing DOM contexts 49 | * @access protected 50 | */ 51 | getContexts() { 52 | let ctx, filteredCtx = []; 53 | if (typeof this.ctx === "undefined" || !this.ctx) { 54 | ctx = []; 55 | } else if (NodeList.prototype.isPrototypeOf(this.ctx)) { 56 | ctx = Array.prototype.slice.call(this.ctx); 57 | } else if (Array.isArray(this.ctx)) { 58 | ctx = this.ctx; 59 | } else if (typeof this.ctx === "string") { 60 | ctx = Array.prototype.slice.call( 61 | document.querySelectorAll(this.ctx) 62 | ); 63 | } else { 64 | ctx = [this.ctx]; 65 | } 66 | ctx.forEach((ctx2) => { 67 | const isDescendant = filteredCtx.filter((contexts) => { 68 | return contexts.contains(ctx2); 69 | }).length > 0; 70 | if (filteredCtx.indexOf(ctx2) === -1 && !isDescendant) { 71 | filteredCtx.push(ctx2); 72 | } 73 | }); 74 | return filteredCtx; 75 | } 76 | /** 77 | * @callback DOMIterator~getIframeContentsSuccessCallback 78 | * @param {HTMLDocument} contents - The contentDocument of the iframe 79 | */ 80 | /** 81 | * Calls the success callback function with the iframe document. If it can't 82 | * be accessed it calls the error callback function 83 | * @param {HTMLElement} ifr - The iframe DOM element 84 | * @param {DOMIterator~getIframeContentsSuccessCallback} successFn 85 | * @param {function} [errorFn] 86 | * @access protected 87 | */ 88 | getIframeContents(ifr, successFn, errorFn = () => { 89 | }) { 90 | let doc; 91 | try { 92 | const ifrWin = ifr.contentWindow; 93 | doc = ifrWin.document; 94 | if (!ifrWin || !doc) { 95 | throw new Error("iframe inaccessible"); 96 | } 97 | } catch (e) { 98 | errorFn(); 99 | } 100 | if (doc) { 101 | successFn(doc); 102 | } 103 | } 104 | /** 105 | * Checks if an iframe is empty (if about:blank is the shown page) 106 | * @param {HTMLElement} ifr - The iframe DOM element 107 | * @return {boolean} 108 | * @access protected 109 | */ 110 | isIframeBlank(ifr) { 111 | const bl = "about:blank", src = ifr.getAttribute("src").trim(), href = ifr.contentWindow.location.href; 112 | return href === bl && src !== bl && src; 113 | } 114 | /** 115 | * Observes the onload event of an iframe and calls the success callback or 116 | * the error callback if the iframe is inaccessible. If the event isn't 117 | * fired within the specified {@link DOMIterator#iframesTimeout}, then it'll 118 | * call the error callback too 119 | * @param {HTMLElement} ifr - The iframe DOM element 120 | * @param {DOMIterator~getIframeContentsSuccessCallback} successFn 121 | * @param {function} errorFn 122 | * @access protected 123 | */ 124 | observeIframeLoad(ifr, successFn, errorFn) { 125 | let called = false, tout = null; 126 | const listener = () => { 127 | if (called) { 128 | return; 129 | } 130 | called = true; 131 | clearTimeout(tout); 132 | try { 133 | if (!this.isIframeBlank(ifr)) { 134 | ifr.removeEventListener("load", listener); 135 | this.getIframeContents(ifr, successFn, errorFn); 136 | } 137 | } catch (e) { 138 | errorFn(); 139 | } 140 | }; 141 | ifr.addEventListener("load", listener); 142 | tout = setTimeout(listener, this.iframesTimeout); 143 | } 144 | /** 145 | * Callback when the iframe is ready 146 | * @callback DOMIterator~onIframeReadySuccessCallback 147 | * @param {HTMLDocument} contents - The contentDocument of the iframe 148 | */ 149 | /** 150 | * Callback if the iframe can't be accessed 151 | * @callback DOMIterator~onIframeReadyErrorCallback 152 | */ 153 | /** 154 | * Calls the callback if the specified iframe is ready for DOM access 155 | * @param {HTMLElement} ifr - The iframe DOM element 156 | * @param {DOMIterator~onIframeReadySuccessCallback} successFn - Success 157 | * callback 158 | * @param {DOMIterator~onIframeReadyErrorCallback} errorFn - Error callback 159 | * @see {@link http://stackoverflow.com/a/36155560/3894981} for 160 | * background information 161 | * @access protected 162 | */ 163 | onIframeReady(ifr, successFn, errorFn) { 164 | try { 165 | if (ifr.contentWindow.document.readyState === "complete") { 166 | if (this.isIframeBlank(ifr)) { 167 | this.observeIframeLoad(ifr, successFn, errorFn); 168 | } else { 169 | this.getIframeContents(ifr, successFn, errorFn); 170 | } 171 | } else { 172 | this.observeIframeLoad(ifr, successFn, errorFn); 173 | } 174 | } catch (e) { 175 | errorFn(); 176 | } 177 | } 178 | /** 179 | * Callback when all iframes are ready for DOM access 180 | * @callback DOMIterator~waitForIframesDoneCallback 181 | */ 182 | /** 183 | * Iterates over all iframes and calls the done callback when all of them 184 | * are ready for DOM access (including nested ones) 185 | * @param {HTMLElement} ctx - The context DOM element 186 | * @param {DOMIterator~waitForIframesDoneCallback} done - Done callback 187 | */ 188 | waitForIframes(ctx, done) { 189 | let eachCalled = 0; 190 | this.forEachIframe(ctx, () => true, (ifr) => { 191 | eachCalled++; 192 | this.waitForIframes(ifr.querySelector("html"), () => { 193 | if (!--eachCalled) { 194 | done(); 195 | } 196 | }); 197 | }, (handled) => { 198 | if (!handled) { 199 | done(); 200 | } 201 | }); 202 | } 203 | /** 204 | * Callback allowing to filter an iframe. Must return true when the element 205 | * should remain, otherwise false 206 | * @callback DOMIterator~forEachIframeFilterCallback 207 | * @param {HTMLElement} iframe - The iframe DOM element 208 | */ 209 | /** 210 | * Callback for each iframe content 211 | * @callback DOMIterator~forEachIframeEachCallback 212 | * @param {HTMLElement} content - The iframe document 213 | */ 214 | /** 215 | * Callback if all iframes inside the context were handled 216 | * @callback DOMIterator~forEachIframeEndCallback 217 | * @param {number} handled - The number of handled iframes (those who 218 | * wheren't filtered) 219 | */ 220 | /** 221 | * Iterates over all iframes inside the specified context and calls the 222 | * callbacks when they're ready. Filters iframes based on the instance 223 | * exclusion selectors 224 | * @param {HTMLElement} ctx - The context DOM element 225 | * @param {DOMIterator~forEachIframeFilterCallback} filter - Filter callback 226 | * @param {DOMIterator~forEachIframeEachCallback} each - Each callback 227 | * @param {DOMIterator~forEachIframeEndCallback} [end] - End callback 228 | * @access protected 229 | */ 230 | forEachIframe(ctx, filter, each, end = () => { 231 | }) { 232 | let ifr = ctx.querySelectorAll("iframe"), open = ifr.length, handled = 0; 233 | ifr = Array.prototype.slice.call(ifr); 234 | const checkEnd = () => { 235 | if (--open <= 0) { 236 | end(handled); 237 | } 238 | }; 239 | if (!open) { 240 | checkEnd(); 241 | } 242 | ifr.forEach((ifr2) => { 243 | if (_DOMIterator.matches(ifr2, this.exclude)) { 244 | checkEnd(); 245 | } else { 246 | this.onIframeReady(ifr2, (con) => { 247 | if (filter(ifr2)) { 248 | handled++; 249 | each(con); 250 | } 251 | checkEnd(); 252 | }, checkEnd); 253 | } 254 | }); 255 | } 256 | /** 257 | * Creates a NodeIterator on the specified context 258 | * @see {@link https://developer.mozilla.org/en/docs/Web/API/NodeIterator} 259 | * @param {HTMLElement} ctx - The context DOM element 260 | * @param {DOMIterator~whatToShow} whatToShow 261 | * @param {DOMIterator~filterCb} filter 262 | * @return {NodeIterator} 263 | * @access protected 264 | */ 265 | createIterator(ctx, whatToShow, filter) { 266 | return document.createNodeIterator(ctx, whatToShow, filter, false); 267 | } 268 | /** 269 | * Creates an instance of DOMIterator in an iframe 270 | * @param {HTMLDocument} contents - Iframe document 271 | * @return {DOMIterator} 272 | * @access protected 273 | */ 274 | createInstanceOnIframe(contents) { 275 | return new _DOMIterator(contents.querySelector("html"), this.iframes); 276 | } 277 | /** 278 | * Checks if an iframe occurs between two nodes, more specifically if an 279 | * iframe occurs before the specified node and after the specified prevNode 280 | * @param {HTMLElement} node - The node that should occur after the iframe 281 | * @param {HTMLElement} prevNode - The node that should occur before the 282 | * iframe 283 | * @param {HTMLElement} ifr - The iframe to check against 284 | * @return {boolean} 285 | * @access protected 286 | */ 287 | compareNodeIframe(node, prevNode, ifr) { 288 | const compCurr = node.compareDocumentPosition(ifr), prev = Node.DOCUMENT_POSITION_PRECEDING; 289 | if (compCurr & prev) { 290 | if (prevNode !== null) { 291 | const compPrev = prevNode.compareDocumentPosition(ifr), after = Node.DOCUMENT_POSITION_FOLLOWING; 292 | if (compPrev & after) { 293 | return true; 294 | } 295 | } else { 296 | return true; 297 | } 298 | } 299 | return false; 300 | } 301 | /** 302 | * @typedef {DOMIterator~getIteratorNodeReturn} 303 | * @type {object.} 304 | * @property {HTMLElement} prevNode - The previous node or null if there is 305 | * no 306 | * @property {HTMLElement} node - The current node 307 | */ 308 | /** 309 | * Returns the previous and current node of the specified iterator 310 | * @param {NodeIterator} itr - The iterator 311 | * @return {DOMIterator~getIteratorNodeReturn} 312 | * @access protected 313 | */ 314 | getIteratorNode(itr) { 315 | const prevNode = itr.previousNode(); 316 | let node; 317 | if (prevNode === null) { 318 | node = itr.nextNode(); 319 | } else { 320 | node = itr.nextNode() && itr.nextNode(); 321 | } 322 | return { 323 | prevNode, 324 | node 325 | }; 326 | } 327 | /** 328 | * An array containing objects. The object key "val" contains an iframe 329 | * DOM element. The object key "handled" contains a boolean indicating if 330 | * the iframe was handled already. 331 | * It wouldn't be enough to save all open or all already handled iframes. 332 | * The information of open iframes is necessary because they may occur after 333 | * all other text nodes (and compareNodeIframe would never be true). The 334 | * information of already handled iframes is necessary as otherwise they may 335 | * be handled multiple times 336 | * @typedef DOMIterator~checkIframeFilterIfr 337 | * @type {object[]} 338 | */ 339 | /** 340 | * Checks if an iframe wasn't handled already and if so, calls 341 | * {@link DOMIterator#compareNodeIframe} to check if it should be handled. 342 | * Information wheter an iframe was or wasn't handled is given within the 343 | * ifr dictionary 344 | * @param {HTMLElement} node - The node that should occur after the iframe 345 | * @param {HTMLElement} prevNode - The node that should occur before the 346 | * iframe 347 | * @param {HTMLElement} currIfr - The iframe to check 348 | * @param {DOMIterator~checkIframeFilterIfr} ifr - The iframe dictionary. 349 | * Will be manipulated (by reference) 350 | * @return {boolean} Returns true when it should be handled, otherwise false 351 | * @access protected 352 | */ 353 | checkIframeFilter(node, prevNode, currIfr, ifr) { 354 | let key = false, handled = false; 355 | ifr.forEach((ifrDict, i) => { 356 | if (ifrDict.val === currIfr) { 357 | key = i; 358 | handled = ifrDict.handled; 359 | } 360 | }); 361 | if (this.compareNodeIframe(node, prevNode, currIfr)) { 362 | if (key === false && !handled) { 363 | ifr.push({ 364 | val: currIfr, 365 | handled: true 366 | }); 367 | } else if (key !== false && !handled) { 368 | ifr[key].handled = true; 369 | } 370 | return true; 371 | } 372 | if (key === false) { 373 | ifr.push({ 374 | val: currIfr, 375 | handled: false 376 | }); 377 | } 378 | return false; 379 | } 380 | /** 381 | * Creates an iterator on all open iframes in the specified array and calls 382 | * the end callback when finished 383 | * @param {DOMIterator~checkIframeFilterIfr} ifr 384 | * @param {DOMIterator~whatToShow} whatToShow 385 | * @param {DOMIterator~forEachNodeCallback} eCb - Each callback 386 | * @param {DOMIterator~filterCb} fCb 387 | * @access protected 388 | */ 389 | handleOpenIframes(ifr, whatToShow, eCb, fCb) { 390 | ifr.forEach((ifrDict) => { 391 | if (!ifrDict.handled) { 392 | this.getIframeContents(ifrDict.val, (con) => { 393 | this.createInstanceOnIframe(con).forEachNode( 394 | whatToShow, 395 | eCb, 396 | fCb 397 | ); 398 | }); 399 | } 400 | }); 401 | } 402 | /** 403 | * Iterates through all nodes in the specified context and handles iframe 404 | * nodes at the correct position 405 | * @param {DOMIterator~whatToShow} whatToShow 406 | * @param {HTMLElement} ctx - The context 407 | * @param {DOMIterator~forEachNodeCallback} eachCb - Each callback 408 | * @param {DOMIterator~filterCb} filterCb - Filter callback 409 | * @param {DOMIterator~forEachNodeEndCallback} doneCb - End callback 410 | * @access protected 411 | */ 412 | iterateThroughNodes(whatToShow, ctx, eachCb, filterCb, doneCb) { 413 | const itr = this.createIterator(ctx, whatToShow, filterCb); 414 | let ifr = [], elements = [], node, prevNode, retrieveNodes = () => { 415 | ({ 416 | prevNode, 417 | node 418 | } = this.getIteratorNode(itr)); 419 | return node; 420 | }; 421 | while (retrieveNodes()) { 422 | if (this.iframes) { 423 | this.forEachIframe(ctx, (currIfr) => { 424 | return this.checkIframeFilter(node, prevNode, currIfr, ifr); 425 | }, (con) => { 426 | this.createInstanceOnIframe(con).forEachNode( 427 | whatToShow, 428 | (ifrNode) => elements.push(ifrNode), 429 | filterCb 430 | ); 431 | }); 432 | } 433 | elements.push(node); 434 | } 435 | elements.forEach((node2) => { 436 | eachCb(node2); 437 | }); 438 | if (this.iframes) { 439 | this.handleOpenIframes(ifr, whatToShow, eachCb, filterCb); 440 | } 441 | doneCb(); 442 | } 443 | /** 444 | * Callback for each node 445 | * @callback DOMIterator~forEachNodeCallback 446 | * @param {HTMLElement} node - The DOM text node element 447 | */ 448 | /** 449 | * Callback if all contexts were handled 450 | * @callback DOMIterator~forEachNodeEndCallback 451 | */ 452 | /** 453 | * Iterates over all contexts and initializes 454 | * {@link DOMIterator#iterateThroughNodes iterateThroughNodes} on them 455 | * @param {DOMIterator~whatToShow} whatToShow 456 | * @param {DOMIterator~forEachNodeCallback} each - Each callback 457 | * @param {DOMIterator~filterCb} filter - Filter callback 458 | * @param {DOMIterator~forEachNodeEndCallback} done - End callback 459 | * @access public 460 | */ 461 | forEachNode(whatToShow, each, filter, done = () => { 462 | }) { 463 | const contexts = this.getContexts(); 464 | let open = contexts.length; 465 | if (!open) { 466 | done(); 467 | } 468 | contexts.forEach((ctx) => { 469 | const ready = () => { 470 | this.iterateThroughNodes(whatToShow, ctx, each, filter, () => { 471 | if (--open <= 0) { 472 | done(); 473 | } 474 | }); 475 | }; 476 | if (this.iframes) { 477 | this.waitForIframes(ctx, ready); 478 | } else { 479 | ready(); 480 | } 481 | }); 482 | } 483 | /** 484 | * Callback to filter nodes. Can return e.g. NodeFilter.FILTER_ACCEPT or 485 | * NodeFilter.FILTER_REJECT 486 | * @see {@link http://tinyurl.com/zdczmm2} 487 | * @callback DOMIterator~filterCb 488 | * @param {HTMLElement} node - The node to filter 489 | */ 490 | /** 491 | * @typedef DOMIterator~whatToShow 492 | * @see {@link http://tinyurl.com/zfqqkx2} 493 | * @type {number} 494 | */ 495 | }; 496 | 497 | // node_modules/mark.js/src/lib/mark.js 498 | var Mark = class { 499 | // eslint-disable-line no-unused-vars 500 | /** 501 | * @param {HTMLElement|HTMLElement[]|NodeList|string} ctx - The context DOM 502 | * element, an array of DOM elements, a NodeList or a selector 503 | */ 504 | constructor(ctx) { 505 | this.ctx = ctx; 506 | this.ie = false; 507 | const ua = window.navigator.userAgent; 508 | if (ua.indexOf("MSIE") > -1 || ua.indexOf("Trident") > -1) { 509 | this.ie = true; 510 | } 511 | } 512 | /** 513 | * Options defined by the user. They will be initialized from one of the 514 | * public methods. See {@link Mark#mark}, {@link Mark#markRegExp}, 515 | * {@link Mark#markRanges} and {@link Mark#unmark} for option properties. 516 | * @type {object} 517 | * @param {object} [val] - An object that will be merged with defaults 518 | * @access protected 519 | */ 520 | set opt(val) { 521 | this._opt = Object.assign({}, { 522 | "element": "", 523 | "className": "", 524 | "exclude": [], 525 | "iframes": false, 526 | "iframesTimeout": 5e3, 527 | "separateWordSearch": true, 528 | "diacritics": true, 529 | "synonyms": {}, 530 | "accuracy": "partially", 531 | "acrossElements": false, 532 | "caseSensitive": false, 533 | "ignoreJoiners": false, 534 | "ignoreGroups": 0, 535 | "ignorePunctuation": [], 536 | "wildcards": "disabled", 537 | "each": () => { 538 | }, 539 | "noMatch": () => { 540 | }, 541 | "filter": () => true, 542 | "done": () => { 543 | }, 544 | "debug": false, 545 | "log": window.console 546 | }, val); 547 | } 548 | get opt() { 549 | return this._opt; 550 | } 551 | /** 552 | * An instance of DOMIterator 553 | * @type {DOMIterator} 554 | * @access protected 555 | */ 556 | get iterator() { 557 | return new DOMIterator( 558 | this.ctx, 559 | this.opt.iframes, 560 | this.opt.exclude, 561 | this.opt.iframesTimeout 562 | ); 563 | } 564 | /** 565 | * Logs a message if log is enabled 566 | * @param {string} msg - The message to log 567 | * @param {string} [level="debug"] - The log level, e.g. warn 568 | * error, debug 569 | * @access protected 570 | */ 571 | log(msg, level = "debug") { 572 | const log = this.opt.log; 573 | if (!this.opt.debug) { 574 | return; 575 | } 576 | if (typeof log === "object" && typeof log[level] === "function") { 577 | log[level](`mark.js: ${msg}`); 578 | } 579 | } 580 | /** 581 | * Escapes a string for usage within a regular expression 582 | * @param {string} str - The string to escape 583 | * @return {string} 584 | * @access protected 585 | */ 586 | escapeStr(str) { 587 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 588 | } 589 | /** 590 | * Creates a regular expression string to match the specified search 591 | * term including synonyms, diacritics and accuracy if defined 592 | * @param {string} str - The search term to be used 593 | * @return {string} 594 | * @access protected 595 | */ 596 | createRegExp(str) { 597 | if (this.opt.wildcards !== "disabled") { 598 | str = this.setupWildcardsRegExp(str); 599 | } 600 | str = this.escapeStr(str); 601 | if (Object.keys(this.opt.synonyms).length) { 602 | str = this.createSynonymsRegExp(str); 603 | } 604 | if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) { 605 | str = this.setupIgnoreJoinersRegExp(str); 606 | } 607 | if (this.opt.diacritics) { 608 | str = this.createDiacriticsRegExp(str); 609 | } 610 | str = this.createMergedBlanksRegExp(str); 611 | if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) { 612 | str = this.createJoinersRegExp(str); 613 | } 614 | if (this.opt.wildcards !== "disabled") { 615 | str = this.createWildcardsRegExp(str); 616 | } 617 | str = this.createAccuracyRegExp(str); 618 | return str; 619 | } 620 | /** 621 | * Creates a regular expression string to match the defined synonyms 622 | * @param {string} str - The search term to be used 623 | * @return {string} 624 | * @access protected 625 | */ 626 | createSynonymsRegExp(str) { 627 | const syn = this.opt.synonyms, sens = this.opt.caseSensitive ? "" : "i", joinerPlaceholder = this.opt.ignoreJoiners || this.opt.ignorePunctuation.length ? "\0" : ""; 628 | for (let index in syn) { 629 | if (syn.hasOwnProperty(index)) { 630 | const value = syn[index], k1 = this.opt.wildcards !== "disabled" ? this.setupWildcardsRegExp(index) : this.escapeStr(index), k2 = this.opt.wildcards !== "disabled" ? this.setupWildcardsRegExp(value) : this.escapeStr(value); 631 | if (k1 !== "" && k2 !== "") { 632 | str = str.replace( 633 | new RegExp( 634 | `(${this.escapeStr(k1)}|${this.escapeStr(k2)})`, 635 | `gm${sens}` 636 | ), 637 | joinerPlaceholder + `(${this.processSynomyms(k1)}|${this.processSynomyms(k2)})` + joinerPlaceholder 638 | ); 639 | } 640 | } 641 | } 642 | return str; 643 | } 644 | /** 645 | * Setup synonyms to work with ignoreJoiners and or ignorePunctuation 646 | * @param {string} str - synonym key or value to process 647 | * @return {string} - processed synonym string 648 | */ 649 | processSynomyms(str) { 650 | if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) { 651 | str = this.setupIgnoreJoinersRegExp(str); 652 | } 653 | return str; 654 | } 655 | /** 656 | * Sets up the regular expression string to allow later insertion of 657 | * wildcard regular expression matches 658 | * @param {string} str - The search term to be used 659 | * @return {string} 660 | * @access protected 661 | */ 662 | setupWildcardsRegExp(str) { 663 | str = str.replace(/(?:\\)*\?/g, (val) => { 664 | return val.charAt(0) === "\\" ? "?" : ""; 665 | }); 666 | return str.replace(/(?:\\)*\*/g, (val) => { 667 | return val.charAt(0) === "\\" ? "*" : ""; 668 | }); 669 | } 670 | /** 671 | * Sets up the regular expression string to allow later insertion of 672 | * wildcard regular expression matches 673 | * @param {string} str - The search term to be used 674 | * @return {string} 675 | * @access protected 676 | */ 677 | createWildcardsRegExp(str) { 678 | let spaces = this.opt.wildcards === "withSpaces"; 679 | return str.replace(/\u0001/g, spaces ? "[\\S\\s]?" : "\\S?").replace(/\u0002/g, spaces ? "[\\S\\s]*?" : "\\S*"); 680 | } 681 | /** 682 | * Sets up the regular expression string to allow later insertion of 683 | * designated characters (soft hyphens & zero width characters) 684 | * @param {string} str - The search term to be used 685 | * @return {string} 686 | * @access protected 687 | */ 688 | setupIgnoreJoinersRegExp(str) { 689 | return str.replace(/[^(|)\\]/g, (val, indx, original) => { 690 | let nextChar = original.charAt(indx + 1); 691 | if (/[(|)\\]/.test(nextChar) || nextChar === "") { 692 | return val; 693 | } else { 694 | return val + "\0"; 695 | } 696 | }); 697 | } 698 | /** 699 | * Creates a regular expression string to allow ignoring of designated 700 | * characters (soft hyphens, zero width characters & punctuation) based on 701 | * the specified option values of ignorePunctuation and 702 | * ignoreJoiners 703 | * @param {string} str - The search term to be used 704 | * @return {string} 705 | * @access protected 706 | */ 707 | createJoinersRegExp(str) { 708 | let joiner = []; 709 | const ignorePunctuation = this.opt.ignorePunctuation; 710 | if (Array.isArray(ignorePunctuation) && ignorePunctuation.length) { 711 | joiner.push(this.escapeStr(ignorePunctuation.join(""))); 712 | } 713 | if (this.opt.ignoreJoiners) { 714 | joiner.push("\\u00ad\\u200b\\u200c\\u200d"); 715 | } 716 | return joiner.length ? str.split(/\u0000+/).join(`[${joiner.join("")}]*`) : str; 717 | } 718 | /** 719 | * Creates a regular expression string to match diacritics 720 | * @param {string} str - The search term to be used 721 | * @return {string} 722 | * @access protected 723 | */ 724 | createDiacriticsRegExp(str) { 725 | const sens = this.opt.caseSensitive ? "" : "i", dct = this.opt.caseSensitive ? [ 726 | "aàáảãạăằắẳẵặâầấẩẫậäåāą", 727 | "AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ", 728 | "cçćč", 729 | "CÇĆČ", 730 | "dđď", 731 | "DĐĎ", 732 | "eèéẻẽẹêềếểễệëěēę", 733 | "EÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ", 734 | "iìíỉĩịîïī", 735 | "IÌÍỈĨỊÎÏĪ", 736 | "lł", 737 | "LŁ", 738 | "nñňń", 739 | "NÑŇŃ", 740 | "oòóỏõọôồốổỗộơởỡớờợöøō", 741 | "OÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ", 742 | "rř", 743 | "RŘ", 744 | "sšśșş", 745 | "SŠŚȘŞ", 746 | "tťțţ", 747 | "TŤȚŢ", 748 | "uùúủũụưừứửữựûüůū", 749 | "UÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ", 750 | "yýỳỷỹỵÿ", 751 | "YÝỲỶỸỴŸ", 752 | "zžżź", 753 | "ZŽŻŹ" 754 | ] : [ 755 | "aàáảãạăằắẳẵặâầấẩẫậäåāąAÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ", 756 | "cçćčCÇĆČ", 757 | "dđďDĐĎ", 758 | "eèéẻẽẹêềếểễệëěēęEÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ", 759 | "iìíỉĩịîïīIÌÍỈĨỊÎÏĪ", 760 | "lłLŁ", 761 | "nñňńNÑŇŃ", 762 | "oòóỏõọôồốổỗộơởỡớờợöøōOÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ", 763 | "rřRŘ", 764 | "sšśșşSŠŚȘŞ", 765 | "tťțţTŤȚŢ", 766 | "uùúủũụưừứửữựûüůūUÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ", 767 | "yýỳỷỹỵÿYÝỲỶỸỴŸ", 768 | "zžżźZŽŻŹ" 769 | ]; 770 | let handled = []; 771 | str.split("").forEach((ch) => { 772 | dct.every((dct2) => { 773 | if (dct2.indexOf(ch) !== -1) { 774 | if (handled.indexOf(dct2) > -1) { 775 | return false; 776 | } 777 | str = str.replace( 778 | new RegExp(`[${dct2}]`, `gm${sens}`), 779 | `[${dct2}]` 780 | ); 781 | handled.push(dct2); 782 | } 783 | return true; 784 | }); 785 | }); 786 | return str; 787 | } 788 | /** 789 | * Creates a regular expression string that merges whitespace characters 790 | * including subsequent ones into a single pattern, one or multiple 791 | * whitespaces 792 | * @param {string} str - The search term to be used 793 | * @return {string} 794 | * @access protected 795 | */ 796 | createMergedBlanksRegExp(str) { 797 | return str.replace(/[\s]+/gmi, "[\\s]+"); 798 | } 799 | /** 800 | * Creates a regular expression string to match the specified string with 801 | * the defined accuracy. As in the regular expression of "exactly" can be 802 | * a group containing a blank at the beginning, all regular expressions will 803 | * be created with two groups. The first group can be ignored (may contain 804 | * the said blank), the second contains the actual match 805 | * @param {string} str - The searm term to be used 806 | * @return {str} 807 | * @access protected 808 | */ 809 | createAccuracyRegExp(str) { 810 | const chars = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~¡¿"; 811 | let acc = this.opt.accuracy, val = typeof acc === "string" ? acc : acc.value, ls = typeof acc === "string" ? [] : acc.limiters, lsJoin = ""; 812 | ls.forEach((limiter) => { 813 | lsJoin += `|${this.escapeStr(limiter)}`; 814 | }); 815 | switch (val) { 816 | case "partially": 817 | default: 818 | return `()(${str})`; 819 | case "complementary": 820 | lsJoin = "\\s" + (lsJoin ? lsJoin : this.escapeStr(chars)); 821 | return `()([^${lsJoin}]*${str}[^${lsJoin}]*)`; 822 | case "exactly": 823 | return `(^|\\s${lsJoin})(${str})(?=$|\\s${lsJoin})`; 824 | } 825 | } 826 | /** 827 | * @typedef Mark~separatedKeywords 828 | * @type {object.} 829 | * @property {array.} keywords - The list of keywords 830 | * @property {number} length - The length 831 | */ 832 | /** 833 | * Returns a list of keywords dependent on whether separate word search 834 | * was defined. Also it filters empty keywords 835 | * @param {array} sv - The array of keywords 836 | * @return {Mark~separatedKeywords} 837 | * @access protected 838 | */ 839 | getSeparatedKeywords(sv) { 840 | let stack = []; 841 | sv.forEach((kw) => { 842 | if (!this.opt.separateWordSearch) { 843 | if (kw.trim() && stack.indexOf(kw) === -1) { 844 | stack.push(kw); 845 | } 846 | } else { 847 | kw.split(" ").forEach((kwSplitted) => { 848 | if (kwSplitted.trim() && stack.indexOf(kwSplitted) === -1) { 849 | stack.push(kwSplitted); 850 | } 851 | }); 852 | } 853 | }); 854 | return { 855 | // sort because of https://git.io/v6USg 856 | "keywords": stack.sort((a, b) => { 857 | return b.length - a.length; 858 | }), 859 | "length": stack.length 860 | }; 861 | } 862 | /** 863 | * Check if a value is a number 864 | * @param {number|string} value - the value to check; 865 | * numeric strings allowed 866 | * @return {boolean} 867 | * @access protected 868 | */ 869 | isNumeric(value) { 870 | return Number(parseFloat(value)) == value; 871 | } 872 | /** 873 | * @typedef Mark~rangeObject 874 | * @type {object} 875 | * @property {number} start - The start position within the composite value 876 | * @property {number} length - The length of the string to mark within the 877 | * composite value. 878 | */ 879 | /** 880 | * @typedef Mark~setOfRanges 881 | * @type {object[]} 882 | * @property {Mark~rangeObject} 883 | */ 884 | /** 885 | * Returns a processed list of integer offset indexes that do not overlap 886 | * each other, and remove any string values or additional elements 887 | * @param {Mark~setOfRanges} array - unprocessed raw array 888 | * @return {Mark~setOfRanges} - processed array with any invalid entries 889 | * removed 890 | * @throws Will throw an error if an array of objects is not passed 891 | * @access protected 892 | */ 893 | checkRanges(array) { 894 | if (!Array.isArray(array) || Object.prototype.toString.call(array[0]) !== "[object Object]") { 895 | this.log("markRanges() will only accept an array of objects"); 896 | this.opt.noMatch(array); 897 | return []; 898 | } 899 | const stack = []; 900 | let last = 0; 901 | array.sort((a, b) => { 902 | return a.start - b.start; 903 | }).forEach((item) => { 904 | let { start, end, valid } = this.callNoMatchOnInvalidRanges(item, last); 905 | if (valid) { 906 | item.start = start; 907 | item.length = end - start; 908 | stack.push(item); 909 | last = end; 910 | } 911 | }); 912 | return stack; 913 | } 914 | /** 915 | * @typedef Mark~validObject 916 | * @type {object} 917 | * @property {number} start - The start position within the composite value 918 | * @property {number} end - The calculated end position within the composite 919 | * value. 920 | * @property {boolean} valid - boolean value indicating that the start and 921 | * calculated end range is valid 922 | */ 923 | /** 924 | * Initial validation of ranges for markRanges. Preliminary checks are done 925 | * to ensure the start and length values exist and are not zero or non- 926 | * numeric 927 | * @param {Mark~rangeObject} range - the current range object 928 | * @param {number} last - last index of range 929 | * @return {Mark~validObject} 930 | * @access protected 931 | */ 932 | callNoMatchOnInvalidRanges(range, last) { 933 | let start, end, valid = false; 934 | if (range && typeof range.start !== "undefined") { 935 | start = parseInt(range.start, 10); 936 | end = start + parseInt(range.length, 10); 937 | if (this.isNumeric(range.start) && this.isNumeric(range.length) && end - last > 0 && end - start > 0) { 938 | valid = true; 939 | } else { 940 | this.log( 941 | `Ignoring invalid or overlapping range: ${JSON.stringify(range)}` 942 | ); 943 | this.opt.noMatch(range); 944 | } 945 | } else { 946 | this.log(`Ignoring invalid range: ${JSON.stringify(range)}`); 947 | this.opt.noMatch(range); 948 | } 949 | return { 950 | start, 951 | end, 952 | valid 953 | }; 954 | } 955 | /** 956 | * Check valid range for markRanges. Check ranges with access to the context 957 | * string. Range values are double checked, lengths that extend the mark 958 | * beyond the string length are limitied and ranges containing only 959 | * whitespace are ignored 960 | * @param {Mark~rangeObject} range - the current range object 961 | * @param {number} originalLength - original length of the context string 962 | * @param {string} string - current content string 963 | * @return {Mark~validObject} 964 | * @access protected 965 | */ 966 | checkWhitespaceRanges(range, originalLength, string) { 967 | let end, valid = true, max = string.length, offset = originalLength - max, start = parseInt(range.start, 10) - offset; 968 | start = start > max ? max : start; 969 | end = start + parseInt(range.length, 10); 970 | if (end > max) { 971 | end = max; 972 | this.log(`End range automatically set to the max value of ${max}`); 973 | } 974 | if (start < 0 || end - start < 0 || start > max || end > max) { 975 | valid = false; 976 | this.log(`Invalid range: ${JSON.stringify(range)}`); 977 | this.opt.noMatch(range); 978 | } else if (string.substring(start, end).replace(/\s+/g, "") === "") { 979 | valid = false; 980 | this.log("Skipping whitespace only range: " + JSON.stringify(range)); 981 | this.opt.noMatch(range); 982 | } 983 | return { 984 | start, 985 | end, 986 | valid 987 | }; 988 | } 989 | /** 990 | * @typedef Mark~getTextNodesDict 991 | * @type {object.} 992 | * @property {string} value - The composite value of all text nodes 993 | * @property {object[]} nodes - An array of objects 994 | * @property {number} nodes.start - The start position within the composite 995 | * value 996 | * @property {number} nodes.end - The end position within the composite 997 | * value 998 | * @property {HTMLElement} nodes.node - The DOM text node element 999 | */ 1000 | /** 1001 | * Callback 1002 | * @callback Mark~getTextNodesCallback 1003 | * @param {Mark~getTextNodesDict} 1004 | */ 1005 | /** 1006 | * Calls the callback with an object containing all text nodes (including 1007 | * iframe text nodes) with start and end positions and the composite value 1008 | * of them (string) 1009 | * @param {Mark~getTextNodesCallback} cb - Callback 1010 | * @access protected 1011 | */ 1012 | getTextNodes(cb) { 1013 | let val = "", nodes = []; 1014 | this.iterator.forEachNode(NodeFilter.SHOW_TEXT, (node) => { 1015 | nodes.push({ 1016 | start: val.length, 1017 | end: (val += node.textContent).length, 1018 | node 1019 | }); 1020 | }, (node) => { 1021 | if (this.matchesExclude(node.parentNode)) { 1022 | return NodeFilter.FILTER_REJECT; 1023 | } else { 1024 | return NodeFilter.FILTER_ACCEPT; 1025 | } 1026 | }, () => { 1027 | cb({ 1028 | value: val, 1029 | nodes 1030 | }); 1031 | }); 1032 | } 1033 | /** 1034 | * Checks if an element matches any of the specified exclude selectors. Also 1035 | * it checks for elements in which no marks should be performed (e.g. 1036 | * script and style tags) and optionally already marked elements 1037 | * @param {HTMLElement} el - The element to check 1038 | * @return {boolean} 1039 | * @access protected 1040 | */ 1041 | matchesExclude(el) { 1042 | return DOMIterator.matches(el, this.opt.exclude.concat([ 1043 | // ignores the elements itself, not their childrens (selector *) 1044 | "script", 1045 | "style", 1046 | "title", 1047 | "head", 1048 | "html" 1049 | ])); 1050 | } 1051 | /** 1052 | * Wraps the instance element and class around matches that fit the start 1053 | * and end positions within the node 1054 | * @param {HTMLElement} node - The DOM text node 1055 | * @param {number} start - The position where to start wrapping 1056 | * @param {number} end - The position where to end wrapping 1057 | * @return {HTMLElement} Returns the splitted text node that will appear 1058 | * after the wrapped text node 1059 | * @access protected 1060 | */ 1061 | wrapRangeInTextNode(node, start, end) { 1062 | const hEl = !this.opt.element ? "mark" : this.opt.element, startNode = node.splitText(start), ret = startNode.splitText(end - start); 1063 | let repl = document.createElement(hEl); 1064 | repl.setAttribute("data-markjs", "true"); 1065 | if (this.opt.className) { 1066 | repl.setAttribute("class", this.opt.className); 1067 | } 1068 | repl.textContent = startNode.textContent; 1069 | startNode.parentNode.replaceChild(repl, startNode); 1070 | return ret; 1071 | } 1072 | /** 1073 | * @typedef Mark~wrapRangeInMappedTextNodeDict 1074 | * @type {object.} 1075 | * @property {string} value - The composite value of all text nodes 1076 | * @property {object[]} nodes - An array of objects 1077 | * @property {number} nodes.start - The start position within the composite 1078 | * value 1079 | * @property {number} nodes.end - The end position within the composite 1080 | * value 1081 | * @property {HTMLElement} nodes.node - The DOM text node element 1082 | */ 1083 | /** 1084 | * Each callback 1085 | * @callback Mark~wrapMatchesEachCallback 1086 | * @param {HTMLElement} node - The wrapped DOM element 1087 | * @param {number} lastIndex - The last matching position within the 1088 | * composite value of text nodes 1089 | */ 1090 | /** 1091 | * Filter callback 1092 | * @callback Mark~wrapMatchesFilterCallback 1093 | * @param {HTMLElement} node - The matching text node DOM element 1094 | */ 1095 | /** 1096 | * Determines matches by start and end positions using the text node 1097 | * dictionary even across text nodes and calls 1098 | * {@link Mark#wrapRangeInTextNode} to wrap them 1099 | * @param {Mark~wrapRangeInMappedTextNodeDict} dict - The dictionary 1100 | * @param {number} start - The start position of the match 1101 | * @param {number} end - The end position of the match 1102 | * @param {Mark~wrapMatchesFilterCallback} filterCb - Filter callback 1103 | * @param {Mark~wrapMatchesEachCallback} eachCb - Each callback 1104 | * @access protected 1105 | */ 1106 | wrapRangeInMappedTextNode(dict, start, end, filterCb, eachCb) { 1107 | dict.nodes.every((n, i) => { 1108 | const sibl = dict.nodes[i + 1]; 1109 | if (typeof sibl === "undefined" || sibl.start > start) { 1110 | if (!filterCb(n.node)) { 1111 | return false; 1112 | } 1113 | const s = start - n.start, e = (end > n.end ? n.end : end) - n.start, startStr = dict.value.substr(0, n.start), endStr = dict.value.substr(e + n.start); 1114 | n.node = this.wrapRangeInTextNode(n.node, s, e); 1115 | dict.value = startStr + endStr; 1116 | dict.nodes.forEach((k, j) => { 1117 | if (j >= i) { 1118 | if (dict.nodes[j].start > 0 && j !== i) { 1119 | dict.nodes[j].start -= e; 1120 | } 1121 | dict.nodes[j].end -= e; 1122 | } 1123 | }); 1124 | end -= e; 1125 | eachCb(n.node.previousSibling, n.start); 1126 | if (end > n.end) { 1127 | start = n.end; 1128 | } else { 1129 | return false; 1130 | } 1131 | } 1132 | return true; 1133 | }); 1134 | } 1135 | /** 1136 | * Filter callback before each wrapping 1137 | * @callback Mark~wrapMatchesFilterCallback 1138 | * @param {string} match - The matching string 1139 | * @param {HTMLElement} node - The text node where the match occurs 1140 | */ 1141 | /** 1142 | * Callback for each wrapped element 1143 | * @callback Mark~wrapMatchesEachCallback 1144 | * @param {HTMLElement} element - The marked DOM element 1145 | */ 1146 | /** 1147 | * Callback on end 1148 | * @callback Mark~wrapMatchesEndCallback 1149 | */ 1150 | /** 1151 | * Wraps the instance element and class around matches within single HTML 1152 | * elements in all contexts 1153 | * @param {RegExp} regex - The regular expression to be searched for 1154 | * @param {number} ignoreGroups - A number indicating the amount of RegExp 1155 | * matching groups to ignore 1156 | * @param {Mark~wrapMatchesFilterCallback} filterCb 1157 | * @param {Mark~wrapMatchesEachCallback} eachCb 1158 | * @param {Mark~wrapMatchesEndCallback} endCb 1159 | * @access protected 1160 | */ 1161 | wrapMatches(regex, ignoreGroups, filterCb, eachCb, endCb) { 1162 | const matchIdx = ignoreGroups === 0 ? 0 : ignoreGroups + 1; 1163 | this.getTextNodes((dict) => { 1164 | dict.nodes.forEach((node) => { 1165 | node = node.node; 1166 | let match; 1167 | while ((match = regex.exec(node.textContent)) !== null && match[matchIdx] !== "") { 1168 | if (!filterCb(match[matchIdx], node)) { 1169 | continue; 1170 | } 1171 | let pos = match.index; 1172 | if (matchIdx !== 0) { 1173 | for (let i = 1; i < matchIdx; i++) { 1174 | pos += match[i].length; 1175 | } 1176 | } 1177 | node = this.wrapRangeInTextNode( 1178 | node, 1179 | pos, 1180 | pos + match[matchIdx].length 1181 | ); 1182 | eachCb(node.previousSibling); 1183 | regex.lastIndex = 0; 1184 | } 1185 | }); 1186 | endCb(); 1187 | }); 1188 | } 1189 | /** 1190 | * Callback for each wrapped element 1191 | * @callback Mark~wrapMatchesAcrossElementsEachCallback 1192 | * @param {HTMLElement} element - The marked DOM element 1193 | */ 1194 | /** 1195 | * Filter callback before each wrapping 1196 | * @callback Mark~wrapMatchesAcrossElementsFilterCallback 1197 | * @param {string} match - The matching string 1198 | * @param {HTMLElement} node - The text node where the match occurs 1199 | */ 1200 | /** 1201 | * Callback on end 1202 | * @callback Mark~wrapMatchesAcrossElementsEndCallback 1203 | */ 1204 | /** 1205 | * Wraps the instance element and class around matches across all HTML 1206 | * elements in all contexts 1207 | * @param {RegExp} regex - The regular expression to be searched for 1208 | * @param {number} ignoreGroups - A number indicating the amount of RegExp 1209 | * matching groups to ignore 1210 | * @param {Mark~wrapMatchesAcrossElementsFilterCallback} filterCb 1211 | * @param {Mark~wrapMatchesAcrossElementsEachCallback} eachCb 1212 | * @param {Mark~wrapMatchesAcrossElementsEndCallback} endCb 1213 | * @access protected 1214 | */ 1215 | wrapMatchesAcrossElements(regex, ignoreGroups, filterCb, eachCb, endCb) { 1216 | const matchIdx = ignoreGroups === 0 ? 0 : ignoreGroups + 1; 1217 | this.getTextNodes((dict) => { 1218 | let match; 1219 | while ((match = regex.exec(dict.value)) !== null && match[matchIdx] !== "") { 1220 | let start = match.index; 1221 | if (matchIdx !== 0) { 1222 | for (let i = 1; i < matchIdx; i++) { 1223 | start += match[i].length; 1224 | } 1225 | } 1226 | const end = start + match[matchIdx].length; 1227 | this.wrapRangeInMappedTextNode(dict, start, end, (node) => { 1228 | return filterCb(match[matchIdx], node); 1229 | }, (node, lastIndex) => { 1230 | regex.lastIndex = lastIndex; 1231 | eachCb(node); 1232 | }); 1233 | } 1234 | endCb(); 1235 | }); 1236 | } 1237 | /** 1238 | * Callback for each wrapped element 1239 | * @callback Mark~wrapRangeFromIndexEachCallback 1240 | * @param {HTMLElement} element - The marked DOM element 1241 | * @param {Mark~rangeObject} range - the current range object; provided 1242 | * start and length values will be numeric integers modified from the 1243 | * provided original ranges. 1244 | */ 1245 | /** 1246 | * Filter callback before each wrapping 1247 | * @callback Mark~wrapRangeFromIndexFilterCallback 1248 | * @param {HTMLElement} node - The text node which includes the range 1249 | * @param {Mark~rangeObject} range - the current range object 1250 | * @param {string} match - string extracted from the matching range 1251 | * @param {number} counter - A counter indicating the number of all marks 1252 | */ 1253 | /** 1254 | * Callback on end 1255 | * @callback Mark~wrapRangeFromIndexEndCallback 1256 | */ 1257 | /** 1258 | * Wraps the indicated ranges across all HTML elements in all contexts 1259 | * @param {Mark~setOfRanges} ranges 1260 | * @param {Mark~wrapRangeFromIndexFilterCallback} filterCb 1261 | * @param {Mark~wrapRangeFromIndexEachCallback} eachCb 1262 | * @param {Mark~wrapRangeFromIndexEndCallback} endCb 1263 | * @access protected 1264 | */ 1265 | wrapRangeFromIndex(ranges, filterCb, eachCb, endCb) { 1266 | this.getTextNodes((dict) => { 1267 | const originalLength = dict.value.length; 1268 | ranges.forEach((range, counter) => { 1269 | let { start, end, valid } = this.checkWhitespaceRanges( 1270 | range, 1271 | originalLength, 1272 | dict.value 1273 | ); 1274 | if (valid) { 1275 | this.wrapRangeInMappedTextNode(dict, start, end, (node) => { 1276 | return filterCb( 1277 | node, 1278 | range, 1279 | dict.value.substring(start, end), 1280 | counter 1281 | ); 1282 | }, (node) => { 1283 | eachCb(node, range); 1284 | }); 1285 | } 1286 | }); 1287 | endCb(); 1288 | }); 1289 | } 1290 | /** 1291 | * Unwraps the specified DOM node with its content (text nodes or HTML) 1292 | * without destroying possibly present events (using innerHTML) and 1293 | * normalizes the parent at the end (merge splitted text nodes) 1294 | * @param {HTMLElement} node - The DOM node to unwrap 1295 | * @access protected 1296 | */ 1297 | unwrapMatches(node) { 1298 | const parent = node.parentNode; 1299 | let docFrag = document.createDocumentFragment(); 1300 | while (node.firstChild) { 1301 | docFrag.appendChild(node.removeChild(node.firstChild)); 1302 | } 1303 | parent.replaceChild(docFrag, node); 1304 | if (!this.ie) { 1305 | parent.normalize(); 1306 | } else { 1307 | this.normalizeTextNode(parent); 1308 | } 1309 | } 1310 | /** 1311 | * Normalizes text nodes. It's a workaround for the native normalize method 1312 | * that has a bug in IE (see attached link). Should only be used in IE 1313 | * browsers as it's slower than the native method. 1314 | * @see {@link http://tinyurl.com/z5asa8c} 1315 | * @param {HTMLElement} node - The DOM node to normalize 1316 | * @access protected 1317 | */ 1318 | normalizeTextNode(node) { 1319 | if (!node) { 1320 | return; 1321 | } 1322 | if (node.nodeType === 3) { 1323 | while (node.nextSibling && node.nextSibling.nodeType === 3) { 1324 | node.nodeValue += node.nextSibling.nodeValue; 1325 | node.parentNode.removeChild(node.nextSibling); 1326 | } 1327 | } else { 1328 | this.normalizeTextNode(node.firstChild); 1329 | } 1330 | this.normalizeTextNode(node.nextSibling); 1331 | } 1332 | /** 1333 | * Callback when finished 1334 | * @callback Mark~commonDoneCallback 1335 | * @param {number} totalMatches - The number of marked elements 1336 | */ 1337 | /** 1338 | * @typedef Mark~commonOptions 1339 | * @type {object.} 1340 | * @property {string} [element="mark"] - HTML element tag name 1341 | * @property {string} [className] - An optional class name 1342 | * @property {string[]} [exclude] - An array with exclusion selectors. 1343 | * Elements matching those selectors will be ignored 1344 | * @property {boolean} [iframes=false] - Whether to search inside iframes 1345 | * @property {Mark~commonDoneCallback} [done] 1346 | * @property {boolean} [debug=false] - Wheter to log messages 1347 | * @property {object} [log=window.console] - Where to log messages (only if 1348 | * debug is true) 1349 | */ 1350 | /** 1351 | * Callback for each marked element 1352 | * @callback Mark~markRegExpEachCallback 1353 | * @param {HTMLElement} element - The marked DOM element 1354 | */ 1355 | /** 1356 | * Callback if there were no matches 1357 | * @callback Mark~markRegExpNoMatchCallback 1358 | * @param {RegExp} regexp - The regular expression 1359 | */ 1360 | /** 1361 | * Callback to filter matches 1362 | * @callback Mark~markRegExpFilterCallback 1363 | * @param {HTMLElement} textNode - The text node which includes the match 1364 | * @param {string} match - The matching string for the RegExp 1365 | * @param {number} counter - A counter indicating the number of all marks 1366 | */ 1367 | /** 1368 | * These options also include the common options from 1369 | * {@link Mark~commonOptions} 1370 | * @typedef Mark~markRegExpOptions 1371 | * @type {object.} 1372 | * @property {Mark~markRegExpEachCallback} [each] 1373 | * @property {Mark~markRegExpNoMatchCallback} [noMatch] 1374 | * @property {Mark~markRegExpFilterCallback} [filter] 1375 | */ 1376 | /** 1377 | * Marks a custom regular expression 1378 | * @param {RegExp} regexp - The regular expression 1379 | * @param {Mark~markRegExpOptions} [opt] - Optional options object 1380 | * @access public 1381 | */ 1382 | markRegExp(regexp, opt) { 1383 | this.opt = opt; 1384 | this.log(`Searching with expression "${regexp}"`); 1385 | let totalMatches = 0, fn = "wrapMatches"; 1386 | const eachCb = (element) => { 1387 | totalMatches++; 1388 | this.opt.each(element); 1389 | }; 1390 | if (this.opt.acrossElements) { 1391 | fn = "wrapMatchesAcrossElements"; 1392 | } 1393 | this[fn](regexp, this.opt.ignoreGroups, (match, node) => { 1394 | return this.opt.filter(node, match, totalMatches); 1395 | }, eachCb, () => { 1396 | if (totalMatches === 0) { 1397 | this.opt.noMatch(regexp); 1398 | } 1399 | this.opt.done(totalMatches); 1400 | }); 1401 | } 1402 | /** 1403 | * Callback for each marked element 1404 | * @callback Mark~markEachCallback 1405 | * @param {HTMLElement} element - The marked DOM element 1406 | */ 1407 | /** 1408 | * Callback if there were no matches 1409 | * @callback Mark~markNoMatchCallback 1410 | * @param {RegExp} term - The search term that was not found 1411 | */ 1412 | /** 1413 | * Callback to filter matches 1414 | * @callback Mark~markFilterCallback 1415 | * @param {HTMLElement} textNode - The text node which includes the match 1416 | * @param {string} match - The matching term 1417 | * @param {number} totalCounter - A counter indicating the number of all 1418 | * marks 1419 | * @param {number} termCounter - A counter indicating the number of marks 1420 | * for the specific match 1421 | */ 1422 | /** 1423 | * @typedef Mark~markAccuracyObject 1424 | * @type {object.} 1425 | * @property {string} value - A accuracy string value 1426 | * @property {string[]} limiters - A custom array of limiters. For example 1427 | * ["-", ","] 1428 | */ 1429 | /** 1430 | * @typedef Mark~markAccuracySetting 1431 | * @type {string} 1432 | * @property {"partially"|"complementary"|"exactly"|Mark~markAccuracyObject} 1433 | * [accuracy="partially"] - Either one of the following string values: 1434 | *
    1435 | *
  • partially: When searching for "lor" only "lor" inside 1436 | * "lorem" will be marked
  • 1437 | *
  • complementary: When searching for "lor" the whole word 1438 | * "lorem" will be marked
  • 1439 | *
  • exactly: When searching for "lor" only those exact words 1440 | * will be marked. In this example nothing inside "lorem". This value 1441 | * is equivalent to the previous option wordBoundary
  • 1442 | *
1443 | * Or an object containing two properties: 1444 | *
    1445 | *
  • value: One of the above named string values
  • 1446 | *
  • limiters: A custom array of string limiters for accuracy 1447 | * "exactly" or "complementary"
  • 1448 | *
1449 | */ 1450 | /** 1451 | * @typedef Mark~markWildcardsSetting 1452 | * @type {string} 1453 | * @property {"disabled"|"enabled"|"withSpaces"} 1454 | * [wildcards="disabled"] - Set to any of the following string values: 1455 | *
    1456 | *
  • disabled: Disable wildcard usage
  • 1457 | *
  • enabled: When searching for "lor?m", the "?" will match zero 1458 | * or one non-space character (e.g. "lorm", "loram", "lor3m", etc). When 1459 | * searching for "lor*m", the "*" will match zero or more non-space 1460 | * characters (e.g. "lorm", "loram", "lor123m", etc).
  • 1461 | *
  • withSpaces: When searching for "lor?m", the "?" will 1462 | * match zero or one space or non-space character (e.g. "lor m", "loram", 1463 | * etc). When searching for "lor*m", the "*" will match zero or more space 1464 | * or non-space characters (e.g. "lorm", "lore et dolor ipsum", "lor: m", 1465 | * etc).
  • 1466 | *
1467 | */ 1468 | /** 1469 | * @typedef Mark~markIgnorePunctuationSetting 1470 | * @type {string[]} 1471 | * @property {string} The strings in this setting will contain punctuation 1472 | * marks that will be ignored: 1473 | *
    1474 | *
  • These punctuation marks can be between any characters, e.g. setting 1475 | * this option to ["'"] would match "Worlds", "World's" and 1476 | * "Wo'rlds"
  • 1477 | *
  • One or more apostrophes between the letters would still produce a 1478 | * match (e.g. "W'o''r'l'd's").
  • 1479 | *
  • A typical setting for this option could be as follows: 1480 | *
    ignorePunctuation: ":;.,-–—‒_(){}[]!'\"+=".split(""),
    This 1481 | * setting includes common punctuation as well as a minus, en-dash, 1482 | * em-dash and figure-dash 1483 | * ({@link https://en.wikipedia.org/wiki/Dash#Figure_dash ref}), as well 1484 | * as an underscore.
  • 1485 | *
1486 | */ 1487 | /** 1488 | * These options also include the common options from 1489 | * {@link Mark~commonOptions} 1490 | * @typedef Mark~markOptions 1491 | * @type {object.} 1492 | * @property {boolean} [separateWordSearch=true] - Whether to search for 1493 | * each word separated by a blank instead of the complete term 1494 | * @property {boolean} [diacritics=true] - If diacritic characters should be 1495 | * matched. ({@link https://en.wikipedia.org/wiki/Diacritic Diacritics}) 1496 | * @property {object} [synonyms] - An object with synonyms. The key will be 1497 | * a synonym for the value and the value for the key 1498 | * @property {Mark~markAccuracySetting} [accuracy] 1499 | * @property {Mark~markWildcardsSetting} [wildcards] 1500 | * @property {boolean} [acrossElements=false] - Whether to find matches 1501 | * across HTML elements. By default, only matches within single HTML 1502 | * elements will be found 1503 | * @property {boolean} [ignoreJoiners=false] - Whether to ignore word 1504 | * joiners inside of key words. These include soft-hyphens, zero-width 1505 | * space, zero-width non-joiners and zero-width joiners. 1506 | * @property {Mark~markIgnorePunctuationSetting} [ignorePunctuation] 1507 | * @property {Mark~markEachCallback} [each] 1508 | * @property {Mark~markNoMatchCallback} [noMatch] 1509 | * @property {Mark~markFilterCallback} [filter] 1510 | */ 1511 | /** 1512 | * Marks the specified search terms 1513 | * @param {string|string[]} [sv] - Search value, either a search string or 1514 | * an array containing multiple search strings 1515 | * @param {Mark~markOptions} [opt] - Optional options object 1516 | * @access public 1517 | */ 1518 | mark(sv, opt) { 1519 | this.opt = opt; 1520 | let totalMatches = 0, fn = "wrapMatches"; 1521 | const { 1522 | keywords: kwArr, 1523 | length: kwArrLen 1524 | } = this.getSeparatedKeywords(typeof sv === "string" ? [sv] : sv), sens = this.opt.caseSensitive ? "" : "i", handler = (kw) => { 1525 | let regex = new RegExp(this.createRegExp(kw), `gm${sens}`), matches = 0; 1526 | this.log(`Searching with expression "${regex}"`); 1527 | this[fn](regex, 1, (term, node) => { 1528 | return this.opt.filter(node, kw, totalMatches, matches); 1529 | }, (element) => { 1530 | matches++; 1531 | totalMatches++; 1532 | this.opt.each(element); 1533 | }, () => { 1534 | if (matches === 0) { 1535 | this.opt.noMatch(kw); 1536 | } 1537 | if (kwArr[kwArrLen - 1] === kw) { 1538 | this.opt.done(totalMatches); 1539 | } else { 1540 | handler(kwArr[kwArr.indexOf(kw) + 1]); 1541 | } 1542 | }); 1543 | }; 1544 | if (this.opt.acrossElements) { 1545 | fn = "wrapMatchesAcrossElements"; 1546 | } 1547 | if (kwArrLen === 0) { 1548 | this.opt.done(totalMatches); 1549 | } else { 1550 | handler(kwArr[0]); 1551 | } 1552 | } 1553 | /** 1554 | * Callback for each marked element 1555 | * @callback Mark~markRangesEachCallback 1556 | * @param {HTMLElement} element - The marked DOM element 1557 | * @param {array} range - array of range start and end points 1558 | */ 1559 | /** 1560 | * Callback if a processed range is invalid, out-of-bounds, overlaps another 1561 | * range, or only matches whitespace 1562 | * @callback Mark~markRangesNoMatchCallback 1563 | * @param {Mark~rangeObject} range - a range object 1564 | */ 1565 | /** 1566 | * Callback to filter matches 1567 | * @callback Mark~markRangesFilterCallback 1568 | * @param {HTMLElement} node - The text node which includes the range 1569 | * @param {array} range - array of range start and end points 1570 | * @param {string} match - string extracted from the matching range 1571 | * @param {number} counter - A counter indicating the number of all marks 1572 | */ 1573 | /** 1574 | * These options also include the common options from 1575 | * {@link Mark~commonOptions} 1576 | * @typedef Mark~markRangesOptions 1577 | * @type {object.} 1578 | * @property {Mark~markRangesEachCallback} [each] 1579 | * @property {Mark~markRangesNoMatchCallback} [noMatch] 1580 | * @property {Mark~markRangesFilterCallback} [filter] 1581 | */ 1582 | /** 1583 | * Marks an array of objects containing a start with an end or length of the 1584 | * string to mark 1585 | * @param {Mark~setOfRanges} rawRanges - The original (preprocessed) 1586 | * array of objects 1587 | * @param {Mark~markRangesOptions} [opt] - Optional options object 1588 | * @access public 1589 | */ 1590 | markRanges(rawRanges, opt) { 1591 | this.opt = opt; 1592 | let totalMatches = 0, ranges = this.checkRanges(rawRanges); 1593 | if (ranges && ranges.length) { 1594 | this.log( 1595 | "Starting to mark with the following ranges: " + JSON.stringify(ranges) 1596 | ); 1597 | this.wrapRangeFromIndex( 1598 | ranges, 1599 | (node, range, match, counter) => { 1600 | return this.opt.filter(node, range, match, counter); 1601 | }, 1602 | (element, range) => { 1603 | totalMatches++; 1604 | this.opt.each(element, range); 1605 | }, 1606 | () => { 1607 | this.opt.done(totalMatches); 1608 | } 1609 | ); 1610 | } else { 1611 | this.opt.done(totalMatches); 1612 | } 1613 | } 1614 | /** 1615 | * Removes all marked elements inside the context with their HTML and 1616 | * normalizes the parent at the end 1617 | * @param {Mark~commonOptions} [opt] - Optional options object 1618 | * @access public 1619 | */ 1620 | unmark(opt) { 1621 | this.opt = opt; 1622 | let sel = this.opt.element ? this.opt.element : "*"; 1623 | sel += "[data-markjs]"; 1624 | if (this.opt.className) { 1625 | sel += `.${this.opt.className}`; 1626 | } 1627 | this.log(`Removal selector "${sel}"`); 1628 | this.iterator.forEachNode(NodeFilter.SHOW_ELEMENT, (node) => { 1629 | this.unwrapMatches(node); 1630 | }, (node) => { 1631 | const matchesSel = DOMIterator.matches(node, sel), matchesExclude = this.matchesExclude(node); 1632 | if (!matchesSel || matchesExclude) { 1633 | return NodeFilter.FILTER_REJECT; 1634 | } else { 1635 | return NodeFilter.FILTER_ACCEPT; 1636 | } 1637 | }, this.opt.done); 1638 | } 1639 | }; 1640 | 1641 | // node_modules/mark.js/src/vanilla.js 1642 | function Mark2(ctx) { 1643 | const instance = new Mark(ctx); 1644 | this.mark = (sv, opt) => { 1645 | instance.mark(sv, opt); 1646 | return this; 1647 | }; 1648 | this.markRegExp = (sv, opt) => { 1649 | instance.markRegExp(sv, opt); 1650 | return this; 1651 | }; 1652 | this.markRanges = (sv, opt) => { 1653 | instance.markRanges(sv, opt); 1654 | return this; 1655 | }; 1656 | this.unmark = (opt) => { 1657 | instance.unmark(opt); 1658 | return this; 1659 | }; 1660 | return this; 1661 | } 1662 | export { 1663 | Mark2 as default 1664 | }; 1665 | //# sourceMappingURL=vitepress___mark__js_src_vanilla__js.js.map 1666 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vue.js: -------------------------------------------------------------------------------- 1 | import { 2 | BaseTransition, 3 | BaseTransitionPropsValidators, 4 | Comment, 5 | DeprecationTypes, 6 | EffectScope, 7 | ErrorCodes, 8 | ErrorTypeStrings, 9 | Fragment, 10 | KeepAlive, 11 | ReactiveEffect, 12 | Static, 13 | Suspense, 14 | Teleport, 15 | Text, 16 | TrackOpTypes, 17 | Transition, 18 | TransitionGroup, 19 | TriggerOpTypes, 20 | VueElement, 21 | assertNumber, 22 | callWithAsyncErrorHandling, 23 | callWithErrorHandling, 24 | camelize, 25 | capitalize, 26 | cloneVNode, 27 | compatUtils, 28 | compile, 29 | computed, 30 | createApp, 31 | createBaseVNode, 32 | createBlock, 33 | createCommentVNode, 34 | createElementBlock, 35 | createHydrationRenderer, 36 | createPropsRestProxy, 37 | createRenderer, 38 | createSSRApp, 39 | createSlots, 40 | createStaticVNode, 41 | createTextVNode, 42 | createVNode, 43 | customRef, 44 | defineAsyncComponent, 45 | defineComponent, 46 | defineCustomElement, 47 | defineEmits, 48 | defineExpose, 49 | defineModel, 50 | defineOptions, 51 | defineProps, 52 | defineSSRCustomElement, 53 | defineSlots, 54 | devtools, 55 | effect, 56 | effectScope, 57 | getCurrentInstance, 58 | getCurrentScope, 59 | getCurrentWatcher, 60 | getTransitionRawChildren, 61 | guardReactiveProps, 62 | h, 63 | handleError, 64 | hasInjectionContext, 65 | hydrate, 66 | hydrateOnIdle, 67 | hydrateOnInteraction, 68 | hydrateOnMediaQuery, 69 | hydrateOnVisible, 70 | initCustomFormatter, 71 | initDirectivesForSSR, 72 | inject, 73 | isMemoSame, 74 | isProxy, 75 | isReactive, 76 | isReadonly, 77 | isRef, 78 | isRuntimeOnly, 79 | isShallow, 80 | isVNode, 81 | markRaw, 82 | mergeDefaults, 83 | mergeModels, 84 | mergeProps, 85 | nextTick, 86 | normalizeClass, 87 | normalizeProps, 88 | normalizeStyle, 89 | onActivated, 90 | onBeforeMount, 91 | onBeforeUnmount, 92 | onBeforeUpdate, 93 | onDeactivated, 94 | onErrorCaptured, 95 | onMounted, 96 | onRenderTracked, 97 | onRenderTriggered, 98 | onScopeDispose, 99 | onServerPrefetch, 100 | onUnmounted, 101 | onUpdated, 102 | onWatcherCleanup, 103 | openBlock, 104 | popScopeId, 105 | provide, 106 | proxyRefs, 107 | pushScopeId, 108 | queuePostFlushCb, 109 | reactive, 110 | readonly, 111 | ref, 112 | registerRuntimeCompiler, 113 | render, 114 | renderList, 115 | renderSlot, 116 | resolveComponent, 117 | resolveDirective, 118 | resolveDynamicComponent, 119 | resolveFilter, 120 | resolveTransitionHooks, 121 | setBlockTracking, 122 | setDevtoolsHook, 123 | setTransitionHooks, 124 | shallowReactive, 125 | shallowReadonly, 126 | shallowRef, 127 | ssrContextKey, 128 | ssrUtils, 129 | stop, 130 | toDisplayString, 131 | toHandlerKey, 132 | toHandlers, 133 | toRaw, 134 | toRef, 135 | toRefs, 136 | toValue, 137 | transformVNodeArgs, 138 | triggerRef, 139 | unref, 140 | useAttrs, 141 | useCssModule, 142 | useCssVars, 143 | useHost, 144 | useId, 145 | useModel, 146 | useSSRContext, 147 | useShadowRoot, 148 | useSlots, 149 | useTemplateRef, 150 | useTransitionState, 151 | vModelCheckbox, 152 | vModelDynamic, 153 | vModelRadio, 154 | vModelSelect, 155 | vModelText, 156 | vShow, 157 | version, 158 | warn, 159 | watch, 160 | watchEffect, 161 | watchPostEffect, 162 | watchSyncEffect, 163 | withAsyncContext, 164 | withCtx, 165 | withDefaults, 166 | withDirectives, 167 | withKeys, 168 | withMemo, 169 | withModifiers, 170 | withScopeId 171 | } from "./chunk-LW4I4DCF.js"; 172 | export { 173 | BaseTransition, 174 | BaseTransitionPropsValidators, 175 | Comment, 176 | DeprecationTypes, 177 | EffectScope, 178 | ErrorCodes, 179 | ErrorTypeStrings, 180 | Fragment, 181 | KeepAlive, 182 | ReactiveEffect, 183 | Static, 184 | Suspense, 185 | Teleport, 186 | Text, 187 | TrackOpTypes, 188 | Transition, 189 | TransitionGroup, 190 | TriggerOpTypes, 191 | VueElement, 192 | assertNumber, 193 | callWithAsyncErrorHandling, 194 | callWithErrorHandling, 195 | camelize, 196 | capitalize, 197 | cloneVNode, 198 | compatUtils, 199 | compile, 200 | computed, 201 | createApp, 202 | createBlock, 203 | createCommentVNode, 204 | createElementBlock, 205 | createBaseVNode as createElementVNode, 206 | createHydrationRenderer, 207 | createPropsRestProxy, 208 | createRenderer, 209 | createSSRApp, 210 | createSlots, 211 | createStaticVNode, 212 | createTextVNode, 213 | createVNode, 214 | customRef, 215 | defineAsyncComponent, 216 | defineComponent, 217 | defineCustomElement, 218 | defineEmits, 219 | defineExpose, 220 | defineModel, 221 | defineOptions, 222 | defineProps, 223 | defineSSRCustomElement, 224 | defineSlots, 225 | devtools, 226 | effect, 227 | effectScope, 228 | getCurrentInstance, 229 | getCurrentScope, 230 | getCurrentWatcher, 231 | getTransitionRawChildren, 232 | guardReactiveProps, 233 | h, 234 | handleError, 235 | hasInjectionContext, 236 | hydrate, 237 | hydrateOnIdle, 238 | hydrateOnInteraction, 239 | hydrateOnMediaQuery, 240 | hydrateOnVisible, 241 | initCustomFormatter, 242 | initDirectivesForSSR, 243 | inject, 244 | isMemoSame, 245 | isProxy, 246 | isReactive, 247 | isReadonly, 248 | isRef, 249 | isRuntimeOnly, 250 | isShallow, 251 | isVNode, 252 | markRaw, 253 | mergeDefaults, 254 | mergeModels, 255 | mergeProps, 256 | nextTick, 257 | normalizeClass, 258 | normalizeProps, 259 | normalizeStyle, 260 | onActivated, 261 | onBeforeMount, 262 | onBeforeUnmount, 263 | onBeforeUpdate, 264 | onDeactivated, 265 | onErrorCaptured, 266 | onMounted, 267 | onRenderTracked, 268 | onRenderTriggered, 269 | onScopeDispose, 270 | onServerPrefetch, 271 | onUnmounted, 272 | onUpdated, 273 | onWatcherCleanup, 274 | openBlock, 275 | popScopeId, 276 | provide, 277 | proxyRefs, 278 | pushScopeId, 279 | queuePostFlushCb, 280 | reactive, 281 | readonly, 282 | ref, 283 | registerRuntimeCompiler, 284 | render, 285 | renderList, 286 | renderSlot, 287 | resolveComponent, 288 | resolveDirective, 289 | resolveDynamicComponent, 290 | resolveFilter, 291 | resolveTransitionHooks, 292 | setBlockTracking, 293 | setDevtoolsHook, 294 | setTransitionHooks, 295 | shallowReactive, 296 | shallowReadonly, 297 | shallowRef, 298 | ssrContextKey, 299 | ssrUtils, 300 | stop, 301 | toDisplayString, 302 | toHandlerKey, 303 | toHandlers, 304 | toRaw, 305 | toRef, 306 | toRefs, 307 | toValue, 308 | transformVNodeArgs, 309 | triggerRef, 310 | unref, 311 | useAttrs, 312 | useCssModule, 313 | useCssVars, 314 | useHost, 315 | useId, 316 | useModel, 317 | useSSRContext, 318 | useShadowRoot, 319 | useSlots, 320 | useTemplateRef, 321 | useTransitionState, 322 | vModelCheckbox, 323 | vModelDynamic, 324 | vModelRadio, 325 | vModelSelect, 326 | vModelText, 327 | vShow, 328 | version, 329 | warn, 330 | watch, 331 | watchEffect, 332 | watchPostEffect, 333 | watchSyncEffect, 334 | withAsyncContext, 335 | withCtx, 336 | withDefaults, 337 | withDirectives, 338 | withKeys, 339 | withMemo, 340 | withModifiers, 341 | withScopeId 342 | }; 343 | //# sourceMappingURL=vue.js.map 344 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vue.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": [], 4 | "sourcesContent": [], 5 | "mappings": "", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress'; 2 | 3 | // https://vitepress.dev/reference/site-config 4 | export default defineConfig({ 5 | title: 'Svelte Standalone', 6 | description: 'Transform Svelte components in standalone embedabbles!', 7 | 8 | head: [ 9 | ['link', { rel: 'icon', href: '/favicon.ico' }], 10 | ['meta', { property: 'og:title', content: 'Svelte Standalone' }], 11 | [ 12 | 'meta', 13 | { 14 | property: 'og:description', 15 | content: 'Transform Svelte components in standalone embedabbles!' 16 | } 17 | ], 18 | [ 19 | 'meta', 20 | { 21 | property: 'og:image', 22 | content: 'https://svelte-standalone.vercel.app/svelte-standalone.png' 23 | } 24 | ], 25 | ['meta', { property: 'og:url', content: 'https://svelte-standalone.vercel.app/' }], 26 | ['meta', { property: 'og:type', content: 'website' }], 27 | ['meta', { name: 'twitter:card', content: 'summary_large_image' }], 28 | ['meta', { name: 'twitter:title', content: 'Svelte Standalone' }], 29 | [ 30 | 'meta', 31 | { name: 'twitter:description', content: 'Transform Svelte components in standalone scripts!' } 32 | ], 33 | [ 34 | 'meta', 35 | { 36 | name: 'twitter:image', 37 | content: 'https://svelte-standalone.vercel.app/svelte-standalone.png' 38 | } 39 | ] 40 | ], 41 | 42 | themeConfig: { 43 | // https://vitepress.dev/reference/default-theme-config 44 | nav: [{ text: 'Guide', link: '/introduction' }], 45 | 46 | search: { 47 | provider: 'local' 48 | }, 49 | 50 | sidebar: [ 51 | { 52 | text: 'Getting Started', 53 | items: [ 54 | { text: 'Introduction', link: '/introduction' }, 55 | { text: 'Installation Guide', link: '/install' }, 56 | { text: 'Configuration', link: '/configuration' }, 57 | { text: 'CLI Commands', link: '/cli' }, 58 | { text: 'Boilerplate Files', link: '/boilerplate' }, 59 | { text: 'Embed Methods', link: '/embed' }, 60 | { text: 'Hot Reload', link: '/hot-reload' } 61 | ] 62 | }, 63 | { 64 | text: 'Advanced', 65 | items: [ 66 | { text: 'Shared Components', link: '/shared' }, 67 | { text: 'Svelte Component API', link: '/component-api' }, 68 | { text: 'Web Components', link: '/web-components' } 69 | ] 70 | }, 71 | { 72 | text: 'Examples', 73 | items: [ 74 | { text: 'Bundling an Mode Package', link: '/svelte-notifications' }, 75 | { text: 'Supporting Tailwind', link: '/tailwind' }, 76 | { text: 'Supporting Shadcn', link: '/shadcn' } 77 | ] 78 | } 79 | ], 80 | 81 | footer: { 82 | message: 'Released under the MIT License.', 83 | copyright: 84 | 'Copyright © 2024-present Breno Lira' 85 | }, 86 | 87 | socialLinks: [{ icon: 'github', link: 'https://github.com/brenoliradev/svelte-standalone' }] 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | // https://vitepress.dev/guide/custom-theme 2 | import { h } from 'vue'; 3 | import type { Theme } from 'vitepress'; 4 | import DefaultTheme from 'vitepress/theme'; 5 | import './style.css'; 6 | 7 | export default { 8 | extends: DefaultTheme, 9 | Layout: () => { 10 | return h(DefaultTheme.Layout, null, { 11 | // https://vitepress.dev/guide/extending-default-theme#layout-slots 12 | }); 13 | }, 14 | enhanceApp({ app, router, siteData }) { 15 | // ... 16 | } 17 | } satisfies Theme; 18 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Customize default theme styling by overriding CSS variables: 3 | * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css 4 | */ 5 | 6 | /** 7 | * Colors 8 | * -------------------------------------------------------------------------- */ 9 | 10 | :root { 11 | /* Default colors */ 12 | --vp-c-default-1: var(--vp-c-gray-1); 13 | --vp-c-default-2: var(--vp-c-gray-2); 14 | --vp-c-default-3: var(--vp-c-gray-3); 15 | --vp-c-default-soft: var(--vp-c-gray-soft); 16 | 17 | /* Brand colors (Svelte orange) */ 18 | --vp-c-brand-1: #ff3e00; /* Svelte orange for text and accents */ 19 | --vp-c-brand-2: #e63800; /* Darker orange for hover states */ 20 | --vp-c-brand-3: #cc3200; /* Darkest orange for solid backgrounds */ 21 | --vp-c-brand-soft: rgba(255, 62, 0, 0.14); /* Soft orange for subtle backgrounds */ 22 | 23 | /* Tip colors (use brand colors) */ 24 | --vp-c-tip-1: var(--vp-c-brand-1); 25 | --vp-c-tip-2: var(--vp-c-brand-2); 26 | --vp-c-tip-3: var(--vp-c-brand-3); 27 | --vp-c-tip-soft: var(--vp-c-brand-soft); 28 | 29 | /* Warning colors (yellow) */ 30 | --vp-c-warning-1: var(--vp-c-yellow-1); 31 | --vp-c-warning-2: var(--vp-c-yellow-2); 32 | --vp-c-warning-3: var(--vp-c-yellow-3); 33 | --vp-c-warning-soft: var(--vp-c-yellow-soft); 34 | 35 | /* Danger colors (red) */ 36 | --vp-c-danger-1: var(--vp-c-red-1); 37 | --vp-c-danger-2: var(--vp-c-red-2); 38 | --vp-c-danger-3: var(--vp-c-red-3); 39 | --vp-c-danger-soft: var(--vp-c-red-soft); 40 | } 41 | 42 | /** 43 | * Component: Button 44 | * -------------------------------------------------------------------------- */ 45 | 46 | :root { 47 | --vp-button-brand-border: transparent; 48 | --vp-button-brand-text: var(--vp-c-white); 49 | --vp-button-brand-bg: var(--vp-c-brand-3); /* Svelte orange background */ 50 | --vp-button-brand-hover-border: transparent; 51 | --vp-button-brand-hover-text: var(--vp-c-white); 52 | --vp-button-brand-hover-bg: var(--vp-c-brand-2); /* Darker orange on hover */ 53 | --vp-button-brand-active-border: transparent; 54 | --vp-button-brand-active-text: var(--vp-c-white); 55 | --vp-button-brand-active-bg: var(--vp-c-brand-1); /* Lightest orange on active */ 56 | } 57 | 58 | /** 59 | * Component: Home 60 | * -------------------------------------------------------------------------- */ 61 | 62 | :root { 63 | --vp-home-hero-name-color: transparent; 64 | --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #ff3e00 30%, #f0055f 70%); 65 | 66 | --vp-home-hero-image-background-image: linear-gradient(-45deg, #ff3e00 50%, #f0055f 50%); 67 | --vp-home-hero-image-filter: blur(44px); 68 | } 69 | 70 | @media (min-width: 640px) { 71 | :root { 72 | --vp-home-hero-image-filter: blur(56px); 73 | } 74 | } 75 | 76 | @media (min-width: 960px) { 77 | :root { 78 | --vp-home-hero-image-filter: blur(68px); 79 | } 80 | } 81 | 82 | .main .name { 83 | max-width: 70%; 84 | text-align: center; 85 | margin: 0 auto; 86 | } 87 | 88 | .main .tagline { 89 | max-width: 90%; 90 | text-align: center; 91 | margin: 0 auto; 92 | } 93 | 94 | .main .actions { 95 | justify-content: center; 96 | } 97 | 98 | /** 99 | * Component: Custom Block 100 | * -------------------------------------------------------------------------- */ 101 | 102 | :root { 103 | --vp-custom-block-tip-border: transparent; 104 | --vp-custom-block-tip-text: var(--vp-c-text-1); 105 | --vp-custom-block-tip-bg: var(--vp-c-brand-soft); /* Soft Svelte orange */ 106 | --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft); /* Soft Svelte orange */ 107 | } 108 | 109 | /** 110 | * Component: Algolia 111 | * -------------------------------------------------------------------------- */ 112 | 113 | .DocSearch { 114 | --docsearch-primary-color: var(--vp-c-brand-1) !important; /* Svelte orange */ 115 | } 116 | -------------------------------------------------------------------------------- /docs/boilerplate.md: -------------------------------------------------------------------------------- 1 | # Boilerplate Code 2 | 3 | All these files will be generated at your `src/_standalone/` folder when you use the [standalone create](/cli#create) command. 4 | 5 | ## **index.svelte** 6 | 7 | The component written here will be transformed into a embedabble - you can treat it as **any** Svelte component and Svelte Standalone will bundle it for you. 8 | 9 | :::warning 10 | This is the `entry` for your vite build. You **MUST** explicitly import the global CSS files here. 11 | ::: 12 | 13 | ## **embed.ts (or embed.js)** 14 | 15 | Embedding logic that controls how and where the component is inserted. It follows the chosen embedding strategy (e.g., auto-embed by `id`, explicit call to `start()`). 16 | 17 | This is a example of a `embed file` using the [embed on body method](/embed.html#auto-embed-on-body) 18 | 19 | ```ts 20 | import { autoEmbedWithTarget } from 'svelte-standalone'; 21 | import Example from './index.svelte'; // your embedabble 22 | autoEmbedWithTarget(Example, 'example'); 23 | ``` 24 | 25 | :::tip 26 | You can deep dive about [embed methods here](/embed.html) 27 | ::: 28 | 29 | ## **config.ts (or config.js)** 30 | 31 | Default configuration for your component, including settings like styles or initialization options. 32 | 33 | ```ts 34 | // example 35 | import type { ComponentProps } from 'svelte'; 36 | import type { MultipleEmbedWindow } from 'svelte-standalone'; 37 | import Example from './index.svelte'; // your embedabble 38 | 39 | export type ExampleProps = ComponentProps; // props from your components 40 | export const defaultConfig: ExampleProps = {}; // this will be used - when aplicable - as a config to start your embedabble (it'll also be used at your story when using storybook) 41 | 42 | declare global { 43 | interface Window extends MultipleEmbedWindow {} // typesafe wrapper for window.example 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | # CLI Commands 2 | 3 | **Svelte Standalone** provides CLI commands to handle the creation and bundling process of your embedabbles. 4 | 5 | ## `create` 6 | 7 | Generates [boilerplate code](/boilerplate) for a new standalone component. 8 | 9 | ### Usage: 10 | 11 | ```bash 12 | npx standalone create 13 | ``` 14 | 15 | ### Steps: 16 | 17 | 1. **Name your component**: Enter a name for your component (e.g., `payments`). 18 | 2. **Choose an embedding strategy**: 19 | 20 | - **Explicit Call (Single Instance)**: Mounts the component **once** using `window.payments.start()`. 21 | - **Explicit Call (Multiple Instances)**: Allows mounting **multiple** instances with `window.payments.start()`. 22 | - **Auto-Embed with Target ID**: **Automatically** appends to an element with a specified `id`. 23 | - **Auto-Embed with Target Class**: **Automatically** appends to elements with a specified `class`. 24 | - **Auto-Embed on Body**: **Automatically** appends to the `` when downloaded. 25 | 26 | ## `build` 27 | 28 | Builds your standalone components into production-ready scripts. 29 | 30 | ### Usage: 31 | 32 | ```bash 33 | npx standalone build 34 | ``` 35 | 36 | ### Options: 37 | 38 | - **Production Build**: Minifies and optimizes for production. 39 | 40 | ```bash 41 | npx standalone build --production 42 | ``` 43 | 44 | - **Build All Components**: Builds all standalone components at once. 45 | 46 | ```bash 47 | npx standalone build --all 48 | ``` 49 | 50 | - **Strip Runtime**: Bundles shared styles directly into components (excludes the runtime). 51 | ```bash 52 | npx standalone build --strip-runtime 53 | ``` 54 | - **Mode**: Implements [modes](https://vite.dev/guide/env-and-mode.html#modes) from Vite. 55 | ```bash 56 | npx standalone build --mode dev 57 | ``` 58 | 59 | ### Output: 60 | 61 | The output will be saved in `static/dist/standalone/`: 62 | 63 | - `payments.min.js`: The standalone JavaScript file. 64 | - `payments.status.html`: A visualization of the bundle. 65 | 66 | ## `help` 67 | 68 | Displays help information for the CLI commands. 69 | 70 | ### Usage: 71 | 72 | ```bash 73 | npx standalone --help 74 | ``` 75 | 76 | ## `version` 77 | 78 | Displays the current version of **Svelte Standalone**. 79 | 80 | ### Usage: 81 | 82 | ```bash 83 | npx standalone --version 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/component-api.md: -------------------------------------------------------------------------------- 1 | ::: warning 2 | _This feature is currently only supported in Svelte 4 and `svelte-standalone@1` (version should be 1.X.X)._ 3 | ::: 4 | 5 | # Leveraging Component API 6 | 7 | **Svelte Standalone** exposes the `$set` and `$on` methods from the [Svelte Component API](https://svelte.dev/docs/svelte/legacy-component-api) at `window.`. These methods allow you to interact with your standalone components programmatically. 8 | 9 | ## Available Methods 10 | 11 | ### `$set(props)` 12 | 13 | Updates the component's props dynamically. This method is useful for modifying the component's state after it has been mounted. 14 | 15 | #### Example: 16 | 17 | ```javascript 18 | window.myComponent.$set({ 19 | title: 'Updated Title', 20 | description: 'This is an updated description.' 21 | }); 22 | ``` 23 | 24 | ### `$on(event, callback)` 25 | 26 | Listens for custom events emitted by the component. This method allows you to handle events such as user interactions or state changes. 27 | 28 | #### Example: 29 | 30 | ```javascript 31 | window.myComponent.$on('click', (event) => { 32 | console.log('Component clicked!', event.detail); 33 | }); 34 | ``` 35 | 36 | ## Example Usage 37 | 38 | Here’s a complete example of how to use these methods with a standalone component: 39 | 40 | 1. **Create a Component**: 41 | 42 | ```bash 43 | npx standalone create 44 | ``` 45 | 46 | Name your component (e.g., `myComponent`) and choose an embedding strategy. 47 | 48 | 2. **Build the Component**: 49 | 50 | ```bash 51 | npx standalone build 52 | ``` 53 | 54 | 3. **Include the Component in Your HTML**: 55 | 56 | ```html 57 | 58 | ``` 59 | 60 | 4. **Interact with the Component**: 61 | 62 | ```javascript 63 | // Start the component 64 | window.myComponent.start({ 65 | title: 'Initial Title', 66 | description: 'This is the initial description.' 67 | }); 68 | 69 | // Update the component's props 70 | window.myComponent.$set({ 71 | title: 'Updated Title', 72 | description: 'This is an updated description.' 73 | }); 74 | 75 | // Listen for a custom event 76 | window.myComponent.$on('click', (event) => { 77 | console.log('Component clicked!', event.detail); 78 | }); 79 | ``` 80 | 81 | ``` 82 | 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Path Aliases 2 | 3 | **Svelte Standalone** supports [path aliases](https://vite.dev/config/shared-options.html#resolve-alias). 4 | 5 | :::tip 6 | _Configure path aliases as you normally would in your `vite.config.js` or `svelte.config.js`, and **Svelte Standalone** will handle the bundling process for you._ 7 | ::: 8 | 9 | ## Environment Variables 10 | 11 | **Svelte Standalone** aims to include **Svelte/Kit** as an **opt-in** dependency. To manage environment variables, you should configure via [Vite environment variables](https://vite.dev/guide/env-and-mode.html#env-files). 12 | 13 | - **Environment Files**: Use `.env` files to define environment variables. - `.env.[mode]` is also supported. 14 | - **Vite Options**: Customize the behavior of environment variables using Vite's `define`, `envDir`, and `envPrefix` options. 15 | -------------------------------------------------------------------------------- /docs/embed.md: -------------------------------------------------------------------------------- 1 | # Embed Methods 2 | 3 | While [creating a standalone component](/cli#create), you can specify how your embeddable should be inserted after bundling it. 4 | 5 | By default, all embed methods include a way to programmatically stop them. Additionally, after selecting your embed type, **Svelte Standalone** will automatically generate the necessary boilerplate for it. 6 | 7 | ::: tip 8 | _You can change an existing embeddable's embed type by updating its embed method._ 9 | ::: 10 | 11 | ## Explicit Call (Single Instance) 12 | 13 | Embeds a Svelte component as a singleton. This method allows you to programmatically start and stop the component. 14 | 15 | ### Embed Method: 16 | 17 | ```javascript 18 | import { embed } from 'svelte-standalone'; 19 | ``` 20 | 21 | ### Params: 22 | 23 | - `mount`: The Svelte component to embed. 24 | - `id`: A unique identifier for the component. 25 | 26 | #### Usage: 27 | 28 | - Start the component: 29 | ```javascript 30 | window.myComponent.start({ 31 | /* props */ 32 | }); 33 | ``` 34 | - Stop the component: 35 | ```javascript 36 | window.myComponent.stop(); 37 | ``` 38 | 39 | #### Key Points: 40 | 41 | - **Single Instance**: Ensures only one instance of the component is active at a time. 42 | - **Component Props**: When calling `start`, you can include custom initial props. 43 | 44 | ## Explicit Call (Multiple Instances) 45 | 46 | Embeds multiple instances of a Svelte component. This method allows you to create and manage multiple instances of the same component. 47 | 48 | ### Embed Method: 49 | 50 | ```javascript 51 | import { embedMultiple } from 'svelte-standalone'; 52 | ``` 53 | 54 | ### Params: 55 | 56 | - `mount`: The Svelte component to embed. 57 | - `id`: A unique identifier for the component. 58 | 59 | #### Usage: 60 | 61 | - Start a new instance: 62 | ```javascript 63 | const instance = window.myComponent.start( 64 | { 65 | /* props */ 66 | }, 67 | 'targetElementId' 68 | ); 69 | ``` 70 | - Stop an instance: 71 | ```javascript 72 | instance.stop(); 73 | ``` 74 | 75 | #### Key Points: 76 | 77 | - **Multiple Instances**: Allows multiple instances of the same component. 78 | - **Component Props**: When calling `start`, you can include custom initial props. 79 | - **Control Over Instances**: You can programmatically stop a specific instance. 80 | 81 | ## Auto-Embed on Body 82 | 83 | Automatically embeds a Svelte component into the document body. 84 | 85 | ### Embed Method: 86 | 87 | ```javascript 88 | import { autoEmbedOnBody } from 'svelte-standalone'; 89 | ``` 90 | 91 | ### Params: 92 | 93 | - `mount`: The Svelte component to embed. 94 | - `id`: A unique identifier for the component. 95 | 96 | #### Usage: 97 | 98 | - Include the script in your HTML: (It'll be mounted at the provided `id`) 99 | ```html 100 |
101 | 102 | ``` 103 | 104 | #### Key Points: 105 | 106 | - **Simple Setup**: No need to specify a target element. 107 | - **Automatic Embedding**: Automatically mounts the component to the ``. 108 | 109 | ## Auto-Embed with Target 110 | 111 | Automatically embeds a Svelte component into a target element based on the URL query string. 112 | 113 | ### Embed Method: 114 | 115 | ```javascript 116 | import { autoEmbedWithTarget } from 'svelte-standalone'; 117 | ``` 118 | 119 | ### Params: 120 | 121 | - `mount`: The Svelte component to embed. 122 | - `id`: A unique identifier for the component. 123 | 124 | #### Usage: 125 | 126 | - Include the script in your HTML: (It'll be mounted at the provided `id`) 127 | ```html 128 | 129 | 130 |
131 | 132 | ``` 133 | 134 | #### Dynamic target: 135 | 136 | - Include the script in your HTML with a `target` selector: 137 | ```html 138 |
139 | 140 | ``` 141 | 142 | #### Key Points: 143 | 144 | - **Automatic Embedding**: Automatically mounts the component to a target element. 145 | - **Dynamic Targeting**: Uses URL query strings to determine the target element. 146 | 147 | ## Auto-Embed Multiple Element 148 | 149 | Batch mount components to multiple elements using CSS class selector. 150 | 151 | ###Embed Method: 152 | 153 | ```javascript 154 | import { autoEmbedMultiple } from 'svelte-standalone'; 155 | ``` 156 | 157 | ### Params: 158 | 159 | - `mount`: The Svelte component to embed. 160 | - `id`: A unique identifier for the component. 161 | 162 | #### Usage: 163 | 164 | ```html 165 | 166 | 167 |
168 |
169 | 170 | ``` 171 | 172 | #### Dynamic target: 173 | 174 | - Include the script in your HTML with a `target` selector: 175 | ```html 176 |
177 |
178 | 179 | ``` 180 | 181 | #### Key Points: 182 | 183 | - **CSS Class Targeting** - Batch element selection. 184 | - **Bulk Initialization** - Create multiple instances at once. 185 | - **Unified Control** - Remove all instances at once. 186 | -------------------------------------------------------------------------------- /docs/hot-reload.md: -------------------------------------------------------------------------------- 1 | # Hot Reload 2 | 3 | When [creating a standalone component](/cli.html#create), you'll also generate an [embed file](/boilerplate.html#embed-ts-or-embed-js). By directly importing this embed file, you'll enable hot reload for your embedabbles. 4 | 5 | ```html 6 | 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: 'Integrate Svelte Anywhere' 7 | tagline: A cross-platform, developer-friendly tool for creating JavaScript embeddables. 8 | actions: 9 | - theme: brand 10 | text: Get Started 11 | link: /introduction 12 | - theme: alt 13 | text: Quickstart 14 | link: /install 15 | 16 | features: 17 | - icon: ⚡ 18 | title: Zero Config 19 | details: Write Svelte components, and Svelte Standalone handles the rest. 20 | - icon: 🎛️ 21 | title: Fully Customizable 22 | details: Build independent components without altering your existing Svelte environment. 23 | - icon: 🔥 24 | title: Hot Reload 25 | details: See changes instantly in your embedabbles without page refresh. 26 | --- 27 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation Guide 2 | 3 | Set up **Svelte Standalone** as you would set up any Svelte project: 4 | 5 | 1. **Create a Svelte App** – Use the [Svelte CLI](https://svelte.dev/docs/kit/creating-a-project) and select your preferred tools. 6 | 2. **Clean Up** – Remove svelte boilerplate code (e.g., routes, components). 7 | 3. **Install Svelte Standalone** 8 | 9 | ```bash 10 | npm install -D svelte-standalone@latest 11 | ``` 12 | 13 | ## Troubleshooting 14 | 15 | If you encounter any issues during installation, [open an issue](https://github.com/brenoliradev/svelte-standalone/issues) on GitHub for assistance. 16 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # What is Svelte Standalone? 2 | 3 | **Svelte Standalone** is a CLI tool designed to simplify the process of creating embedabbles to **just writing Svelte**. 4 | 5 | Featuring _opt-in_ support for **Tailwind**, **TypeScript** or **Storybook**, it seamlessly integrates into your existing workflow, allowing you to focus on building great user experiences without the tooling hassle. 6 | 7 | [Just want to try it out? Let's install it.](/install) 8 | 9 | ## What Are Embedabbles? 10 | 11 | Simply put, _embedabbles_ are Svelte components designed to work in **any JavaScript environment**. Unlike regular Svelte components, an embedabble **must** be fully self-contained since it should be able to **mount itself** and function independently without relying on a specific framework or setup. 12 | 13 | ## When to Use Embedabbles? 14 | 15 | Embedabbles are great when you need to **embed a Svelte component anywhere**, regardless of the tech stack i.e. **third-party integrations**, **no-framework environments** or even as **microfrontends**. 16 | 17 | ## Why Use Svelte Standalone? 18 | 19 | - **Reactive & Lightweight** – Build fast, self-contained embeddables with Svelte’s reactivity. 20 | - **Flexible** – Opt-in support for Tailwind, TypeScript, and Storybook. 21 | - **Zero Setup** – Handles minification, CSS purging, and boilerplate generation for you. 22 | - **Universal Compatibility** – Works across all OS and node package managers. 23 | 24 | ## Can I Use It With? 25 | 26 | Yes you can! **Svelte Standalone** is made to integrate with any Svelte workflow. It offers **_opt-in_** support for: 27 | 28 | - **Tailwind** 29 | - **TypeScript** 30 | - **Storybook** 31 | - **SvelteKit** 32 | 33 | ## Boilerplate Generation 34 | 35 | **Svelte Standalone** automatically scans your `package.json` and generates boilerplate code based on your setup: 36 | 37 | - **Storybook detected?** → Generates story files. 38 | - **SvelteKit detected?** → Generates route files. 39 | - **TypeScript/Tailwind detected?** → Applies them to the boilerplate. 40 | - **SvelteKit** → Generate routes for bundled embedabbles. 41 | - **Storybook** → Create component stories. 42 | 43 | ## Is It Type-Safe? 44 | 45 | It depends! If you're using **Svelte Standalone** with TypeScript, it will generate a `config.ts` file to ensure a fully type-safe developer experience. 46 | 47 | ## Testing 48 | 49 | Test your components as you would test any Svelte app. 50 | 51 | ## Deployment 52 | 53 | For **deployment**, standalone components are JavaScript files that can be hosted on any static hosting platform or CDN, such as **Netlify**, **Vercel**, or **AWS S3**. 54 | 55 | ::: tip 56 | _If your target app has a file server, you can even include your embedabbles at your `/public` or `/static` folder._ 57 | ::: 58 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brenoliradev/svelte-standalone/fc9521cd88692cde374913899571823875ada22f/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/svelte-standalone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brenoliradev/svelte-standalone/fc9521cd88692cde374913899571823875ada22f/docs/public/svelte-standalone.png -------------------------------------------------------------------------------- /docs/shadcn.md: -------------------------------------------------------------------------------- 1 | # Supporting Shadcn 2 | 3 | With **Svelte Standalone** is possible to integrate the modern UI kits with ease. For example, to implement [shadcn](https://www.shadcn-svelte.com/) you only to follow **two** steps: 4 | 5 | 1. [Install Svelte Standalone](/install) - For the sake of simplicity, this tutorial is using only **Tailwind** and **TypeScript**. 6 | 2. [Install shadcn-svelte](https://www.shadcn-svelte.com/docs/installation/sveltekit) 7 | 8 | :::tip 9 | _You aren't required to setup anything different than what you would do, but to leverage all capabilities of **Svelte Standalone** this is my recommended setup:_ 10 | 11 | - Move `app.css` to `src/shared/app.css`. 12 | - Include your [kit.alias](https://svelte.dev/docs/kit/configuration#alias) to match `/src/shared/lib`. 13 | 14 | _Thil will allow you to: [include your shadcn styles at Runtime](#including-a-runtime)_ 15 | ::: 16 | 17 | After these steps, you can just start creating your embedabbles using the Shadcn CLI. 18 | 19 | ## Adding A Shadcn Component 20 | 21 | To continue explaining how easy is to have an amazing developer experience while using **Svelte Standalone** let's install the [Table component](https://www.shadcn-svelte.com/docs/components/table) from Shadcn. 22 | 23 | 1. **Create Your Embedabble**: For this embedabble, we can use the following options: 24 | - Name your component: `table`. 25 | - Choose an embedding strategy: `Explicit Call (Single Instance)` 26 | 2. **Install Table** 27 | 3. **Import Table To Your Embedabble**: 28 | 29 | ```svelte 30 | 34 | 35 | 36 | A list of your recent invoices. 37 | 38 | 39 | Invoice 40 | Status 41 | Method 42 | Amount 43 | 44 | 45 | 46 | 47 | INV001 48 | Paid 49 | Credit Card 50 | $250.00 51 | 52 | 53 | 54 | ``` 55 | 56 | 4. **Transform It**: Just by running `npm run standalone build -ap` you have a minified, production-ready standalone version of `Table`: 57 | 58 | ```bash 59 | ➜ shadcn-standalone bun standalone build -ap 60 | vite v5.4.13 building for production... 61 | ✓ 41 modules transformed. 62 | static/dist/standalone/table.min.js 45.37 kB │ gzip: 13.38 kB 63 | ✓ built in 815ms 64 | ``` 65 | 66 | ## **Including a Runtime** 67 | 68 | Turning the code into an embedabble isn't the only feature from **Svelte Standalone** if you're planning to have multipel embedabbles in the same app I would recommend you to [include styles into a runtime](/shared). It seems hard, but isn't. To handle it we just have to do the following: 69 | 70 | - **Create Another Embedabble**: For this embedabble, we can use the following options: 71 | - Name your component: `$runtime`. 72 | - Choose an embedding strategy: `Auto-Embed on Body` 73 | 74 | Initially, your runtime would be just: 75 | 76 | ```svelte 77 | 80 | ``` 81 | 82 | By creating an runtime and bundling it all of your styles from `/src/shared` are **only** included at `runtime.min.js`. Which means you'll can use multiple embedabbles in the same page, but download it's CSS only once. 83 | 84 | :::tip 85 | _If your target app has Tailwind, you can remove the following styles from app.css_ 86 | 87 | `@tailwind base;` 88 | 89 | `@tailwind components;` 90 | 91 | _If your target app doesn't include Tailwind and you plan to have multiple embedabbles, consider creating [a runtime](/shared) and including these directives once_ 92 | ::: 93 | 94 | ## Troubleshooting 95 | 96 | You can see all the code used for this example [here](https://github.com/brenoliradev/shadcn-and-svelte-standalone/tree/main) but If you encounter any problems [open an issue](https://github.com/brenoliradev/svelte-standalone/issues) on GitHub for assistance. 97 | -------------------------------------------------------------------------------- /docs/shared.md: -------------------------------------------------------------------------------- 1 | # Shared Components 2 | 3 | With **Svelte Standalone**, you can simplify the process of creating new embeddables by sharing common components across your projects. 4 | 5 | For example, if you're building a Dashboard, you can reuse shared components (i.e. `Tabs` or `Tables`) across multiple Svelte components. 6 | 7 | ## How to Share Components 8 | 9 | You can write plain Svelte components, stores, or utilities and import them into your `index.svelte` file, just as you would in any Svelte app. 10 | 11 | To leverage CSS purging and reuse styles across your components, create a `/src/shared` folder. By placing your components in this folder, their styles will automatically be shared across all your Svelte embeddables, ensuring consistency and reducing redundancy. 12 | 13 | This approach works great if you plan to use these components independently. For example, you could create a `Tooltip` component and reuse it across multiple embeddables in different apps. [But what if you want to use both in the same app?](#runtime-components) 14 | 15 | ## Runtime Components 16 | 17 | **Svelte Standalone** supports a special component called `runtime` (you can also name it `$runtime` or `+runtime`). 18 | 19 | A `runtime` component is designed to deduplicate styles from the `/src/shared` folder. For example, if you have a `Card` component, creating a `runtime` ensures that all your cards will include the CSS only once, reducing redundancy and improving performance. 20 | 21 | ::: tip 22 | _You can also use a `runtime` to run logic that will be reused across your app. For instance, you can handle authentication in your `runtime` and synchronize it across your other embeddables._ 23 | 24 | _Additionally, you can pass props to your `runtime` to inject environment variables or configuration for your components. For example, by adding an `authUrl` prop to your runtime, you can manage authentication across multiple environments._ 25 | ::: 26 | 27 | ## Create a Runtime Component 28 | 29 | To create a `runtime` component, run the following command: 30 | 31 | ```bash 32 | npx standalone create 33 | ``` 34 | 35 | When prompted for the component name, enter `runtime`, `$runtime`, or `+runtime`. 36 | 37 | ## Ignore Runtime 38 | 39 | By default, styles from the `src/shared` folder are included across the runtime component during builds. If you want to bundle styles directly into each component instead, use the `--strip-runtime` flag: 40 | 41 | ```bash 42 | npx standalone build --strip-runtime 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/svelte-notifications.md: -------------------------------------------------------------------------------- 1 | # Bundling `svelte-notifications` 2 | 3 | This example demonstrates how to integrate the [svelte-notifications](https://github.com/beyonk-group/svelte-notifications) repository, which includes a "lib" structure and JavaScript, bundled with **Svelte Standalone**. 4 | 5 | You can also check how the component works: [Live Demo](https://svelte-standalone-beyonk.vercel.app/beyonk) 6 | 7 | As you can see, even though `svelte-notifications` uses `"svelte": "^3.47.0"`, it still works. 8 | 9 | ## How Does This Work? 10 | 11 | 1. **Download the Code**: 12 | I downloaded the `svelte-notifications` code from its repository. 13 | 14 | 2. **Create a Standalone Component**: 15 | I created a standalone component using: 16 | 17 | ```bash 18 | npx standalone create 19 | ``` 20 | 21 | Named it `beyonk` and chose an embedding strategy. 22 | 23 | 3. **Paste the Code**: 24 | I pasted the code from `svelte-notifications` into the `beyonk` component I created. 25 | 26 | 4. **Update Folder Structure**: 27 | I swapped `$lib` to `lib` since it was the new folder structure. 28 | 29 | 5. **Build the Component**: 30 | I built the component using: 31 | ```bash 32 | npx standalone build 33 | ``` 34 | 35 | ## Key Takeaways 36 | 37 | - **Seamless Integration**: Even though `svelte-notifications` was built for Svelte 3, it works flawlessly with **Svelte Standalone**. 38 | - **Flexible Folder Structure**: You can adapt existing projects by updating the folder structure (e.g., `$lib` to `lib`). 39 | - **Quick Setup**: The entire process is straightforward and requires minimal setup. 40 | 41 | ## Try It Yourself 42 | 43 | Follow the steps above to integrate your own Svelte components or third-party libraries with **Svelte Standalone**. If you run into any issues, feel free to [open an issue](https://github.com/brenoliradev/svelte-standalone/issues) or [join the discussion](https://github.com/brenoliradev/svelte-standalone/discussions). 44 | -------------------------------------------------------------------------------- /docs/tailwind.md: -------------------------------------------------------------------------------- 1 | # Supporting Tailwind 2 | 3 | **Svelte Standalone** includes **opt-in** support for Tailwind. You don't have to do any setup to use it. All you have to do is add Tailwind to your project using the [svelte CLI](https://svelte.dev/docs/cli/sv-add). 4 | 5 | The only caveat is: you **MUST** to import the [tailwind directives](https://tailwindcss.com/docs/functions-and-directives) directly into your embedabbles. Since **Svelte Standalone** bundles them separatedly each `index.svelte` should include their own directives. 6 | 7 | ## **Include Tailwind Directives** 8 | 9 | Since **Svelte Standalone** bundles each component separately, you **must** include the [Tailwind directives](https://tailwindcss.com/docs/functions-and-directives) directly in your embeddables. Each `index.svelte` file should include its own directives. 10 | 11 | ## **Using Tailwind in Your App** 12 | 13 | - **If your app already includes Tailwind**, you only need to include `@tailwind utilities`. 14 | - **If your app doesn't include Tailwind**, you may need to include additional directives (e.g., `@tailwind base`, `@tailwind components`). Be aware that this will increase the size of your embeddables. 15 | 16 | ::: tip 17 | _If you're using multiple embeddables, consider including the base Tailwind directives in a [runtime component](/shared) to avoid duplication and reduce bundle size._ 18 | ::: 19 | -------------------------------------------------------------------------------- /docs/web-components.md: -------------------------------------------------------------------------------- 1 | :::warning 2 | _This is currently a workaround. It only works in this specific setup._ 3 | ::: 4 | 5 | # Bundling Web Components 6 | 7 | You can bundle Web Components using **Svelte Standalone** as a tool. To setup it, you need to: 8 | 9 | 1. **Setup your `svelte.config.js`**: Set `compilerOptions.customElement` to `true`. 10 | 2. **Generate a Standalone Component**: Run `standalone create` and create a component. The [embed method](/embed) isn't important. 11 | 3. **Setup your Web Component**: Add the following options to your svelte component. 12 | 13 | ```javascript 14 | 20 | ``` 21 | 22 | 4. **Update Your Embed File**: This is the correct [embed method](/embed) for Web Components. 23 | 24 | ```javascript 25 | import Once from './index.svelte' 26 | 27 | customElements.define('my-element', Once as any); 28 | ``` 29 | 30 | 5. **Bundle it**: Once you run `standalone build` you have your Web Component fully setup. 31 | 32 | ## Troubleshooting 33 | 34 | If you encounter any issues during installation, [open an issue](https://github.com/brenoliradev/svelte-standalone/issues) on GitHub for assistance. 35 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import ts from 'typescript-eslint'; 3 | 4 | import prettier from 'eslint-config-prettier'; 5 | import globals from 'globals'; 6 | 7 | /** @type {import('eslint').Linter.Config[]} */ 8 | export default [ 9 | js.configs.recommended, 10 | ...ts.configs.recommended, 11 | prettier, 12 | { 13 | languageOptions: { 14 | globals: { 15 | ...globals.browser, 16 | ...globals.node 17 | } 18 | } 19 | }, 20 | { 21 | languageOptions: { 22 | parserOptions: { 23 | parser: ts.parser 24 | } 25 | } 26 | }, 27 | { 28 | ignores: ['build/', 'dist/', 'static/dist'] 29 | } 30 | ]; 31 | -------------------------------------------------------------------------------- /lib/cli/cli-build.ts: -------------------------------------------------------------------------------- 1 | import { checkbox } from '@inquirer/prompts'; 2 | 3 | import { glob } from 'glob'; 4 | import { buildStandalone } from './methods/index.js'; 5 | import path from 'path'; 6 | 7 | const rootDir = process.cwd(); 8 | 9 | const c = glob 10 | .sync(`${rootDir}/src/_standalone/**/embed.{js,ts}`) // Matches both .js and .ts 11 | .map((file) => { 12 | const normalizedPath = path.normalize(file); 13 | const match = normalizedPath.match(/src[\\/]_standalone[\\/](.*?)[\\/]embed\.(js|ts)/); 14 | return match ? { name: match[1], value: file, checked: true } : null; 15 | }) 16 | .filter(Boolean) as { 17 | name: string; 18 | value: string; 19 | checked: boolean; 20 | }[]; 21 | 22 | export const buildStrategy = { 23 | name: 'components', 24 | message: 'Which components should be builded?', 25 | choices: c 26 | } as const; 27 | 28 | export async function build(prod: boolean, all: boolean, stripRuntime: boolean, mode: string) { 29 | if (buildStrategy.choices.length === 0) { 30 | console.warn( 31 | "You don't have any standalone component. Create them running: standalone create." 32 | ); 33 | 34 | return; 35 | } 36 | 37 | const hasRuntime = stripRuntime 38 | ? false 39 | : c.some(({ name }) => /(\$runtime|\+runtime|runtime)/.test(name ?? '')); 40 | 41 | if (all) { 42 | buildStandalone( 43 | c.map((co) => co.value), 44 | prod, 45 | hasRuntime, 46 | mode 47 | ); 48 | 49 | return; 50 | } 51 | 52 | const answers = await checkbox(buildStrategy); 53 | 54 | try { 55 | buildStandalone(answers, prod, hasRuntime, mode); 56 | } catch (error) { 57 | if (error instanceof Error && error.name === 'ExitPromptError') { 58 | // noop; silence this error 59 | } else { 60 | throw error; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/cli/cli-create.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { rootDir } from '../dir.js'; 3 | import { input, select } from '@inquirer/prompts'; 4 | import { create } from './methods/create.js'; 5 | 6 | const dynamicPaths = [`runtime`, `+runtime`, `$runtime`].map( 7 | (suffix) => `${rootDir}/src/_standalone/${suffix}/index.svelte` 8 | ); 9 | 10 | const embeddableName = { 11 | type: 'input', 12 | name: 'name', 13 | message: 'Name your embeddable:', 14 | validate: (input: string) => { 15 | const runtimePattern = /^[+$](?!runtime)/; 16 | const isRuntime = input.startsWith('$') || input.startsWith('+'); 17 | 18 | if (runtimePattern.test(input)) { 19 | console.error( 20 | `Invalid name. "${input}" cannot start with "$" or "+", unless it's a runtime component.` 21 | ); 22 | return false; 23 | } 24 | 25 | if (!isRuntime && !/^[a-zA-Z0-9_]+$/.test(input)) { 26 | console.error('Invalid component name. Please use only alphanumeric characters.'); 27 | return false; 28 | } 29 | 30 | if (isRuntime && dynamicPaths.some((path) => fs.existsSync(path))) { 31 | console.error(`Invalid name. You can define only one runtime.`); 32 | return false; 33 | } 34 | 35 | const componentPath = `${rootDir}/src/_standalone/${input}/index.svelte`; 36 | if (fs.existsSync(componentPath)) { 37 | console.error(`Invalid name. "${input}" already exists.`); 38 | return false; 39 | } 40 | 41 | return true; 42 | } 43 | }; 44 | 45 | const embeddableStrategy = { 46 | type: 'list', 47 | name: 'type', 48 | message: 'When should your embeddable be triggered?', 49 | choices: [ 50 | { 51 | name: 'On explicit call (can only be mounted once)', 52 | value: 'embed', 53 | short: 'Explicit call' 54 | }, 55 | { 56 | name: 'On explicit call (can be mounted multiple times)', 57 | value: 'embedMultiple', 58 | short: 'Explicit call w/ instances' 59 | }, 60 | { 61 | name: 'Automatically append to the target id when downloaded', 62 | value: 'autoEmbedWithTarget', 63 | short: 'Auto-embed with id selector' 64 | }, 65 | { 66 | name: 'Automatically append to the target class when downloaded', 67 | value: 'autoEmbedMultiple', 68 | short: 'Auto-embed with class selector' 69 | }, 70 | { 71 | name: 'Automatically append to the when downloaded', 72 | value: 'autoEmbedOnBody', 73 | short: 'Auto-embed on body' 74 | } 75 | ] 76 | } as const; 77 | 78 | export type EmbeddableStrategies = (typeof embeddableStrategy.choices)[number]['value']; 79 | 80 | export async function generate() { 81 | try { 82 | const a1 = await select(embeddableStrategy); 83 | const a2 = await input(embeddableName); 84 | 85 | create(a2, a1); 86 | } catch (error) { 87 | if (error instanceof Error && error.name === 'ExitPromptError') { 88 | // noop; silence this error 89 | } else { 90 | throw error; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/cli/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Command } from 'commander'; 4 | 5 | import { generate } from './cli-create.js'; 6 | import { build } from './cli-build.js'; 7 | 8 | const program = new Command(); 9 | 10 | program 11 | .name('standalone') 12 | .description('Transform Svelte components in standalone scripts!') 13 | .showHelpAfterError('(add --help for additional information)') 14 | .version('2.1.1', '-v, --version', 'output the current version'); 15 | 16 | program 17 | .command('create') 18 | .description('Generate code for start your standalone components') 19 | .action(generate); 20 | 21 | program 22 | .command('build') 23 | .description('Build your standalone components') 24 | .option('-p, --production', 'Build for production') 25 | .option('-a, --all', 'Build all Standalone components') 26 | .option( 27 | '--strip-runtime', 28 | 'Exclude "runtime" styles sharing and bundle shared styles directly into the selected components' 29 | ) 30 | .option('-m, --mode ', 'Set the Vite mode') 31 | .action((cmd) => { 32 | if (cmd.stripRuntime) { 33 | console.log('Including shared styles in all components'); 34 | } 35 | build(cmd.production, cmd.all, cmd.stripRuntime, cmd.mode); 36 | }); 37 | 38 | if (process.argv.length < 3) { 39 | program.help(); 40 | } 41 | 42 | program.parse(process.argv); 43 | -------------------------------------------------------------------------------- /lib/cli/methods/build.ts: -------------------------------------------------------------------------------- 1 | import { build, defineConfig, loadConfigFromFile, UserConfig, type PluginOption } from 'vite'; 2 | 3 | import fs from 'fs'; 4 | import tailwindcss from '@tailwindcss/vite'; 5 | 6 | import path from 'path'; 7 | import { visualizer } from 'rollup-plugin-visualizer'; 8 | import resolve from '@rollup/plugin-node-resolve'; 9 | import { svelte } from '@sveltejs/vite-plugin-svelte'; 10 | import cssnanoPlugin from 'cssnano'; 11 | import { purgeCSSPlugin } from '@fullhuman/postcss-purgecss'; 12 | import { libInjectCss } from 'vite-plugin-lib-inject-css'; 13 | import strip from '@rollup/plugin-strip'; 14 | import terser from '@rollup/plugin-terser'; 15 | import { rootDir } from '../../dir.js'; 16 | 17 | import { getPath } from '../utils/getPath.js'; 18 | 19 | import { pathToFileURL } from 'url'; 20 | import { includesTailwind } from '../utils/isDependency.js'; 21 | 22 | const tailwind = includesTailwind(); 23 | 24 | const svelteConfig = path.resolve(rootDir, 'svelte.config.js'); 25 | const svelteAliases = fs.existsSync(svelteConfig) 26 | ? ( 27 | (await import(pathToFileURL(svelteConfig).href)) as { 28 | default: { kit: { alias: Record } }; 29 | } 30 | ).default?.kit?.alias 31 | : undefined; 32 | 33 | const parseAlias = (alias: Record | undefined) => { 34 | if (!alias) return undefined; 35 | 36 | return Object.fromEntries( 37 | Object.entries(alias).map(([key, value]) => { 38 | const newKey = key.replace('/*', ''); // Remove '/*' from the key 39 | const newValue = path.resolve(rootDir, value.replace('/*', '')); // Resolve the path 40 | return [newKey, newValue]; 41 | }) 42 | ); 43 | }; 44 | 45 | const normalizeComponentName = (componentName: string) => componentName.replace(/^[+$]/, ''); 46 | 47 | const isRuntime = (componentName: string) => 48 | componentName === 'runtime' || componentName === '$runtime' || componentName === '+runtime'; 49 | 50 | const getContent = (purgeDir: string, componentName: string, hasRuntime: boolean) => { 51 | const content = [path.resolve(rootDir, `${purgeDir}/**/*.{svelte,ts,js,css}`)]; 52 | 53 | if (!hasRuntime || isRuntime(componentName)) { 54 | content.push(path.resolve(rootDir, './src/shared/**/*.{svelte,ts,js,css}')); 55 | } 56 | 57 | return content; 58 | }; 59 | 60 | const getPostCSSPlugins = (purgeDir: string, componentName: string, hasRuntime: boolean) => { 61 | const content = getContent(purgeDir, componentName, hasRuntime); 62 | 63 | const s = new RegExp(`s-${componentName}`); 64 | 65 | return tailwind 66 | ? undefined 67 | : [ 68 | purgeCSSPlugin({ 69 | content, 70 | extractors: [ 71 | { 72 | extractor: (c) => c.match(/[A-Za-z0-9-_:/\.]+/g) || [], 73 | extensions: ['svelte', 'js', 'ts', 'css'] 74 | } 75 | ], 76 | safelist: { 77 | standard: [s] 78 | } 79 | }), 80 | cssnanoPlugin() 81 | ]; 82 | }; 83 | 84 | const getProd = (prod: boolean) => 85 | prod 86 | ? [ 87 | strip({ 88 | functions: ['console.log', 'console.warn', 'console.error', 'assert.*'] 89 | }), 90 | terser({ 91 | compress: { 92 | drop_console: true, 93 | unused: true, 94 | reduce_vars: true, 95 | pure_funcs: ['console.debug', 'debug'] 96 | }, 97 | output: { 98 | comments: false 99 | } 100 | }) 101 | ] 102 | : []; 103 | 104 | const commonPlugins = (componentName: string, visualizerDir: string) => 105 | [ 106 | svelte({ 107 | configFile: svelteConfig, 108 | compilerOptions: { 109 | cssHash: ({ name }) => `s-${name?.toLowerCase()}` 110 | } 111 | }), 112 | visualizer({ 113 | filename: `${visualizerDir}.status.html`, 114 | title: `${componentName} status` 115 | }), 116 | libInjectCss(), 117 | tailwind && tailwindcss() 118 | ] as PluginOption[]; 119 | 120 | const handleBuild = ( 121 | files: string[], 122 | prod: boolean, 123 | hasRuntime: boolean, 124 | viteConfig?: UserConfig 125 | ) => { 126 | return files.map((file) => { 127 | const rawComponentName = path.dirname(file).split(path.sep).at(-1) || ''; 128 | const componentName = normalizeComponentName(rawComponentName); 129 | const visualizerDir = path 130 | .dirname(file) 131 | .replace('src', 'static') 132 | .replace('_standalone', `dist${path.sep}visualizer`); 133 | const purgeDir = path.dirname(file).replace('embed.ts', ''); 134 | 135 | if (!componentName) { 136 | console.error('Invalid fileName: ', file); 137 | return; 138 | } 139 | 140 | return defineConfig({ 141 | css: { 142 | postcss: { 143 | plugins: getPostCSSPlugins(purgeDir, rawComponentName, hasRuntime) 144 | } 145 | }, 146 | plugins: commonPlugins(componentName, visualizerDir), 147 | build: { 148 | minify: prod, 149 | emptyOutDir: false, 150 | lib: { 151 | formats: ['umd'], 152 | entry: file, 153 | name: componentName, 154 | fileName: componentName 155 | }, 156 | outDir: path.resolve(rootDir, 'static/dist/standalone'), 157 | rollupOptions: { 158 | output: { 159 | chunkFileNames: 'chunks/[name].[hash].js', 160 | assetFileNames: 'assets/[name][extname]', 161 | entryFileNames: `${componentName}.min.js` 162 | }, 163 | plugins: [resolve({ browser: true, dedupe: ['svelte'] }), ...getProd(prod)] 164 | } 165 | }, 166 | resolve: { 167 | alias: { 168 | ...parseAlias(svelteAliases), 169 | ...parseAlias(viteConfig?.resolve?.alias as Record) 170 | } 171 | }, 172 | mode: viteConfig?.mode, 173 | envPrefix: viteConfig?.envPrefix, 174 | define: viteConfig?.define, 175 | envDir: viteConfig?.envDir 176 | }); 177 | }); 178 | }; 179 | 180 | export const buildStandalone = async ( 181 | files: string[], 182 | prod: boolean, 183 | hasRuntime: boolean, 184 | mode?: string 185 | ) => { 186 | const viteConfig = await loadConfigFromFile( 187 | { command: 'build', mode: mode ?? 'production' }, 188 | getPath('vite.config'), 189 | rootDir 190 | ).then((result) => result?.config); 191 | 192 | try { 193 | const configs = handleBuild(files, prod, hasRuntime, viteConfig); 194 | await Promise.all(configs.map((c) => build({ ...c, configFile: false, mode }))); 195 | } catch (handleBuildError) { 196 | console.error('Error during handleBuild:', handleBuildError); 197 | } 198 | }; 199 | -------------------------------------------------------------------------------- /lib/cli/methods/create.ts: -------------------------------------------------------------------------------- 1 | import { EmbeddableStrategies } from '../cli-create.js'; 2 | import { 3 | generateEmbedFiles, 4 | generateRoutesFile, 5 | generateStoryFile, 6 | generateSvelteFile, 7 | generateTypesFile 8 | } from '../../generate-plop.js'; 9 | 10 | import { TYPE_TO_ROUTE, TYPE_TO_STORY, TYPE_TO_TYPESCRIPT } from '../utils/hashmaps.js'; 11 | import { includesStorybook, includesSvelteKit } from '../utils/isDependency.js'; 12 | 13 | export const create = (componentName: string, type: EmbeddableStrategies) => { 14 | const isRuntime = 15 | componentName === 'runtime' || componentName === '$runtime' || componentName === '+runtime'; 16 | 17 | if (includesStorybook() && !isRuntime) { 18 | generateStoryFile(componentName, TYPE_TO_STORY[type]); 19 | } 20 | 21 | if (includesSvelteKit() && !isRuntime) { 22 | generateRoutesFile(componentName, TYPE_TO_ROUTE[type]); 23 | } 24 | 25 | generateEmbedFiles(componentName, type); 26 | 27 | generateTypesFile(componentName, TYPE_TO_TYPESCRIPT[type]); 28 | 29 | generateSvelteFile(componentName); 30 | }; 31 | -------------------------------------------------------------------------------- /lib/cli/methods/index.ts: -------------------------------------------------------------------------------- 1 | import { buildStandalone } from './build.js'; 2 | import { create } from './create.js'; 3 | 4 | export { buildStandalone, create }; 5 | -------------------------------------------------------------------------------- /lib/cli/utils/getPath.ts: -------------------------------------------------------------------------------- 1 | import { rootDir } from '../../dir.js'; 2 | 3 | import path from 'path'; 4 | import { existsSync } from 'fs'; 5 | 6 | export const getPath = (p: string) => 7 | ['.js', '.ts'].map((ext) => path.resolve(rootDir, `${p}${ext}`)).find(existsSync) || undefined; 8 | -------------------------------------------------------------------------------- /lib/cli/utils/hashmaps.ts: -------------------------------------------------------------------------------- 1 | const TYPE_TO_ROUTE = { 2 | autoEmbedOnBody: 'route-auto-start', 3 | autoEmbedWithTarget: 'route-with-target', 4 | autoEmbedMultiple: 'route-with-class', 5 | embed: 'route-callable', 6 | embedMultiple: 'route-multiple' 7 | } as const; 8 | 9 | const TYPE_TO_STORY = { 10 | autoEmbedOnBody: 'story-no-config', 11 | autoEmbedWithTarget: 'story-no-config', 12 | autoEmbedMultiple: 'story-no-config', 13 | embed: 'story-with-config', 14 | embedMultiple: 'story-with-config' 15 | } as const; 16 | 17 | const TYPE_TO_TYPESCRIPT = { 18 | autoEmbedOnBody: 'types-auto', 19 | autoEmbedWithTarget: 'types-auto', 20 | autoEmbedMultiple: 'types-auto', 21 | embed: 'types', 22 | embedMultiple: 'types-multiple' 23 | } as const; 24 | 25 | export { TYPE_TO_ROUTE, TYPE_TO_TYPESCRIPT, TYPE_TO_STORY }; 26 | -------------------------------------------------------------------------------- /lib/cli/utils/isDependency.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { rootDir } from '../../dir.js'; 4 | 5 | function getPack(): Record | null { 6 | const packPath = path.join(rootDir, 'package.json'); 7 | 8 | try { 9 | const p = fs.readFileSync(packPath, 'utf-8'); 10 | return JSON.parse(p); 11 | } catch { 12 | return null; 13 | } 14 | } 15 | 16 | function includes(dependency: string): boolean { 17 | const pack = getPack(); 18 | 19 | if (!pack) return false; 20 | 21 | const { dependencies = {}, devDependencies = {} } = pack; 22 | const allDependencies = { ...dependencies, ...devDependencies }; 23 | return dependency in allDependencies; 24 | } 25 | 26 | export function includesStorybook(): boolean { 27 | return includes('@storybook/react') || includes('@storybook/svelte') || includes('storybook'); 28 | } 29 | 30 | export function includesSvelteKit(): boolean { 31 | return includes('@sveltejs/kit'); 32 | } 33 | 34 | export function includesTypeScript(): boolean { 35 | return includes('typescript'); 36 | } 37 | 38 | export function includesTailwind(): boolean { 39 | return includes('tailwindcss'); 40 | } 41 | -------------------------------------------------------------------------------- /lib/dir.ts: -------------------------------------------------------------------------------- 1 | import { findUpSync } from 'find-up'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | const p = findUpSync('package.json'); 9 | 10 | const root = p ? path.dirname(p) : null; 11 | 12 | export const rootDir = root ?? process.cwd(); 13 | export const distDir = __dirname; 14 | export const moduleDir = __dirname.replace(path.sep + 'dist', ''); 15 | -------------------------------------------------------------------------------- /lib/embed.ts: -------------------------------------------------------------------------------- 1 | import { type Component, type ComponentProps, mount, unmount } from 'svelte'; 2 | 3 | type UnmountOptions = Parameters['1']; 4 | 5 | /** 6 | * Global singleton component controller interface 7 | * @template T - Svelte component class 8 | * @template R - Window property key 9 | */ 10 | export type EmbedWindow = { 11 | [id in R]: { 12 | /** Initializes singleton instance with props */ 13 | start: (props: ComponentProps) => void; 14 | /** Destroys instance and cleans up DOM */ 15 | stop: (options?: UnmountOptions) => void; 16 | }; 17 | }; 18 | 19 | /** 20 | * Multi-instance component factory interface 21 | * @template T - Component constructor type 22 | * @template R - Window namespace key 23 | */ 24 | export type MultipleEmbedWindow = { 25 | [id in R]: { 26 | /** Creates new instance with optional target */ 27 | start: ( 28 | props: ComponentProps, 29 | target?: string 30 | ) => { stop: (options?: UnmountOptions) => void }; 31 | }; 32 | }; 33 | 34 | /** 35 | * Contextual auto-embedding controller interface 36 | * @template R - DOM identifier (ID or class) 37 | */ 38 | export type TargetEmbeddedWindow = { 39 | [id in R]: { 40 | /** Destroys all instances in context */ 41 | stop: (options?: UnmountOptions) => void; 42 | }; 43 | }; 44 | 45 | /** 46 | * Singleton component manager 47 | * @param component - Svelte component to manage 48 | * @param id - Global window property key 49 | * 50 | * @example 51 | * embed(Modal, 'authDialog'); 52 | * window.authDialog.start({ title: 'Login' }); 53 | * window.authDialog.stop(); 54 | */ 55 | export function embed(component: T, id: R): void { 56 | let instance: ReturnType | undefined; 57 | 58 | (window as unknown as EmbedWindow)[id] = { 59 | start: (props) => { 60 | if (!instance) { 61 | instance = mount(component, { 62 | target: document.body, 63 | props 64 | }); 65 | } 66 | }, 67 | stop: (options) => { 68 | if (instance) { 69 | unmount(instance, options); 70 | instance = undefined; 71 | } 72 | } 73 | }; 74 | } 75 | 76 | /** 77 | * Multi-instance component factory 78 | * @param component - Component to instantiate 79 | * @param id - Window namespace key 80 | * 81 | * @example 82 | * embedMultiple(Toast, 'notifications'); 83 | * const toast = window.notifications.start({ message: 'Saved!' }, 'status-area'); 84 | * toast.stop(); 85 | */ 86 | export function embedMultiple(component: T, id: R): void { 87 | (window as unknown as MultipleEmbedWindow)[id] = { 88 | start: (props, target) => { 89 | const instance = mount(component, { 90 | target: document.getElementById(target!) ?? document.body, 91 | props 92 | }); 93 | 94 | return { 95 | stop: () => unmount(instance) 96 | }; 97 | } 98 | }; 99 | } 100 | 101 | /** 102 | * Auto-mount to elements by URL parameter target 103 | * @param component - Component to auto-install 104 | * @param targetId - Target css id to mount your Component - can be provided dynamically by adding a `target` at your widget search params. 105 | * 106 | * @example 107 | * // Load script with ?target=chart-container 108 | * autoEmbedWithTarget(DataChart, 'chart'); 109 | * window['chart-container'].stop(); 110 | */ 111 | export function autoEmbedWithTarget( 112 | component: T, 113 | targetId: R 114 | ): void { 115 | const target = 116 | (new URLSearchParams(window.location.search).get('target') as string as R) ?? targetId; 117 | const instance = mount(component, { 118 | target: document.getElementById(target) ?? document.body 119 | }); 120 | 121 | (window as unknown as TargetEmbeddedWindow)[target] = { 122 | stop: () => unmount(instance) 123 | }; 124 | } 125 | 126 | /** 127 | * Full-page component auto-mounter 128 | * @param component - Component to render 129 | * @param id - Window property key 130 | * 131 | * @example 132 | * autoEmbedOnBody(Loader, 'pageLoader'); 133 | * window.pageLoader.stop(); 134 | */ 135 | export function autoEmbedOnBody(component: T, id: R): void { 136 | const instance = mount(component, { target: document.body }); 137 | 138 | (window as unknown as TargetEmbeddedWindow)[id] = { 139 | stop: () => unmount(instance) 140 | }; 141 | } 142 | 143 | /** 144 | * Batch mount by CSS class selector 145 | * @param component - Component to replicate 146 | * @param targetClass - Target css class to mount your Component - can be provided dynamically by adding a `target` at your widget search params. 147 | * 148 | * @example 149 | * autoEmbedMultiple(Tooltip, 'hint'); 150 | * window.hint.stop(); // Removes all tooltips 151 | */ 152 | export function autoEmbedMultiple( 153 | component: T, 154 | targetClass: R 155 | ): void { 156 | const target = 157 | (new URLSearchParams(window.location.search).get('target') as string as R) ?? targetClass; 158 | 159 | const e = Array.from(document.getElementsByClassName(target)).map((el) => 160 | mount(component, { target: el }) 161 | ); 162 | 163 | (window as unknown as TargetEmbeddedWindow)[target] = { 164 | stop: (options) => e.forEach((instance) => unmount(instance, options)) 165 | }; 166 | } 167 | -------------------------------------------------------------------------------- /lib/generate-plop.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import nodePlop, { NodePlopAPI, PlopGenerator } from 'node-plop'; 3 | import path from 'path'; 4 | 5 | import { distDir, rootDir } from './dir.js'; 6 | import { EmbeddableStrategies } from './cli/cli-create.js'; 7 | import { TYPE_TO_ROUTE, TYPE_TO_STORY, TYPE_TO_TYPESCRIPT } from './cli/utils/hashmaps.js'; 8 | 9 | import { includesTailwind, includesTypeScript } from './cli/utils/isDependency.js'; 10 | 11 | const routesDir = path.resolve(rootDir, 'src', 'routes'); 12 | 13 | const initialContent = `
14 | 15 | `; 33 | 34 | const newLink = (componentName: string) => 35 | `Redirect to ${componentName} script\n`; 36 | 37 | const typescript = includesTypeScript(); 38 | const tailwind = includesTailwind(); 39 | 40 | const plop: NodePlopAPI = await nodePlop( 41 | path.resolve(distDir, typescript ? 'plopfile.js' : 'plopfile-with-js.js') 42 | ); 43 | 44 | function capitalizeFirstLetter(string: string): string { 45 | return string.charAt(0).toUpperCase() + string.slice(1); 46 | } 47 | 48 | async function generateFile>( 49 | generatorName: string, 50 | componentName: string, 51 | additionalArgs: { 52 | embedType?: EmbeddableStrategies; 53 | strategy?: T[EmbeddableStrategies]; 54 | tailwind?: boolean; 55 | } 56 | ): Promise { 57 | const generator: PlopGenerator = plop.getGenerator(generatorName); 58 | 59 | try { 60 | await generator.runActions({ 61 | componentName, 62 | capitalizeName: capitalizeFirstLetter(componentName), 63 | typescript, 64 | ...additionalArgs 65 | }); 66 | console.log( 67 | `${generatorName.charAt(0).toUpperCase() + generatorName.slice(1)} for ${componentName} generated successfully.` 68 | ); 69 | } catch (err) { 70 | console.error(`Error generating ${generatorName} for ${componentName}:`, err); 71 | } 72 | } 73 | 74 | /** 75 | * Generates a Storybook story file for a given component. 76 | */ 77 | export async function generateStoryFile( 78 | componentName: string, 79 | strategy: (typeof TYPE_TO_STORY)[EmbeddableStrategies] 80 | ): Promise { 81 | await generateFile('story', componentName, { strategy }); 82 | } 83 | 84 | /** 85 | * Generates embed files for a given component. 86 | */ 87 | export async function generateEmbedFiles( 88 | componentName: string, 89 | embedType: EmbeddableStrategies 90 | ): Promise { 91 | await generateFile('embed files', componentName, { embedType }); 92 | } 93 | 94 | /** 95 | * Generates type declaration files for a given component. 96 | */ 97 | export async function generateTypesFile( 98 | componentName: string, 99 | strategy?: (typeof TYPE_TO_TYPESCRIPT)[EmbeddableStrategies] 100 | ): Promise { 101 | await generateFile('config files', componentName, { 102 | strategy: typescript ? strategy : 'no-typescript' 103 | }); 104 | } 105 | 106 | /** 107 | * Generates route files for a given component and appends a link to the routes page. 108 | */ 109 | export async function generateRoutesFile( 110 | componentName: string, 111 | strategy: (typeof TYPE_TO_ROUTE)[EmbeddableStrategies] 112 | ): Promise { 113 | const layoutGenerator: PlopGenerator = plop.getGenerator('layout files'); 114 | 115 | await layoutGenerator.runActions({}); 116 | await generateFile('routes files', componentName, { strategy }); 117 | 118 | const pageFilePath = path.join(routesDir, '+page.svelte'); 119 | 120 | await fs 121 | .readFile(pageFilePath, 'utf8') 122 | .then(async (data) => { 123 | await fs 124 | .writeFile(pageFilePath, data.replace(/(<\/div>)/g, `${newLink(componentName)}$1`), 'utf8') 125 | .then(() => { 126 | console.log(`Link added for ${componentName} successfully.`); 127 | }) 128 | .catch((err) => { 129 | console.error(`Error updating ${pageFilePath}:`, err); 130 | }); 131 | }) 132 | .catch(async (err) => { 133 | if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') { 134 | await fs 135 | .writeFile( 136 | pageFilePath, 137 | initialContent.replace(/(<\/div>)/g, `${newLink(componentName)}$1`), 138 | 'utf8' 139 | ) 140 | .then(() => { 141 | console.log(`Created ${pageFilePath} with initial content.`); 142 | }); 143 | return; 144 | } 145 | 146 | throw err; 147 | }); 148 | } 149 | 150 | /** 151 | * Generates a Svelte file for a given component. 152 | */ 153 | export async function generateSvelteFile(componentName: string): Promise { 154 | await generateFile('svelte files', componentName, { tailwind }); 155 | } 156 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | embed, 3 | autoEmbedOnBody, 4 | autoEmbedWithTarget, 5 | embedMultiple, 6 | autoEmbedMultiple 7 | } from './embed.js'; 8 | import type { EmbedWindow, MultipleEmbedWindow, TargetEmbeddedWindow } from './embed.js'; 9 | 10 | export type ComponentList = keyof Omit; 11 | 12 | export { 13 | embed, 14 | autoEmbedOnBody, 15 | autoEmbedWithTarget, 16 | embedMultiple, 17 | autoEmbedMultiple, 18 | type EmbedWindow, 19 | type MultipleEmbedWindow, 20 | type TargetEmbeddedWindow 21 | }; 22 | -------------------------------------------------------------------------------- /lib/plop-templates/config/no-typescript.hbs: -------------------------------------------------------------------------------- 1 | export const defaultConfig = {}; -------------------------------------------------------------------------------- /lib/plop-templates/config/types-auto.hbs: -------------------------------------------------------------------------------- 1 | import {{capitalizeName}} from './index.svelte'; 2 | 3 | import type { TargetEmbeddedWindow } from 'svelte-standalone'; 4 | 5 | declare global { 6 | interface Window extends TargetEmbeddedWindow {} 7 | } -------------------------------------------------------------------------------- /lib/plop-templates/config/types-multiple.hbs: -------------------------------------------------------------------------------- 1 | import type { ComponentProps } from 'svelte'; 2 | import type { MultipleEmbedWindow } from 'svelte-standalone'; 3 | 4 | import {{capitalizeName}} from './index.svelte'; 5 | 6 | export type {{capitalizeName}}Props = ComponentProps<{{capitalizeName}}>; 7 | 8 | export const defaultConfig: {{capitalizeName}}Props = {}; 9 | 10 | declare global { 11 | interface Window extends MultipleEmbedWindow {} 12 | } -------------------------------------------------------------------------------- /lib/plop-templates/config/types.hbs: -------------------------------------------------------------------------------- 1 | import type { ComponentProps } from 'svelte'; 2 | import type { EmbedWindow } from 'svelte-standalone'; 3 | 4 | import {{capitalizeName}} from './index.svelte'; 5 | 6 | export type {{capitalizeName}}Props = ComponentProps<{{capitalizeName}}>; 7 | 8 | export const defaultConfig: {{capitalizeName}}Props = {}; 9 | 10 | declare global { 11 | interface Window extends EmbedWindow {} 12 | } -------------------------------------------------------------------------------- /lib/plop-templates/embed.hbs: -------------------------------------------------------------------------------- 1 | import { {{embedType}} } from 'svelte-standalone' 2 | 3 | import {{capitalizeName}} from './index.svelte' 4 | 5 | {{embedType}}({{capitalizeName}}, '{{componentName}}') -------------------------------------------------------------------------------- /lib/plop-templates/route/layout.hbs: -------------------------------------------------------------------------------- 1 | 17 | 18 | {@render children()} 19 | 20 | 21 | {#if runtimeExists} 22 | 23 | {/if} 24 | 25 | 26 | 31 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /lib/plop-templates/route/route-auto-start.hbs: -------------------------------------------------------------------------------- 1 | 2 | import { onMount } from 'svelte'; 3 | 4 | onMount(() => { 5 | const script = document.createElement('script'); 6 | script.src = '/src/_standalone/{{componentName}}/embed{{#if typescript}}.ts{{else}}.js{{/if}}'; 7 | script.async = true; 8 | script.type = 'module'; 9 | 10 | document.head.appendChild(script); 11 | 12 | return () => { 13 | document.head.removeChild(script); 14 | window.{{componentName}}.stop(); 15 | }; 16 | }); 17 | -------------------------------------------------------------------------------- /lib/plop-templates/route/route-callable.hbs: -------------------------------------------------------------------------------- 1 | 2 | import { onDestroy } from 'svelte'; 3 | import { browser } from '$app/environment'; 4 | 5 | {{#if typescript}} 6 | import { defaultConfig } from '../../_standalone/{{componentName}}/config'; 7 | {{else}} 8 | import { defaultConfig } from '../../_standalone/{{componentName}}/config.js'; 9 | {{/if}} 10 | 11 | const initScript = () => { 12 | if (browser) { 13 | if (window) { 14 | window.{{componentName}}.start(defaultConfig); 15 | } 16 | } 17 | }; 18 | 19 | const stopScript = () => { 20 | if (browser) { 21 | if (window) { 22 | window.{{componentName}}.stop(); 23 | } 24 | } 25 | }; 26 | 27 | onDestroy(() => { 28 | stopScript(); 29 | }) 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 40 | 43 |

defaultConfig: {JSON.stringify(defaultConfig)}

44 |
45 | -------------------------------------------------------------------------------- /lib/plop-templates/route/route-multiple.hbs: -------------------------------------------------------------------------------- 1 | 2 | import { onDestroy } from 'svelte'; 3 | 4 | {{#if typescript}} 5 | import { defaultConfig } from '../../_standalone/{{componentName}}/config'; 6 | {{else}} 7 | import { defaultConfig } from '../../_standalone/{{componentName}}/config.js'; 8 | {{/if}} 9 | 10 | let validInstances{{#if typescript}}: Map void>{{/if}} = new Map(); 11 | 12 | const generateInstance = () => { 13 | const i = window.{{componentName}}.start(defaultConfig); 14 | 15 | validInstances = validInstances.set(Math.random() * 100, () => i.stop()); 16 | }; 17 | 18 | const removeInstance = (instance{{#if typescript}}: () => void, i: number{{/if}}) => { 19 | instance(); 20 | 21 | validInstances.delete(i); 22 | validInstances = validInstances; 23 | }; 24 | 25 | onDestroy(() => { 26 | validInstances.forEach((stop) => { 27 | stop(); 28 | }) 29 | validInstances.clear(); 30 | }) 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | 41 |

defaultConfig: {JSON.stringify(defaultConfig)}

42 | {#each validInstances as [i, instance]} 43 |
44 | 47 |
48 | {/each} 49 |
50 | -------------------------------------------------------------------------------- /lib/plop-templates/route/route-with-class.hbs: -------------------------------------------------------------------------------- 1 | 2 | import { onMount } from 'svelte'; 3 | 4 | onMount(() => { 5 | const script = document.createElement('script'); 6 | script.src = '/src/_standalone/{{componentName}}/embed{{#if typescript}}.ts{{else}}.js{{/if}}'; 7 | script.async = true; 8 | script.type = 'module'; 9 | 10 | document.head.appendChild(script); 11 | 12 | return () => { 13 | document.head.removeChild(script); 14 | window.{{componentName}}.stop(); 15 | }; 16 | }); 17 | 18 | 19 |
20 |
-------------------------------------------------------------------------------- /lib/plop-templates/route/route-with-target.hbs: -------------------------------------------------------------------------------- 1 | 2 | import { onMount } from 'svelte'; 3 | 4 | onMount(() => { 5 | const script = document.createElement('script'); 6 | script.src = '/src/_standalone/{{componentName}}/embed{{#if typescript}}.ts{{else}}.js{{/if}}'; 7 | script.async = true; 8 | script.type = 'module'; 9 | 10 | document.head.appendChild(script); 11 | 12 | return () => { 13 | document.head.removeChild(script); 14 | window.{{componentName}}.stop(); 15 | }; 16 | }); 17 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /lib/plop-templates/story/story-no-config.hbs: -------------------------------------------------------------------------------- 1 | {{#if typescript}} 2 | import type { Meta, StoryObj } from '@storybook/svelte'; 3 | {{/if}} 4 | import {{capitalizeName}} from '../_standalone/{{componentName}}/index.svelte'; 5 | 6 | export default { 7 | title: 'Standalone/{{capitalizeName}}', 8 | component: {{capitalizeName}} 9 | }{{#if typescript}} satisfies Meta<{{capitalizeName}}>{{/if}}; 10 | 11 | export const {{capitalizeName}}Story{{#if typescript}}: StoryObj<{{capitalizeName}}>{{/if}} = { 12 | args: {} // don't have args since auto-embeddable 13 | }; 14 | -------------------------------------------------------------------------------- /lib/plop-templates/story/story-with-config.hbs: -------------------------------------------------------------------------------- 1 | {{#if typescript}} 2 | import type { Meta, StoryObj } from '@storybook/svelte'; 3 | import { defaultConfig } from '../_standalone/{{componentName}}/config'; 4 | {{else}} 5 | import { defaultConfig } from '../_standalone/{{componentName}}/config.js'; 6 | {{/if}} 7 | import {{capitalizeName}} from '../_standalone/{{componentName}}/index.svelte'; 8 | 9 | export default { 10 | title: 'Standalone/{{capitalizeName}}', 11 | component: {{capitalizeName}} 12 | }{{#if typescript}} satisfies Meta<{{capitalizeName}}>{{/if}}; 13 | 14 | export const {{capitalizeName}}Story{{#if typescript}}: StoryObj<{{capitalizeName}}>{{/if}} = { 15 | args: defaultConfig 16 | }; 17 | -------------------------------------------------------------------------------- /lib/plop-templates/structure/component.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#if tailwind}} 3 | // you MUST explicitly import tailwind 4 | // refer to: https://svelte-standalone.vercel.app/tailwind 5 | {{/if}} 6 | const greeting = () => { 7 | alert('thank you for using svelte-standalone!') 8 | } 9 | 10 | 11 | {{#if tailwind}} 12 | 18 | {{else}} 19 | 25 | 26 | 44 | {{/if}} -------------------------------------------------------------------------------- /lib/plopfile-with-js.ts: -------------------------------------------------------------------------------- 1 | import { moduleDir, rootDir } from './dir.js'; 2 | import path from 'path'; 3 | import { NodePlopAPI } from 'node-plop'; 4 | 5 | export default function (plop: NodePlopAPI) { 6 | plop.setGenerator('story', { 7 | description: 'Generate a Storybook story', 8 | actions: [ 9 | { 10 | type: 'add', 11 | path: path.resolve(rootDir, 'src/stories/{{componentName}}.stories.js'), 12 | templateFile: path.resolve(moduleDir, 'lib/plop-templates/story/{{strategy}}.hbs') 13 | } 14 | ] 15 | }); 16 | 17 | plop.setGenerator('embed files', { 18 | description: 'Generate an embed.js default file', 19 | actions: [ 20 | { 21 | type: 'add', 22 | path: path.resolve(rootDir, 'src/_standalone/{{componentName}}/embed.js'), 23 | templateFile: path.resolve(moduleDir, 'lib/plop-templates/embed/{{strategy}}.hbs') 24 | } 25 | ] 26 | }); 27 | 28 | plop.setGenerator('config files', { 29 | description: 'Generate a config.js default file', 30 | actions: [ 31 | { 32 | type: 'add', 33 | path: path.resolve(rootDir, 'src/_standalone/{{componentName}}/config.js'), 34 | templateFile: path.resolve(moduleDir, 'lib/plop-templates/config/{{strategy}}.hbs') 35 | } 36 | ] 37 | }); 38 | 39 | plop.setGenerator('routes files', { 40 | description: 'Generate a default route handler that imports the script', 41 | actions: [ 42 | { 43 | type: 'add', 44 | path: path.resolve(rootDir, 'src/routes/{{componentName}}/+page.svelte'), 45 | templateFile: path.resolve(moduleDir, 'lib/plop-templates/route/{{strategy}}.hbs') 46 | } 47 | ] 48 | }); 49 | 50 | plop.setGenerator('layout files', { 51 | description: 'Generate a layout handler once', 52 | actions: [ 53 | { 54 | type: 'add', 55 | path: path.resolve(rootDir, 'src/routes/+layout.svelte'), 56 | templateFile: path.resolve(moduleDir, 'lib/plop-templates/route/layout.hbs') 57 | } 58 | ] 59 | }); 60 | 61 | plop.setGenerator('svelte files', { 62 | description: 'Generate a default Svelte component to be embedded', 63 | actions: [ 64 | { 65 | type: 'add', 66 | path: path.resolve(rootDir, 'src/_standalone/{{componentName}}/index.svelte'), 67 | templateFile: path.resolve(moduleDir, 'lib/plop-templates/structure/component.hbs') 68 | } 69 | ] 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /lib/plopfile.ts: -------------------------------------------------------------------------------- 1 | import { moduleDir, rootDir } from './dir.js'; 2 | import path from 'path'; 3 | import { NodePlopAPI } from 'node-plop'; 4 | 5 | export default function (plop: NodePlopAPI) { 6 | plop.setGenerator('story', { 7 | description: 'Generate a Storybook story', 8 | actions: [ 9 | { 10 | type: 'add', 11 | path: path.join(rootDir, 'src/stories/{{componentName}}.stories.ts'), 12 | templateFile: path.join(moduleDir, 'lib/plop-templates/story/{{strategy}}.hbs') 13 | } 14 | ] 15 | }); 16 | 17 | plop.setGenerator('embed files', { 18 | description: 'Generate an embed.ts default file', 19 | actions: [ 20 | { 21 | type: 'add', 22 | path: path.join(rootDir, 'src/_standalone/{{componentName}}/embed.ts'), 23 | templateFile: path.join(moduleDir, 'lib/plop-templates/embed.hbs') 24 | } 25 | ] 26 | }); 27 | 28 | plop.setGenerator('config files', { 29 | description: 'Generate a config.ts default file', 30 | actions: [ 31 | { 32 | type: 'add', 33 | path: path.join(rootDir, 'src/_standalone/{{componentName}}/config.ts'), 34 | templateFile: path.join(moduleDir, 'lib/plop-templates/config/{{strategy}}.hbs') 35 | } 36 | ] 37 | }); 38 | 39 | plop.setGenerator('routes files', { 40 | description: 'Generate a default route handler that imports the script', 41 | actions: [ 42 | { 43 | type: 'add', 44 | path: path.join(rootDir, 'src/routes/{{componentName}}/+page.svelte'), 45 | templateFile: path.join(moduleDir, 'lib/plop-templates/route/{{strategy}}.hbs') 46 | } 47 | ] 48 | }); 49 | 50 | plop.setGenerator('layout files', { 51 | description: 'Generate a layout handler once', 52 | actions: [ 53 | { 54 | type: 'add', 55 | path: path.join(rootDir, 'src/routes/+layout.svelte'), 56 | templateFile: path.join(moduleDir, 'lib/plop-templates/route/layout.hbs') 57 | } 58 | ] 59 | }); 60 | 61 | plop.setGenerator('svelte files', { 62 | description: 'Generate a default Svelte component to be embedded', 63 | actions: [ 64 | { 65 | type: 'add', 66 | path: path.join(rootDir, 'src/_standalone/{{componentName}}/index.svelte'), 67 | templateFile: path.join(moduleDir, 'lib/plop-templates/structure/component.hbs') 68 | } 69 | ] 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-standalone", 3 | "version": "2.1.1", 4 | "description": "Transform Svelte components in standalone scripts!", 5 | "bin": { 6 | "standalone": "dist/cli/cli.js" 7 | }, 8 | "main": "./dist/index.js", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.js" 12 | } 13 | }, 14 | "files": [ 15 | "dist", 16 | "lib/plop-templates" 17 | ], 18 | "devDependencies": { 19 | "@eslint/js": "^9.17.0", 20 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 21 | "@types/glob": "^8.1.0", 22 | "eslint-config-prettier": "^9.1.0", 23 | "globals": "^15.14.0", 24 | "prettier": "^3.1.1", 25 | "svelte": "^5.20.1", 26 | "typescript": "^5.5.4", 27 | "typescript-eslint": "^8.0.1", 28 | "vitepress": "^1.5.0", 29 | "vite": "^6.0.0" 30 | }, 31 | "dependencies": { 32 | "@fullhuman/postcss-purgecss": "^7.0.2", 33 | "@inquirer/prompts": "^7.2.1", 34 | "@rollup/plugin-node-resolve": "^15.2.3", 35 | "@rollup/plugin-strip": "^3.0.4", 36 | "@rollup/plugin-terser": "^0.4.4", 37 | "@tailwindcss/vite": "^4.0.6", 38 | "commander": "^12.1.0", 39 | "cssnano": "^7.0.6", 40 | "find-up": "^7.0.0", 41 | "glob": "^11.0.1", 42 | "node-plop": "^0.32.0", 43 | "rollup-plugin-visualizer": "^5.12.0", 44 | "vite-plugin-lib-inject-css": "^2.1.1" 45 | }, 46 | "scripts": { 47 | "build": "tsc", 48 | "format": "prettier --write .", 49 | "lint": "prettier --write . && eslint .", 50 | "docs:dev": "vitepress dev docs", 51 | "docs:build": "vitepress build docs", 52 | "docs:preview": "vitepress preview docs" 53 | }, 54 | "repository": { 55 | "type": "git", 56 | "url": "git+https://github.com/brenoliradev/svelte-standalone.git" 57 | }, 58 | "keywords": [ 59 | "svelte", 60 | "scripts", 61 | "standalone", 62 | "tailwind", 63 | "typescript" 64 | ], 65 | "author": "brenoliradev", 66 | "license": "MIT", 67 | "bugs": { 68 | "url": "https://github.com/brenoliradev/svelte-standalone/issues" 69 | }, 70 | "homepage": "https://svelte-standalone.vercel.app/", 71 | "type": "module" 72 | } 73 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ESNext", 5 | "moduleResolution": "bundler", 6 | "esModuleInterop": true, 7 | "declaration": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "resolveJsonModule": true, 11 | "outDir": "dist", 12 | "rootDir": "lib" 13 | }, 14 | "include": ["lib/**/*"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | --------------------------------------------------------------------------------