@dative-gpi/foundation-shared-components 0.0.6 → 0.0.8

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.
Files changed (37) hide show
  1. package/components/FSButton.vue +170 -164
  2. package/components/FSCalendar.vue +171 -0
  3. package/components/FSCalendarTwin.vue +394 -0
  4. package/components/FSCard.vue +63 -0
  5. package/components/FSCheckbox.vue +7 -8
  6. package/components/FSClock.vue +38 -0
  7. package/components/FSDatePicker.vue +226 -0
  8. package/components/FSIcon.vue +1 -1
  9. package/components/FSNumberField.vue +4 -4
  10. package/components/FSPasswordField.vue +14 -12
  11. package/components/FSRadio.vue +0 -1
  12. package/components/FSRadioGroup.vue +6 -6
  13. package/components/FSRichTextField.vue +558 -0
  14. package/components/FSSearchField.vue +103 -102
  15. package/components/FSSlider.vue +132 -0
  16. package/components/FSSwitch.vue +9 -9
  17. package/components/FSTagField.vue +186 -127
  18. package/components/FSTextArea.vue +207 -0
  19. package/components/FSTextField.vue +151 -146
  20. package/composables/index.ts +2 -1
  21. package/composables/useBreakpoints.ts +14 -0
  22. package/composables/useDates.ts +39 -0
  23. package/models/FSTextFields.ts +12 -6
  24. package/package.json +12 -4
  25. package/styles/components/fs_button.scss +2 -10
  26. package/styles/components/fs_calendar.scss +115 -0
  27. package/styles/components/fs_card.scss +7 -0
  28. package/styles/components/fs_date_picker.scss +0 -0
  29. package/styles/components/fs_icon.scss +3 -9
  30. package/styles/components/fs_rich_text_field.scss +67 -0
  31. package/styles/components/fs_slider.scss +20 -0
  32. package/styles/components/fs_tag_field.scss +9 -0
  33. package/styles/components/fs_text_area.scss +105 -0
  34. package/styles/components/index.scss +6 -0
  35. package/utils/FSRichTextField.ts +27 -0
  36. package/utils/index.ts +1 -0
  37. package/composables/useTouch.ts +0 -9
