@bagelink/vue 0.0.374 → 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.
@@ -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
  });
@@ -242,10 +282,10 @@ onBeforeUnmount(() => editor?.destroy());
242
282
  }
243
283
 
244
284
  .tiptap {
245
- min-height: 500px;
285
+ min-height: 200px;
246
286
  background: var(--input-bg);
247
287
  overflow: auto;
248
- max-height: 600px;
288
+ max-height: 500px;
249
289
  padding: 0.7rem;
250
290
  color: var(--input-color);
251
291
  border-radius: var(--input-border-radius);
@@ -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;
@@ -1,119 +1,119 @@
1
1
  .bg-dark {
2
- position: fixed;
3
- top: 0;
4
- right: 0;
5
- left: 0;
6
- bottom: 0;
7
- background-color: rgba(0, 0, 0, 0.7);
8
- z-index: 999;
9
- pointer-events: none;
10
- opacity: 0;
11
- transition: all ease-in-out 0.3s;
12
- max-height: 100vh;
13
- overflow: auto;
14
- margin: 0 auto;
15
- width: 100%;
16
- text-align: center;
17
- display: grid;
18
- align-items: center;
2
+ position: fixed;
3
+ top: 0;
4
+ right: 0;
5
+ left: 0;
6
+ bottom: 0;
7
+ background-color: rgba(0, 0, 0, 0.7);
8
+ z-index: 999;
9
+ pointer-events: none;
10
+ opacity: 0;
11
+ transition: all ease-in-out 0.3s;
12
+ max-height: 100vh;
13
+ overflow: auto;
14
+ margin: 0 auto;
15
+ width: 100%;
16
+ text-align: center;
17
+ display: grid;
18
+ align-items: center;
19
19
  }
20
20
 
21
21
  .bg-lignt {
22
- background-color: var(--bgl-white);
22
+ background-color: var(--bgl-white);
23
23
  }
24
24
 
25
25
  .modal {
26
- width: 96%;
27
- max-width: 680px;
28
- transform: scale(0.5);
29
- opacity: 0;
30
- transition: all ease-in-out 0.15s;
31
- margin-left: auto;
32
- margin-right: auto;
33
- height: fit-content;
26
+ width: 96%;
27
+ max-width: 720px;
28
+ transform: scale(0.5);
29
+ opacity: 0;
30
+ transition: all ease-in-out 0.15s;
31
+ margin-left: auto;
32
+ margin-right: auto;
33
+ height: fit-content;
34
34
  }
35
35
 
36
36
  .small-modal .modal {
37
- max-width: 300px;
38
- text-align: center;
37
+ max-width: 300px;
38
+ text-align: center;
39
39
  }
40
40
 
41
41
  .tool-bar {
42
- margin: -2rem -1rem 1rem;
43
- display: flex;
44
- justify-content: space-between;
45
- position: -webkit-sticky;
46
- position: sticky;
47
- padding-top: 1rem;
48
- top: 0rem;
49
- z-index: 3;
50
- background: var(--bgl-white);
42
+ margin: -2rem -1rem 1rem;
43
+ display: flex;
44
+ justify-content: space-between;
45
+ position: -webkit-sticky;
46
+ position: sticky;
47
+ padding-top: 1rem;
48
+ top: 0rem;
49
+ z-index: 3;
50
+ background: var(--bgl-white);
51
51
  }
52
52
 
53
53
  .modal-size {
54
- cursor: pointer;
54
+ cursor: pointer;
55
55
  }
56
56
 
57
57
  .is-side .modal {
58
- inset-inline-end: -600px;
59
- transform: scale(1);
60
- opacity: 1;
61
- /* position: fixed; */
62
- /* top: 20px; */
63
- /* bottom: 20px; */
64
- max-width: 600px;
65
- width: 90%;
66
- margin-top: 20px;
67
- margin-bottom: 20px;
68
- margin-inline-start: auto;
69
- margin-inline-end: 20px;
70
- min-height: calc(100vh - 40px);
71
- transform: translateX(100%);
58
+ inset-inline-end: -600px;
59
+ transform: scale(1);
60
+ opacity: 1;
61
+ /* position: fixed; */
62
+ /* top: 20px; */
63
+ /* bottom: 20px; */
64
+ max-width: 600px;
65
+ width: 90%;
66
+ margin-top: 20px;
67
+ margin-bottom: 20px;
68
+ margin-inline-start: auto;
69
+ margin-inline-end: 20px;
70
+ min-height: calc(100vh - 40px);
71
+ transform: translateX(100%);
72
72
  }
73
73
 
74
74
  .is-active .modal {
75
- transform: scale(1);
76
- opacity: 1;
77
- box-shadow: 6px 6px 20px 20px #0000001c;
75
+ transform: scale(1);
76
+ opacity: 1;
77
+ box-shadow: 6px 6px 20px 20px #0000001c;
78
78
  }
79
79
 
80
80
  .bg-lignt .modal {
81
- transform: scale(1);
82
- border: 1px solid var(--border-color);
81
+ transform: scale(1);
82
+ border: 1px solid var(--border-color);
83
83
  }
84
84
 
85
85
  .bg-lignt.is-active .modal {
86
- box-shadow: none;
86
+ box-shadow: none;
87
87
  }
88
88
 
89
89
  .is-active.is-side .modal {
90
- inset-inline-end: 20px;
91
- transform: translateX(0%);
90
+ inset-inline-end: 20px;
91
+ transform: translateX(0%);
92
92
  }
93
93
 
94
94
  .bg-dark.is-active {
95
- opacity: 1;
96
- pointer-events: all;
95
+ opacity: 1;
96
+ pointer-events: all;
97
97
  }
98
98
 
99
99
  .is-side.bg-dark.is-active {
100
- opacity: 1;
101
- align-items: stretch;
100
+ opacity: 1;
101
+ align-items: stretch;
102
102
  }
103
103
 
104
104
  .is-side.is-active .modal {
105
- pointer-events: all;
105
+ pointer-events: all;
106
106
  }
107
107
 
108
108
  @media screen and (max-width: 910px) {
109
- .tool-bar {
110
- margin: -1rem 0rem 1rem;
111
- padding-bottom: 1rem;
112
- align-items: center;
113
- }
109
+ .tool-bar {
110
+ margin: -1rem 0rem 1rem;
111
+ padding-bottom: 1rem;
112
+ align-items: center;
113
+ }
114
114
 
115
- .is-active.is-side .modal {
116
- margin-inline-end: 5%;
117
- margin-inline-start: 5%;
118
- }
115
+ .is-active.is-side .modal {
116
+ margin-inline-end: 5%;
117
+ margin-inline-start: 5%;
118
+ }
119
119
  }