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