@@ -0,0 +1,558 @@
1
+ <template>
2
+ <FSCol>
3
+ <FSRow v-if="!readonly">
4
+ <slot name="label">
5
+ <FSRow :wrap="false">
6
+ <FSSpan
7
+ v-if="$props.label"
8
+ class="fs-rich-text-field-label"
9
+ font="text-overline"
10
+ :style="style"
11
+ >
12
+ {{ $props.label }}
13
+ </FSSpan>
14
+ <FSSpan
15
+ v-if="$props.label && $props.required"
16
+ class="fs-rich-text-field-label"
17
+ style="margin-left: -8px;"
18
+ font="text-overline"
19
+ :ellipsis="false"
20
+ :style="style"
21
+ >
22
+ *
23
+ </FSSpan>
24
+ </FSRow>
25
+ </slot>
26
+ <v-spacer />
27
+ <template v-if="editable">
28
+ <FSIcon
29
+ class="fs-rich-text-field-icon"
30
+ :color="toolbarColors.undo"
31
+ :style="style"
32
+ @click="editor.dispatchCommand(UNDO_COMMAND)"
33
+ >
34
+ mdi-undo-variant
35
+ </FSIcon>
36
+ <v-divider vertical />
37
+ <FSIcon
38
+ class="fs-rich-text-field-icon"
39
+ :style="style"
40
+ @click="formatText('h1')"
41
+ >
42
+ mdi-format-header-1
43
+ </FSIcon>
44
+ <FSIcon
45
+ class="fs-rich-text-field-icon"
46
+ :style="style"
47
+ @click="formatText('h2')"
48
+ >
49
+ mdi-format-header-2
50
+ </FSIcon>
51
+ <FSIcon
52
+ class="fs-rich-text-field-icon"
53
+ :style="style"
54
+ @click="formatText('h3')"
55
+ >
56
+ mdi-format-header-3
57
+ </FSIcon>
58
+ <FSIcon
59
+ class="fs-rich-text-field-icon"
60
+ :style="style"
61
+ @click="formatParagraph()"
62
+ >
63
+ mdi-format-paragraph
64
+ </FSIcon>
65
+ <v-divider vertical />
66
+ <FSIcon
67
+ class="fs-rich-text-field-icon"
68
+ :color="toolbarColors.bold"
69
+ :style="style"
70
+ @click="editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')"
71
+ >
72
+ mdi-format-bold
73
+ </FSIcon>
74
+ <FSIcon
75
+ class="fs-rich-text-field-icon"
76
+ :color="toolbarColors.italic"
77
+ :style="style"
78
+ @click="editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')"
79
+ >
80
+ mdi-format-italic
81
+ </FSIcon>
82
+ <FSIcon
83
+ class="fs-rich-text-field-icon"
84
+ :color="toolbarColors.underline"
85
+ :style="style"
86
+ @click="editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')"
87
+ >
88
+ mdi-format-underline
89
+ </FSIcon>
90
+ <FSIcon
91
+ class="fs-rich-text-field-icon"
92
+ :color="toolbarColors.strikethrough"
93
+ :style="style"
94
+ @click="editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')"
95
+ >
96
+ mdi-format-strikethrough
97
+ </FSIcon>
98
+ <FSIcon
99
+ class="fs-rich-text-field-icon"
100
+ :color="toolbarColors.link"
101
+ :style="style"
102
+ @click="openLink"
103
+ >
104
+ mdi-link
105
+ </FSIcon>
106
+ <v-divider vertical />
107
+ <FSIcon
108
+ class="fs-rich-text-field-icon"
109
+ :style="style"
110
+ @click="editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left')"
111
+ >
112
+ mdi-format-align-left
113
+ </FSIcon>
114
+ <FSIcon
115
+ class="fs-rich-text-field-icon"
116
+ :style="style"
117
+ @click="editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center')"
118
+ >
119
+ mdi-format-align-center
120
+ </FSIcon>
121
+ <FSIcon
122
+ class="fs-rich-text-field-icon"
123
+ :style="style"
124
+ @click="editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right')"
125
+ >
126
+ mdi-format-align-right
127
+ </FSIcon>
128
+ <FSIcon
129
+ class="fs-rich-text-field-icon"
130
+ :style="style"
131
+ @click="editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify')"
132
+ >
133
+ mdi-format-align-justify
134
+ </FSIcon>
135
+ </template>
136
+ </FSRow>
137
+ <div
138
+ class="fs-rich-text-field"
139
+ :id="id"
140
+ :style="style"
141
+ :contenteditable="!readonly && $props.editable"
142
+ />
143
+ <FSTextField
144
+ v-if="isLink && editable"
145
+ v-model="linkUrl"
146
+ @keypress.enter.stop="toggleLink"
147
+ />
148
+ <slot name="description">
149
+ <FSSpan
150
+ v-if="!readonly && $props.description"
151
+ class="fs-rich-text-field-description"
152
+ font="text-underline"
153
+ :style="style"
154
+ >
155
+ {{ $props.description }}
156
+ </FSSpan>
157
+ </slot>
158
+ </FSCol>
159
+ </template>
160
+
161
+ <script lang="ts">
162
+ import { $createParagraphNode, $getSelection, $isElementNode, $isRangeSelection, $setSelection, CAN_UNDO_COMMAND, createEditor, ElementNode, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, ParagraphNode, UNDO_COMMAND } from "lexical";
163
+ import { $createHeadingNode, HeadingNode, HeadingTagType, registerRichText } from "@lexical/rich-text";
164
+ import { computed, defineComponent, onMounted, PropType, ref, toRefs } from "vue";
165
+ import { createEmptyHistoryState, registerHistory } from "@lexical/history";
166
+ import { $createLinkNode, $isLinkNode, LinkNode } from "@lexical/link";
167
+ import { $wrapNodes } from "@lexical/selection";
168
+
169
+ import { useBreakpoints, useColors } from "@dative-gpi/foundation-shared-components/composables";
170
+ import { getAncestor, getSelectedNode } from "@dative-gpi/foundation-shared-components/utils";
171
+ import { ColorBase } from "@dative-gpi/foundation-shared-components/themes";
172
+
173
+ import FSTextField from "./FSTextField.vue";
174
+ import FSIcon from "./FSIcon.vue";
175
+ import FSCol from "./FSCol.vue";
176
+ import FSRow from "./FSRow.vue";
177
+
178
+ export default defineComponent({
179
+ name: "FSRichTextField",
180
+ components: {
181
+ FSTextField,
182
+ FSIcon,
183
+ FSCol,
184
+ FSRow
185
+ },
186
+ props: {
187
+ label: {
188
+ type: String,
189
+ required: true,
190
+ default: null
191
+ },
192
+ description: {
193
+ type: String,
194
+ required: false,
195
+ default: null
196
+ },
197
+ modelValue: {
198
+ type: String,
199
+ required: false,
200
+ default: null
201
+ },
202
+ color: {
203
+ type: String as PropType<ColorBase>,
204
+ required: false,
205
+ default: ColorBase.Dark
206
+ },
207
+ linkColor: {
208
+ type: String as PropType<ColorBase>,
209
+ required: false,
210
+ default: ColorBase.Primary
211
+ },
212
+ required: {
213
+ type: Boolean,
214
+ required: false,
215
+ default: false
216
+ },
217
+ rows: {
218
+ type: Number,
219
+ required: false,
220
+ default: 5
221
+ },
222
+ variant: {
223
+ type: String as PropType<"standard" | "readonly">,
224
+ required: false,
225
+ default: "standard"
226
+ },
227
+ editable: {
228
+ type: Boolean,
229
+ required: false,
230
+ default: true
231
+ }
232
+ },
233
+ emits: ["update:modelValue"],
234
+ setup(props, { emit }) {
235
+ const { modelValue, color, linkColor, rows, variant, editable } = toRefs(props);
236
+
237
+ const colors = useColors().getColors(color.value);
238
+ const linkColors = useColors().getColors(linkColor.value);
239
+
240
+ const lights = useColors().getColors(ColorBase.Light);
241
+ const darks = useColors().getColors(ColorBase.Dark);
242
+
243
+ const canUndo = ref(false);
244
+ const isLink = ref(false);
245
+ const isBold = ref(false);
246
+ const isItalic = ref(false);
247
+ const isUnderline = ref(false);
248
+ const isStrikethrough = ref(false);
249
+
250
+ const id = `${Math.random()}-editor`;
251
+
252
+ const linkUrl = ref("https://");
253
+
254
+ const config = {
255
+ namespace: "MyEditor",
256
+ theme: {
257
+ paragraph: 'text-body',
258
+ heading: {
259
+ h1: 'text-h1',
260
+ h2: 'text-h2',
261
+ h3: 'text-h3'
262
+ },
263
+ link: 'editor-link',
264
+ text: {
265
+ bold: 'editor-text-bold',
266
+ italic: 'editor-text-italic',
267
+ underline: 'editor-text-underline',
268
+ strikethrough: 'editor-text-strikethrough',
269
+ underlineStrikethrough: 'editor-text-underline-strikethrough'
270
+ }
271
+ },
272
+ nodes: [
273
+ HeadingNode,
274
+ LinkNode,
275
+ ParagraphNode
276
+ ],
277
+ onError: console.error
278
+ }
279
+
280
+ const editor = createEditor(config);
281
+
282
+ onMounted((): void => {
283
+ const contentEditableElement = document.getElementById(id);
284
+ editor.setRootElement(contentEditableElement);
285
+ registerRichText(editor);
286
+ registerHistory(editor, createEmptyHistoryState(), 250);
287
+
288
+ if (modelValue.value != null) {
289
+ editor.update((): void => {
290
+ editor.setEditorState(editor.parseEditorState(modelValue.value));
291
+ });
292
+ }
293
+ });
294
+
295
+ const readonly = computed((): boolean => {
296
+ return variant.value === "readonly";
297
+ });
298
+
299
+ const style = computed((): {[code: string]: string} & Partial<CSSStyleDeclaration> => {
300
+ let minHeight: string | undefined = undefined;
301
+ const base = useBreakpoints().isMobileSized() ? 30 : 42;
302
+ const row = useBreakpoints().isMobileSized() ? 16 : 20;
303
+ if (rows.value > 1) {
304
+ minHeight = `${base + (rows.value - 1) * row}px`;
305
+ }
306
+ else {
307
+ minHeight = `${base}px`;
308
+ }
309
+
310
+ switch (variant.value) {
311
+ case "standard": {
312
+ if (!editable.value) {
313
+ return {
314
+ "--fs-rich-text-field-undo-cursor" : "default",
315
+ "--fs-rich-text-field-icon-cursor" : "default",
316
+ "--fs-rich-text-field-border-color" : lights.base,
317
+ "--fs-rich-text-field-color" : lights.dark,
318
+ "--fs-rich-text-field-active-border-color": lights.base,
319
+ "--fs-rich-text-field-link-color" : linkColors.light,
320
+ "--fs-rich-text-field-min-height" : minHeight
321
+ };
322
+ }
323
+ else {
324
+ return {
325
+ "--fs-rich-text-field-undo-cursor" : canUndo ? "pointer" : "default",
326
+ "--fs-rich-text-field-icon-cursor" : "pointer",
327
+ "--fs-rich-text-field-border-color" : colors.base,
328
+ "--fs-rich-text-field-color" : darks.base,
329
+ "--fs-rich-text-field-active-border-color": colors.dark,
330
+ "--fs-rich-text-field-link-color" : linkColors.dark,
331
+ "--fs-rich-text-field-min-height" : minHeight
332
+ };
333
+ }
334
+ };
335
+ case "readonly": return {
336
+ "--fs-rich-text-field-border-color" : "transparent",
337
+ "--fs-rich-text-field-color" : darks.base,
338
+ "--fs-rich-text-field-active-border-color": "transparent",
339
+ "--fs-rich-text-field-link-color" : linkColors.dark,
340
+ "--fs-rich-text-field-min-height" : minHeight
341
+ }
342
+ }
343
+ });
344
+
345
+ const toolbarColors = computed((): {[code: string]: string} => {
346
+ if (editable.value) {
347
+ return {
348
+ undo: canUndo.value ? darks.base : lights.base,
349
+ bold: isBold.value ? darks.base : lights.base,
350
+ italic: isItalic.value ? darks.base : lights.base,
351
+ underline: isUnderline.value ? darks.base : lights.base,
352
+ strikethrough: isStrikethrough.value ? darks.base : lights.base,
353
+ link: isLink.value ? darks.base : lights.base
354
+ };
355
+ }
356
+ else {
357
+ return {
358
+ undo: lights.base,
359
+ bold: lights.base,
360
+ italic: lights.base,
361
+ underline: lights.base,
362
+ strikethrough: lights.base,
363
+ link: lights.base
364
+ };
365
+ }
366
+ });
367
+
368
+ const updateToolbar = (): void => {
369
+ const selection = $getSelection();
370
+ if ($isRangeSelection(selection)) {
371
+ isBold.value = selection.hasFormat("bold");
372
+ isItalic.value = selection.hasFormat("italic");
373
+ isUnderline.value = selection.hasFormat("underline");
374
+ isStrikethrough.value = selection.hasFormat("strikethrough");
375
+ isLink.value = $isLinkNode(getSelectedNode(selection)) || $isLinkNode(getSelectedNode(selection).getParent());
376
+ }
377
+ };
378
+
379
+ editor.registerUpdateListener(({ editorState }) => {
380
+ editorState.read(() => {
381
+ updateToolbar();
382
+ emit("update:modelValue", JSON.stringify(editorState.toJSON()));
383
+ });
384
+ });
385
+
386
+ editor.registerCommand(CAN_UNDO_COMMAND, (payload) => {
387
+ canUndo.value = payload;
388
+ return false;
389
+ }, 1);
390
+
391
+ const formatText = (type: HeadingTagType) => {
392
+ editor.update(() => {
393
+ const selection = $getSelection();
394
+
395
+ if ($isRangeSelection(selection)) {
396
+ $wrapNodes(selection, () => $createHeadingNode(type));
397
+ }
398
+ });
399
+ };
400
+
401
+ const formatParagraph = (): void => {
402
+ editor.update(() => {
403
+ const selection = $getSelection();
404
+
405
+ if ($isRangeSelection(selection)) {
406
+ $wrapNodes(selection, () => $createParagraphNode());
407
+ }
408
+ });
409
+ };
410
+
411
+ const openLink = (): void => {
412
+ if (!isLink.value) {
413
+ isLink.value = true;
414
+ }
415
+ else {
416
+ editor.update(() => {
417
+ const selection = $getSelection();
418
+
419
+ if ($isRangeSelection(selection)) {
420
+ toggleLink();
421
+ }
422
+ });
423
+ }
424
+ };
425
+
426
+ const toggleLink = (): void => {
427
+ editor.update(() => {
428
+ const target = "_blank";
429
+ const title = "";
430
+ const rel = "noreferrer";
431
+ const selection = $getSelection();
432
+ $setSelection(null);
433
+
434
+ const nodes = selection.extract();
435
+
436
+ if (linkUrl.value === null) {
437
+ // Remove LinkNodes
438
+ nodes.forEach((node) => {
439
+ const parent = node.getParent();
440
+
441
+ if ($isLinkNode(parent)) {
442
+ const children = parent.getChildren();
443
+
444
+ for (let i = 0; i < children.length; i++) {
445
+ parent.insertBefore(children[i]);
446
+ }
447
+
448
+ parent.remove();
449
+ }
450
+ });
451
+ }
452
+ else {
453
+ if (nodes.length === 1) {
454
+ const firstNode = nodes[0];
455
+ const linkNode = getAncestor(firstNode, $isLinkNode);
456
+
457
+ if (linkNode !== null) {
458
+ linkNode.setURL(linkUrl.value);
459
+ if (target !== undefined) {
460
+ linkNode.setTarget(target);
461
+ }
462
+ if (rel !== null) {
463
+ linkNode.setRel(rel);
464
+ }
465
+ if (title !== undefined) {
466
+ linkNode.setTitle(title);
467
+ }
468
+ return;
469
+ }
470
+ }
471
+
472
+ let prevParent: ElementNode | LinkNode | null = null;
473
+ let linkNode: LinkNode | null = null;
474
+
475
+ nodes.forEach((node) => {
476
+ const parent = node.getParent();
477
+
478
+ if ( parent === linkNode || parent === null || ($isElementNode(node) && !node.isInline())) {
479
+ return;
480
+ }
481
+
482
+ if ($isLinkNode(parent)) {
483
+ linkNode = parent;
484
+ parent.setURL(linkUrl.value);
485
+ if (target !== undefined) {
486
+ parent.setTarget(target);
487
+ }
488
+ if (rel !== null) {
489
+ linkNode.setRel(rel);
490
+ }
491
+ if (title !== undefined) {
492
+ linkNode.setTitle(title);
493
+ }
494
+ return;
495
+ }
496
+
497
+ if (!parent.is(prevParent)) {
498
+ prevParent = parent;
499
+ linkNode = $createLinkNode(linkUrl.value, {rel, target, title});
500
+
501
+ if ($isLinkNode(parent)) {
502
+ if (node.getPreviousSibling() === null) {
503
+ parent.insertBefore(linkNode);
504
+ }
505
+ else {
506
+ parent.insertAfter(linkNode);
507
+ }
508
+ }
509
+ else {
510
+ node.insertBefore(linkNode);
511
+ }
512
+ }
513
+
514
+ if ($isLinkNode(node)) {
515
+ if (node.is(linkNode)) {
516
+ return;
517
+ }
518
+ if (linkNode !== null) {
519
+ const children = node.getChildren();
520
+
521
+ for (let i = 0; i < children.length; i++) {
522
+ linkNode.append(children[i]);
523
+ }
524
+ }
525
+
526
+ node.remove();
527
+ return;
528
+ }
529
+
530
+ if (linkNode !== null) {
531
+ linkNode.append(node);
532
+ }
533
+ });
534
+ }
535
+ });
536
+ isLink.value = false;
537
+ }
538
+
539
+ return {
540
+ readonly,
541
+ editable,
542
+ style,
543
+ id,
544
+ editor,
545
+ isLink,
546
+ linkUrl,
547
+ toolbarColors,
548
+ openLink,
549
+ toggleLink,
550
+ formatText,
551
+ formatParagraph,
552
+ UNDO_COMMAND,
553
+ FORMAT_TEXT_COMMAND,
554
+ FORMAT_ELEMENT_COMMAND,
555
+ }
556
+ }
557
+ });
558
+ </script>