hx-tomselect 1.0.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.
@@ -0,0 +1,19 @@
1
+ name: Deploy to GitHub Pages
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ deploy:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Checkout Repository
13
+ uses: actions/checkout@v2
14
+
15
+ - name: Deploy to GitHub Pages
16
+ uses: peaceiris/actions-gh-pages@v3
17
+ with:
18
+ github_token: ${{ secrets.GITHUB_TOKEN }}
19
+ publish_dir: ./
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # hx-tomselect
2
+
3
+ Provides a hx-ext="tomselect" htmx extention tag
4
+
5
+ [create a github pr/issue if you see any bugs/feature opportunities]
6
+
7
+
8
+ <a href="https://kiwikid.github.io/hx-tomselect/">Full Examples List</a>
9
+
10
+ ### Install
11
+ ```html
12
+ <script src="https://kiwikid.github.io/hx-tomselect/hx-tom-select.js"></script>
13
+ ```
14
+
15
+
16
+ (Include htmx and tom-select before the extention)
17
+ ```html
18
+ <script src="https://unpkg.com/htmx.org"></script>
19
+ <link href="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.css" rel="stylesheet"/>
20
+ <script src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"></script>
21
+ <script src="https://kiwikid.github.io/hx-tomselect/hx-tom-select.js"></script>
22
+ ```
23
+
24
+
25
+
26
+ ### Example Usage:
27
+ ```html
28
+ <select hx-ext="tomselect" ts-max-options="20" ts-remove-button-title="Remove this player" multiple>
29
+ <option value="">N/A</option>
30
+ <option value="1">Option 1</option>
31
+ <option value="2">Option 2</option>
32
+ </select>
33
+ ```
34
+
35
+ Config Options are prefixed with a `ts-` and generally match TomSelect config options. If a non-valid key is found on an element a warning will be issued
36
+
37
+ After processing, one of three attributes will be added to each select box:
38
+
39
+ - tom-select-success - Tom Select was launch succesfully
40
+ - tom-select-warning - non-breaking error (e.g. tag name is no recognised)
41
+ - tom-select-error - Breaking error - (i.e. invalid TomConfig json in ts-raw-config)
42
+
43
+ hx-oob swaps works too (and was the main motivation for writing this extention)
44
+
45
+ Check the code for details how how each attribute works
46
+
47
+ ```html
48
+ <div id="select-oob"> </div>
49
+ ```
50
+
51
+ ```html
52
+ <div hx-swap-oob="true" id="select-oob">
53
+ <select hx-ext="tomselect" name="inputName">
54
+ <option value="">N/A</option>
55
+ <option value = "1">Option 1</option>
56
+ <option value = "2">Option 2</option>
57
+ </select>
58
+ </div>
59
+ ```
60
+
61
+
62
+
63
+ <a href="https://kiwikid.github.io/hx-tomselect/">Full Examples List</a>
@@ -0,0 +1,35 @@
1
+ <div class="mb-10 bg-gray-900">
2
+ <label>[i came from somewhere else]
3
+ <select hx-ext="tomselect" ts-clear-after-add="true">
4
+ <option selected value="1">
5
+ Remote Option 1
6
+ </option selected>
7
+ <option selected value="2">
8
+ Remote Option 2
9
+ </option>
10
+ <option selected value="3">
11
+ Remote Option 3
12
+ </option>
13
+ </select>
14
+ </label>
15
+ </div>
16
+ <pre class="mb-10 language-html" hx-swap-oob="true" id="source-holder"><code class="language-html">&lt;select
17
+ hx-ext="tomselect"
18
+ max-options="3"
19
+ remove-button-title="Remove this item"
20
+ multiple
21
+ &gt;
22
+ &lt;option value=""&gt;N/A&lt;/option&gt;
23
+ &lt;option value="1"&gt;Option 1&lt;/option&gt;
24
+ &lt;option value="2"&gt;Option 2&lt;/option&gt;
25
+ &lt;option value="3"&gt;Option 3&lt;/option&gt;
26
+ &lt;/select&gt;</code>
27
+
28
+ </pre>
29
+ <pre class="mb-10">
30
+
31
+ <code>&lt;button
32
+ hx-get="another-place.html"
33
+ hx-trigger="click"
34
+ &gt;
35
+ &lt;/button&gt;</code></pre>
@@ -0,0 +1,328 @@
1
+ (function() {
2
+ /** stable build*/
3
+ const version = '06'
4
+
5
+ /**
6
+ * @typedef {Object} SupportedAttribute
7
+ * Defines an attribute supported by a configuration modification system.
8
+ * @property {string} key - The key of the configuration attribute to modify.
9
+ * @property {ConfigChange} configChange - The modifications to apply to the TomSelect configuration.
10
+ */
11
+
12
+ /**
13
+ * @typedef {'simple' | 'callback'} AttributeType
14
+ */
15
+
16
+ /**
17
+ * @typedef {function(HTMLElement, Object):void} CallbackFunction
18
+ * Description of what the callback does and its parameters.
19
+ * @param {string} a - The first number parameter.
20
+
21
+ */
22
+ /**
23
+ * @typedef {Object} AttributeConfig
24
+ * Defines an attribute supported by a configuration modification system.
25
+ * @property {string} key - The key of the configuration attribute to modify.
26
+ * @property {string} _description
27
+ * @property {ConfidenceLevel} _isBeta
28
+ * @property {CallbackFunction|string|null} configChange - The modifications to apply to the TomSelect configuration.
29
+ *
30
+ */
31
+
32
+
33
+
34
+ /**
35
+ * @type {SupportedAttribute[]}
36
+ */
37
+
38
+ /**
39
+ * @typedef {'ts-max-items' | 'ts-max-options' | 'ts-create' | 'ts-sort' | 'ts-sort-direction' | 'ts-allow-empty-option', 'ts-clear-after-add', 'ts-raw-config', 'ts-create-on-blur', 'ts-no-delete'} TomSelectConfigKey
40
+ * Defines the valid keys for configuration options in TomSelect.
41
+ * Each key is a string literal corresponding to a specific property that can be configured in TomSelect.
42
+ */
43
+
44
+ /**
45
+ * @type {Array<AttributeConfig>}
46
+ */
47
+ const attributeConfigs = [
48
+ {
49
+ key: 'ts-create',
50
+ configChange: 'create',
51
+ _description: 'Allow creating new items'
52
+ },{
53
+ key: 'ts-create-on-blur',
54
+ configChange: 'createOnBlur'
55
+ },{
56
+ key: 'ts-create-filter',
57
+ configChange: (elm, config) => ({
58
+ createFilter: function(input) {
59
+ try {
60
+ const filter = elm.getAttribute('ts-create-filter')
61
+ const matchEx = filter == "true" ? /^[^,]*$/ : elm.getAttribute('ts-create-filter')
62
+ var match = input.match(matchEx); // Example filter: disallow commas in input
63
+ if(match) return !this.options.hasOwnProperty(input);
64
+ elm.setAttribute('tom-select-warning', JSON.stringify(err));
65
+ return false;
66
+ } catch (err) {
67
+ return false
68
+ }
69
+ }
70
+ })
71
+ },{
72
+ key: 'ts-delimiter',
73
+ configChange: 'delimiter'
74
+ },{
75
+ key: 'ts-highlight',
76
+ configChange: 'highlight'
77
+ },{
78
+ key: 'ts-multiple',
79
+ configChange: 'multiple'
80
+ },{
81
+ key: 'ts-persist',
82
+ configChange: 'persist'
83
+ },{
84
+ key: 'ts-open-on-focus',
85
+ configChange: 'openOnFocus'
86
+ },{
87
+ key: 'ts-max-items',
88
+ configChange: 'maxItems'
89
+ },{
90
+ key: 'ts-hide-selected',
91
+ configChange: 'hideSelected'
92
+ },{
93
+ key: 'tx-close-after-select',
94
+ configChange: 'closeAfterSelect'
95
+ },{
96
+ key: 'tx-duplicates',
97
+ configChange: 'duplicates'
98
+ },
99
+ {
100
+ key: 'ts-max-options',
101
+ configChange: 'maxOptions'
102
+ },{
103
+ key: 'ts-sort',
104
+ configChange: (elm, config) => ({
105
+ sortField: {
106
+ field: elm.getAttribute('ts-sort'),
107
+ },
108
+ })
109
+ },{
110
+ key: 'ts-sort-direction',
111
+ configChange: (elm, config) => ({
112
+ sortField: {
113
+ direction: elm.getAttribute('ts-sort-direction') ?? 'asc'
114
+ },
115
+ })
116
+ },{
117
+ key: 'ts-allow-empty-option',
118
+ type: 'simple',
119
+ configChange: 'allowEmptyOption'
120
+ },{
121
+ key: 'ts-clear-after-add',
122
+ configChange: {
123
+ create: true,
124
+ onItemAdd: function() {
125
+ this.setTextboxValue('');
126
+ // this.refreshOptions();
127
+ }
128
+ }
129
+ },{
130
+ key: 'ts-remove-button-title',
131
+ configChange: (elm, config) => deepAssign(config,{
132
+ plugins: {
133
+ remove_button: {
134
+ title: elm.getAttribute('ts-remove-button-title') == 'true' ? 'Remove this item' : elm.getAttribute('ts-remove-button-title')
135
+ }
136
+ },
137
+ })
138
+ },{
139
+ key: 'ts-delete-confirm',
140
+ configChange: (elm, config) => ({
141
+ onDelete: function(values) {
142
+ if(elm.getAttribute('ts-delete-confirm') == "true"){
143
+ return confirm(values.length > 1 ? 'Are you sure you want to remove these ' + values.length + ' items?' : 'Are you sure you want to remove "' + values[0] + '"?');
144
+ }else {
145
+ return confirm(elm.getAttribute('ts-delete-confirm'));
146
+ }
147
+
148
+ }
149
+ })
150
+ },{
151
+ key: 'ts-add-post-url',
152
+ configChange: (elm, config) => ({
153
+ onOptionAdd: function(value, item) {
154
+ this.lock();
155
+ const valueKeyName = elm.getAttribute('ts-add-post-url-body-value') ?? 'value'
156
+ const body = {}
157
+ body[valueKeyName] = value
158
+ fetch(elm.getAttribute('ts-add-post-url'), {
159
+ method: 'POST',
160
+ headers: {
161
+ 'Content-Type': 'application/json',
162
+ },
163
+ body: JSON.stringify(body),
164
+ })
165
+ .then((res) => {
166
+ if (!res.ok) {
167
+ throw new Error(`HTTP status ${res.status}`);
168
+ }
169
+ return res.text();
170
+ })
171
+ .then((responseHtml) => htmx.process(elm, responseHtml))
172
+ .catch(error => {
173
+ console.error('Error adding item', error)
174
+ elm.setAttribute('tom-select-warning', `ts-add-post-url - Error processing item: ${JSON.stringify(error)}`);
175
+ this.removeItem(value);
176
+ })
177
+ .finally(() => {
178
+ this.unlock();
179
+ });
180
+ }
181
+ }),
182
+ _isBeta: true,
183
+ },{
184
+ key: 'ts-add-post-url-body-value',
185
+ configChange: '',
186
+ _isBeta: true,
187
+ },
188
+ {
189
+ key: 'ts-no-active',
190
+ configChange: {
191
+ plugins: ['no_active_items'],
192
+ persist: false,
193
+ create: true
194
+ }
195
+ },{
196
+ key: 'ts-remove-selector-on-select',
197
+ type: 'simple',
198
+ configChange: null
199
+ },{
200
+ key: 'ts-no-delete',
201
+ configChange: {
202
+ onDelete: () => { return false},
203
+ }
204
+ },{
205
+ key: 'ts-option-class',
206
+ configChange: 'optionClass'
207
+ },{
208
+ key: 'ts-option-class-ext',
209
+ configChange: (elm, config) => ({
210
+ 'optionClass': `${elm.getAttribute('ts-option-class-ext')} option`
211
+ })
212
+ },{
213
+ key: 'ts-item-class',
214
+ configChange: 'itemClass'
215
+ },{
216
+ key: 'ts-item-class-ext',
217
+ configChange:(elm, config) => ({
218
+ key: 'ts-option-class-ext',
219
+ configChange: {
220
+ 'itemClass': `${elm.getAttribute('ts-option-class-ext')} item`
221
+ }
222
+ })
223
+ },
224
+ {
225
+ key: 'ts-raw-config',
226
+ configChange: (elm, config) => elm.getAttribute('ts-raw-config')
227
+ }
228
+ ]
229
+
230
+ /**
231
+ * Deeply assigns properties to an object, merging any existing nested properties.
232
+ *
233
+ * @param {Object} target The target object to which properties will be assigned.
234
+ * @param {Object} updates The updates to apply. This object can contain deeply nested properties.
235
+ * @returns {Object} The updated target object.
236
+ */
237
+ function deepAssign(target, updates) {
238
+ Object.keys(updates).forEach(key => {
239
+ if (typeof updates[key] === 'object' && updates[key] !== null && !Array.isArray(updates[key])) {
240
+ if (!target[key]) target[key] = {};
241
+ deepAssign(target[key], updates[key]);
242
+ } else {
243
+ target[key] = updates[key];
244
+ }
245
+ });
246
+ return target;
247
+ }
248
+
249
+ function attachTomSelect(s){
250
+ try {
251
+ if(s.attributes?.length == 0){
252
+ throw new Error("no attributes on select?")
253
+ }
254
+
255
+ let config = {
256
+ maxItems: 999,
257
+ plugins: {}
258
+ };
259
+ const debug = s.getAttribute('hx-ext')?.split(',').map(item => item.trim()).includes('debug');
260
+ if (debug) { console.log(s.attributes) }
261
+
262
+ Array.from(s.attributes).forEach((a) => {
263
+ const attributeConfig = attributeConfigs.find((ac) => ac.key == a.name)
264
+ if (attributeConfig != null){
265
+ let configChange = {}
266
+ if(typeof attributeConfig.configChange == 'string'){
267
+ configChange[attributeConfig.configChange] = a.value
268
+ }else if(typeof attributeConfig.configChange == 'function'){
269
+ configChange = attributeConfig.configChange(s, config)
270
+ }else if(typeof attributeConfig.configChange == 'object'){
271
+ configChange = attributeConfig.configChange
272
+ }else if(a.name.startsWith('ts-')) {
273
+ s.setAttribute('tom-select-warning', `Invalid config key found: ${attr.name}`);
274
+ console.warn(`Could not find config match:${JSON.stringify(attributeConfig)}`)
275
+ }
276
+
277
+ deepAssign(config, configChange)
278
+ }else if(a.name.startsWith('ts-')){
279
+ console.warn(`Invalid config key found: ${a.name}`);
280
+ s.setAttribute(`tom-select-warning_${a.name}`, `Invalid config key found`);
281
+ }
282
+ })
283
+
284
+ if (debug) { console.info('hx-tomselect - tom-select-success - config', config) }
285
+ const ts = new TomSelect(s, config);
286
+ s.setAttribute('tom-select-success', `success`);
287
+ s.setAttribute('hx-tom-select-version', `hx-ts-${version}_ts-${ts.version}`);
288
+
289
+ } catch (err) {
290
+ s.setAttribute('tom-select-error', JSON.stringify(err));
291
+ console.error(`htmx-tomselect - Failed to load hx-tomsselect ${err}`);
292
+ }
293
+ }
294
+
295
+ htmx.defineExtension('tomselect', {
296
+ // This is doing all the tom-select attachment at this stage, but relies on this full document scan (would prefer onLoad of speicfic content):
297
+ onEvent: function (name, evt) {
298
+ if (name === "htmx:afterProcessNode") {
299
+ const newSelects = document.querySelectorAll('select[hx-ext*="tomselect"]:not([tom-select-success]):not([tom-select-error])')
300
+ newSelects.forEach((s) => {
301
+ attachTomSelect(s)
302
+ })
303
+ }
304
+ },
305
+ onLoad: function (content) {
306
+ console.log('onLoad')
307
+ const newSelects = content.querySelectorAll('select[hx-ext*="tomselect"]:not([tom-select-success]):not([tom-select-error])')
308
+ newSelects.forEach((s) => {
309
+ attachTomSelect(s)
310
+ })
311
+
312
+ // When the DOM changes, this block ensures TomSelect will reflect the current html state (i.e. new <option selected></option> will be respected)
313
+ // Still evaulating the need of this
314
+ /* const selectors = document.querySelectorAll('select[hx-ext*="tomselect"]')
315
+ selectors.forEach((s) => {
316
+ console.log('SYNC RAN')
317
+ s.tomselect.clear();
318
+ s.tomselect.clearOptions();
319
+ s.tomselect.sync();
320
+ })
321
+ },
322
+ beforeHistorySave: function(){
323
+ document.querySelectorAll('select[hx-ext*="tomselect"]')
324
+ .forEach(elt => elt.tomselect.destroy())*/
325
+ }
326
+ });
327
+
328
+ })();
package/index.html ADDED
@@ -0,0 +1,378 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>hx-tomselect</title>
8
+ <script src="https://unpkg.com/htmx.org"></script>
9
+ <link href="https://cdn.jsdelivr.net/npm/tom-select@latest/dist/css/tom-select.css" rel="stylesheet" />
10
+ <script src="https://cdn.jsdelivr.net/npm/tom-select@latest/dist/js/tom-select.complete.min.js"></script>
11
+ <script defer src="./hx-tom-select.js"></script>
12
+ <link href="https://cdn.jsdelivr.net/npm/tailwindcss@latest/dist/tailwind.min.css" rel="stylesheet">
13
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.5.0/themes/prism.min.css" />
14
+ <link href="./prism-holi-theme.css" rel="stylesheet" />
15
+ <!-- Google tag (gtag.js) -->
16
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-S2BDGEP73W"></script>
17
+ <script>
18
+ window.dataLayer = window.dataLayer || [];
19
+ function gtag() { dataLayer.push(arguments); }
20
+ gtag('js', new Date());
21
+
22
+ gtag('config', 'G-S2BDGEP73W');
23
+ </script>
24
+
25
+ </head>
26
+
27
+ <body class="bg-gray-900 text-white p-10">
28
+ <div class=" max-w-4xl mx-auto">
29
+
30
+ <p class="p-4 text-xl mb-10">
31
+ <a class="underline" href="https://github.com/kiwiKid/hx-tomselect">hx-tomselect</a> - a <a
32
+ class="underline" href="https://v1.htmx.org/docs/#extensions" target="_">htmx extention</a> for <a
33
+ class="underline" href="https://tom-select.js.org/">tom-select.js</a>
34
+ </p>
35
+ <section class="mb-20">
36
+ <h1 class="text-xl">Install:</h1>
37
+ <pre
38
+ class="mb-2"><code class="language-html">&lt;script src="https://kiwikid.github.io/hx-tomselect/hx-tom-select.js"&gt;&lt;/script&gt;</code></pre>
39
+
40
+ This extention relies on <a class="underline" href="https://v1.htmx.org">htmx</a> and <a class="underline"
41
+ href="https://tom-select.js.org/">tom-select.js</a> being loaded before it is included:
42
+ <pre class="mb-2"><code class="language-html">&lt;!DOCTYPE html&gt;
43
+ &lt;html&gt;
44
+ &lt;head&gt;
45
+ &lt;script src="https://unpkg.com/htmx.org@1.9.12"&gt;
46
+ &lt;link href="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.css" rel="stylesheet" /&gt;
47
+ &lt;script src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"&gt;
48
+ &lt;script src="https://kiwikid.github.io/hx-tomselect/hx-tom-select.js"/&gt;
49
+ &lt;/head&gt;
50
+ &lt;body&gt;
51
+ &lt;select hx-ext="tomselect"&gt;
52
+ &lt;option value=""&gt;Select an option&lt;/option&gt;
53
+ &lt;option value="1"&gt;Option 1&lt;/option&gt;
54
+ &lt;option value="2"&gt;Option 2&lt;/option&gt;
55
+ &lt;option value="3"&gt;Option 3&lt;/option&gt;
56
+ &lt;/select&gt;
57
+ &lt;/body&gt;
58
+ &lt;/html&gt;
59
+ </code></pre>
60
+ </section>
61
+ <h1 class="mb-2 text-xl">Demo:</h1>
62
+ <section class="mb-20">
63
+ <select hx-ext="tomselect,debug" class="block w-full mb-2">
64
+ <option value="">Select</option>
65
+ <option value="1">Option 1</option>
66
+ <option value="2">Option 2</option>
67
+ <option value="3">Option 3</option>
68
+ </select>
69
+ </section>
70
+ <p>(Not really interested in publishing on npm, can on request)</p>
71
+
72
+ <section class="mb-20">
73
+ <h2 class="text-xl font-semibold mb-3">Input</h2>
74
+ <p class="p-4">Allow creating new items</p>
75
+
76
+ <select hx-ext="tomselect" ts-create="true" ts-create-on-blur="true" ts-max-items="10"
77
+ class="block w-full mb-2">
78
+ <option value="">Brilliant</option>
79
+ <option value="1">Excellent</option>
80
+ <option value="2">Top Notch</option>
81
+ <option value="3">Awesome</option>
82
+ </select>
83
+ <pre class="mb-2"><code class="language-html">&lt;select hx-ext="tomselect" ts-create="true"&gt;
84
+ &lt;option value=""&gt;Select an option&lt;/option&gt;
85
+ &lt;option value="1"&gt;Option 1&lt;/option&gt;
86
+ &lt;option value="2"&gt;Option 2&lt;/option&gt;
87
+ &lt;option value="3"&gt;Option 3&lt;/option&gt;
88
+ &lt;/select&gt;</code></pre>
89
+ </section>
90
+
91
+ <section class="mb-20">
92
+ <h2 class="text-xl font-semibold mb-3">Select</h2>
93
+ <p class="p-4">Allow selecting multiple items</p>
94
+
95
+ <select hx-ext="tomselect" ts-create="true" ts-create-on-blur="true" class="block w-full mb-2">
96
+ <option value="">Brilliant</option>
97
+ <option value="1">Excellent</option>
98
+ <option value="2">Top Notch</option>
99
+ <option value="3">Awesome</option>
100
+ </select>
101
+ <pre class="mb-2"><code class="language-html">&lt;select hx-ext="tomselect" multiple&gt;
102
+ &lt;option value=""&gt;Select an option&lt;/option&gt;
103
+ &lt;option value="1"&gt;Option 1&lt;/option&gt;
104
+ &lt;option value="2"&gt;Option 2&lt;/option&gt;
105
+ &lt;option value="3"&gt;Option 3&lt;/option&gt;
106
+ &lt;/select&gt;</code></pre>
107
+ </section>
108
+
109
+
110
+
111
+
112
+ <section class="mb-20">
113
+ <h2 class="text-xl font-semibold mb-3">Delete</h2>
114
+ <p class="p-4">(ts-remove-button-title also takes an custom string message)</p>
115
+ <select hx-ext="tomselect" ts-max-items="3" ts-remove-button-title="true" multiple>
116
+ <option value="">N/A</option>
117
+ <option selected value="1">Option 1</option>
118
+ <option selected value="2">Option 2</option>
119
+ <option selected value="3">Option 3</option>
120
+ <option selected value="4">Option 4</option>
121
+ <option selected value="5">Option 5</option>
122
+ <option value="6">Option 6</option>
123
+ <option value="7">Option 7</option>
124
+ <option value="8">Option 8</option>
125
+ <option value="9">Option 9</option>
126
+ </select>
127
+ <pre class="mb-2"><code class="language-html">&lt;select
128
+ hx-ext="tomselect"
129
+ ts-max-items="3"
130
+ ts-remove-button-title="true"
131
+ multiple
132
+ &gt;
133
+ &lt;option value=""&gt;Select an option&lt;/option&gt;
134
+ &lt;option value="1"&gt;Option 1&lt;/option&gt;
135
+ &lt;option value="2"&gt;Option 2&lt;/option&gt;
136
+ ...
137
+ &lt;/select&gt;</code></pre>
138
+ </section>
139
+
140
+ <section class="mb-20">
141
+ <h2 class="text-xl font-semibold mb-3 text-2xl">Customisable</h2>
142
+ <select class="text-3xl" hx-ext="tomselect" ts-item-class="text-3xl py-3"
143
+ ts-option-class="text-3xl w-full py-3 bg-green-100" multiple>
144
+ <option value="">N/A</option>
145
+ <option selected value="1">Option 1</option>
146
+ <option selected value="2">Option 2</option>
147
+ <option selected value="3">Option 3</option>
148
+ <option selected value="4">Option 4</option>
149
+ <option selected value="5">Option 5</option>
150
+ <option value="6">Option 6</option>
151
+ <option value="7">Option 7</option>
152
+ <option value="8">Option 8</option>
153
+ <option value="9">Option 9</option>
154
+ </select>
155
+ <pre class="mb-2 text-md"><code class="language-html">&lt;select
156
+ hx-ext="tomselect"
157
+ ts-item-class="text-3xl py-3"
158
+ ts-option-class="text-3xl w-full py-3 bg-red-100"
159
+ ts-item-class="text-3xl py-3"
160
+ multiple
161
+ &gt;
162
+ &lt;option value=""&gt;Select an option&lt;/option&gt;
163
+ &lt;option value="1"&gt;Option 1&lt;/option&gt;
164
+ &lt;option value="2"&gt;Option 2&lt;/option&gt;
165
+ ...
166
+ &lt;/select&gt;</code></pre>
167
+ </section>
168
+
169
+
170
+ <section class="mb-20">
171
+ <h2 class="text-xl font-semibold mb-3">Configure:</h2>
172
+ <p class="p-4">Config Options are prefixed with a `ts-` and generally match <a
173
+ href="https://tom-select.js.org/docs/" class="underline">TomSelect config options:</a>
174
+ <script>
175
+ document.addEventListener('DOMContentLoaded', function () {
176
+ var attributeConfigs = window.hxTomSelectAttributeConfigOptions;
177
+ var configString = '', betaConfigString = '';
178
+
179
+ if (attributeConfigs && Array.isArray(attributeConfigs)) {
180
+ attributeConfigs.filter((ac) => !ac?._isBeta).forEach(function (config) {
181
+ var strToAdd = ""
182
+
183
+ switch (typeof config.configChange) {
184
+ case 'string': {
185
+ strToAdd = `${config.key} - "${config.configChange}"\n${config?._description ? `${config?._description}\n` : ''} ` + '\n\n';
186
+ break
187
+ }
188
+ case 'function': /*{
189
+ configString += `${config.key}\n${config?._description ? `${config?._description}\n` : ''}[custom-js-function]:\n\t\t${config.configChange} ` + '\n\n';
190
+ break
191
+ }*/
192
+ default:
193
+ case 'object': {
194
+ strToAdd = `${config.key}\n${config?._description ? `${config?._description}\n` : ''}${JSON.stringify(config, null, 2)}` + '\n\n';
195
+ break
196
+ }
197
+ }
198
+ if (config._isBeta) {
199
+ betaConfigString += strToAdd
200
+ } else {
201
+ configString += strToAdd
202
+ }
203
+
204
+ });
205
+
206
+ configString = configString += "\n"
207
+
208
+ document.getElementById('attributeConfigsOutput').textContent = configString;
209
+ document.getElementById('betaAttributeConfigsOutput').textContent = betaConfigString;
210
+ }
211
+ });
212
+ </script>
213
+ <pre><code class="language-html" id="attributeConfigsOutput">
214
+
215
+ </code></pre>
216
+
217
+ <pre><code class="language-html" id="betaAttributeConfigsOutput">
218
+
219
+ </code></pre>
220
+ </p>
221
+ <p class="p-4">If a non-valid key
222
+ is
223
+ found on an element a warning will be logged to the console and a `tom-select-warning` attribute
224
+ added
225
+ to
226
+ the &lt;select&gt; tag
227
+ </p>
228
+ <p class="p-4">After processing, one of three attributes will be added to each select box:</p>
229
+ OLD
230
+ <pre><code>
231
+ &lt;select ts-create="true"&gt; - allow item create&lt;/select&gt;
232
+
233
+ &lt;select ts-max-items="10"&gt; - (default 1) only allow selection of this many options&lt;/select&gt;
234
+ &lt;!-- (ensure the select tag has the 'multiple' attribute) --&gt
235
+
236
+ &lt;select ts-max-options="3"&gt; - the max number of options to display&lt;/select&gt;
237
+
238
+ &lt;select ts-remove-button-title="Remove this"&gt; - add a remove button with this hover text&lt;/select&gt;
239
+
240
+ &lt;select ts-delete-confirm="Are you sure?"&gt; - after deleting item confirm with a message ('true' or message to confirm)&lt;/select&gt;
241
+
242
+ &lt;select ts-create-filter="true"&gt; - add a regex on the add items (or 'true' for no commas)&lt;/select&gt;
243
+
244
+ &lt;select ts-allow-empty-options="true"&gt;&lt;/select&gt;
245
+
246
+ &lt;select ts-sort="field"&gt; - sets config.sortField.field &lt;/select&gt;
247
+
248
+ &lt;select ts-sort-direction="asc"&gt; - sets config.sortField.direction &lt;/select&gt;
249
+
250
+ &lt;select ts-raw-config="{JsonConfig}"&gt; - use raw tom-select config&lt;/select&gt;
251
+
252
+ &lt;!-- Non-standard config options --&gt;
253
+ &lt;select ts-debug="true"&gt; - (wip) debug logging (may remove)&gt;&lt;/select&gt;
254
+
255
+ &lt;select ts-add-post-url="[ts-add-post-url]"&gt; - after input of a new item, make a POST request to this url&lt;/select&gt;
256
+ &lt;select ts-add-post-url-body-value="[name-in-body]"&gt; - when making a POST request, use this [body-value] as the name in the body of the request&lt;/select&gt;
257
+
258
+ The request will similar to:
259
+ POST [ts-add-post-url]
260
+ {
261
+ [ts-add-post-url-body-value]: value
262
+ }
263
+
264
+ &lt;select ts-clear-after-add="true"&gt; - clear the search input after adding an item&lt;/select&gt;
265
+ </code></pre>
266
+ <pre><code class="language-html">- &lt;select tom-select-success="true"&gt;
267
+ - &lt;select tom-select-warning="[warning details]"&gt; - non-breaking error (e.g. tag name is not recognised)
268
+ - &lt;select tom-select-error="[error details]"&gt; - Breaking error</code></pre>
269
+
270
+
271
+
272
+
273
+ </section>
274
+
275
+ <section class="mb-20">
276
+
277
+ <p class="p-4">hx-oob swaps works too (and was the main motivation for writing this extention)</code></p>
278
+ <button type="button" hx-get="another-place.html" hx-trigger="click" hx-swap="outerHTML"
279
+ class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
280
+ (Click to dynamically load options)
281
+ </button>
282
+ <div id="holder"></div>
283
+ <div id="source-holder">
284
+ <pre class="mb-2"><code class="language-html">&lt;button
285
+ hx-get="another-place.html"
286
+ hx-trigger="click"
287
+ &gt;(Click to dynamically load options)&lt;/button&gt;</code></pre>
288
+ </div>
289
+ </section>
290
+
291
+
292
+ <section class="mb-20">
293
+ <h2 class="text-xl font-semibold mb-3">Configuring Item Deletion Confirmation</h2>
294
+ <p class="p-4">Supports a custom message or `true` for default text</p>
295
+ <select hx-ext="tomselect" ts-remove-button-title="This DELETES THE ITEM!!"
296
+ ts-delete-confirm="ARE YOU SURE!?!" multiple>
297
+ <option value="1" selected>Item 1</option>
298
+ <option value="2" selected>Item 2</option>
299
+ <option value="3" selected>Item 3</option>
300
+ </select>
301
+ <pre><code class="language-html">&lt;select
302
+ hx-ext="tomselect"
303
+ ts-remove-button-title="This DELETES THE ITEM!!"
304
+ ts-delete-confirm="ARE YOU SURE!?!"
305
+ multiple
306
+ &gt;
307
+ &lt;option value="1"&gt;Item 1&lt;/option&gt;
308
+ &lt;option value="2"&gt;Item 2&lt;/option&gt;
309
+ &lt;option value="3"&gt;Item 3&lt;/option&gt;
310
+ &lt;/select&gt;</code></pre>
311
+ </section>
312
+
313
+
314
+
315
+
316
+ <section class="mb-20">
317
+ <h2 class="text-xl font-semibold mb-3">Create + Regex new item names</h2>
318
+ <p>Allow creating new items + ts-create-filter="true" will prevent commas in the title, any other string
319
+ will be used as a regex match</p>
320
+ <select hx-ext="tomselect" ts-create="true" ts-max-items="10" ts-create-filter="^[^,]*$">
321
+ <option value="">Select</option>
322
+ <option value="1">ValidOption</option>
323
+ </select>
324
+ <pre><code class="language-html-dark">&lt;select
325
+ hx-ext="tomselect"
326
+ ts-max-items="10"
327
+ ts-create-filter="^[^,]*$"
328
+ &gt;
329
+ &lt;option value=""&gt;Select an option&lt;/option&gt;
330
+ &lt;option value="1"&gt;ValidOption&lt;/option&gt;
331
+ &lt;/select&gt;</code></pre>
332
+ </section>
333
+
334
+ <section class="mb-20">
335
+ <h2 class="text-xl font-semibold mb-3">Create + POST request</h2>
336
+ <p>(type to add a new option and see the network tab)</p>
337
+ <select id="select-fine" hx-ext="tomselect" ts-create="true" ts-create-filter="true"
338
+ ts-add-post-url="/this-could-be-your-new-thing-api" ts-persist="true" multiple
339
+ placeholder="Add options(s)..." class="border border-gray-300 rounded-md text-gray-700 flex-grow mb-2">
340
+ <option value="">Select</option>
341
+ <option value="1">Option 1</option>
342
+ <option value="2">Option 2</option>
343
+ <option value="3">Option 3</option>
344
+ </select>
345
+ <pre><code class="language-html">&lt;select
346
+ hx-ext="tomselect"
347
+ ts-create-filter="true"
348
+ ts-no-active="true"
349
+ ts-add-post-url="/this-could-be-your-new-thing-api"
350
+ ts-persist="true"
351
+ multiple
352
+ &gt;
353
+ &lt;option value=""&gt;Select&lt;/option&gt;
354
+ &lt;option value="1"&gt;Option 1&lt;/option&gt;
355
+ &lt;option value="2"&gt;Option 2&lt;/option&gt;
356
+ &lt;option value="3"&gt;Option 3&lt;/option&gt;
357
+ &lt;/select&gt;
358
+ </code></pre>
359
+ </section>
360
+
361
+ <section class="mb-20">
362
+ <h2 class="text-xl font-semibold mb-3">Advanced Configuration with Raw Config</h2>
363
+ <select hx-ext="tomselect"
364
+ ts-raw-config='{"create": true, "maxOptions":10, "plugins": {"dropdown_input": {}}}'>
365
+ <option value="">Select</option>
366
+ <option value="1">Option 1</option>
367
+ <option value="2">Option 2</option>
368
+ <option value="3">Option 3</option>
369
+ </select>
370
+ </section>
371
+
372
+
373
+
374
+ </div>
375
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.5.0/prism.min.js"></script>
376
+ </body>
377
+
378
+ </html>
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "hx-tomselect",
3
+ "version": "1.0.0",
4
+ "description": "Provides a hx-ext=\"tomselect\" htmx extention tag",
5
+ "main": "hx-tom-select.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/KiwiKid/hx-tomselect.git"
12
+ },
13
+ "keywords": [
14
+ "htmx",
15
+ "tom-select"
16
+ ],
17
+ "author": "Greg C.",
18
+ "license": "MIT",
19
+ "bugs": {
20
+ "url": "https://github.com/KiwiKid/hx-tomselect/issues"
21
+ },
22
+ "homepage": "https://github.com/KiwiKid/hx-tomselect#readme"
23
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * MIT License
3
+ * Copyright (c) 2021 Ayush Saini
4
+ * Holi Theme for prism.js
5
+ * @author Ayush Saini <@AyushCodes on Twitter>
6
+ */
7
+
8
+ code[class*='language-'],
9
+ pre[class*='language-'] {
10
+ color: #d6e7ff;
11
+ background: #030314;
12
+ text-shadow: none;
13
+ font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
14
+ font-size: 1em;
15
+ line-height: 1.5;
16
+ letter-spacing: .2px;
17
+ white-space: pre;
18
+ word-spacing: normal;
19
+ word-break: normal;
20
+ word-wrap: normal;
21
+ text-align: left;
22
+
23
+ -moz-tab-size: 4;
24
+ -o-tab-size: 4;
25
+ tab-size: 4;
26
+
27
+ -webkit-hyphens: none;
28
+ -moz-hyphens: none;
29
+ -ms-hyphens: none;
30
+ hyphens: none;
31
+ }
32
+
33
+ pre[class*='language-']::-moz-selection,
34
+ pre[class*='language-'] ::-moz-selection,
35
+ code[class*='language-']::-moz-selection,
36
+ code[class*='language-'] ::-moz-selection,
37
+ pre[class*='language-']::selection,
38
+ pre[class*='language-'] ::selection,
39
+ code[class*='language-']::selection,
40
+ code[class*='language-'] ::selection {
41
+ color: inherit;
42
+ background: #1d3b54;
43
+ text-shadow: none;
44
+ }
45
+
46
+ pre[class*='language-'] {
47
+ border: 1px solid #2a4555;
48
+ border-radius: 5px;
49
+ padding: 1.5em 1em;
50
+ margin: 1em 0;
51
+ overflow: auto;
52
+ }
53
+
54
+ :not(pre) > code[class*='language-'] {
55
+ color: #f0f6f6;
56
+ background: #2a4555;
57
+ padding: 0.2em 0.3em;
58
+ border-radius: 0.2em;
59
+ box-decoration-break: clone;
60
+ }
61
+
62
+ .token.comment,
63
+ .token.prolog,
64
+ .token.doctype,
65
+ .token.cdata {
66
+ color: #446e69;
67
+ }
68
+
69
+ .token.punctuation {
70
+ color: #d6b007;
71
+ }
72
+
73
+ .token.property,
74
+ .token.tag,
75
+ .token.boolean,
76
+ .token.number,
77
+ .token.constant,
78
+ .token.symbol,
79
+ .token.deleted {
80
+ color: #d6e7ff;
81
+ }
82
+
83
+ .token.selector,
84
+ .token.attr-name,
85
+ .token.builtin,
86
+ .token.inserted {
87
+ color: #e60067;
88
+ }
89
+
90
+ .token.string,
91
+ .token.char {
92
+ color: #49c6ec;
93
+ }
94
+
95
+ .token.operator,
96
+ .token.entity,
97
+ .token.url,
98
+ .language-css .token.string,
99
+ .style .token.string {
100
+ color: #ec8e01;
101
+ background: transparent;
102
+ }
103
+
104
+ .token.atrule,
105
+ .token.attr-value,
106
+ .token.keyword {
107
+ color: #0fe468;
108
+ }
109
+
110
+ .token.function,
111
+ .token.class-name {
112
+ color: #78f3e9;
113
+ }
114
+
115
+ .token.regex,
116
+ .token.important,
117
+ .token.variable {
118
+ color: #d6e7ff;
119
+ }