@bagelink/vue 0.0.378 → 0.0.384

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.
@@ -1,13 +1,27 @@
1
1
  <template>
2
2
  <Modal
3
- @onUpdate:isModalVisible="props['onUpdate:isModalVisible']" :side="side" ref="modal" :dismissable="dismissable"
4
- :title="title"
5
- >
6
- <BagelForm @submit="runSubmit" ref="form" v-model="formData" :schema="computedFormSchema" />
3
+ @onUpdate:isModalVisible="props['onUpdate:isModalVisible']"
4
+ :side
5
+ ref="modal"
6
+ :width
7
+ :dismissable
8
+ :title="title">
9
+ <BagelForm
10
+ @submit="runSubmit"
11
+ ref="form"
12
+ v-model="formData"
13
+ :schema="computedFormSchema" />
7
14
  <template #footer v-if="onDelete || onSubmit">
8
15
  <div>
9
16
  <Btn thin flat value="Cancel" @click="closeModal" />
10
- <Btn thin icon="delete" v-if="onDelete" flat value="Delete" @click="runDelete" color="red" />
17
+ <Btn
18
+ thin
19
+ icon="delete"
20
+ v-if="onDelete"
21
+ flat
22
+ value="Delete"
23
+ @click="runDelete"
24
+ color="red" />
11
25
  </div>
12
26
  <Btn value="Submit" @click="runSubmit" />
13
27
  </template>
@@ -16,28 +30,33 @@
16
30
 
17
31
  <script lang="ts" setup>
18
32
  import {
19
- Modal, Btn, BagelForm, type BglFormSchemaT, type BtnOptions, useBagel,
33
+ Modal,
34
+ Btn,
35
+ BagelForm,
36
+ type BglFormSchemaT,
37
+ type BtnOptions,
38
+ useBagel,
20
39
  } from '@bagelink/vue';
21
40
 
22
41
  const bagel = useBagel();
23
42
 
24
43
  const props = defineProps<{
25
- side?: boolean;
26
- title?: string;
27
- dismissable?: boolean;
28
- actions?: BtnOptions[];
29
- schema: BglFormSchemaT | (() => BglFormSchemaT);
44
+ 'side'?: boolean;
45
+ 'title'?: string;
46
+ 'width'?: string;
47
+ 'dismissable'?: boolean;
48
+ 'actions'?: BtnOptions[];
49
+ 'schema': BglFormSchemaT | (() => BglFormSchemaT);
30
50
  // eslint-disable-next-line no-unused-vars
31
- onSubmit?: ((formData: any) => Promise<void>);
51
+ 'onSubmit'?: (formData: any) => Promise<void>;
32
52
  // eslint-disable-next-line no-unused-vars
33
- onDelete?: ((id: string) => void);
53
+ 'onDelete'?: (id: string) => void;
34
54
  // eslint-disable-next-line no-unused-vars
35
- 'onUpdate:isModalVisible'?: ((visible: boolean) => void);
55
+ 'onUpdate:isModalVisible'?: (visible: boolean) => void;
36
56
  // eslint-disable-next-line no-unused-vars
37
- onError?: ((err: any) => void);
38
-
39
- modelValue?: Record<string, any>;
57
+ 'onError'?: (err: any) => void;
40
58
 
59
+ 'modelValue'?: Record<string, any>;
41
60
  }>();
42
61
 
43
62
  const modal = $ref<InstanceType<typeof Modal>>();
@@ -47,7 +66,9 @@ const computedFormSchema = $computed(() => {
47
66
  return props.schema;
48
67
  });
49
68
 
50
- const formData = defineModel<Record<string, any>>('modelValue', { default: {} });
69
+ const formData = defineModel<Record<string, any>>('modelValue', {
70
+ default: {},
71
+ });
51
72
  const form = $ref<InstanceType<typeof BagelForm>>();
52
73
  const closeModal = () => modal?.closeModal();
53
74
 
@@ -79,17 +79,24 @@ type QueueFile = {
79
79
  uploaded?: boolean;
80
80
  };
