quasar-ui-sellmate-ui-kit 2.2.33 → 2.3.1

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 (62) hide show
  1. package/README.md +142 -142
  2. package/dist/index.common.js +3 -3
  3. package/dist/index.css +2 -2
  4. package/dist/index.esm.js +3 -3
  5. package/dist/index.min.css +2 -2
  6. package/dist/index.rtl.css +2 -2
  7. package/dist/index.rtl.min.css +2 -2
  8. package/dist/index.umd.js +3558 -3550
  9. package/dist/index.umd.min.js +3 -3
  10. package/package.json +75 -75
  11. package/src/components/SBreadcrumbs.vue +55 -55
  12. package/src/components/SButton.vue +206 -206
  13. package/src/components/SButtonGroup.vue +41 -41
  14. package/src/components/SCaution.vue +102 -102
  15. package/src/components/SCheckbox.vue +123 -123
  16. package/src/components/SChip.vue +99 -99
  17. package/src/components/SDate.vue +717 -717
  18. package/src/components/SDateAutoRangePicker.vue +341 -341
  19. package/src/components/SDatePicker.vue +472 -472
  20. package/src/components/SDateRange.vue +470 -470
  21. package/src/components/SDateRangePicker.vue +660 -660
  22. package/src/components/SDateTimePicker.vue +349 -349
  23. package/src/components/SDialog.vue +250 -250
  24. package/src/components/SDropdown.vue +216 -216
  25. package/src/components/SEditor.vue +490 -490
  26. package/src/components/SFilePicker.vue +207 -207
  27. package/src/components/SHelp.vue +146 -146
  28. package/src/components/SInput.vue +343 -343
  29. package/src/components/SInputCounter.vue +46 -46
  30. package/src/components/SInputNumber.vue +179 -179
  31. package/src/components/SList.vue +29 -29
  32. package/src/components/SMarkupTable.vue +141 -141
  33. package/src/components/SPagination.vue +266 -266
  34. package/src/components/SRadio.vue +78 -78
  35. package/src/components/SRouteTab.vue +67 -67
  36. package/src/components/SSelect.vue +294 -294
  37. package/src/components/SSelectCheckbox.vue +222 -225
  38. package/src/components/SSelectCustom.vue +25 -17
  39. package/src/components/SSelectGroupCheckbox.vue +235 -235
  40. package/src/components/SSelectSearch.vue +261 -261
  41. package/src/components/SSelectSearchAutoComplete.vue +172 -172
  42. package/src/components/SSelectSearchCheckbox.vue +356 -356
  43. package/src/components/SStringToInput.vue +66 -66
  44. package/src/components/STab.vue +80 -80
  45. package/src/components/STableTree.vue +208 -208
  46. package/src/components/STimePicker.vue +159 -159
  47. package/src/components/SToggle.vue +68 -68
  48. package/src/components/STooltip.vue +209 -209
  49. package/src/components/SelelctItem.vue +21 -21
  50. package/src/components/TimePickerCard.vue +352 -352
  51. package/src/composables/date.js +11 -11
  52. package/src/composables/modelBinder.js +13 -13
  53. package/src/composables/table/use-navigator.js +110 -110
  54. package/src/composables/table/use-resizable.js +80 -80
  55. package/src/css/app.scss +90 -90
  56. package/src/css/extends.scss +154 -154
  57. package/src/css/quasar.variables.scss +189 -189
  58. package/src/index.common.js +1 -1
  59. package/src/index.esm.js +4 -4
  60. package/src/index.scss +9 -9
  61. package/src/index.umd.js +3 -3
  62. package/src/vue-plugin.js +92 -92
@@ -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>