quasar-ui-sellmate-ui-kit 3.0.3 → 3.0.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.
Files changed (67) hide show
  1. package/.prettierrc +25 -25
  2. package/README.md +156 -156
  3. package/dist/index.common.js +2 -2
  4. package/dist/index.css +1 -1
  5. package/dist/index.esm.js +2 -2
  6. package/dist/index.min.css +1 -1
  7. package/dist/index.rtl.css +1 -1
  8. package/dist/index.rtl.min.css +1 -1
  9. package/dist/index.umd.js +3829 -3829
  10. package/dist/index.umd.min.js +2 -2
  11. package/package.json +83 -83
  12. package/src/assets/icons.js +28 -28
  13. package/src/components/SBreadcrumbs.vue +55 -55
  14. package/src/components/SButton.vue +206 -206
  15. package/src/components/SButtonGroup.vue +41 -41
  16. package/src/components/SButtonToggle.vue +200 -200
  17. package/src/components/SCaution.vue +102 -102
  18. package/src/components/SCheckbox.vue +123 -123
  19. package/src/components/SChip.vue +99 -99
  20. package/src/components/SDate.vue +717 -717
  21. package/src/components/SDateAutoRangePicker.vue +341 -341
  22. package/src/components/SDatePicker.vue +472 -472
  23. package/src/components/SDateRange.vue +470 -470
  24. package/src/components/SDateRangePicker.vue +660 -660
  25. package/src/components/SDateTimePicker.vue +349 -349
  26. package/src/components/SDialog.vue +250 -250
  27. package/src/components/SDropdown.vue +216 -216
  28. package/src/components/SEditor.vue +490 -490
  29. package/src/components/SFilePicker.vue +207 -207
  30. package/src/components/SHelp.vue +146 -146
  31. package/src/components/SInput.vue +343 -343
  32. package/src/components/SInputCounter.vue +46 -46
  33. package/src/components/SInputNumber.vue +179 -179
  34. package/src/components/SList.vue +29 -29
  35. package/src/components/SMarkupTable.vue +141 -141
  36. package/src/components/SPagination.vue +266 -266
  37. package/src/components/SRadio.vue +78 -78
  38. package/src/components/SRouteTab.vue +67 -67
  39. package/src/components/SSelect.vue +294 -294
  40. package/src/components/SSelectCheckbox.vue +222 -222
  41. package/src/components/SSelectCustom.vue +189 -189
  42. package/src/components/SSelectGroupCheckbox.vue +235 -235
  43. package/src/components/SSelectSearch.vue +261 -261
  44. package/src/components/SSelectSearchAutoComplete.vue +172 -172
  45. package/src/components/SSelectSearchCheckbox.vue +356 -356
  46. package/src/components/SStringToInput.vue +66 -66
  47. package/src/components/STab.vue +77 -77
  48. package/src/components/STable.vue +425 -425
  49. package/src/components/STableTree.vue +210 -210
  50. package/src/components/STabs.vue +32 -32
  51. package/src/components/STimePicker.vue +159 -159
  52. package/src/components/SToggle.vue +68 -68
  53. package/src/components/STooltip.vue +209 -209
  54. package/src/components/SelelctItem.vue +21 -21
  55. package/src/components/TimePickerCard.vue +352 -352
  56. package/src/composables/date.js +11 -11
  57. package/src/composables/modelBinder.js +13 -13
  58. package/src/composables/table/use-navigator.js +110 -110
  59. package/src/composables/table/use-resizable.js +80 -80
  60. package/src/css/app.scss +90 -90
  61. package/src/css/default.scss +875 -875
  62. package/src/css/extends.scss +154 -154
  63. package/src/css/quasar.variables.scss +189 -189
  64. package/src/directives/Directive.js +7 -7
  65. package/src/index.scss +3 -3
  66. package/src/vue-plugin.js +91 -91
  67. package/tsconfig.json +35 -35
