@navette-js/forms 1.0.0-beta.3 → 1.0.0-beta.5
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/lib/builder.d.ts +45 -4
- package/lib/builder.js +99 -16
- package/lib/index.d.ts +812 -71
- package/lib/index.js +1945 -253
- package/lib/rich-content.d.ts +138 -0
- package/lib/rich-content.js +350 -0
- package/lib/rich-editor-tool.d.ts +60 -0
- package/lib/rich-editor-tool.js +89 -0
- package/lib/rich-editor-tools.d.ts +2 -0
- package/lib/rich-editor-tools.js +394 -0
- package/lib/rules.d.ts +19 -0
- package/lib/rules.js +148 -0
- package/package.json +4 -4
- package/views/components/text-input.ejs +14 -5
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
export const RICH_EDITOR_TOOL_DEFINITIONS = [
|
|
2
|
+
{
|
|
3
|
+
"name": "bold",
|
|
4
|
+
"label": "filament-forms::components.rich_editor.tools.bold",
|
|
5
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleBold().run()",
|
|
6
|
+
"toggle": true,
|
|
7
|
+
"icon": "m-bold"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"name": "italic",
|
|
11
|
+
"label": "filament-forms::components.rich_editor.tools.italic",
|
|
12
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleItalic().run()",
|
|
13
|
+
"toggle": true,
|
|
14
|
+
"icon": "m-italic"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "underline",
|
|
18
|
+
"label": "filament-forms::components.rich_editor.tools.underline",
|
|
19
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleUnderline().run()",
|
|
20
|
+
"toggle": true,
|
|
21
|
+
"icon": "m-underline"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "strike",
|
|
25
|
+
"label": "filament-forms::components.rich_editor.tools.strike",
|
|
26
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleStrike().run()",
|
|
27
|
+
"toggle": true,
|
|
28
|
+
"icon": "m-strikethrough"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "subscript",
|
|
32
|
+
"label": "filament-forms::components.rich_editor.tools.subscript",
|
|
33
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleSubscript().run()",
|
|
34
|
+
"toggle": true,
|
|
35
|
+
"icon": "fi-o-subscript"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "superscript",
|
|
39
|
+
"label": "filament-forms::components.rich_editor.tools.superscript",
|
|
40
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleSuperscript().run()",
|
|
41
|
+
"toggle": true,
|
|
42
|
+
"icon": "fi-o-superscript"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "link",
|
|
46
|
+
"label": "filament-forms::components.rich_editor.tools.link",
|
|
47
|
+
"action": "{ url: $getEditor().getAttributes('link')?.href, shouldOpenInNewTab: $getEditor().getAttributes('link')?.target === '_blank' }",
|
|
48
|
+
"toggle": true,
|
|
49
|
+
"icon": "m-link"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "textColor",
|
|
53
|
+
"label": "filament-forms::components.rich_editor.tools.text_color",
|
|
54
|
+
"action": "{ color: $getEditor().getAttributes('textColor')['data-color'] ?? null }",
|
|
55
|
+
"toggle": true,
|
|
56
|
+
"icon": "m-swatch"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "h1",
|
|
60
|
+
"label": "filament-forms::components.rich_editor.tools.h1",
|
|
61
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleHeading({ level: 1 }).run()",
|
|
62
|
+
"toggle": true,
|
|
63
|
+
"icon": "fi-o-h1",
|
|
64
|
+
"activeKey": "heading",
|
|
65
|
+
"activeOptions": {
|
|
66
|
+
"level": 1
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "h2",
|
|
71
|
+
"label": "filament-forms::components.rich_editor.tools.h2",
|
|
72
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleHeading({ level: 2 }).run()",
|
|
73
|
+
"toggle": true,
|
|
74
|
+
"icon": "fi-o-h2",
|
|
75
|
+
"activeKey": "heading",
|
|
76
|
+
"activeOptions": {
|
|
77
|
+
"level": 2
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "h3",
|
|
82
|
+
"label": "filament-forms::components.rich_editor.tools.h3",
|
|
83
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleHeading({ level: 3 }).run()",
|
|
84
|
+
"toggle": true,
|
|
85
|
+
"icon": "fi-o-h3",
|
|
86
|
+
"activeKey": "heading",
|
|
87
|
+
"activeOptions": {
|
|
88
|
+
"level": 3
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "paragraph",
|
|
93
|
+
"label": "filament-forms::components.rich_editor.tools.paragraph",
|
|
94
|
+
"jsHandler": "$getEditor()?.chain().focus().setParagraph().run()",
|
|
95
|
+
"toggle": true,
|
|
96
|
+
"icon": "fi-o-paragraph"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "h4",
|
|
100
|
+
"label": "filament-forms::components.rich_editor.tools.h4",
|
|
101
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleHeading({ level: 4 }).run()",
|
|
102
|
+
"toggle": true,
|
|
103
|
+
"icon": "fi-o-h4",
|
|
104
|
+
"activeKey": "heading",
|
|
105
|
+
"activeOptions": {
|
|
106
|
+
"level": 4
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"name": "h5",
|
|
111
|
+
"label": "filament-forms::components.rich_editor.tools.h5",
|
|
112
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleHeading({ level: 5 }).run()",
|
|
113
|
+
"toggle": true,
|
|
114
|
+
"icon": "fi-o-h5",
|
|
115
|
+
"activeKey": "heading",
|
|
116
|
+
"activeOptions": {
|
|
117
|
+
"level": 5
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "h6",
|
|
122
|
+
"label": "filament-forms::components.rich_editor.tools.h6",
|
|
123
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleHeading({ level: 6 }).run()",
|
|
124
|
+
"toggle": true,
|
|
125
|
+
"icon": "fi-o-h6",
|
|
126
|
+
"activeKey": "heading",
|
|
127
|
+
"activeOptions": {
|
|
128
|
+
"level": 6
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "blockquote",
|
|
133
|
+
"label": "filament-forms::components.rich_editor.tools.blockquote",
|
|
134
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleBlockquote().run()",
|
|
135
|
+
"toggle": true,
|
|
136
|
+
"icon": "m-chat-bubble-bottom-center-text"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "code",
|
|
140
|
+
"label": "filament-forms::components.rich_editor.tools.code",
|
|
141
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleCode().run()",
|
|
142
|
+
"toggle": true,
|
|
143
|
+
"icon": "fi-o-code"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"name": "codeBlock",
|
|
147
|
+
"label": "filament-forms::components.rich_editor.tools.code_block",
|
|
148
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleCodeBlock().run()",
|
|
149
|
+
"toggle": true,
|
|
150
|
+
"icon": "fi-o-code-block"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"name": "bulletList",
|
|
154
|
+
"label": "filament-forms::components.rich_editor.tools.bullet_list",
|
|
155
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleBulletList().run()",
|
|
156
|
+
"toggle": true,
|
|
157
|
+
"icon": "m-list-bullet"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"name": "orderedList",
|
|
161
|
+
"label": "filament-forms::components.rich_editor.tools.ordered_list",
|
|
162
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleOrderedList().run()",
|
|
163
|
+
"toggle": true,
|
|
164
|
+
"icon": "m-numbered-list"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "table",
|
|
168
|
+
"label": "filament-forms::components.rich_editor.tools.table",
|
|
169
|
+
"jsHandler": "$getEditor()?.commands.insertTable({ rows: 2, cols: 3, withHeaderRow: true })",
|
|
170
|
+
"icon": "fi-o-table"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "tableAddColumnBefore",
|
|
174
|
+
"label": "filament-forms::components.rich_editor.tools.table_add_column_before",
|
|
175
|
+
"jsHandler": "$getEditor()?.chain().focus().addColumnBefore().run()",
|
|
176
|
+
"icon": "fi-o-table-add-column-before"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"name": "tableAddColumnAfter",
|
|
180
|
+
"label": "filament-forms::components.rich_editor.tools.table_add_column_after",
|
|
181
|
+
"jsHandler": "$getEditor()?.chain().focus().addColumnAfter().run()",
|
|
182
|
+
"icon": "fi-o-table-add-column-after"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"name": "tableDeleteColumn",
|
|
186
|
+
"label": "filament-forms::components.rich_editor.tools.table_delete_column",
|
|
187
|
+
"jsHandler": "$getEditor()?.chain().focus().deleteColumn().run()",
|
|
188
|
+
"icon": "fi-o-table-delete-column"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "tableAddRowBefore",
|
|
192
|
+
"label": "filament-forms::components.rich_editor.tools.table_add_row_before",
|
|
193
|
+
"jsHandler": "$getEditor()?.chain().focus().addRowBefore().run()",
|
|
194
|
+
"icon": "fi-o-table-add-row-before"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"name": "tableAddRowAfter",
|
|
198
|
+
"label": "filament-forms::components.rich_editor.tools.table_add_row_after",
|
|
199
|
+
"jsHandler": "$getEditor()?.chain().focus().addRowAfter().run()",
|
|
200
|
+
"icon": "fi-o-table-add-row-after"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"name": "tableDeleteRow",
|
|
204
|
+
"label": "filament-forms::components.rich_editor.tools.table_delete_row",
|
|
205
|
+
"jsHandler": "$getEditor()?.chain().focus().deleteRow().run()",
|
|
206
|
+
"icon": "fi-o-table-delete-row"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"name": "tableMergeCells",
|
|
210
|
+
"label": "filament-forms::components.rich_editor.tools.table_merge_cells",
|
|
211
|
+
"jsHandler": "$getEditor()?.chain().focus().mergeCells().run()",
|
|
212
|
+
"icon": "fi-o-table-merge-cells"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"name": "tableSplitCell",
|
|
216
|
+
"label": "filament-forms::components.rich_editor.tools.table_split_cell",
|
|
217
|
+
"jsHandler": "$getEditor()?.chain().focus().splitCell().run()",
|
|
218
|
+
"icon": "fi-o-table-split-cell"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"name": "tableToggleHeaderRow",
|
|
222
|
+
"label": "filament-forms::components.rich_editor.tools.table_toggle_header_row",
|
|
223
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleHeaderRow().run()",
|
|
224
|
+
"icon": "fi-o-table-toggle-header-row"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"name": "tableToggleHeaderCell",
|
|
228
|
+
"label": "filament-forms::components.rich_editor.tools.table_toggle_header_cell",
|
|
229
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleHeaderCell().run()",
|
|
230
|
+
"icon": "fi-o-table-toggle-header-cell"
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"name": "tableDelete",
|
|
234
|
+
"label": "filament-forms::components.rich_editor.tools.table_delete",
|
|
235
|
+
"jsHandler": "$getEditor()?.chain().focus().deleteTable().run()",
|
|
236
|
+
"icon": "fi-o-table-delete"
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"name": "attachFiles",
|
|
240
|
+
"label": "filament-forms::components.rich_editor.tools.attach_files",
|
|
241
|
+
"action": "{ alt: $getEditor().getAttributes('image')?.alt, id: $getEditor().getAttributes('image')?.id, src: $getEditor().getAttributes('image')?.src }",
|
|
242
|
+
"icon": "m-paper-clip",
|
|
243
|
+
"activeKey": "image"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"name": "customBlocks",
|
|
247
|
+
"label": "filament-forms::components.rich_editor.tools.custom_blocks",
|
|
248
|
+
"jsHandler": "togglePanel('customBlocks')",
|
|
249
|
+
"icon": "m-squares-plus",
|
|
250
|
+
"activeJsExpression": "isPanelActive('customBlocks')"
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"name": "mergeTags",
|
|
254
|
+
"label": "filament-forms::components.rich_editor.tools.merge_tags",
|
|
255
|
+
"jsHandler": "togglePanel('mergeTags')",
|
|
256
|
+
"icon": "fi-o-merge-tag",
|
|
257
|
+
"activeJsExpression": "isPanelActive('mergeTags')"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"name": "horizontalRule",
|
|
261
|
+
"label": "filament-forms::components.rich_editor.tools.horizontal_rule",
|
|
262
|
+
"jsHandler": "$getEditor()?.chain().focus().setHorizontalRule().run()",
|
|
263
|
+
"icon": "m-minus"
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"name": "highlight",
|
|
267
|
+
"label": "filament-forms::components.rich_editor.tools.highlight",
|
|
268
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleHighlight().run()",
|
|
269
|
+
"toggle": true,
|
|
270
|
+
"icon": "fi-o-highlight"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"name": "small",
|
|
274
|
+
"label": "filament-forms::components.rich_editor.tools.small",
|
|
275
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleSmall().run()",
|
|
276
|
+
"toggle": true,
|
|
277
|
+
"icon": "fi-o-small"
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"name": "lead",
|
|
281
|
+
"label": "filament-forms::components.rich_editor.tools.lead",
|
|
282
|
+
"jsHandler": "$getEditor()?.chain().focus().toggleLead().run()",
|
|
283
|
+
"toggle": true,
|
|
284
|
+
"icon": "fi-o-lead"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"name": "undo",
|
|
288
|
+
"label": "filament-forms::components.rich_editor.tools.undo",
|
|
289
|
+
"jsHandler": "$getEditor()?.chain().focus().undo().run()",
|
|
290
|
+
"icon": "m-arrow-uturn-left"
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"name": "redo",
|
|
294
|
+
"label": "filament-forms::components.rich_editor.tools.redo",
|
|
295
|
+
"jsHandler": "$getEditor()?.chain().focus().redo().run()",
|
|
296
|
+
"icon": "m-arrow-uturn-right"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"name": "alignStart",
|
|
300
|
+
"label": "filament-forms::components.rich_editor.tools.align_start",
|
|
301
|
+
"jsHandler": "$getEditor()?.chain().focus().setTextAlign('start').run()",
|
|
302
|
+
"toggle": true,
|
|
303
|
+
"icon": "fi-o-align-start",
|
|
304
|
+
"activeJsExpression": "$getEditor()?.isActive({ textAlign: 'start' })"
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"name": "alignCenter",
|
|
308
|
+
"label": "filament-forms::components.rich_editor.tools.align_center",
|
|
309
|
+
"jsHandler": "$getEditor()?.chain().focus().setTextAlign('center').run()",
|
|
310
|
+
"toggle": true,
|
|
311
|
+
"icon": "fi-o-align-center",
|
|
312
|
+
"activeJsExpression": "$getEditor()?.isActive({ textAlign: 'center' })"
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"name": "alignEnd",
|
|
316
|
+
"label": "filament-forms::components.rich_editor.tools.align_end",
|
|
317
|
+
"jsHandler": "$getEditor()?.chain().focus().setTextAlign('end').run()",
|
|
318
|
+
"toggle": true,
|
|
319
|
+
"icon": "fi-o-align-end",
|
|
320
|
+
"activeJsExpression": "$getEditor()?.isActive({ textAlign: 'end' })"
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"name": "alignJustify",
|
|
324
|
+
"label": "filament-forms::components.rich_editor.tools.align_justify",
|
|
325
|
+
"jsHandler": "$getEditor()?.chain().focus().setTextAlign('justify').run()",
|
|
326
|
+
"toggle": true,
|
|
327
|
+
"icon": "fi-o-align-justify",
|
|
328
|
+
"activeJsExpression": "$getEditor()?.isActive({ textAlign: 'justify' })"
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
"name": "grid",
|
|
332
|
+
"label": "filament-forms::components.rich_editor.tools.grid",
|
|
333
|
+
"action": "{}",
|
|
334
|
+
"icon": "fi-o-columns",
|
|
335
|
+
"activeJsExpression": "false"
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"name": "gridAddColumnBefore",
|
|
339
|
+
"label": "filament-forms::components.rich_editor.tools.table_add_column_before",
|
|
340
|
+
"jsHandler": "$getEditor()?.chain().focus().addGridColumnBefore().run()",
|
|
341
|
+
"icon": "fi-o-table-add-column-before",
|
|
342
|
+
"activeJsExpression": "$getEditor()?.can().addGridColumnBefore()",
|
|
343
|
+
"activeStyling": false,
|
|
344
|
+
"disabledWhenNotActive": true,
|
|
345
|
+
"extraAttributes": {
|
|
346
|
+
"data-testid": "grid-add-column-before"
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"name": "gridAddColumnAfter",
|
|
351
|
+
"label": "filament-forms::components.rich_editor.tools.table_add_column_after",
|
|
352
|
+
"jsHandler": "$getEditor()?.chain().focus().addGridColumnAfter().run()",
|
|
353
|
+
"icon": "fi-o-table-add-column-after",
|
|
354
|
+
"activeJsExpression": "$getEditor()?.can().addGridColumnAfter()",
|
|
355
|
+
"activeStyling": false,
|
|
356
|
+
"disabledWhenNotActive": true,
|
|
357
|
+
"extraAttributes": {
|
|
358
|
+
"data-testid": "grid-add-column-after"
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"name": "gridDeleteColumn",
|
|
363
|
+
"label": "filament-forms::components.rich_editor.tools.table_delete_column",
|
|
364
|
+
"jsHandler": "$getEditor()?.chain().focus().deleteGridColumn().run()",
|
|
365
|
+
"icon": "fi-o-table-delete-column",
|
|
366
|
+
"activeJsExpression": "$getEditor()?.can().deleteGridColumn()",
|
|
367
|
+
"activeStyling": false,
|
|
368
|
+
"disabledWhenNotActive": true,
|
|
369
|
+
"extraAttributes": {
|
|
370
|
+
"data-testid": "grid-delete-column"
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
"name": "gridDelete",
|
|
375
|
+
"label": "filament-forms::components.rich_editor.tools.grid_delete",
|
|
376
|
+
"jsHandler": "$getEditor()?.chain().focus().deleteGrid().run()",
|
|
377
|
+
"icon": "fi-o-columns-delete",
|
|
378
|
+
"activeKey": "grid",
|
|
379
|
+
"activeStyling": false,
|
|
380
|
+
"disabledWhenNotActive": true
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
"name": "details",
|
|
384
|
+
"label": "filament-forms::components.rich_editor.tools.details",
|
|
385
|
+
"jsHandler": "$getEditor()?.chain().focus().setDetails().run()",
|
|
386
|
+
"icon": "fi-o-details"
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"name": "clearFormatting",
|
|
390
|
+
"label": "filament-forms::components.rich_editor.tools.clear_formatting",
|
|
391
|
+
"jsHandler": "$getEditor()?.chain().focus().clearNodes().unsetAllMarks().run()",
|
|
392
|
+
"icon": "fi-o-clear-formatting"
|
|
393
|
+
}
|
|
394
|
+
];
|
package/lib/rules.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ComponentContext } from '@navette-js/schemas';
|
|
2
|
+
/** `implicit`: the rule also runs on an empty value (required, accepted...); other rules are skipped when the value is empty. */
|
|
3
|
+
export type Rule = {
|
|
4
|
+
name: string;
|
|
5
|
+
passes: (value: unknown, ctx: ComponentContext) => boolean | Promise<boolean>;
|
|
6
|
+
message: (attribute: string) => string;
|
|
7
|
+
implicit?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare const isBlank: (v: unknown) => boolean;
|
|
10
|
+
/** A Laravel `regex:` parameter (`/pattern/flags`) as a RegExp; only `i`, `m`, `s` and `u` flags. */
|
|
11
|
+
export declare function parseRegex(source: string): RegExp;
|
|
12
|
+
/**
|
|
13
|
+
* Builds a rule from a string such as `'max:255'`. `numeric` tells whether sizes are compared as numbers (numeric fields).
|
|
14
|
+
* An unknown rule throws when the field is declared, instead of being silently ignored.
|
|
15
|
+
*/
|
|
16
|
+
export declare function stringRule(spec: string, options: {
|
|
17
|
+
numeric: boolean;
|
|
18
|
+
name: string;
|
|
19
|
+
}): Rule | null;
|
package/lib/rules.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export const isBlank = (v) => v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0);
|
|
2
|
+
const isNumeric = (value) => (typeof value === 'number' && Number.isFinite(value)) || (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value)));
|
|
3
|
+
/** Size of a value: its number when numeric (for a numeric field), its length for text and lists. */
|
|
4
|
+
const sizeOf = (value, numeric) => (Array.isArray(value) ? value.length : numeric && isNumeric(value) ? Number(value) : String(value ?? '').length);
|
|
5
|
+
const unit = (value, numeric) => (Array.isArray(value) ? ' items' : numeric ? '' : ' characters');
|
|
6
|
+
const dateOf = (value) => { const time = Date.parse(String(value ?? '')); return Number.isFinite(time) ? time : null; };
|
|
7
|
+
const list = (parameter) => parameter.split(',').map((item) => item.trim());
|
|
8
|
+
/** A Laravel `regex:` parameter (`/pattern/flags`) as a RegExp; only `i`, `m`, `s` and `u` flags. */
|
|
9
|
+
export function parseRegex(source) {
|
|
10
|
+
const match = /^\/(.*)\/([imsu]*)$/s.exec(source.trim());
|
|
11
|
+
if (!match)
|
|
12
|
+
throw new Error(`Invalid regex rule: ${source} (expected /pattern/flags).`);
|
|
13
|
+
return new RegExp(match[1], match[2]);
|
|
14
|
+
}
|
|
15
|
+
/** A sibling value of the same form, read from the component context (`confirmed`, `same`, `different`, `gt`...). */
|
|
16
|
+
const sibling = (ctx, name) => ctx.get(name);
|
|
17
|
+
const compareTo = (parameter, ctx) => (isNumeric(parameter) ? Number(parameter) : isNumeric(sibling(ctx, parameter)) ? Number(sibling(ctx, parameter)) : null);
|
|
18
|
+
/**
|
|
19
|
+
* Builds a rule from a string such as `'max:255'`. `numeric` tells whether sizes are compared as numbers (numeric fields).
|
|
20
|
+
* An unknown rule throws when the field is declared, instead of being silently ignored.
|
|
21
|
+
*/
|
|
22
|
+
export function stringRule(spec, options) {
|
|
23
|
+
const colon = spec.indexOf(':');
|
|
24
|
+
const name = (colon < 0 ? spec : spec.slice(0, colon)).trim();
|
|
25
|
+
const parameter = colon < 0 ? '' : spec.slice(colon + 1);
|
|
26
|
+
const numeric = options.numeric;
|
|
27
|
+
const make = (passes, message) => ({ name, passes, message });
|
|
28
|
+
const implicit = (passes, message) => ({ name, passes, message, implicit: true });
|
|
29
|
+
const [otherField, ...otherValues] = list(parameter);
|
|
30
|
+
const otherIs = (ctx) => { const other = sibling(ctx, otherField); return (Array.isArray(other) ? other : [other]).some((item) => otherValues.includes(String(item ?? ''))); };
|
|
31
|
+
const otherName = otherField?.replace(/_/g, ' ');
|
|
32
|
+
switch (name) {
|
|
33
|
+
case 'nullable':
|
|
34
|
+
case 'sometimes':
|
|
35
|
+
case 'bail':
|
|
36
|
+
case 'string':
|
|
37
|
+
case 'present': return null;
|
|
38
|
+
case 'required':
|
|
39
|
+
case 'filled': return implicit((value) => !isBlank(value), (a) => `The ${a} field is required.`);
|
|
40
|
+
case 'required_if': return implicit((value, ctx) => !otherIs(ctx) || !isBlank(value), (a) => `The ${a} field is required when ${otherName} is ${otherValues.join(', ')}.`);
|
|
41
|
+
case 'required_unless': return implicit((value, ctx) => otherIs(ctx) || !isBlank(value), (a) => `The ${a} field is required unless ${otherName} is in ${otherValues.join(', ')}.`);
|
|
42
|
+
case 'required_if_accepted': return implicit((value, ctx) => ![true, 1, '1', 'yes', 'on', 'true'].includes(sibling(ctx, otherField)) || !isBlank(value), (a) => `The ${a} field is required when ${otherName} is accepted.`);
|
|
43
|
+
case 'required_with':
|
|
44
|
+
case 'required_with_all':
|
|
45
|
+
case 'required_without':
|
|
46
|
+
case 'required_without_all': {
|
|
47
|
+
const fields = list(parameter);
|
|
48
|
+
const present = (ctx) => fields.map((field) => !isBlank(sibling(ctx, field)));
|
|
49
|
+
const needed = { required_with: (p) => p.some(Boolean), required_with_all: (p) => p.every(Boolean), required_without: (p) => p.some((x) => !x), required_without_all: (p) => p.every((x) => !x) }[name];
|
|
50
|
+
const words = { required_with: 'is present', required_with_all: 'are present', required_without: 'is not present', required_without_all: 'none of them are present' }[name];
|
|
51
|
+
return implicit((value, ctx) => !needed(present(ctx)) || !isBlank(value), (a) => `The ${a} field is required when ${fields.map((field) => field.replace(/_/g, ' ')).join(' / ')} ${words}.`);
|
|
52
|
+
}
|
|
53
|
+
case 'prohibited': return implicit((value) => isBlank(value), (a) => `The ${a} field is prohibited.`);
|
|
54
|
+
case 'prohibited_if': return implicit((value, ctx) => !otherIs(ctx) || isBlank(value), (a) => `The ${a} field is prohibited when ${otherName} is ${otherValues.join(', ')}.`);
|
|
55
|
+
case 'prohibited_unless': return implicit((value, ctx) => otherIs(ctx) || isBlank(value), (a) => `The ${a} field is prohibited unless ${otherName} is in ${otherValues.join(', ')}.`);
|
|
56
|
+
case 'prohibits': return make((_value, ctx) => list(parameter).every((field) => isBlank(sibling(ctx, field))), (a) => `The ${a} field prohibits ${list(parameter).map((field) => field.replace(/_/g, ' ')).join(' / ')} from being present.`);
|
|
57
|
+
case 'accepted': return implicit((value) => [true, 1, '1', 'yes', 'on', 'true'].includes(value), (a) => `The ${a} field must be accepted.`);
|
|
58
|
+
case 'declined': return implicit((value) => [false, 0, '0', 'no', 'off', 'false'].includes(value), (a) => `The ${a} field must be declined.`);
|
|
59
|
+
case 'numeric': return make(isNumeric, (a) => `The ${a} field must be a number.`);
|
|
60
|
+
case 'integer': return make((value) => /^-?\d+$/.test(String(value).trim()), (a) => `The ${a} field must be an integer.`);
|
|
61
|
+
case 'boolean': return make((value) => [true, false, 0, 1, '0', '1'].includes(value), (a) => `The ${a} field must be true or false.`);
|
|
62
|
+
case 'array': return make((value) => Array.isArray(value), (a) => `The ${a} field must be an array.`);
|
|
63
|
+
case 'email': return make((value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value)), (a) => `The ${a} field must be a valid email address.`);
|
|
64
|
+
case 'url': return make((value) => { try {
|
|
65
|
+
return ['http:', 'https:'].includes(new URL(String(value)).protocol);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
} }, (a) => `The ${a} field must be a valid URL.`);
|
|
70
|
+
case 'uuid': return make((value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(value)), (a) => `The ${a} field must be a valid UUID.`);
|
|
71
|
+
case 'ulid': return make((value) => /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i.test(String(value)), (a) => `The ${a} field must be a valid ULID.`);
|
|
72
|
+
case 'alpha': return make((value) => /^[\p{L}\p{M}]+$/u.test(String(value)), (a) => `The ${a} field must only contain letters.`);
|
|
73
|
+
case 'alpha_dash': return make((value) => /^[\p{L}\p{M}\p{N}_-]+$/u.test(String(value)), (a) => `The ${a} field must only contain letters, numbers, dashes, and underscores.`);
|
|
74
|
+
case 'alpha_num': return make((value) => /^[\p{L}\p{M}\p{N}]+$/u.test(String(value)), (a) => `The ${a} field must only contain letters and numbers.`);
|
|
75
|
+
case 'ascii': return make((value) => /^[\x00-\x7F]*$/.test(String(value)), (a) => `The ${a} field must only contain single-byte alphanumeric characters and symbols.`);
|
|
76
|
+
case 'lowercase': return make((value) => String(value) === String(value).toLowerCase(), (a) => `The ${a} field must be lowercase.`);
|
|
77
|
+
case 'uppercase': return make((value) => String(value) === String(value).toUpperCase(), (a) => `The ${a} field must be uppercase.`);
|
|
78
|
+
case 'json': return make((value) => { try {
|
|
79
|
+
JSON.parse(String(value));
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
} }, (a) => `The ${a} field must be a valid JSON string.`);
|
|
85
|
+
case 'ip': return make((value) => isIpv4(String(value)) || isIpv6(String(value)), (a) => `The ${a} field must be a valid IP address.`);
|
|
86
|
+
case 'ipv4': return make((value) => isIpv4(String(value)), (a) => `The ${a} field must be a valid IPv4 address.`);
|
|
87
|
+
case 'ipv6': return make((value) => isIpv6(String(value)), (a) => `The ${a} field must be a valid IPv6 address.`);
|
|
88
|
+
case 'mac_address': return make((value) => /^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$/.test(String(value)), (a) => `The ${a} field must be a valid MAC address.`);
|
|
89
|
+
case 'hex_color': return make((value) => /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(String(value)), (a) => `The ${a} field must be a valid hexadecimal color.`);
|
|
90
|
+
case 'date': return make((value) => dateOf(value) !== null, (a) => `The ${a} field must be a valid date.`);
|
|
91
|
+
case 'min': return make((value) => sizeOf(value, numeric) >= Number(parameter), (a) => (numeric ? `The ${a} field must be at least ${parameter}.` : `The ${a} field must be at least ${parameter}${unit(undefined, numeric)}.`));
|
|
92
|
+
case 'max': return make((value) => sizeOf(value, numeric) <= Number(parameter), (a) => (numeric ? `The ${a} field must not be greater than ${parameter}.` : `The ${a} field must not be greater than ${parameter}${unit(undefined, numeric)}.`));
|
|
93
|
+
case 'size': return make((value) => sizeOf(value, numeric) === Number(parameter), (a) => (numeric ? `The ${a} field must be ${parameter}.` : `The ${a} field must be ${parameter}${unit(undefined, numeric)}.`));
|
|
94
|
+
case 'between': {
|
|
95
|
+
const [low, high] = list(parameter).map(Number);
|
|
96
|
+
return make((value) => { const size = sizeOf(value, numeric); return size >= low && size <= high; }, (a) => `The ${a} field must be between ${low} and ${high}${unit(undefined, numeric)}.`);
|
|
97
|
+
}
|
|
98
|
+
case 'digits': return make((value) => new RegExp(`^\\d{${Number(parameter)}}$`).test(String(value)), (a) => `The ${a} field must be ${parameter} digits.`);
|
|
99
|
+
case 'digits_between': {
|
|
100
|
+
const [low, high] = list(parameter).map(Number);
|
|
101
|
+
return make((value) => /^\d+$/.test(String(value)) && String(value).length >= low && String(value).length <= high, (a) => `The ${a} field must be between ${low} and ${high} digits.`);
|
|
102
|
+
}
|
|
103
|
+
case 'multiple_of': return make((value) => isNumeric(value) && Math.abs(Number(value) / Number(parameter) - Math.round(Number(value) / Number(parameter))) < 1e-9, (a) => `The ${a} field must be a multiple of ${parameter}.`);
|
|
104
|
+
case 'in': return make((value) => (Array.isArray(value) ? value : [value]).every((item) => list(parameter).includes(String(item))), (a) => `The selected ${a} is invalid.`);
|
|
105
|
+
case 'not_in': return make((value) => (Array.isArray(value) ? value : [value]).every((item) => !list(parameter).includes(String(item))), (a) => `The selected ${a} is invalid.`);
|
|
106
|
+
case 'regex': {
|
|
107
|
+
const pattern = parseRegex(parameter);
|
|
108
|
+
return make((value) => pattern.test(String(value)), (a) => `The ${a} field format is invalid.`);
|
|
109
|
+
}
|
|
110
|
+
case 'not_regex': {
|
|
111
|
+
const pattern = parseRegex(parameter);
|
|
112
|
+
return make((value) => !pattern.test(String(value)), (a) => `The ${a} field format is invalid.`);
|
|
113
|
+
}
|
|
114
|
+
case 'starts_with': return make((value) => list(parameter).some((prefix) => String(value).startsWith(prefix)), (a) => `The ${a} field must start with one of the following: ${list(parameter).join(', ')}.`);
|
|
115
|
+
case 'ends_with': return make((value) => list(parameter).some((suffix) => String(value).endsWith(suffix)), (a) => `The ${a} field must end with one of the following: ${list(parameter).join(', ')}.`);
|
|
116
|
+
case 'doesnt_start_with': return make((value) => !list(parameter).some((prefix) => String(value).startsWith(prefix)), (a) => `The ${a} field must not start with one of the following: ${list(parameter).join(', ')}.`);
|
|
117
|
+
case 'doesnt_end_with': return make((value) => !list(parameter).some((suffix) => String(value).endsWith(suffix)), (a) => `The ${a} field must not end with one of the following: ${list(parameter).join(', ')}.`);
|
|
118
|
+
case 'distinct': return make((value) => !Array.isArray(value) || new Set(value.map(String)).size === value.length, (a) => `The ${a} field has a duplicate value.`);
|
|
119
|
+
case 'confirmed': return make((value, ctx) => String(value) === String(sibling(ctx, parameter || `${options.name}_confirmation`) ?? ''), (a) => `The ${a} field confirmation does not match.`);
|
|
120
|
+
case 'same': return make((value, ctx) => String(value) === String(sibling(ctx, parameter) ?? ''), (a) => `The ${a} field must match ${parameter.replace(/_/g, ' ')}.`);
|
|
121
|
+
case 'different': return make((value, ctx) => String(value) !== String(sibling(ctx, parameter) ?? ''), (a) => `The ${a} field and ${parameter.replace(/_/g, ' ')} must be different.`);
|
|
122
|
+
case 'gt':
|
|
123
|
+
case 'gte':
|
|
124
|
+
case 'lt':
|
|
125
|
+
case 'lte': {
|
|
126
|
+
const operator = { gt: (x, y) => x > y, gte: (x, y) => x >= y, lt: (x, y) => x < y, lte: (x, y) => x <= y }[name];
|
|
127
|
+
const words = { gt: 'greater than', gte: 'greater than or equal to', lt: 'less than', lte: 'less than or equal to' }[name];
|
|
128
|
+
return make((value, ctx) => { const other = compareTo(parameter, ctx); return other === null || operator(sizeOf(value, true), other); }, (a) => `The ${a} field must be ${words} ${isNumeric(parameter) ? parameter : parameter.replace(/_/g, ' ')}.`);
|
|
129
|
+
}
|
|
130
|
+
case 'after':
|
|
131
|
+
case 'after_or_equal':
|
|
132
|
+
case 'before':
|
|
133
|
+
case 'before_or_equal': {
|
|
134
|
+
const reference = (ctx) => dateOf(parameter === 'today' ? new Date().toISOString().slice(0, 10) : parameter === 'tomorrow' ? new Date(Date.now() + 86400000).toISOString().slice(0, 10) : (dateOf(parameter) !== null ? parameter : sibling(ctx, parameter)));
|
|
135
|
+
const test = { after: (x, y) => x > y, after_or_equal: (x, y) => x >= y, before: (x, y) => x < y, before_or_equal: (x, y) => x <= y }[name];
|
|
136
|
+
const words = { after: 'after', after_or_equal: 'after or equal to', before: 'before', before_or_equal: 'before or equal to' }[name];
|
|
137
|
+
return make((value, ctx) => { const own = dateOf(value), other = reference(ctx); return own !== null && (other === null || test(own, other)); }, (a) => `The ${a} field must be a date ${words} ${parameter.replace(/_/g, ' ')}.`);
|
|
138
|
+
}
|
|
139
|
+
default: throw new Error(`Unknown validation rule "${name}": write it as a custom rule with rule().`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function isIpv4(value) { return /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/.test(value); }
|
|
143
|
+
function isIpv6(value) {
|
|
144
|
+
if (!/^[0-9a-fA-F:.]+$/.test(value) || (value.match(/::/g) ?? []).length > 1)
|
|
145
|
+
return false;
|
|
146
|
+
const groups = value.split(':');
|
|
147
|
+
return groups.length >= 3 && groups.length <= 8 && groups.every((group) => group === '' || /^[0-9a-fA-F]{1,4}$/.test(group) || isIpv4(group));
|
|
148
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navette-js/forms",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.5",
|
|
4
4
|
"description": "Form fields and validation for Navette admin panels.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"node"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@navette-js/actions": "1.0.0-beta.
|
|
46
|
-
"@navette-js/schemas": "1.0.0-beta.
|
|
47
|
-
"@navette-js/support": "1.0.0-beta.
|
|
45
|
+
"@navette-js/actions": "1.0.0-beta.5",
|
|
46
|
+
"@navette-js/schemas": "1.0.0-beta.5",
|
|
47
|
+
"@navette-js/support": "1.0.0-beta.5",
|
|
48
48
|
"@tiptap/core": "^3.33.1",
|
|
49
49
|
"@tiptap/extension-highlight": "^3.33.1",
|
|
50
50
|
"@tiptap/extension-image": "^3.33.1",
|
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
<%# Text input with its prefix/suffix wrapper. %>
|
|
1
|
+
<%# Text input with its prefix/suffix wrapper: labels, icons and actions. %>
|
|
2
2
|
<div <%- wrapper %>>
|
|
3
|
-
<% if (
|
|
4
|
-
<div x-on:click="if (! $event.target.closest('.fi-input-wrp-actions')) $dispatch('focus-input')" class="fi-input-wrp-prefix fi-input-wrp-prefix-has-content fi-input-wrp-prefix-has-label">
|
|
3
|
+
<% if (hasPrefix) { %>
|
|
4
|
+
<div<% if (prefix !== null || prefixIcon) { %> x-on:click="if (! $event.target.closest('.fi-input-wrp-actions')) $dispatch('focus-input')"<% } %> class="<%- cls(['fi-input-wrp-prefix', 'fi-input-wrp-prefix-has-content', { 'fi-inline': prefixInline, 'fi-input-wrp-prefix-has-label': prefix !== null }]) %>">
|
|
5
|
+
<% if (prefixActions.length) { %><div class="fi-input-wrp-actions"><%- prefixActions.join('') %></div><% } %>
|
|
6
|
+
<%- prefixIcon %>
|
|
7
|
+
<% if (prefix !== null) { %>
|
|
5
8
|
<span class="fi-input-wrp-label">
|
|
6
9
|
<%= prefix %>
|
|
7
10
|
</span>
|
|
11
|
+
<% } %>
|
|
8
12
|
</div>
|
|
9
13
|
<% } %>
|
|
10
14
|
<div class="fi-input-wrp-content-ctn">
|
|
11
15
|
<input <%- input %> />
|
|
16
|
+
<% if (datalist) { %><datalist id="<%= datalist.id %>"><% for (const option of datalist.options) { %><option value="<%= option %>"></option><% } %></datalist><% } %>
|
|
12
17
|
</div>
|
|
13
|
-
<% if (
|
|
14
|
-
<div x-on:click="if (! $event.target.closest('.fi-input-wrp-actions')) $dispatch('focus-input')" class="fi-input-wrp-suffix fi-input-wrp-suffix-has-label">
|
|
18
|
+
<% if (hasSuffix) { %>
|
|
19
|
+
<div<% if (suffix !== null || suffixIcon) { %> x-on:click="if (! $event.target.closest('.fi-input-wrp-actions')) $dispatch('focus-input')"<% } %> class="<%- cls(['fi-input-wrp-suffix', { 'fi-inline': suffixInline, 'fi-input-wrp-suffix-has-label': suffix !== null }]) %>">
|
|
20
|
+
<% if (suffix !== null) { %>
|
|
15
21
|
<span class="fi-input-wrp-label">
|
|
16
22
|
<%= suffix %>
|
|
17
23
|
</span>
|
|
24
|
+
<% } %>
|
|
25
|
+
<%- suffixIcon %>
|
|
26
|
+
<% if (suffixActions.length) { %><div class="fi-input-wrp-actions"><%- suffixActions.join('') %></div><% } %>
|
|
18
27
|
</div>
|
|
19
28
|
<% } %>
|
|
20
29
|
<% if (revealable) { %>
|