@csedl/hotwire-svelte-helpers 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,12 +12,21 @@ Dropdown with stimulus, based on floating-UI.
12
12
  ## Setup
13
13
 
14
14
  ```javascript
15
- import {HotwireSvelteHelpers} from "@csedl/hotwire-svelte-helpers/init"
15
+ import { HotwireSvelteHelpers } from "@csedl/hotwire-svelte-helpers/init"
16
16
  HotwireSvelteHelpers.debug = true
17
17
  HotwireSvelteHelpers.initializeOverlays()
18
18
  ```
19
19
 
20
- You MUST add the file `<vite-sourcee-code-dir>/config/hotwire-svelte-helpers.js`, with content like:
20
+ This package has a Server-Side Rendering (SSR) safe configuration, which means:
21
+
22
+ You must add the file `<vite-sourcee-code-dir>/config/hotwire-svelte-helpers.js`, at least with:
23
+
24
+ ```javascript
25
+ export default {
26
+ }
27
+ ```
28
+
29
+ The package-internal default configs, which can be overwritten hereby, are:
21
30
 
22
31
  ```javascript
23
32
  export default {
@@ -31,23 +40,24 @@ export default {
31
40
  persistTooltipOnClick: false,
32
41
  // clicking on the tooltip-label causes the tooltip-panel to persist open
33
42
  // this may mostly be helpful for development, so you may make it environment-dependent
34
- closeButtonSvg: '',
35
- })
43
+ closeButtonSvg: null,
44
+ // raw import of svg-icon,
45
+ }
36
46
  ```
37
47
 
38
- But, at least with content:
48
+ You can add as many keys as you like and fetch them from anywhere by:
39
49
 
40
50
  ```javascript
41
- export default {
42
- }
51
+ import { hotwireSvelteConfig } from "@csedl/hotwire-svelte-helpers";
52
+ hotwireSvelteConfig('closeButtonSelector')
43
53
  ```
44
54
 
45
- The reason for this is that we need to have ssr safe configs.
46
-
47
- ## Example
55
+ ## Example App
48
56
 
