@westartcom/bread-quasar-fields 0.0.2

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,119 @@
1
+ <template>
2
+ <div class="q-mb-md" :id="id">
3
+ <div class="field-wrap-inner">
4
+ <q-input
5
+ :label="field.label + ((field.required) ? ' *' : '')"
6
+ :name="field.name"
7
+ :id="id + '-input'"
8
+ :placeholder="field.placeholder"
9
+ type="text"
10
+ class="field"
11
+ :class="{'invalid': (invalid && internalErrors && internalErrors.length)}"
12
+ :maxlength="(field.extra_data && field.extra_data.max) ? field.extra_data.max : null"
13
+ :minlength="(field.extra_data && field.extra_data.min) ? field.extra_data.min : null"
14
+ v-model="internalValue"
15
+ :rules="(field.required) ? [val => !!val || 'Field is required'] : []"
16
+ :hint="field.description"
17
+ outlined>
18
+ </q-input>
19
+ </div>
20
+
21
+ <div class="validation-errors" v-if="invalid && internalErrors && internalErrors.length">
22
+ <span class="error danger-txt-color" v-for="(error, index) in internalErrors" :key="index + 'error'">{{ error }}</span>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script lang="ts">
28
+ import { defineComponent, PropType } from 'vue';
29
+ import { ModelFieldDefinition } from '@westartcom/bread-quasar-fields/interfaces/ModelDefinition';
30
+ import { FieldType } from '@westartcom/bread-quasar-fields/enums/FieldType';
31
+ import { InputType } from '@westartcom/bread-quasar-fields/enums/InputType';
32
+
33
+ interface TextFieldDefinition extends ModelFieldDefinition<FieldType.TEXT, InputType.TEXT> {
34
+ default: string | null;
35
+ extra_data?: {
36
+ max?: number;
37
+ min?: number;
38
+ }
39
+ }
40
+
41
+ export default defineComponent({
42
+ props: {
43
+ id: {
44
+ type: String,
45
+ required: true
46
+ },
47
+ field: {
48
+ type: Object as PropType<TextFieldDefinition>,
49
+ required: true
50
+ },
51
+ modelValue: String,
52
+ errors: Array as PropType<string[]>
53
+ },
54
+
55
+ emits: [
56
+ 'update:modelValue',
57
+ 'update:invalid'
58
+ ],
59
+
60
+ data() {
61
+ return {
62
+ invalid: null as boolean | null,
63
+ internalValue: '',
64
+ internalErrors: [] as string[]
65
+ };
66
+ },
67
+
68
+ mounted() {
69
+ if (this.modelValue) {
70
+ this.internalValue = this.modelValue;
71
+ } else if (this.field.default) {
72
+ this.internalValue = this.field.default;
73
+ }
74
+
75
+ this.validate(this.internalValue);
76
+ },
77
+
78
+ watch: {
79
+ modelValue: function(val) {
80
+ if (val !== this.internalValue) {
81
+ this.internalValue = val;
82
+ }
83
+ },
84
+
85
+ internalValue: function(val) {
86
+ this.validate(val);
87
+ this.$emit('update:modelValue', val);
88
+ },
89
+
90
+ errors: {
91
+ handler: function(val) {
92
+ this.internalErrors = val;
93
+ },
94
+ deep: true
95
+ },
96
+
97
+ internalErrors: {
98
+ handler: function(val) {
99
+ this.invalid = !!(val && val.length);
100
+ },
101
+ deep: true
102
+ },
103
+
104
+ invalid: function(val) {
105
+ this.$emit('update:invalid', val);
106
+ }
107
+ },
108
+
109
+ methods: {
110
+ validate: function(val) {
111
+ this.invalid = this.field.required && !val;
112
+ }
113
+ }
114
+ });
115
+ </script>
116
+
117
+ <style scoped lang="scss">
118
+
119
+ </style>
@@ -0,0 +1,120 @@
1
+ <template>
2
+ <div class="field-wrap" :id="id">
3
+ <div class="field-wrap-inner">
4
+ <div class="field-label-wrap">
5
+ <label v-if="field.label" :for="id + '-input'">{{ field.label }}<span class="required-asterix" v-if="field.required"> *</span></label>
6
+ </div>
7
+
8
+ <textarea
9
+ :name="field.name"
10
+ :id="id + '-input'"
11
+ :placeholder="field.placeholder"
12
+ :maxlength="(field.extra_data && field.extra_data.max) ? field.extra_data.max : null"
13
+ :minlength="(field.extra_data && field.extra_data.min) ? field.extra_data.min : null"
14
+ rows="5"
15
+ class="field"
16
+ :class="{'invalid': (invalid && internalErrors && internalErrors.length)}"
17
+ v-model="internalValue">
18
+ </textarea>
19
+ </div>
20
+
21
+ <div class="field-description" v-if="field.description">{{ field.description }}</div>
22
+
23
+ <div class="validation-errors" v-if="invalid && internalErrors && internalErrors.length">
24
+ <span class="error danger-txt-color" v-for="(error, index) in internalErrors" :key="index + 'error'">{{ error }}</span>
25
+ </div>
26
+ </div>
27
+ </template>
28
+
29
+ <script lang="ts">
30
+ import { defineComponent, PropType } from 'vue';
31
+ import { ModelFieldDefinition } from '@westartcom/bread-quasar-fields/interfaces/ModelDefinition';
32
+ import { FieldType } from '@westartcom/bread-quasar-fields/enums/FieldType';
33
+ import { InputType } from '@westartcom/bread-quasar-fields/enums/InputType';
34
+
35
+ interface TextareaFieldDefinition extends ModelFieldDefinition<FieldType.TEXT, InputType.TEXTAREA> {
36
+ default: string | null;
37
+ extra_data?: {
38
+ max?: number;
39
+ min?: number;
40
+ }
41
+ }
42
+
43
+ export default defineComponent({
44
+ props: {
45
+ id: {
46
+ type: String,
47
+ required: true
48
+ },
49
+ field: {
50
+ type: Object as PropType<TextareaFieldDefinition>,
51
+ required: true
52
+ },
53
+ modelValue: String,
54
+ errors: Array as PropType<string[]>
55
+ },
56
+
57
+ emits: [
58
+ 'update:modelValue',
59
+ 'update:invalid'
60
+ ],
61
+
62
+ data() {
63
+ return {
64
+ invalid: null as boolean | null,
65
+ internalValue: '',
66
+ internalErrors: [] as string[]
67
+ };
68
+ },
69
+
70
+ mounted() {
71
+ if (this.modelValue) {
72
+ this.internalValue = this.modelValue;
73
+ } else if (this.field.default) {
74
+ this.internalValue = this.field.default;
75
+ }
76
+
77
+ this.validate(this.internalValue);
78
+ },
79
+
80
+ watch: {
81
+ modelValue: function(val) {
82
+ if (val !== this.internalValue) {
83
+ this.internalValue = val;
84
+ }
85
+ },
86
+
87
+ internalValue: function(val) {
88
+ this.validate(val);
89
+ this.$emit('update:modelValue', val);
90
+ },
91
+
92
+ errors: {
93
+ handler: function(val) {
94
+ this.internalErrors = val;
95
+ },
96
+ deep: true
97
+ },
98
+
99
+ internalErrors: {
100
+ handler: function(val) {
101
+ this.invalid = !!(val && val.length);
102
+ },
103
+ deep: true
104
+ },
105
+
106
+ invalid: function(val) {
107
+ this.$emit('update:invalid', val);
108
+ }
109
+ },
110
+
111
+ methods: {
112
+ validate: function(val) {
113
+ this.invalid = this.field.required && !val;
114
+ }
115
+ }
116
+ });
117
+ </script>
118
+
119
+ <style scoped lang="scss">
120
+ </style>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <div class="wysiwyg-display" v-html="output"></div>
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ import { generateHTML } from '@tiptap/html';
7
+ import { JSONContent } from '@tiptap/core';
8
+ import StarterKit from '@tiptap/starter-kit';
9
+ import { defineComponent } from 'vue';
10
+
11
+ export default defineComponent({
12
+ props: {
13
+ modelValue: String
14
+ },
15
+
16
+ mounted() {
17
+ if (this.modelValue) {
18
+ this.internalValue = JSON.parse(this.modelValue);
19
+ }
20
+ },
21
+
22
+ data() {
23
+ return {
24
+ internalValue: null as JSONContent | null
25
+ };
26
+ },
27
+
28
+ computed: {
29
+ output() {
30
+ if (this.internalValue) {
31
+ return generateHTML(this.internalValue, [
32
+ StarterKit
33
+ ]);
34
+ }
35
+
36
+ return '';
37
+ }
38
+ },
39
+
40
+ watch: {
41
+ modelValue: function(val) {
42
+ const stringRepVal = (this.internalValue) ? JSON.stringify(this.internalValue) : '';
43
+ if (val !== stringRepVal) {
44
+ this.internalValue = (val ? JSON.parse(val) : null);
45
+ }
46
+ }
47
+ }
48
+ });
49
+ </script>
50
+
51
+ <style lang="scss">
52
+
53
+ </style>
@@ -0,0 +1,263 @@
1
+ <template>
2
+ <div class="field-wrap" :id="id">
3
+ <div class="field-wrap-inner">
4
+ <div class="field-label-wrap">
5
+ <label v-if="field.label">{{ field.label }}<span class="required-asterix" v-if="field.required"> *</span></label>
6
+ </div>
7
+
8
+ <div class="editor-wrap">
9
+ <div class="menubar" v-if="editor">
10
+ <button
11
+ type="button"
12
+ :class="{ 'is-active': editor.isActive('bold') }"
13
+ @click="editor.chain().focus().toggleBold().run()">
14
+ <bold-icon class="icon-1-5x"></bold-icon>
15
+ </button>
16
+
17
+ <button
18
+ type="button"
19
+ :class="{ 'is-active': editor.isActive('italic') }"
20
+ @click="editor.chain().focus().toggleItalic().run()">
21
+ <italic-icon class="icon-1-5x"></italic-icon>
22
+ </button>
23
+
24
+ <button
25
+ type="button"
26
+ :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
27
+ @click="editor.chain().focus().toggleHeading({ level: 2 }).run()">
28
+ H2
29
+ </button>
30
+
31
+ <button
32
+ type="button"
33
+ :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
34
+ @click="editor.chain().focus().toggleHeading({ level: 3 }).run()">
35
+ H3
36
+ </button>
37
+
38
+ <button
39
+ type="button"
40
+ :class="{ 'is-active': editor.isActive('heading', { level: 4 }) }"
41
+ @click="editor.chain().focus().toggleHeading({ level: 4 }).run()">
42
+ H4
43
+ </button>
44
+
45
+ <button
46
+ type="button"
47
+ :class="{ 'is-active': editor.isActive('bulletList') }"
48
+ @click="editor.chain().focus().toggleBulletList().run()">
49
+ <bullet-list-icon class="icon-1-5x"></bullet-list-icon>
50
+ </button>
51
+
52
+ <button
53
+ type="button"
54
+ :class="{ 'is-active': editor.isActive('orderedList') }"
55
+ @click="editor.chain().focus().toggleOrderedList().run()">
56
+ <ordered-list-icon class="icon-1-5x"></ordered-list-icon>
57
+ </button>
58
+
59
+ <button
60
+ type="button"
61
+ :class="{ 'is-active': editor.isActive('blockquote') }"
62
+ @click="editor.chain().focus().toggleBlockquote().run()">
63
+ <quote-icon class="icon-1-5x"></quote-icon>
64
+ </button>
65
+ </div>
66
+ <editor-content :editor="editor" />
67
+ </div>
68
+ </div>
69
+
70
+ <div class="field-description" v-if="field.description">{{ field.description }}</div>
71
+
72
+ <div class="validation-errors" v-if="invalid && internalErrors && internalErrors.length">
73
+ <span class="error danger-txt-color" v-for="(error, index) in internalErrors" :key="index + 'error'">{{ error }}</span>
74
+ </div>
75
+ </div>
76
+ </template>
77
+
78
+ <script lang="ts">
79
+ import { defineAsyncComponent, defineComponent, PropType } from 'vue';
80
+ import debounce from 'lodash-es/debounce';
81
+ import { Editor, EditorContent } from '@tiptap/vue-3';
82
+ import StarterKit from '@tiptap/starter-kit';
83
+ import { JSONContent } from '@tiptap/core';
84
+ import { ModelFieldDefinition } from '@westartcom/bread-quasar-fields/interfaces/ModelDefinition';
85
+ import { FieldType } from '@westartcom/bread-quasar-fields/enums/FieldType';
86
+ import { InputType } from '@westartcom/bread-quasar-fields/enums/InputType';
87
+
88
+ interface TextareaFieldDefinition extends ModelFieldDefinition<FieldType.JSON, InputType.WYSIWYG> {
89
+ default: JSONContent | null;
90
+ }
91
+
92
+ export default defineComponent({
93
+ components: {
94
+ EditorContent,
95
+ BoldIcon: defineAsyncComponent(() => import('@westartcom/bread-quasar-fields/icons/BoldIcon.vue')),
96
+ ItalicIcon: defineAsyncComponent(() => import('@westartcom/bread-quasar-fields/icons/ItalicIcon.vue')),
97
+ BulletListIcon: defineAsyncComponent(() => import('@westartcom/bread-quasar-fields/icons/BulletListIcon.vue')),
98
+ OrderedListIcon: defineAsyncComponent(() => import('@westartcom/bread-quasar-fields/icons/OrderedListIcon.vue')),
99
+ QuoteIcon: defineAsyncComponent(() => import('@westartcom/bread-quasar-fields/icons/QuoteIcon.vue')),
100
+ },
101
+
102
+ props: {
103
+ id: {
104
+ type: String,
105
+ required: true
106
+ },
107
+ field: {
108
+ type: Object as PropType<TextareaFieldDefinition>,
109
+ required: true
110
+ },
111
+ modelValue: [String, Object],
112
+ errors: Array as PropType<string[]>
113
+ },
114
+
115
+ emits: [
116
+ 'update:modelValue',
117
+ 'update:invalid'
118
+ ],
119
+
120
+ data() {
121
+ return {
122
+ editor: null as Editor | null,
123
+ invalid: null as boolean | null,
124
+ internalValue: null as JSONContent | null,
125
+ internalErrors: [] as string[]
126
+ };
127
+ },
128
+
129
+ mounted() {
130
+ if (this.modelValue) {
131
+ this.internalValue = (typeof this.modelValue === 'string') ? JSON.parse(this.modelValue) : this.modelValue;
132
+ } else if (this.field.default) {
133
+ this.internalValue = this.field.default;
134
+ }
135
+
136
+ this.setupEditor(this.internalValue);
137
+ this.validate(this.internalValue);
138
+ },
139
+
140
+ beforeUnmount()
141
+ {
142
+ this.editor?.destroy();
143
+ },
144
+
145
+ watch: {
146
+ modelValue: function(val) {
147
+ const stringRepInternalVal = (this.internalValue) ? JSON.stringify(this.internalValue) : '';
148
+ const stringRepVal = (typeof val === 'string') ? val : ((val) ? JSON.stringify(val) : '');
149
+ if (stringRepVal !== stringRepInternalVal) {
150
+ this.editor?.commands.setContent(val ? JSON.parse(val) : null, false);
151
+ }
152
+ },
153
+
154
+ internalValue: function(val) {
155
+ this.validate(val);
156
+ this.$emit('update:modelValue', (val) ? JSON.stringify(val) : '');
157
+ },
158
+
159
+ errors: {
160
+ handler: function(val) {
161
+ this.internalErrors = val;
162
+ },
163
+ deep: true
164
+ },
165
+
166
+ internalErrors: {
167
+ handler: function(val) {
168
+ this.invalid = !!(val && val.length);
169
+ },
170
+ deep: true
171
+ },
172
+
173
+ invalid: function(val) {
174
+ this.$emit('update:invalid', val);
175
+ }
176
+ },
177
+
178
+ methods: {
179
+ setupEditor(content) {
180
+ this.editor = new Editor({
181
+ content,
182
+ extensions: [
183
+ StarterKit
184
+ ],
185
+ onUpdate: debounce(this.onUpdate, 300)
186
+ });
187
+ },
188
+
189
+ onUpdate() {
190
+ if (!this.editor) {
191
+ return;
192
+ }
193
+
194
+ this.internalValue = this.editor.getJSON();
195
+ },
196
+
197
+ validate(val) {
198
+ if (!this.editor) {
199
+ return;
200
+ }
201
+
202
+ const hasContent = this.editor.state &&
203
+ this.editor.state.doc &&
204
+ this.editor.state.doc.textContent &&
205
+ this.editor.state.doc.textContent.length;
206
+
207
+ this.invalid = (this.field.required && !hasContent);
208
+ }
209
+ }
210
+ });
211
+ </script>
212
+
213
+ <style lang="scss">
214
+ .editor-wrap {
215
+ max-width: var(--max-content-width);
216
+ width: 100%;
217
+ }
218
+
219
+ [contenteditable] {
220
+ border: 2px solid var(--color-medium);
221
+ border-radius: var(--global-radius);
222
+ -webkit-user-select: text;
223
+ user-select: text;
224
+ padding: var(--global-spacing-half) var(--global-spacing);
225
+ outline: none;
226
+ min-height: 100px;
227
+ line-height: 1.1;
228
+ max-height: 900px;
229
+ overflow-y: auto;
230
+
231
+ &:focus {
232
+ border-color: var(--color-primary);
233
+ }
234
+ }
235
+
236
+ .menubar {
237
+ display: flex;
238
+ align-items: stretch;
239
+ margin-bottom: var(--global-spacing-half);
240
+
241
+ button {
242
+ display: inline-flex;
243
+ align-items: center;
244
+ background: none;
245
+ border: 2px solid transparent;
246
+ outline: none;
247
+ padding: var(--global-spacing-half);
248
+ border-radius: calc(var(--global-radius) / 2);
249
+ margin-right: var(--global-spacing-half);
250
+ transition: all 0.1s ease;
251
+
252
+ &:hover {
253
+ border-color: var(--color-primary);
254
+ }
255
+
256
+ &.is-active {
257
+ background-color: var(--color-primary);
258
+ color: var(--color-light-txt);
259
+ box-shadow: var(--global-radius);
260
+ }
261
+ }
262
+ }
263
+ </style>
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <svg-icon type="fa" :path="iconPath"></svg-icon>
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ import SvgIcon from '@westartcom/bread-quasar-fields/icons/SvgBaseIcon.vue';
7
+ import { defineComponent } from 'vue';
8
+ import {farSquare} from '@quasar/extras/fontawesome-v6';
9
+
10
+ export default defineComponent({
11
+ components: {
12
+ SvgIcon
13
+ },
14
+ data() {
15
+ return {
16
+ iconPath: farSquare
17
+ };
18
+ }
19
+ });
20
+ </script>
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <svg-icon type="fa" :path="iconPath"></svg-icon>
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ import SvgIcon from '@westartcom/bread-quasar-fields/icons/SvgBaseIcon.vue';
7
+ import { defineComponent } from 'vue';
8
+ import {farSquareCheck} from '@quasar/extras/fontawesome-v6';
9
+
10
+ export default defineComponent({
11
+ components: {
12
+ SvgIcon
13
+ },
14
+ data() {
15
+ return {
16
+ iconPath: farSquareCheck
17
+ };
18
+ }
19
+ });
20
+ </script>
@@ -0,0 +1,70 @@
1
+ <template>
2
+ <svg
3
+ :width="sizeValue"
4
+ :height="sizeValue"
5
+ :viewBox="viewboxValue"
6
+ :style="styles">
7
+ <path :d="path" />
8
+ </svg>
9
+ </template>
10
+
11
+ <script lang="ts">
12
+ import { defineComponent, PropType } from 'vue';
13
+
14
+ const types = {
15
+ mdi: {
16
+ size: 24,
17
+ viewbox: '0 0 24 24',
18
+ },
19
+ default: {
20
+ size: 0,
21
+ viewbox: '0 0 0 0',
22
+ },
23
+ fa: {
24
+ size: 24,
25
+ viewbox: '0 0 24 24',
26
+ }
27
+ };
28
+
29
+ export default defineComponent({
30
+ props: {
31
+ type: String,
32
+ path: { type: String, required: true },
33
+ size: { type: [String, Number], default: 24 },
34
+ viewbox: String,
35
+ flip: {
36
+ type: String as PropType<'horizontal' | 'vertical' | 'both' | 'none'>,
37
+ validator: (value: string) =>
38
+ ['horizontal', 'vertical', 'both', 'none'].includes(value),
39
+ },
40
+ rotate: { type: Number, default: 0 },
41
+ },
42
+ computed: {
43
+ styles() {
44
+ return {
45
+ '--sx': this.flip && ['both', 'horizontal'].includes(this.flip) ? '-1' : '1',
46
+ '--sy': this.flip && ['both', 'vertical'].includes(this.flip) ? '-1' : '1',
47
+ '--r': isNaN(this.rotate) ? this.rotate : this.rotate + 'deg',
48
+ };
49
+ },
50
+ defaults() {
51
+ return (this.type && types[this.type]) ? types[this.type] : types.default;
52
+ },
53
+ sizeValue() {
54
+ return this.size || this.defaults.size;
55
+ },
56
+ viewboxValue() {
57
+ return this.viewbox || this.defaults.viewbox;
58
+ },
59
+ },
60
+ });
61
+ </script>
62
+
63
+ <style scoped lang="scss">
64
+ svg {
65
+ transform: rotate(var(--r, 0deg)) scale(var(--sx, 1), var(--sy, 1));
66
+ }
67
+ path {
68
+ fill: currentColor;
69
+ }
70
+ </style>