@@ -1,490 +1,490 @@
1
- <template>
2
- <q-editor
3
- ref="editorRef"
4
- :content-style="{
5
- 'font-size': `${defaultFontSize}px`,
6
- resize: resizable ? 'vertical' : 'none',
7
- }"
8
- :min-height="`${minHeight}px`"
9
- :max-height="maxHeight != null ? `${maxHeight}px` : null"
10
- paragraph-tag="div"
11
- v-bind="$attrs"
12
- :definitions="toolDefinitions"
13
- :toolbar="toolbarDefinitions"
14
- v-model="model"
15
- class="s-editor"
16
- v-on="usePlaintextPasting ? { paste: onPaste } : {}"
17
- />
18
- <q-dialog v-model="isOpened" @keyup.esc="isOpened = false">
19
- <q-card class="q-pa-sm bg-Grey_Lighten-5">
20
- <q-card-section class="q-pa-none">
21
- <q-input
22
- class="bg-white"
23
- style="width: 348px; font-size: 12px"
24
- v-model="value"
25
- dense
26
- outlined
27
- :placeholder="placeholder"
28
- autogrow
29
- autofocus
30
- />
31
- <div class="row reverse q-mt-sm">
32
- <s-button
33
- outline
34
- label="삽입"
35
- @click="onOkClick"
36
- color="Blue_B_Default"
37
- class="bg-white"
38
- />
39
- </div>
40
- </q-card-section>
41
- </q-card>
42
- </q-dialog>
43
- </template>
44
-
45
- <script>
46
- import { defineComponent, ref, watch } from 'vue';
47
- import { extend, QCard, QCardSection, QDialog, QEditor, QInput, useQuasar } from 'quasar';
48
-
49
- export default defineComponent({
50
- name: 'SEditor',
51
- components: {
52
- QEditor,
53
- QDialog,
54
- QCard,
55
- QCardSection,
56
- QInput,
57
- },
58
- props: {
59
- definitions: Object,
60
- toolbar: {
61
- type: Array,
62
- validator: v => v.length === 0 || v.every(group => group.length),
63
- default() {
64
- return [
65
- ['left', 'center', 'right', 'justify'],
66
- ['bold', 'italic', 'underline', 'strike'],
67
- ['link', 'hr'],
68
- ['fontSizes', 'removeFormat'],
69
- ['unordered', 'ordered', 'outdent', 'indent'],
70
- ['undo', 'redo'],
71
- ['viewsource'],
72
- ];
73
- },
74
- },
75
- defaultFontSize: {
76
- type: Number,
77
- default: 13,
78
- },
79
- fontSizes: {
80
- type: Object,
81
- default: null,
82
- },
83
- resizable: {
84
- type: Boolean,
85
- default: false,
86
- },
87
- minHeight: {
88
- type: Number,
89
- default: 250,
90
- },
91
- maxHeight: {
92
- type: Number,
93
- default: null,
94
- },
95
- modelValue: {
96
- type: String,
97
- required: true,
98
- },
99
- useImageUpload: {
100
- type: Boolean,
101
- default: true,
102
- },
103
- imgInputPlaceholder: {
104
- type: String,
105
- default:
106
- '이미지 파일 경로를 엔터키로 구분하여 여러개 입력할 수 있습니다.\n예시)\nhttps://www.sellmate.co.kr/img/new/logo01.png\nhttps://www.sellmate.co.kr/img/new/logo02.png\nhttps://www.sellmate.co.kr/img/new/logo03.png',
107
- },
108
- useListOpts: {
109
- type: Boolean,
110
- default: false,
111
- },
112
- usePlaintextPasting: {
113
- type: Boolean,
114
- default: false,
115
- },
116
- },
117
- setup(props, { emit }) {
118
- const model = ref(props.modelValue);
119
- const $q = useQuasar();
120
-
121
- // Toolbar Begin
122
- const placeholder = ref(props.imgInputPlaceholder);
123
- const isOpened = ref(false);
124
- const value = ref('');
125
-
126
- function uploadImage() {
127
- isOpened.value = true;
128
- }
129
-
130
- function setFontSize(fontSize) {
131
- function isRootNode(node) {
132
- return (
133
- node.nodeType !== Node.TEXT_NODE
134
- && (node.className.indexOf('q-editor__content') >= 0
135
- || node.hasAttribute('contenteditable'))
136
- );
137
- }
138
-
139
- function wrapAndReplaceRange(range) {
140
- const content = document.createTextNode(range.toString());
141
- const wrapperNode = document.createElement('span');
142
- wrapperNode.append(content);
143
-
144
- range.deleteContents();
145
- range.insertNode(wrapperNode);
146
-
147
- return wrapperNode;
148
- }
149
-
150
- function wrapAndReplaceTextNodeRange(node) {
151
- const nodeRange = document.createRange();
152
- nodeRange.setStart(node, 0);
153
- nodeRange.setEnd(node, node.length);
154
- return wrapAndReplaceRange(nodeRange);
155
- }
156
-
157
- function wrapTextNode(node) {
158
- const wrapper = document.createElement('span');
159
- wrapper.innerText = node.textContent;
160
- return wrapper;
161
- }
162
-
163
- function getNonTextNode(top, node) {
164
- const parent = node.parentNode;
165
- if (
166
- top !== parent
167
- && Array.from(parent.childNodes).length === 1
168
- && parent.childNodes[0] === node
169
- ) {
170
- return parent;
171
- }
172
- return wrapAndReplaceTextNodeRange(node);
173
- }
174
-
175
- function getDirectChildNode(parent, node) {
176
- if (node.parentNode === parent) {
177
- return node;
178
- }
179
- return getDirectChildNode(parent, node.parentNode);
180
- }
181
-
182
- function removeChildrenFontStyle(node) {
183
- node.childNodes.forEach(child => {
184
- if (child.nodeType !== Node.TEXT_NODE) {
185
- child.style.fontSize = null;
186
- removeChildrenFontStyle(child);
187
- }
188
- });
189
- }
190
-
191
- function applyStyleToChildNode(fontSizeStyle, node, childIndex) {
192
- const childNode = node.childNodes[childIndex];
193
- if (childNode.nodeType === Node.TEXT_NODE) {
194
- const nodeWrapper = wrapTextNode(childNode);
195
- node.replaceChild(nodeWrapper, childNode);
196
- }
197
- node.childNodes[childIndex].style.fontSize = fontSizeStyle;
198
- removeChildrenFontStyle(node.childNodes[childIndex]);
199
- }
200
-
201
- function applyStyleToSiblings(fontSizeStyle, top, node, toBackward = false) {
202
- const parent = node.parentNode;
203
- if (top === parent) return;
204
- const nodeIndex = Array.from(parent.childNodes).indexOf(node);
205
- for (
206
- let index = toBackward ? 0 : nodeIndex + 1;
207
- index < (toBackward ? nodeIndex : parent.childNodes.length);
208
- index++
209
- ) {
210
- applyStyleToChildNode(fontSizeStyle, parent, index);
211
- }
212
- applyStyleToSiblings(fontSizeStyle, top, parent, toBackward);
213
- }
214
-
215
- function applyStyleForward(fontSizeStyle, top, node) {
216
- applyStyleToSiblings(fontSizeStyle, top, node);
217
- }
218
-
219
- function applyStyleBackward(fontSizeStyle, top, node) {
220
- applyStyleToSiblings(fontSizeStyle, top, node, true);
221
- }
222
-
223
- const fontSizeStyle = `${fontSize}px`;
224
-
225
- const selection = window.getSelection();
226
- const range = selection.getRangeAt(0);
227
- if (range.collapsed) {
228
- const emptyNode = document.createElement('span');
229
- emptyNode.style.fontSize = fontSizeStyle;
230
- range.insertNode(emptyNode);
231
- return;
232
- }
233
-
234
- const topNode = range.commonAncestorContainer;
235
- const start = range.startContainer;
236
- const end = range.endContainer;
237
-
238
- const isSingleNode = start === end;
239
- const isNotFromNodeStart = range.startOffset > 0;
240
- const isNotToNodeEnd = range.endOffset < end.length;
241
- const isPartialSelection = isNotFromNodeStart || isNotToNodeEnd;
242
- const isRootChildren = isRootNode(topNode);
243
-
244
- if (isSingleNode) {
245
- if (isPartialSelection || isRootChildren) {
246
- const wrapperNode = wrapAndReplaceRange(range);
247
- wrapperNode.style.fontSize = fontSizeStyle;
248
- } else {
249
- const targetNode = topNode.nodeType === Node.TEXT_NODE
250
- ? wrapAndReplaceTextNodeRange(topNode)
251
- : topNode;
252
- targetNode.style.fontSize = fontSizeStyle;
253
- selection.extend(targetNode.childNodes[0], targetNode.textContent.length);
254
- }
255
- } else {
256
- const startNode = getDirectChildNode(topNode, start);
257
- const endNode = getDirectChildNode(topNode, end);
258
- const startIndex = Array.from(topNode.childNodes).indexOf(startNode);
259
- const endIndex = Array.from(topNode.childNodes).indexOf(endNode);
260
- // middle line(s)
261
- for (let index = startIndex + 1; index < endIndex; index++) {
262
- applyStyleToChildNode(fontSizeStyle, topNode, index);
263
- }
264
- // first line
265
- if (isNotFromNodeStart) {
266
- const startRange = document.createRange();
267
- startRange.setStart(start, range.startOffset);
268
- startRange.setEnd(start, start.length);
269
- const startWrapper = wrapAndReplaceRange(startRange);
270
- startWrapper.style.fontSize = fontSizeStyle;
271
- // range.setStart(startWrapper, 0);
272
- // selection.collapse(startWrapper);
273
- // selection.extend(end, range.endOffset);
274
- applyStyleForward(fontSizeStyle, topNode, startWrapper);
275
- } else {
276
- const firstNode = start.nodeType === Node.TEXT_NODE
277
- ? getNonTextNode(topNode, start)
278
- : start;
279
- firstNode.style.fontSize = fontSizeStyle;
280
- if (start !== startNode) applyStyleForward(fontSizeStyle, topNode, firstNode);
281
- }
282
- // last line
283
- if (isNotToNodeEnd) {
284
- const endRange = document.createRange();
285
- endRange.setStart(end, 0);
286
- endRange.setEnd(end, range.endOffset);
287
- const endWrapper = wrapAndReplaceRange(endRange);
288
- endWrapper.style.fontSize = fontSizeStyle;
289
- range.setEnd(endWrapper.childNodes[0], endWrapper.textContent.length);
290
- selection.extend(endWrapper.childNodes[0], endWrapper.textContent.length);
291
- applyStyleBackward(fontSizeStyle, topNode, endWrapper);
292
- } else {
293
- const lastNode = end.nodeType === Node.TEXT_NODE ? getNonTextNode(topNode, end) : end;
294
- lastNode.style.fontSize = fontSizeStyle;
295
- if (end !== endNode) applyStyleBackward(fontSizeStyle, topNode, lastNode);
296
- }
297
- }
298
-
299
- // FIXME: exeCommand API 를 호출하지 않으면 Quasar Editor 컴포넌트에서 모델 업데이트가 일어나지 않음.
300
- // 강제로 bold 활성화 비활성화 동작 수행으로 모델 변경 적용. 원인 파악 후 적절히 해결할 것
301
- // 참고> execCommand 만으로 스타일 적용 가능하지만, div 태그로 감싸지지 않은 첫 줄에서 수행 시 태그 위치 설정 관련 오류 발생
302
- //
303
- // const spanString = `<span style="font-size: ${fontSize}px;">${document
304
- // .getSelection()
305
- // .toString()}</span>`;
306
- // document.execCommand('insertHTML', false, spanString);
307
- //
308
- document.execCommand('bold', false);
309
- document.execCommand('bold', false);
310
- }
311
-
312
- const sizeDefinitions = extend(
313
- true,
314
- {
315
- size1: {
316
- label: '9px',
317
- size: 9,
318
- },
319
- size2: {
320
- label: '11px',
321
- size: 11,
322
- },
323
- size3: {
324
- label: '13px',
325
- size: 13,
326
- },
327
- size4: {
328
- label: '15px',
329
- size: 15,
330
- },
331
- size5: {
332
- label: '18px',
333
- size: 18,
334
- },
335
- size6: {
336
- label: '20px',
337
- size: 20,
338
- },
339
- adjustLabelSize: true,
340
- },
341
- props.fontSizes || {},
342
- );
343
-
344
- const toolDefinitions = extend(true, {}, props.definitions || {}, {
345
- upload: {
346
- tip: 'Upload Image',
347
- icon: 'image',
348
- handler: uploadImage,
349
- },
350
- size1: {
351
- tip: sizeDefinitions.size1.label,
352
- htmlTip: sizeDefinitions.adjustLabelSize
353
- ? `<span style="font-size: ${sizeDefinitions.size1.size}px">${sizeDefinitions.size1.label}</span>`
354
- : null,
355
- handler: () => setFontSize(sizeDefinitions.size1.size),
356
- },
357
- size2: {
358
- tip: sizeDefinitions.size2.label,
359
- htmlTip: sizeDefinitions.adjustLabelSize
360
- ? `<span style="font-size: ${sizeDefinitions.size2.size}px">${sizeDefinitions.size2.label}</span>`
361
- : null,
362
- handler: () => setFontSize(sizeDefinitions.size2.size),
363
- },
364
- size3: {
365
- tip: sizeDefinitions.size3.label,
366
- htmlTip: sizeDefinitions.adjustLabelSize
367
- ? `<span style="font-size: ${sizeDefinitions.size3.size}px">${sizeDefinitions.size3.label}</span>`
368
- : null,
369
- handler: () => setFontSize(sizeDefinitions.size3.size),
370
- },
371
- size4: {
372
- tip: sizeDefinitions.size4.label,
373
- htmlTip: sizeDefinitions.adjustLabelSize
374
- ? `<span style="font-size: ${sizeDefinitions.size4.size}px">${sizeDefinitions.size4.label}</span>`
375
- : null,
376
- handler: () => setFontSize(sizeDefinitions.size4.size),
377
- },
378
- size5: {
379
- tip: sizeDefinitions.size5.label,
380
- htmlTip: sizeDefinitions.adjustLabelSize
381
- ? `<span style="font-size: ${sizeDefinitions.size5.size}px">${sizeDefinitions.size5.label}</span>`
382
- : null,
383
- handler: () => setFontSize(sizeDefinitions.size5.size),
384
- },
385
- size6: {
386
- tip: sizeDefinitions.size6.label,
387
- htmlTip: sizeDefinitions.adjustLabelSize
388
- ? `<span style="font-size: ${sizeDefinitions.size6.size}px">${sizeDefinitions.size6.label}</span>`
389
- : null,
390
- handler: () => setFontSize(sizeDefinitions.size6.size),
391
- },
392
- });
393
-
394
- const fontSizes = {
395
- label: $q.lang.editor.fontSize,
396
- icon: $q.iconSet.editor.fontSize,
397
- fixedLabel: true,
398
- fixedIcon: true,
399
- list: 'no-icons',
400
- options: ['size1', 'size2', 'size3', 'size4', 'size5', 'size6'],
401
- };
402
-
403
- const toolbarDefinitions = props.toolbar.map(group => group.map(item => (item === 'fontSizes' ? fontSizes : item)));
404
-
405
- function onOkClick() {
406
- value.value.split('\n').forEach((el, idx) => {
407
- if (el === '') return;
408
- model.value += `<p><img style="max-width:100%;" src=${el} :alt="image${idx}"/></p>`;
409
- });
410
- isOpened.value = false;
411
- value.value = '';
412
- }
413
- // Toolbar End
414
-
415
- watch(
416
- () => props.modelValue,
417
- newValue => {
418
- model.value = newValue;
419
- },
420
- );
421
-
422
- watch(
423
- () => model.value,
424
- () => {
425
- emit('update:modelValue', model.value);
426
- },
427
- );
428
-
429
- const editorRef = ref(null);
430
-
431
- // https://quasar.dev/vue-components/editor#plaintext-pasting 참조
432
- function onPaste(event) {
433
- if (event.target.nodeName === 'INPUT') return;
434
- let text = '';
435
- let onPasteStripFormattingIEPaste = false;
436
- event.preventDefault();
437
- event.stopPropagation();
438
- if (event.originalEvent && event.originalEvent.clipboardData.getData) {
439
- text = event.originalEvent.clipboardData.getData('text/plain');
440
- editorRef.value.runCmd('insertText', text);
441
- } else if (event.clipboardData && event.clipboardData.getData) {
442
- text = event.clipboardData.getData('text/plain');
443
- editorRef.value.runCmd('insertText', text);
444
- } else if (window.clipboardData && window.clipboardData.getData) {
445
- if (!onPasteStripFormattingIEPaste) {
446
- onPasteStripFormattingIEPaste = true;
447
- editorRef.value.runCmd('ms-pasteTextOnly', text);
448
- }
449
- onPasteStripFormattingIEPaste = false;
450
- }
451
- }
452
-
453
- return {
454
- toolbarDefinitions,
455
- toolDefinitions,
456
- isOpened,
457
- model,
458
- value,
459
- placeholder,
460
- uploadImage,
461
- onOkClick,
462
- onPaste,
463
- editorRef,
464
- };
465
- },
466
- });
467
- </script>
468
- <style lang="scss">
469
- .s-editor {
470
- > .q-editor__toolbars-container {
471
- > .q-editor__toolbar {
472
- height: 32px;
473
- overflow-y: hidden;
474
- }
475
- }
476
-
477
- // 툴바 순서 변경시 참고 필요
478
- .q-editor__toolbars-container {
479
- > .q-editor__toolbar {
480
- > .q-editor__toolbar-group:nth-child(2) {
481
- > .q-btn-dropdown {
482
- > .q-btn__content {
483
- width: 91px;
484
- }
485
- }
486
- }
487
- }
488
- }
489
- }
490
- </style>
1
+ <template>
2
+ <q-editor
3
+ ref="editorRef"
4
+ :content-style="{
5
+ 'font-size': `${defaultFontSize}px`,
6
+ resize: resizable ? 'vertical' : 'none',
7
+ }"
8
+ :min-height="`${minHeight}px`"
9
+ :max-height="maxHeight != null ? `${maxHeight}px` : null"
10
+ paragraph-tag="div"
11
+ v-bind="$attrs"
12
+ :definitions="toolDefinitions"
13
+ :toolbar="toolbarDefinitions"
14
+ v-model="model"
15
+ class="s-editor"
16
+ v-on="usePlaintextPasting ? { paste: onPaste } : {}"
17
+ />
18
+ <q-dialog v-model="isOpened" @keyup.esc="isOpened = false">
19
+ <q-card class="q-pa-sm bg-Grey_Lighten-5">
20
+ <q-card-section class="q-pa-none">
21
+ <q-input
22
+ class="bg-white"
23
+ style="width: 348px; font-size: 12px"
24
+ v-model="value"
25
+ dense
26
+ outlined
27
+ :placeholder="placeholder"
28
+ autogrow
29
+ autofocus
30
+ />
31
+ <div class="row reverse q-mt-sm">
32
+ <s-button
33
+ outline
34
+ label="삽입"
35
+ @click="onOkClick"
36
+ color="Blue_B_Default"
37
+ class="bg-white"
38
+ />
39
+ </div>
40
+ </q-card-section>
41
+ </q-card>
42
+ </q-dialog>
43
+ </template>
44
+
45
+ <script>
46
+ import { defineComponent, ref, watch } from 'vue';
47
+ import { extend, QCard, QCardSection, QDialog, QEditor, QInput, useQuasar } from 'quasar';
48
+
49
+ export default defineComponent({
50
+ name: 'SEditor',
51
+ components: {
52
+ QEditor,
53
+ QDialog,
54
+ QCard,
55
+ QCardSection,
56
+ QInput,
57
+ },
58
+ props: {
59
+ definitions: Object,
60
+ toolbar: {
61
+ type: Array,
62
+ validator: v => v.length === 0 || v.every(group => group.length),
63
+ default() {
64
+ return [
65
+ ['left', 'center', 'right', 'justify'],
66
+ ['bold', 'italic', 'underline', 'strike'],
67
+ ['link', 'hr'],
68
+ ['fontSizes', 'removeFormat'],
69
+ ['unordered', 'ordered', 'outdent', 'indent'],
70
+ ['undo', 'redo'],
71
+ ['viewsource'],
72
+ ];
73
+ },
74
+ },
75
+ defaultFontSize: {
76
+ type: Number,
77
+ default: 13,
78
+ },
79
+ fontSizes: {
80
+ type: Object,
81
+ default: null,
82
+ },
83
+ resizable: {
84
+ type: Boolean,
85
+ default: false,
86
+ },
87
+ minHeight: {
88
+ type: Number,
89
+ default: 250,
90
+ },
91
+ maxHeight: {
92
+ type: Number,
93
+ default: null,
94
+ },
95
+ modelValue: {
96
+ type: String,
97
+ required: true,
98
+ },
99
+ useImageUpload: {
100
+ type: Boolean,
101
+ default: true,
102
+ },
103
+ imgInputPlaceholder: {
104
+ type: String,
105
+ default:
106
+ '이미지 파일 경로를 엔터키로 구분하여 여러개 입력할 수 있습니다.\n예시)\nhttps://www.sellmate.co.kr/img/new/logo01.png\nhttps://www.sellmate.co.kr/img/new/logo02.png\nhttps://www.sellmate.co.kr/img/new/logo03.png',
107
+ },
108
+ useListOpts: {
109
+ type: Boolean,
110
+ default: false,
111
+ },
112
+ usePlaintextPasting: {
113
+ type: Boolean,
114
+ default: false,
115
+ },
116
+ },
117
+ setup(props, { emit }) {
118
+ const model = ref(props.modelValue);
119
+ const $q = useQuasar();
120
+
121
+ // Toolbar Begin
122
+ const placeholder = ref(props.imgInputPlaceholder);
123
+ const isOpened = ref(false);
124
+ const value = ref('');
125
+
126
+ function uploadImage() {
127
+ isOpened.value = true;
128
+ }
129
+
130
+ function setFontSize(fontSize) {
131
+ function isRootNode(node) {
132
+ return (
133
+ node.nodeType !== Node.TEXT_NODE
134
+ && (node.className.indexOf('q-editor__content') >= 0
135
+ || node.hasAttribute('contenteditable'))
136
+ );
137
+ }
138
+
139
+ function wrapAndReplaceRange(range) {
140
+ const content = document.createTextNode(range.toString());
141
+ const wrapperNode = document.createElement('span');
142
+ wrapperNode.append(content);
143
+
144
+ range.deleteContents();
145
+ range.insertNode(wrapperNode);
146
+
147
+ return wrapperNode;
148
+ }
149
+
150
+ function wrapAndReplaceTextNodeRange(node) {
151
+ const nodeRange = document.createRange();
152
+ nodeRange.setStart(node, 0);
153
+ nodeRange.setEnd(node, node.length);
154
+ return wrapAndReplaceRange(nodeRange);
155
+ }
156
+
157
+ function wrapTextNode(node) {
158
+ const wrapper = document.createElement('span');
159
+ wrapper.innerText = node.textContent;
160
+ return wrapper;
161
+ }
162
+
163
+ function getNonTextNode(top, node) {
164
+ const parent = node.parentNode;
165
+ if (
166
+ top !== parent
167
+ && Array.from(parent.childNodes).length === 1
168
+ && parent.childNodes[0] === node
169
+ ) {
170
+ return parent;
171
+ }
172
+ return wrapAndReplaceTextNodeRange(node);
173
+ }
174
+
175
+ function getDirectChildNode(parent, node) {
176
+ if (node.parentNode === parent) {
177
+ return node;
178
+ }
179
+ return getDirectChildNode(parent, node.parentNode);
180
+ }
181
+
182
+ function removeChildrenFontStyle(node) {
183
+ node.childNodes.forEach(child => {
184
+ if (child.nodeType !== Node.TEXT_NODE) {
185
+ child.style.fontSize = null;
186
+ removeChildrenFontStyle(child);
187
+ }
188
+ });
189
+ }
190
+
191
+ function applyStyleToChildNode(fontSizeStyle, node, childIndex) {
192
+ const childNode = node.childNodes[childIndex];
193
+ if (childNode.nodeType === Node.TEXT_NODE) {
194
+ const nodeWrapper = wrapTextNode(childNode);
195
+ node.replaceChild(nodeWrapper, childNode);
196
+ }
197
+ node.childNodes[childIndex].style.fontSize = fontSizeStyle;
198
+ removeChildrenFontStyle(node.childNodes[childIndex]);
199
+ }
200
+
201
+ function applyStyleToSiblings(fontSizeStyle, top, node, toBackward = false) {
202
+ const parent = node.parentNode;
203
+ if (top === parent) return;
204
+ const nodeIndex = Array.from(parent.childNodes).indexOf(node);
205
+ for (
206
+ let index = toBackward ? 0 : nodeIndex + 1;
207
+ index < (toBackward ? nodeIndex : parent.childNodes.length);
208
+ index++
209
+ ) {
210
+ applyStyleToChildNode(fontSizeStyle, parent, index);
211
+ }
212
+ applyStyleToSiblings(fontSizeStyle, top, parent, toBackward);
213
+ }
214
+
215
+ function applyStyleForward(fontSizeStyle, top, node) {
216
+ applyStyleToSiblings(fontSizeStyle, top, node);
217
+ }
218
+
219
+ function applyStyleBackward(fontSizeStyle, top, node) {
220
+ applyStyleToSiblings(fontSizeStyle, top, node, true);
221
+ }
222
+
223
+ const fontSizeStyle = `${fontSize}px`;
224
+
225
+ const selection = window.getSelection();
226
+ const range = selection.getRangeAt(0);
227
+ if (range.collapsed) {
228
+ const emptyNode = document.createElement('span');
229
+ emptyNode.style.fontSize = fontSizeStyle;
230
+ range.insertNode(emptyNode);
231
+ return;
232
+ }
233
+
234
+ const topNode = range.commonAncestorContainer;
235
+ const start = range.startContainer;
236
+ const end = range.endContainer;
237
+
238
+ const isSingleNode = start === end;
239
+ const isNotFromNodeStart = range.startOffset > 0;
240
+ const isNotToNodeEnd = range.endOffset < end.length;
241
+ const isPartialSelection = isNotFromNodeStart || isNotToNodeEnd;
242
+ const isRootChildren = isRootNode(topNode);
243
+
244
+ if (isSingleNode) {
245
+ if (isPartialSelection || isRootChildren) {
246
+ const wrapperNode = wrapAndReplaceRange(range);
247
+ wrapperNode.style.fontSize = fontSizeStyle;
248
+ } else {
249
+ const targetNode = topNode.nodeType === Node.TEXT_NODE
250
+ ? wrapAndReplaceTextNodeRange(topNode)
251
+ : topNode;
252
+ targetNode.style.fontSize = fontSizeStyle;
253
+ selection.extend(targetNode.childNodes[0], targetNode.textContent.length);
254
+ }
255
+ } else {
256
+ const startNode = getDirectChildNode(topNode, start);
257
+ const endNode = getDirectChildNode(topNode, end);
258
+ const startIndex = Array.from(topNode.childNodes).indexOf(startNode);
259
+ const endIndex = Array.from(topNode.childNodes).indexOf(endNode);
260
+ // middle line(s)
261
+ for (let index = startIndex + 1; index < endIndex; index++) {
262
+ applyStyleToChildNode(fontSizeStyle, topNode, index);
263
+ }
264
+ // first line
265
+ if (isNotFromNodeStart) {
266
+ const startRange = document.createRange();
267
+ startRange.setStart(start, range.startOffset);
268
+ startRange.setEnd(start, start.length);
269
+ const startWrapper = wrapAndReplaceRange(startRange);
270
+ startWrapper.style.fontSize = fontSizeStyle;
271
+ // range.setStart(startWrapper, 0);
272
+ // selection.collapse(startWrapper);
273
+ // selection.extend(end, range.endOffset);
274
+ applyStyleForward(fontSizeStyle, topNode, startWrapper);
275
+ } else {
276
+ const firstNode = start.nodeType === Node.TEXT_NODE
277
+ ? getNonTextNode(topNode, start)
278
+ : start;
279
+ firstNode.style.fontSize = fontSizeStyle;
280
+ if (start !== startNode) applyStyleForward(fontSizeStyle, topNode, firstNode);
281
+ }
282
+ // last line
283
+ if (isNotToNodeEnd) {
284
+ const endRange = document.createRange();
285
+ endRange.setStart(end, 0);
286
+ endRange.setEnd(end, range.endOffset);
287
+ const endWrapper = wrapAndReplaceRange(endRange);
288
+ endWrapper.style.fontSize = fontSizeStyle;
289
+ range.setEnd(endWrapper.childNodes[0], endWrapper.textContent.length);
290
+ selection.extend(endWrapper.childNodes[0], endWrapper.textContent.length);
291
+ applyStyleBackward(fontSizeStyle, topNode, endWrapper);
292
+ } else {
293
+ const lastNode = end.nodeType === Node.TEXT_NODE ? getNonTextNode(topNode, end) : end;
294
+ lastNode.style.fontSize = fontSizeStyle;
295
+ if (end !== endNode) applyStyleBackward(fontSizeStyle, topNode, lastNode);
296
+ }
297
+ }
298
+
299
+ // FIXME: exeCommand API 를 호출하지 않으면 Quasar Editor 컴포넌트에서 모델 업데이트가 일어나지 않음.
300
+ // 강제로 bold 활성화 비활성화 동작 수행으로 모델 변경 적용. 원인 파악 후 적절히 해결할 것
301
+ // 참고> execCommand 만으로 스타일 적용 가능하지만, div 태그로 감싸지지 않은 첫 줄에서 수행 시 태그 위치 설정 관련 오류 발생
302
+ //
303
+ // const spanString = `<span style="font-size: ${fontSize}px;">${document
304
+ // .getSelection()
305
+ // .toString()}</span>`;
306
+ // document.execCommand('insertHTML', false, spanString);
307
+ //
308
+ document.execCommand('bold', false);
309
+ document.execCommand('bold', false);
310
+ }
311
+
312
+ const sizeDefinitions = extend(
313
+ true,
314
+ {
315
+ size1: {
316
+ label: '9px',
317
+ size: 9,
318
+ },
319
+ size2: {
320
+ label: '11px',
321
+ size: 11,
322
+ },
323
+ size3: {
324
+ label: '13px',
325
+ size: 13,
326
+ },
327
+ size4: {
328
+ label: '15px',
329
+ size: 15,
330
+ },
331
+ size5: {
332
+ label: '18px',
333
+ size: 18,
334
+ },
335
+ size6: {
336
+ label: '20px',
337
+ size: 20,
338
+ },
339
+ adjustLabelSize: true,
340
+ },
341
+ props.fontSizes || {},
342
+ );
343
+
344
+ const toolDefinitions = extend(true, {}, props.definitions || {}, {
345
+ upload: {
346
+ tip: 'Upload Image',
347
+ icon: 'image',
348
+ handler: uploadImage,
349
+ },
350
+ size1: {
351
+ tip: sizeDefinitions.size1.label,
352
+ htmlTip: sizeDefinitions.adjustLabelSize
353
+ ? `<span style="font-size: ${sizeDefinitions.size1.size}px">${sizeDefinitions.size1.label}</span>`
354
+ : null,
355
+ handler: () => setFontSize(sizeDefinitions.size1.size),
356
+ },
357
+ size2: {
358
+ tip: sizeDefinitions.size2.label,
359
+ htmlTip: sizeDefinitions.adjustLabelSize
360
+ ? `<span style="font-size: ${sizeDefinitions.size2.size}px">${sizeDefinitions.size2.label}</span>`
361
+ : null,
362
+ handler: () => setFontSize(sizeDefinitions.size2.size),
363
+ },
364
+ size3: {
365
+ tip: sizeDefinitions.size3.label,
366
+ htmlTip: sizeDefinitions.adjustLabelSize
367
+ ? `<span style="font-size: ${sizeDefinitions.size3.size}px">${sizeDefinitions.size3.label}</span>`
368
+ : null,
369
+ handler: () => setFontSize(sizeDefinitions.size3.size),
370
+ },
371
+ size4: {
372
+ tip: sizeDefinitions.size4.label,
373
+ htmlTip: sizeDefinitions.adjustLabelSize
374
+ ? `<span style="font-size: ${sizeDefinitions.size4.size}px">${sizeDefinitions.size4.label}</span>`
375
+ : null,
376
+ handler: () => setFontSize(sizeDefinitions.size4.size),
377
+ },
378
+ size5: {
379
+ tip: sizeDefinitions.size5.label,
380
+ htmlTip: sizeDefinitions.adjustLabelSize
381
+ ? `<span style="font-size: ${sizeDefinitions.size5.size}px">${sizeDefinitions.size5.label}</span>`
382
+ : null,
383
+ handler: () => setFontSize(sizeDefinitions.size5.size),
384
+ },
385
+ size6: {
386
+ tip: sizeDefinitions.size6.label,
387
+ htmlTip: sizeDefinitions.adjustLabelSize
388
+ ? `<span style="font-size: ${sizeDefinitions.size6.size}px">${sizeDefinitions.size6.label}</span>`
389
+ : null,
390
+ handler: () => setFontSize(sizeDefinitions.size6.size),
391
+ },
392
+ });
393
+
394
+ const fontSizes = {
395
+ label: $q.lang.editor.fontSize,
396
+ icon: $q.iconSet.editor.fontSize,
397
+ fixedLabel: true,
398
+ fixedIcon: true,
399
+ list: 'no-icons',
400
+ options: ['size1', 'size2', 'size3', 'size4', 'size5', 'size6'],
401
+ };
402
+
403
+ const toolbarDefinitions = props.toolbar.map(group => group.map(item => (item === 'fontSizes' ? fontSizes : item)));
404
+
405
+ function onOkClick() {
406
+ value.value.split('\n').forEach((el, idx) => {
407
+ if (el === '') return;
408
+ model.value += `<p><img style="max-width:100%;" src=${el} :alt="image${idx}"/></p>`;
409
+ });
410
+ isOpened.value = false;
411
+ value.value = '';
412
+ }
413
+ // Toolbar End
414
+
415
+ watch(
416
+ () => props.modelValue,
417
+ newValue => {
418
+ model.value = newValue;
419
+ },
420
+ );
421
+
422
+ watch(
423
+ () => model.value,
424
+ () => {
425
+ emit('update:modelValue', model.value);
426
+ },
427
+ );
428
+
429
+ const editorRef = ref(null);
430
+
431
+ // https://quasar.dev/vue-components/editor#plaintext-pasting 참조
432
+ function onPaste(event) {
433
+ if (event.target.nodeName === 'INPUT') return;
434
+ let text = '';
435
+ let onPasteStripFormattingIEPaste = false;
436
+ event.preventDefault();
437
+ event.stopPropagation();
438
+ if (event.originalEvent && event.originalEvent.clipboardData.getData) {
439
+ text = event.originalEvent.clipboardData.getData('text/plain');
440
+ editorRef.value.runCmd('insertText', text);
441
+ } else if (event.clipboardData && event.clipboardData.getData) {
442
+ text = event.clipboardData.getData('text/plain');
443
+ editorRef.value.runCmd('insertText', text);
444
+ } else if (window.clipboardData && window.clipboardData.getData) {
445
+ if (!onPasteStripFormattingIEPaste) {
446
+ onPasteStripFormattingIEPaste = true;
447
+ editorRef.value.runCmd('ms-pasteTextOnly', text);
448
+ }
449
+ onPasteStripFormattingIEPaste = false;
450
+ }
451
+ }
452
+
453
+ return {
454
+ toolbarDefinitions,
455
+ toolDefinitions,
456
+ isOpened,
457
+ model,
458
+ value,
459
+ placeholder,
460
+ uploadImage,
461
+ onOkClick,
462
+ onPaste,
463
+ editorRef,
464
+ };
465
+ },
466
+ });
467
+ </script>
468
+ <style lang="scss">
469
+ .s-editor {
470
+ > .q-editor__toolbars-container {
471
+ > .q-editor__toolbar {
472
+ height: 32px;
473
+ overflow-y: hidden;
474
+ }
475
+ }
476
+
477
+ // 툴바 순서 변경시 참고 필요
478
+ .q-editor__toolbars-container {
479
+ > .q-editor__toolbar {
480
+ > .q-editor__toolbar-group:nth-child(2) {
481
+ > .q-btn-dropdown {
482
+ > .q-btn__content {
483
+ width: 91px;
484
+ }
485
+ }
486
+ }
487
+ }
488
+ }
489
+ }
490
+ </style>