49
57
  There is a [online example app](https://hotwire-svelte-helpers.sedlmair.ch/)
50
58
 
59
+ ## Dropdown Example
60
+
51
61
  ```html
52
62
  <div data-controller="csedl-dropdown" data-panel-id="dropdown-panel-3h5k7l4">
53
63
  Button
package/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
 
2
2
  import { cleanMount, unmountAllDetached } from './src/svelte/cleanMount.js';
3
- import { initializeDropdown, openDropdownPanel, getOrSetPanelId, debugLog, hotwireSvelteConfig } from './src/lib/utils.js'
3
+ import { initializeDropdown, openDropdownPanel, getOrSetPanelId, debugLog, hotwireSvelteConfig, getCsrfToken } from './src/lib/utils.js'
4
4
 
5
5
  export {
6
6
  cleanMount, unmountAllDetached, hotwireSvelteConfig,
7
- initializeDropdown, openDropdownPanel, getOrSetPanelId, debugLog
7
+ initializeDropdown, openDropdownPanel, getOrSetPanelId,
8
+ debugLog, getCsrfToken
8
9
  }
package/package.json CHANGED
@@ -1,21 +1,43 @@
1
1
  {
2
2
  "name": "@csedl/hotwire-svelte-helpers",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Hotwire + Svelte helpers for Rails: Stimulus floating dropdowns/toolips + Svelte global panels/modals + RTurbo-friendly utilities. Build together with the rubygem svelte-on-rails and its npm-package.",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1"
8
8
  },
9
9
  "exports": {
10
- ".": "./index.js",
11
- "./init": "./src/lib/initializers.js",
10
+ ".": {
11
+ "import": "./index.js",
12
+ "default": "./index.js"
13
+ },
14
+ "./init": {
15
+ "import": "./src/lib/initializers.js",
16
+ "default": "./src/lib/initializers.js"
17
+ },
18
+ "./DropdownButton.svelte": {
19
+ "svelte": "./src/svelte/templates/DropdownButton.svelte",
20
+ "default": "./src/svelte/templates/DropdownButton.svelte"
21
+ },
12
22
  "./DropdownPanel.svelte": {
13
23
  "svelte": "./src/svelte/templates/DropdownPanel.svelte",
14
24
  "default": "./src/svelte/templates/DropdownPanel.svelte"
15
25
  },
26
+ "./ModalButton.svelte": {
27
+ "svelte": "./src/svelte/templates/ModalButton.svelte",
28
+ "default": "./src/svelte/templates/ModalButton.svelte"
29
+ },
16
30
  "./Modal.svelte": {
17
31
  "svelte": "./src/svelte/templates/Modal.svelte",
18
32
  "default": "./src/svelte/templates/Modal.svelte"
33
+ },
34
+ "./FormInput.svelte": {
35
+ "svelte": "./src/svelte/templates/FormInput.svelte",
36
+ "default": "./src/svelte/templates/FormInput.svelte"
37
+ },
38
+ "./FormSubmitButton.svelte": {
39
+ "svelte": "./src/svelte/templates/FormSubmitButton.svelte",
40
+ "default": "./src/svelte/templates/FormSubmitButton.svelte"
19
41
  }
20
42
  },
21
43
  "repository": {
package/src/lib/utils.js CHANGED
@@ -278,17 +278,17 @@ export function hotwireSvelteConfig(key) {
278
278
  closeButtonSvg: '',
279
279
  }
280
280
 
281
- const validations = {
282
- closeButtonSelector: 'string',
283
- dropdownContentSelector: 'string',
284
- tooltipContentSelector: 'string',
285
- addArrow: 'boolean',
286
- persistTooltipOnClick: 'boolean',
287
- closeButtonSvg: 'string',
281
+ const configs = {...defaultConfig, ...customConfigs}
282
+
283
+ if (!Object.keys(configs).includes(key)) {
284
+ throw new Error(`[@csedl/hotwire-svelte-helpers] unknown config key: ${key}, actual keys are:\n • ${Object.keys(configs).join("\n • ")}`)
288
285
  }
289
- validateOptions(customConfigs, validations)
290
286
 
291
- return customConfigs[key] || defaultConfig[key] || null
287
+ return configs[key] || null
288
+ }
289
+
290
+ export function getCsrfToken() {
291
+ return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
292
292
  }
293
293
 
294
294
  function generateRandomHex(n) {
@@ -0,0 +1,48 @@
1
+ <script>
2
+ import {hotwireSvelteConfig} from "../../lib/utils";
3
+ import DropdownPanel from "./DropdownPanel.svelte";
4
+ import { unmount } from 'svelte'
5
+ import { cleanMount } from "../cleanMount.js";
6
+
7
+ let { panelContent, panelTitle, panelClass, label, svgIcon } = $props();
8
+
9
+ let buttonElement, panelInstance;
10
+
11
+
12
+ export function closeDropdown() {
13
+ if (panelInstance) {
14
+ unmount(panelInstance);
15
+ }
16
+ buttonElement.classList.remove('has-open-panel');
17
+ };
18
+
19
+ function openEditPanel() {
20
+
21
+ if (buttonElement.classList.contains('has-open-panel')) return;
22
+
23
+ panelInstance = cleanMount(
24
+ DropdownPanel, {
25
+ target: document.getElementById('dropdown-panels-box'),
26
+ props: {
27
+ title: panelTitle,
28
+ closeFunction: closeDropdown,
29
+ buttonElement: buttonElement,
30
+ panelClass: panelClass,
31
+ content: panelContent,
32
+ }
33
+ }
34
+ );
35
+ }
36
+ </script>
37
+
38
+ <span class="dropdown-button" onclick={ openEditPanel } bind:this={ buttonElement } role="button" tabindex="0" onkeyup={()=>{}}>
39
+ {#if svgIcon}
40
+ {@html svgIcon}
41
+ {/if}
42
+ {#if label}
43
+ {label}
44
+ {/if}
45
+ {#if !svgIcon && !label}
46
+ ??
47
+ {/if}
48
+ </span>
@@ -1,5 +1,5 @@
1
1
  <script>
2
- import { openDropdownPanel, hotwireSvelteConfig } from "@csedl/hotwire-svelte-helpers";
2
+ import {openDropdownPanel, hotwireSvelteConfig} from "@csedl/hotwire-svelte-helpers";
3
3
  import {onMount} from "svelte";
4
4
 
5
5
  let {
@@ -23,9 +23,15 @@
23
23
  }
24
24
 
25
25
  function closeSvg() {
26
- const path = hotwireSvelteConfig('closeButtonSvg');
27
- const svg = (path ? path : '?')
28
- return svg;
26
+ const value = hotwireSvelteConfig('closeButtonSvg');
27
+
28
+ if (!value) return '?';
29
+
30
+ if (typeof value === 'function') {
31
+ return value();
32
+ }
33
+
34
+ return String(value);
29
35
  }
30
36
 
31
37
  </script>
@@ -34,15 +40,22 @@
34
40
 
35
41
 
36
42
  <div class="header">
37
- <span class="title">
38
- {title || 'missing title'}
39
- </span>
40
- <button class="close-btn"
41
- onclick={closeFunction}>{@html closeSvg()}</button>
43
+ <span class="title">
44
+ {title || 'missing title'}
45
+ </span>
46
+ <span class="buttons">
47
+ <button class={ hotwireSvelteConfig('closeButtonSelector').replace('.', '') || 'close-btn' }
48
+ onclick={closeFunction}>{@html closeSvg()}
49
+ </button>
50
+ </span>
42
51
  </div>
43
52
 
44
53
  <div class="content">
45
- {@render content()}
54
+ {#if content}
55
+ {@render content()}
56
+ {:else}
57
+ <div style="color: red;">Missing required `content` snippet for dropdown panel.</div>
58
+ {/if}
46
59
  </div>
47
60
 
48
61
  </div>
@@ -0,0 +1,89 @@
1
+ <script>
2
+
3
+ let {
4
+ context,
5
+ columnName,
6
+ errors,
7
+ onChangeFunction,
8
+ type,
9
+ autocomplete = 'on'
10
+ } = $props();
11
+
12
+ let model = $derived(context._schema.self.modelKey)
13
+ let columnSchema = $derived(context._schema[columnName])
14
+ let value = $derived(context[columnName])
15
+ let inputType = $derived.by(() => {
16
+ const t = {
17
+ string: 'text',
18
+ text: 'textarea',
19
+ integer: 'number',
20
+ decimal: 'number',
21
+ boolean: 'checkbox',
22
+ }[columnSchema.dbType]
23
+ return (type ? type : t);
24
+ })
25
+
26
+ let fieldErrors = $derived.by(() => {
27
+ const modelErrors = errors?.[model] ?? {};
28
+ const fieldErrors = modelErrors?.[columnSchema.key];
29
+ if (!fieldErrors) return [];
30
+
31
+ console.log('fieldErrors', fieldErrors)
32
+
33
+ return fieldErrors;
34
+ })
35
+
36
+ let fieldErrorsClass = $derived.by(() => {
37
+ return fieldErrors.length > 0 ? 'error' : '';
38
+ })
39
+
40
+ function label() {
41
+ let req = (columnSchema.required) ? '* ' : ''
42
+ return req + columnSchema.label
43
+ }
44
+
45
+ </script>
46
+
47
+
48
+ <div class="grid-x input-wrapper string {model}_{columnName} {inputType} {fieldErrorsClass}">
49
+ <div class="small-12 cell">
50
+ <label class="strong optional" for="{model}_{columnName}">{label()}</label>
51
+ </div>
52
+ <div class="small-12 cell">
53
+
54
+ {#if columnSchema.enum}
55
+ <select
56
+ id="{model}_{columnName}"
57
+ name="{model}[{columnSchema.key}]"
58
+ value="{value}"
59
+ onchange="{onChangeFunction}"
60
+ class="input-field">
61
+ {#each Object.entries(columnSchema.enum) as [key, label]}
62
+ <option value="{key}">{label}</option>
63
+ {/each}
64
+ </select>
65
+ {:else if inputType === 'textarea'}
66
+ <textarea
67
+ id="{model}_{columnName}"
68
+ name="{model}[{columnSchema.key}]"
69
+ value="{value}"
70
+ class="input-field"/>
71
+ {:else}
72
+ <input
73
+ id="{model}_{columnName}"
74
+ type="{inputType}"
75
+ name="{model}[{columnSchema.key}]"
76
+ value="{value}"
77
+ class="input-field"
78
+ autocomplete={autocomplete}
79
+ >
80
+ {/if}
81
+
82
+
83
+ </div>
84
+
85
+ {#each fieldErrors as error}
86
+ <small class="error">{error}</small>
87
+ {/each}
88
+
89
+ </div>
@@ -0,0 +1,6 @@
1
+ <script>
2
+ let { context } = $props();
3
+ let label = $derived(context.id ? context._translations.helpers.submit.update : context._translations.helpers.submit.create);
4
+ </script>
5
+
6
+ <button class="button" type="submit">{label}</button>
@@ -5,12 +5,8 @@
5
5
  let {
6
6
  title,
7
7
  panelClass,
8
- SvelteComponent,
9
- svelteComponentProps,
10
8
  closeFunction,
11
- closeButtonSvg,
12
9
  content,
13
- text,
14
10
  actionButtonLabel,
15
11
  actionButtonFunction,
16
12
  } = $props()
@@ -45,25 +41,23 @@
45
41
  <span class="title">
46
42
  {title || 'missing title'}
47
43
  </span>
48
- <button class="close-btn"
49
- onclick={closeFunction}>{@html closeSvg()}</button>
44
+ <span class="buttons">
45
+ <button class={ hotwireSvelteConfig('closeButtonSelector').replace('.', '') || 'close-btn' }
46
+ onclick={closeFunction}>{@html closeSvg()}
47
+ </button>
48
+ </span>
50
49
  </div>
51
50
 
52
51
  <div class="content">
53
52
 
54
- {#if content}
55
- {@render content()}
56
- {/if}
53
+ <div class="content">
54
+ {#if content}
55
+ {@render content()}
56
+ {:else}
57
+ <div style="color: red;">Missing required `content` snippet for dropdown panel.</div>
58
+ {/if}
59
+ </div>
57
60
 
58
- {#if SvelteComponent}
59
- <SvelteComponent {...svelteComponentProps}/>
60
- {/if}
61
-
62
- {#if text}
63
- {#each text as paragraph}
64
- <p>{paragraph}</p>
65
- {/each}
66
- {/if}
67
61
  </div>
68
62
  <div class="footer">
69
63
  {#if actionButtonLabel}
@@ -0,0 +1,38 @@
1
+ <script>
2
+ import Modal from './Modal.svelte';
3
+ import {unmount} from 'svelte'
4
+ import {cleanMount} from "../cleanMount.js";
5
+
6
+ let {panelTitle, panelContent, label, svgIcon} = $props()
7
+ let modalInstance;
8
+
9
+ export function closeModal() {
10
+ if (modalInstance) {
11
+ unmount(modalInstance);
12
+ }
13
+ };
14
+
15
+ function openModal() {
16
+
17
+ modalInstance = cleanMount(Modal, {
18
+ target: document.getElementById('dropdown-panels-box'),
19
+ props: {
20
+ title: panelTitle,
21
+ content: panelContent,
22
+ closeFunction: closeModal
23
+ }
24
+ });
25
+ }
26
+ </script>
27
+
28
+ <span role="button" tabindex="0" onkeyup={()=>{}} onclick={openModal} class="modal-button">
29
+ {#if svgIcon}
30
+ {@html svgIcon}
31
+ {/if}
32
+ {#if label}
33
+ {label}
34
+ {/if}
35
+ {#if !svgIcon && !label}
36
+ ??
37
+ {/if}
38
+ </span>