81
81
 
82
+ type StrKey = keyof StorageFile;
83
+
84
+ type FSValue = string[] | string | number;
85
+
82
86
  const props = defineProps<{
83
87
  label: string;
84
88
  multiple?: boolean;
85
89
  files?: StorageFile | StorageFile[];
86
90
  deleteEndpoint?: string;
91
+ bindkey?: StrKey;
87
92
  }>();
88
93
 
89
- const file_ids = defineModel<string[] | string>('modelValue');
94
+ const bindKey: StrKey = props.bindkey || 'id';
95
+
96
+ const file_bindkeys = defineModel<FSValue>('modelValue');
90
97
  const storageFiles = $ref<StorageFile[]>([]);
91
98
 
92
- const compareIds = (v1?: string[] | string, v2?: string[] | string) =>
99
+ const compareIds = (v1?: FSValue, v2?: FSValue) =>
93
100
  [v1].flat().join(',') === [v2].flat().join(',');
94
101
 
95
102
  watch(
@@ -103,10 +110,14 @@ watch(
103
110
  watch(
104
111
  () => storageFiles,
105
112
  (newFiles) => {
106
- let idValue: string[] | string = '';
107
- if (props.multiple) idValue = newFiles.map((f) => f.id || '');
108
- else idValue = newFiles[0]?.id || '';
109
- if (!compareIds(file_ids?.value, idValue)) file_ids.value = idValue;
113
+ let idValue: FSValue;
114
+ if (props.multiple) idValue = newFiles.map((f) => f[bindKey]) as FSValue;
115
+ else {
116
+ idValue = (newFiles[0]?.[bindKey] as string) || '';
117
+ }
118
+ if (!compareIds(file_bindkeys?.value, idValue)) {
119
+ file_bindkeys.value = idValue;
120
+ }
110
121
  },
111
122
  { deep: true }
112
123
  );
@@ -0,0 +1,296 @@
1
+ <template>
2
+ <div class="bagel-input">
3
+ <label>
4
+ {{ label }}
5
+ </label>
6
+ <div
7
+ @click="browse"
8
+ @dragover="dragover"
9
+ @drop="drop"
10
+ @dragleave="dragleave"
11
+ class="fileUploadWrap"
12
+ :class="{
13
+ fileDropZone: !storageFiles.length && !fileQueue.length,
14
+ dragover: isDragOver,
15
+ }">
16
+ <template v-for="file in storageFiles" :key="file.id">
17
+ <div class="imagePreviewWrap" v-if="!multiple">
18
+ <img class="preview" :src="file.url" alt="" />
19
+ </div>
20
+ <div class="previewName">
21
+ <img
22
+ height="60"
23
+ v-if="multiple"
24
+ class="preview"
25
+ :src="file.url"
26
+ alt="" />
27
+ <p class="no-margin">
28
+ {{ file.name }}
29
+ </p>
30
+ <Btn
31
+ thin
32
+ @click.stop="removeFile(file)"
33
+ flat
34
+ icon="delete"
35
+ color="red" />
36
+ </div>
37
+ </template>
38
+ <template v-for="fileQ in fileQueue" :key="fileQ.file">
39
+ <div class="imagePreviewWrap" v-if="!multiple">
40
+ <img class="preview" :src="fileToUrl(fileQ.file)" alt="" />
41
+ </div>
42
+ <div class="previewName">
43
+ <img
44
+ height="60"
45
+ v-if="multiple"
46
+ class="preview"
47
+ :src="fileToUrl(fileQ.file)"
48
+ alt="" />
49
+ <p class="no-margin">
50
+ {{ fileQ.name }}
51
+ </p>
52
+ <div
53
+ class="pie"
54
+ :style="`--p:${fileQ.progress}`"
55
+ style="--b: 2px"
56
+ :class="{ complete: fileQ.progress === 100 }">
57
+ <span class="progress" v-if="fileQ.progress < 100">
58
+ {{ `${fileQ.progress.toFixed(0)}` }}
59
+ </span>
60
+ <MaterialIcon class="success" icon="check" />
61
+ </div>
62
+ </div>
63
+ </template>
64
+ </div>
65
+ </div>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { watch } from 'vue';
70
+ import { Btn, type StorageFile, useBagel, MaterialIcon } from '@bagelink/vue';
71
+
72
+ const bagel = useBagel();
73
+
74
+ type QueueFile = {
75
+ file: File;
76
+ progress: number;
77
+ uploading?: boolean;
78
+ name?: string;
79
+ uploaded?: boolean;
80
+ };
81
+
82
+ const props = defineProps<{
83
+ label: string;
84
+ multiple?: boolean;
85
+ files?: StorageFile | StorageFile[];
86
+ deleteEndpoint?: string;
87
+ }>();
88
+
89
+ const fileURLs = defineModel<string[] | string>('modelValue');
90
+ const storageFiles = $ref<StorageFile[]>([]);
91
+
92
+ const compareIds = (v1?: string[] | string, v2?: string[] | string) =>
93
+ [v1].flat().join(',') === [v2].flat().join(',');
94
+
95
+ watch(
96
+ () => props.files,
97
+ (newFiles) => {
98
+ if (newFiles) storageFiles.push(...[newFiles].flat());
99
+ },
100
+ { immediate: true }
101
+ );
102
+
103
+ watch(
104
+ () => storageFiles,
105
+ (newFiles) => {
106
+ let idValue: string[] | string = '';
107
+ if (props.multiple) idValue = newFiles.map((f) => f.url || '');
108
+ else idValue = newFiles[0]?.url || '';
109
+ if (!compareIds(fileURLs?.value, idValue)) fileURLs.value = idValue;
110
+ },
111
+ { deep: true }
112
+ );
113
+
114
+ const fileQueue = $ref<QueueFile[]>([]);
115
+ let isDragOver = $ref(false);
116
+
117
+ const preventDefault = (e: Event) => {
118
+ e.preventDefault();
119
+ e.stopPropagation();
120
+ };
121
+
122
+ const fileToUrl = (file: File) => URL.createObjectURL(file);
123
+ const removeFile = (file: StorageFile) => {
124
+ const index = storageFiles.indexOf(file);
125
+ storageFiles.splice(index, 1);
126
+
127
+ if (props.deleteEndpoint) {
128
+ void bagel.delete(`${props.deleteEndpoint}/${file.id}`);
129
+ }
130
+ };
131
+
132
+ const flushQueue = () => {
133
+ fileQueue.forEach(async (file, i) => {
134
+ if (!props.multiple) storageFiles.splice(0, 1);
135
+ const serverFile = await bagel.uploadFile<StorageFile>(file.file, {
136
+ onUploadProgress: (e: any) => (file.progress = e.progress * 100 - 1),
137
+ });
138
+ storageFiles.push(serverFile);
139
+ fileQueue.splice(i, 1);
140
+ });
141
+ };
142
+
143
+ const browse = () => {
144
+ const input = document.createElement('input');
145
+ input.type = 'file';
146
+ input.multiple = props.multiple;
147
+ input.onchange = (e: Event) => {
148
+ const target = e?.target as HTMLInputElement;
149
+ if (target?.files) {
150
+ Array.from(target.files).forEach((file: File) =>
151
+ fileQueue.push({ name: file.name, file, progress: 0 })
152
+ );
153
+ }
154
+ flushQueue();
155
+ };
156
+ input.click();
157
+ };
158
+
159
+ const dragleave = (e: DragEvent) => {
160
+ preventDefault(e);
161
+ if (e.dataTransfer) isDragOver = false;
162
+ else isDragOver = false;
163
+ };
164
+
165
+ const dragover = (e: DragEvent) => {
166
+ preventDefault(e);
167
+ if (e.dataTransfer) isDragOver = true;
168
+ else isDragOver = false;
169
+ };
170
+
171
+ const drop = (e: DragEvent) => {
172
+ preventDefault(e);
173
+ if (e.dataTransfer) {
174
+ Array.from(e.dataTransfer.files).forEach((file: File) =>
175
+ fileQueue.push({ name: file.name, file, progress: 0 })
176
+ );
177
+ }
178
+ isDragOver = false;
179
+ flushQueue();
180
+ };
181
+ </script>
182
+
183
+ <style scoped>
184
+ .bagel-input .fileUploadWrap {
185
+ outline: 1px solid var(--border-color);
186
+ border-radius: var(--input-border-radius);
187
+ text-align: center;
188
+ cursor: pointer;
189
+ transition: var(--bgl-transition);
190
+ position: relative;
191
+ height: 132px;
192
+ font-size: var(--input-font-size);
193
+ }
194
+
195
+ .previewName {
196
+ padding: 0.5rem;
197
+ text-align: start;
198
+ color: var(--input-color);
199
+ display: grid;
200
+ grid-template-columns: 1fr 22px;
201
+ align-items: center;
202
+ padding-inline: 14px;
203
+ }
204
+
205
+ .previewName p {
206
+ overflow: hidden;
207
+ text-overflow: ellipsis;
208
+ white-space: nowrap;
209
+ }
210
+
211
+ .imagePreviewWrap {
212
+ background: var(--input-bg);
213
+ border-radius: var(--input-border-radius);
214
+ height: 90px;
215
+ padding: 5px;
216
+ }
217
+
218
+ img.preview {
219
+ height: 80px;
220
+ border-radius: var(--input-border-radius);
221
+ object-fit: contain;
222
+ }
223
+
224
+ .fileUploadWrap.dragover,
225
+ .fileUploadWrap:hover {
226
+ box-shadow: inset 0 0 10px #00000012;
227
+ }
228
+
229
+ .bagel-input .fileUploadWrap.fileDropZone {
230
+ background: var(--input-bg);
231
+ display: flex;
232
+ align-items: center;
233
+ justify-content: center;
234
+ color: var(--bgl-gray);
235
+ }
236
+
237
+ .bagel-input .fileUploadWrap.fileDropZone:after {
238
+ content: 'Drop files here or click to upload';
239
+ }
240
+
241
+ .pie {
242
+ width: 30px;
243
+ height: 30px;
244
+ position: relative;
245
+ display: flex;
246
+ align-items: center;
247
+ justify-content: center;
248
+ }
249
+
250
+ .pie:before {
251
+ content: '';
252
+ position: absolute;
253
+ border-radius: 50%;
254
+ inset: 0;
255
+ transition: all 0.2s ease-in-out;
256
+ background: conic-gradient(var(--bgl-primary) calc(var(--p) * 1%), #0000 0);
257
+ -webkit-mask: radial-gradient(
258
+ farthest-side,
259
+ #0000 calc(99% - var(--b)),
260
+ #000 calc(100% - var(--b))
261
+ );
262
+ mask: radial-gradient(
263
+ farthest-side,
264
+ #0000 calc(99% - var(--b)),
265
+ #000 calc(100% - var(--b))
266
+ );
267
+ }
268
+
269
+ .pie .success {
270
+ transform: scale(0);
271
+ opacity: 0;
272
+ transition: all 0.3s ease-in-out;
273
+ }
274
+
275
+ .pie .progress {
276
+ position: absolute;
277
+ font-size: 10px;
278
+ }
279
+
280
+ .pie.complete .progress {
281
+ display: none;
282
+ }
283
+
284
+ .pie.complete .success {
285
+ transform: scale(1.3);
286
+ opacity: 1;
287
+ }
288
+
289
+ .pie.complete:before {
290
+ background: conic-gradient(var(--bgl-green) calc(var(--p) * 1%), #0000 0);
291
+ }
292
+
293
+ .pie.complete {
294
+ color: var(--bgl-green);
295
+ }
296
+ </style>
@@ -2,6 +2,7 @@
2
2
  <div class="RichText">
3
3
  <div class="RichText-tools" v-if="editor">
4
4
  <Btn
5
+ tabindex="-1"
5
6
  :flat="!editor.isActive(item.name, item.option)"
6
7
  @click="item.command"
7
8
  thin
@@ -16,12 +17,21 @@
16
17
  </template>
17
18
 
18
19
  <script setup lang="ts">
19
- import { Btn } from '@bagelink/vue';
20
+ import {
21
+ Btn,
22
+ useModal,
23
+ FileUpload as $el,
24
+ bagelFormUtils,
25
+ BglVideo,
26
+ } from '@bagelink/vue';
20
27
  import { onMounted, onBeforeUnmount, watch } from 'vue';
21
28
  import StarterKit from '@tiptap/starter-kit';
22
29
  import { Editor, EditorContent } from '@tiptap/vue-3';
30
+ import Image from '@tiptap/extension-image';
31
+ import YouTube from '@tiptap/extension-youtube';
23
32
  import type { MaterialIcons } from '@bagelink/vue';
24
-
33
+ // import $el from './FileUploadURL.vue';
34
+ const { txtField } = bagelFormUtils;
25
35
  let editor = $ref<Editor>();
26
36
 
27
37
  const props = defineProps<{ modelValue: string }>();
@@ -30,6 +40,19 @@ const focus = () => {
30
40
  if (!editor) throw new Error('editor is not defined');
31
41
  return editor.chain().focus();
32
42
  };
43
+ const { showModalForm } = useModal();
44
+
45
+ function addImage() {
46
+ showModalForm({
47
+ title: 'Add Image',
48
+ schema: [
49
+ txtField('title', 'Title'),
50
+ { $el, id: 'src', attrs: { bindkey: 'url' } },
51
+ txtField('alt', 'Alt Text'),
52
+ ],
53
+ onSubmit: (imgObj) => focus().setImage(imgObj).run(),
54
+ });
55
+ }
33
56
 
34
57
  const config: {
35
58
  name: string;
@@ -61,11 +84,28 @@ const config: {
61
84
  command: () => focus().toggleCode().run(),
62
85
  icon: 'code',
63
86
  },
64
- // {
65
- // name: 'clearMarks',
66
- // command: () => focus().unsetAllMarks(),
67
- // icon: 'clear',
68
- // },
87
+ {
88
+ name: 'Image',
89
+ command: addImage,
90
+ icon: 'image',
91
+ },
92
+ {
93
+ name: 'YouTube',
94
+ command: () => {
95
+ showModalForm({
96
+ title: 'Add YouTube Video',
97
+ schema: [
98
+ txtField('src', 'YouTube URL'),
99
+ {
100
+ $el: BglVideo,
101
+ attrs: { src: (_, { src }: { src: string }) => src || '' },
102
+ },
103
+ ],
104
+ onSubmit: (videoObj) => editor?.commands.setYoutubeVideo(videoObj),
105
+ });
106
+ },
107
+ icon: 'youtube_activity',
108
+ },
69
109
  {
70
110
  name: 'Clear',
71
111
  command: () => focus().clearNodes().run(),
@@ -160,7 +200,7 @@ const emit = defineEmits(['update:modelValue']);
160
200
 
161
201
  function initEditor() {
162
202
  editor = new Editor({
163
- extensions: [StarterKit],
203
+ extensions: [StarterKit, Image, YouTube],
164
204
  content: props.modelValue,
165
205
  onUpdate: ({ editor }) => emit('update:modelValue', editor.getHTML()),
166
206
  });
@@ -9,6 +9,7 @@ export interface ModalOptions {
9
9
  title?: string;
10
10
  dismissable?: boolean;
11
11
  side?: boolean;
12
+ width?: string;
12
13
  actions?: BtnOptions[];
13
14
  class?: string;
14
15
  visible?: boolean;
@@ -18,6 +19,7 @@ export interface ModalFormOptions {
18
19
  side?: boolean;
19
20
  title?: string;
20
21
  visible?: boolean;
22
+ width?: string;
21
23
  dismissable?: boolean;
22
24
  schema: BglFormSchemaT<any> | (() => BglFormSchemaT) | BglFormSchemaT
23
25
  onSubmit?: (formData: any) => any;