classcard-ui 0.2.1497 → 0.2.1498-beta.0

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 (216) hide show
  1. package/README.md +24 -24
  2. package/dist/classcard-ui.common.js +181 -142
  3. package/dist/classcard-ui.common.js.map +1 -1
  4. package/dist/classcard-ui.css +1 -1
  5. package/dist/classcard-ui.umd.js +181 -142
  6. package/dist/classcard-ui.umd.js.map +1 -1
  7. package/dist/classcard-ui.umd.min.js +2 -2
  8. package/dist/classcard-ui.umd.min.js.map +1 -1
  9. package/package.json +84 -84
  10. package/src/App.vue +16 -16
  11. package/src/colorConfig.js +52 -52
  12. package/src/components/CAlertModal/CAlertModal.vue +179 -179
  13. package/src/components/CAlertModal/index.js +3 -3
  14. package/src/components/CAlerts/CAlerts.vue +114 -114
  15. package/src/components/CAlerts/index.js +2 -2
  16. package/src/components/CAnchorTabs/CAnchorTabs.vue +100 -100
  17. package/src/components/CAnchorTabs/index.js +2 -2
  18. package/src/components/CAnchorTag/CAnchorTag.vue +84 -84
  19. package/src/components/CAnchorTag/index.js +2 -2
  20. package/src/components/CAvatar/CAvatar.vue +232 -232
  21. package/src/components/CAvatar/index.js +2 -2
  22. package/src/components/CAvatarGroup/CAvatarGroup.vue +213 -213
  23. package/src/components/CAvatarGroup/index.js +2 -2
  24. package/src/components/CBasicTable/CBasicTable.vue +184 -184
  25. package/src/components/CBasicTable/index.js +2 -2
  26. package/src/components/CBreadcrumbs/CBreadcrumbs.vue +38 -38
  27. package/src/components/CBreadcrumbs/index.js +2 -2
  28. package/src/components/CButton/CButton.vue +239 -239
  29. package/src/components/CButton/index.js +2 -2
  30. package/src/components/CButtonGroup/CButtonGroup.vue +155 -155
  31. package/src/components/CButtonGroup/index.js +2 -2
  32. package/src/components/CButtonIcon/CButtonIcon.vue +166 -166
  33. package/src/components/CButtonIcon/index.js +2 -2
  34. package/src/components/CButtonLink/CButtonLink.vue +43 -43
  35. package/src/components/CButtonLink/index.js +2 -2
  36. package/src/components/CButtonSelect/CButtonSelect.vue +186 -186
  37. package/src/components/CButtonSelect/index.js +2 -2
  38. package/src/components/CButtonSelectBorder/CButtonSelectBorder.vue +265 -265
  39. package/src/components/CButtonSelectBorder/index.js +3 -3
  40. package/src/components/CButtonWithDropdown/CButtonWithDropdown.vue +208 -208
  41. package/src/components/CButtonWithDropdown/index.js +2 -2
  42. package/src/components/CCalendar/CCalendar.vue +443 -443
  43. package/src/components/CCalendar/index.js +3 -3
  44. package/src/components/CCard/CCard.vue +53 -53
  45. package/src/components/CCard/index.js +2 -2
  46. package/src/components/CCheckbox/CCheckbox.vue +200 -200
  47. package/src/components/CCheckbox/index.js +2 -2
  48. package/src/components/CCircularButton/CCircularButton.vue +57 -57
  49. package/src/components/CCircularButton/index.js +2 -2
  50. package/src/components/CCollapsibleSection/CCollapsibleSection.vue +121 -121
  51. package/src/components/CCollapsibleSection/index.js +2 -2
  52. package/src/components/CColorDots/CColorDots.vue +52 -52
  53. package/src/components/CColorDots/index.js +3 -3
  54. package/src/components/CConfirmActionModal/CConfirmActionModal.vue +253 -253
  55. package/src/components/CConfirmActionModal/index.js +3 -3
  56. package/src/components/CDatepicker/CDatepicker.vue +235 -235
  57. package/src/components/CDatepicker/index.js +2 -2
  58. package/src/components/CDualSelect/CDualSelect.vue +193 -193
  59. package/src/components/CDualSelect/index.js +2 -2
  60. package/src/components/CEditor/CEditor.vue +137 -137
  61. package/src/components/CEditor/index.js +6 -6
  62. package/src/components/CEditor/quill-font-setup.js +133 -133
  63. package/src/components/CFormSectionHeading/CFormSectionHeading.vue +76 -76
  64. package/src/components/CFormSectionHeading/index.js +2 -2
  65. package/src/components/CGroupedFilterDropdown/CGroupedFilterDropdown.vue +331 -331
  66. package/src/components/CGroupedFilterDropdown/index.js +2 -2
  67. package/src/components/CGroupedSelect/CGroupedSelect.vue +393 -393
  68. package/src/components/CGroupedSelect/index.js +3 -3
  69. package/src/components/CIcon/CIcon.vue +112 -112
  70. package/src/components/CIcon/index.js +2 -2
  71. package/src/components/CIconDropdown/CIconDropdown.vue +206 -206
  72. package/src/components/CIconDropdown/index.js +2 -2
  73. package/src/components/CIconSelect/CIconSelect.vue +182 -182
  74. package/src/components/CIconSelect/index.js +3 -3
  75. package/src/components/CInput/CInput.vue +173 -173
  76. package/src/components/CInput/index.js +2 -2
  77. package/src/components/CInputAddon/CInputAddon.vue +302 -297
  78. package/src/components/CInputAddon/index.js +2 -2
  79. package/src/components/CInputEmail/CInputEmail.vue +107 -107
  80. package/src/components/CInputEmail/index.js +2 -2
  81. package/src/components/CInsetTabs/CInsetTabs.vue +141 -141
  82. package/src/components/CInsetTabs/index.js +3 -3
  83. package/src/components/CLinearProgressBar/CLinearProgressBar.vue +35 -35
  84. package/src/components/CLinearProgressBar/index.js +2 -2
  85. package/src/components/CModalHeading/CModalHeading.vue +22 -22
  86. package/src/components/CModalHeading/index.js +2 -2
  87. package/src/components/CModuleHelpLinks/CModuleHelpLinks.vue +88 -88
  88. package/src/components/CModuleHelpLinks/index.js +3 -3
  89. package/src/components/CMultiselect/CMultiselect.vue +1307 -1307
  90. package/src/components/CMultiselect/index.js +2 -2
  91. package/src/components/CMultiselectr/CMultiselectr.vue +44 -44
  92. package/src/components/CMultiselectr/index.js +2 -2
  93. package/src/components/CPageHeading/CPageHeading.vue +83 -83
  94. package/src/components/CPageHeading/index.js +2 -2
  95. package/src/components/CPagination/CPagination.vue +239 -239
  96. package/src/components/CPagination/index.js +2 -2
  97. package/src/components/CPhoneNumber/CPhoneNumber.vue +213 -213
  98. package/src/components/CPhoneNumber/index.js +2 -2
  99. package/src/components/CProgress/CProgress.vue +91 -91
  100. package/src/components/CProgress/index.js +2 -2
  101. package/src/components/CRadio/CRadio.vue +197 -197
  102. package/src/components/CRadio/index.js +2 -2
  103. package/src/components/CRadioGroup/CRadioGroup.vue +96 -96
  104. package/src/components/CRadioGroup/index.js +2 -2
  105. package/src/components/CRangeSlider/CRangeSlider.vue +55 -55
  106. package/src/components/CRangeSlider/index.js +2 -2
  107. package/src/components/CReorderableStackedList/CReorderableStackedList.vue +94 -94
  108. package/src/components/CReorderableStackedList/index.js +2 -2
  109. package/src/components/CSelect/CSelect.vue +1216 -1216
  110. package/src/components/CSelect/index.js +2 -2
  111. package/src/components/CSmallTimeline/CSmallTimeline.vue +40 -40
  112. package/src/components/CSmallTimeline/index.js +2 -2
  113. package/src/components/CStackedList/CStackedList.vue +162 -162
  114. package/src/components/CStackedList/index.js +2 -2
  115. package/src/components/CStats/CStats.vue +157 -157
  116. package/src/components/CStats/index.js +2 -2
  117. package/src/components/CSwitch/CSwitch.vue +200 -200
  118. package/src/components/CSwitch/index.js +2 -2
  119. package/src/components/CTabLazy/CTabLazy.vue +83 -83
  120. package/src/components/CTabLazy/index.js +2 -2
  121. package/src/components/CTable/CTable.vue +1114 -1114
  122. package/src/components/CTable/index.js +2 -2
  123. package/src/components/CTabs/CTabs.vue +250 -250
  124. package/src/components/CTabs/index.js +2 -2
  125. package/src/components/CTag/CTag.vue +109 -109
  126. package/src/components/CTag/index.js +2 -2
  127. package/src/components/CTextarea/CTextarea.vue +118 -118
  128. package/src/components/CTextarea/index.js +2 -2
  129. package/src/components/CTimeline/CTimeline.vue +237 -237
  130. package/src/components/CTimeline/index.js +2 -2
  131. package/src/components/CTokenEditor/CTokenEditor.vue +804 -804
  132. package/src/components/CTokenEditor/index.js +3 -3
  133. package/src/components/CToolTip/CToolTip.vue +108 -108
  134. package/src/components/CToolTip/index.js +3 -3
  135. package/src/components/CUpload/CUpload.vue +332 -332
  136. package/src/components/CUpload/index.js +2 -2
  137. package/src/components/NumberAnimator.vue +112 -112
  138. package/src/components/index.js +60 -60
  139. package/src/helper.js +8 -8
  140. package/src/icons.js +833 -833
  141. package/src/main.js +22 -22
  142. package/src/stories/CAlertModal.stories.js +30 -30
  143. package/src/stories/CAlerts.stories.js +39 -39
  144. package/src/stories/CAnchorTabs.stories.js +29 -29
  145. package/src/stories/CAnchorTag.stories.js +38 -38
  146. package/src/stories/CAvatar.stories.js +38 -38
  147. package/src/stories/CAvatarGroup.stories.js +136 -136
  148. package/src/stories/CBasicTable.stories.js +316 -316
  149. package/src/stories/CBreadcrumbs.stories.js +24 -24
  150. package/src/stories/CButton.stories.js +49 -49
  151. package/src/stories/CButtonGroup.stories.js +43 -43
  152. package/src/stories/CButtonIcon.stories.js +27 -27
  153. package/src/stories/CButtonLink.stories.js +24 -24
  154. package/src/stories/CButtonSelect.stories.js +44 -44
  155. package/src/stories/CButtonSelectBorder.stories.js +56 -56
  156. package/src/stories/CButtonWithDropdown.stories.js +41 -41
  157. package/src/stories/CCalendar.stories.js +16 -16
  158. package/src/stories/CCard.stories.js +30 -30
  159. package/src/stories/CCheckbox.stories.js +38 -38
  160. package/src/stories/CCircularButton.stories.js +29 -29
  161. package/src/stories/CCollapsibleSection.stories.js +29 -29
  162. package/src/stories/CColorDots.stories.js +37 -37
  163. package/src/stories/CConfirmActionModal.stories.js +67 -67
  164. package/src/stories/CDatepicker.stories.js +31 -31
  165. package/src/stories/CDualSelect.stories.js +29 -29
  166. package/src/stories/CEditor.stories.js +36 -36
  167. package/src/stories/CFormSectionHeading.stories.js +37 -37
  168. package/src/stories/CGroupedFilterDropdown.stories.js +131 -131
  169. package/src/stories/CGroupedSelect.stories.js +103 -103
  170. package/src/stories/CIcon.stories.js +31 -31
  171. package/src/stories/CIconDropdown.stories.js +52 -52
  172. package/src/stories/CIconSelect.stories.js +45 -45
  173. package/src/stories/CInput.stories.js +36 -36
  174. package/src/stories/CInputAddon.stories.js +37 -37
  175. package/src/stories/CInputEmail.stories.js +27 -27
  176. package/src/stories/CInsetTabs.stories.js +48 -48
  177. package/src/stories/CLinearProgressBar.stories.js +22 -22
  178. package/src/stories/CModalHeading.stories.js +25 -25
  179. package/src/stories/CModuleHelpLinks.stories.js +25 -25
  180. package/src/stories/CMultiselect.stories.js +146 -146
  181. package/src/stories/CMultiselectr.stories.js +23 -23
  182. package/src/stories/CPageHeading.stories.js +32 -32
  183. package/src/stories/CPagination.stories.js +30 -30
  184. package/src/stories/CPhoneNumber.stories.js +37 -37
  185. package/src/stories/CProgress.stories.js +23 -23
  186. package/src/stories/CRadio.stories.js +44 -44
  187. package/src/stories/CRadioGroup.stories.js +51 -51
  188. package/src/stories/CRangeSlider.stories.js +23 -23
  189. package/src/stories/CReorderableStackedList.stories.js +23 -23
  190. package/src/stories/CSelect.stories.js +157 -157
  191. package/src/stories/CSmallTimeline.stories.js +26 -26
  192. package/src/stories/CStackedList.stories.js +37 -37
  193. package/src/stories/CStats.stories.js +53 -53
  194. package/src/stories/CSwitch.stories.js +28 -28
  195. package/src/stories/CTabLazy.stories.js +42 -42
  196. package/src/stories/CTable.stories.js +203 -203
  197. package/src/stories/CTabs.stories.js +36 -36
  198. package/src/stories/CTag.stories.js +37 -37
  199. package/src/stories/CTextarea.stories.js +32 -32
  200. package/src/stories/CTimeline.stories.js +26 -26
  201. package/src/stories/CTokenEditor.stories.js +136 -136
  202. package/src/stories/CToolTip.stories.js +27 -27
  203. package/src/stories/CUpload.stories.js +36 -36
  204. package/src/stories/Introduction.stories.mdx +207 -207
  205. package/src/stories/Page.vue +88 -88
  206. package/src/stories/assets/code-brackets.svg +0 -0
  207. package/src/stories/assets/colors.svg +0 -0
  208. package/src/stories/assets/comments.svg +0 -0
  209. package/src/stories/assets/direction.svg +0 -0
  210. package/src/stories/assets/flow.svg +0 -0
  211. package/src/stories/assets/plugin.svg +0 -0
  212. package/src/stories/assets/repo.svg +0 -0
  213. package/src/stories/assets/stackalt.svg +0 -0
  214. package/src/stories/header.css +26 -26
  215. package/src/stories/page.css +69 -69
  216. package/src/stories/utils.css +32 -32
@@ -1,804 +1,804 @@
1
- <template>
2
- <div class="text-sm">
3
- <div v-if="label || hint" class="flex justify-between">
4
- <p v-if="label" class="font-medium text-gray-900">{{ label }}</p>
5
- <p v-if="hint" class="text-gray-500">{{ hint }}</p>
6
- </div>
7
- <quill-editor
8
- ref="myQuillEditor"
9
- :class="`mt-1 rounded-md bg-white ${
10
- disabled ? 'cursor-not-allowed opacity-50' : ''
11
- } shadow-sm`"
12
- :value="content"
13
- :options="editorOption"
14
- :id="id"
15
- @change="onChange($event)"
16
- @ready="onReady($event)"
17
- @focus="handleFocus"
18
- @blur="onEditorBlur"
19
- :disabled="disabled"
20
- ></quill-editor>
21
- <p v-if="helpText" class="mt-2 text-gray-500">
22
- {{ helpText }}
23
- </p>
24
- </div>
25
- </template>
26
- <script>
27
- import "quill/dist/quill.core.css";
28
- import "quill/dist/quill.snow.css";
29
- import "quill/dist/quill.bubble.css";
30
- import { quillEditor } from "vue-quill-editor";
31
- import Quill from "quill";
32
-
33
- const Embed = Quill.import("blots/embed");
34
- const Delta = Quill.import("delta");
35
- const TOKEN_PATTERN = /\{\{([^{}]+)\}\}/g;
36
- const DEFAULT_TOKEN_STYLE = {
37
- wrapper:
38
- "inline-flex overflow-hidden rounded-md border border-gray-200 text-sm leading-5",
39
- group: "p-1 text-sm text-gray-500 bg-gray-50",
40
- separator: "bg-white py-1 pr-1 text-sm text-gray-400 bg-gray-50",
41
- label: "bg-indigo-50 px-2 py-1 text-sm text-indigo-600",
42
- remove: "token-remove bg-indigo-50 py-1 pr-2 text-indigo-600",
43
- };
44
- const INVALID_TOKEN_STYLE = {
45
- wrapper:
46
- "inline-flex overflow-hidden rounded-md border border-red-300 text-sm leading-5",
47
- group: "p-1 text-sm text-gray-500 bg-gray-50",
48
- separator: "bg-white py-1 pr-1 text-sm text-gray-400 bg-gray-50",
49
- label: "bg-red-50 px-2 py-1 text-sm text-red-600",
50
- remove: "token-remove bg-red-50 py-1 pr-2 text-red-600",
51
- };
52
-
53
- function createTokenChipContent({
54
- tokenStyle,
55
- tokenGroup,
56
- tokenSeparator,
57
- tokenLabel,
58
- }) {
59
- const wrapper = document.createElement("span");
60
- wrapper.className = tokenStyle.wrapper;
61
-
62
- const groupEl = document.createElement("span");
63
- groupEl.className = tokenStyle.group;
64
- groupEl.textContent = tokenGroup;
65
-
66
- const separatorEl = document.createElement("span");
67
- separatorEl.className = tokenStyle.separator;
68
- separatorEl.textContent = tokenSeparator;
69
-
70
- const labelEl = document.createElement("span");
71
- labelEl.className = tokenStyle.label;
72
- labelEl.textContent = tokenLabel;
73
-
74
- const removeBtn = document.createElement("button");
75
- removeBtn.type = "button";
76
- removeBtn.className = tokenStyle.remove;
77
- removeBtn.setAttribute("data-token-remove", "true");
78
- removeBtn.setAttribute("aria-label", "Remove token");
79
- removeBtn.textContent = "✕";
80
-
81
- wrapper.appendChild(groupEl);
82
- wrapper.appendChild(separatorEl);
83
- wrapper.appendChild(labelEl);
84
- wrapper.appendChild(removeBtn);
85
-
86
- return wrapper;
87
- }
88
-
89
- class TokenBlot extends Embed {
90
- static create(value) {
91
- const node = super.create();
92
- let tokenValue = "";
93
- let tokenLabel = "";
94
- let tokenGroup = "Variable";
95
- let tokenSeparator = "→";
96
- let tokenStyle = { ...DEFAULT_TOKEN_STYLE };
97
-
98
- if (typeof value === "string") {
99
- tokenValue = value;
100
- tokenLabel = value;
101
- } else if (value && typeof value === "object") {
102
- tokenValue = value.value || "";
103
- tokenLabel = value.label || tokenValue;
104
- tokenGroup = value.group || "Variable";
105
- tokenSeparator = value.separator || "→";
106
- if (value.style) {
107
- tokenStyle = { ...tokenStyle, ...value.style };
108
- }
109
- }
110
-
111
- node.setAttribute("contenteditable", "false");
112
- node.setAttribute("data-token", tokenValue);
113
- node.setAttribute("data-label", tokenLabel);
114
- node.setAttribute("data-group", tokenGroup);
115
- node.setAttribute("data-separator", tokenSeparator);
116
- node.setAttribute("data-token-style", JSON.stringify(tokenStyle));
117
- node.className = "ql-token-chip inline-flex align-middle";
118
- node.appendChild(
119
- createTokenChipContent({
120
- tokenStyle,
121
- tokenGroup,
122
- tokenSeparator,
123
- tokenLabel,
124
- }),
125
- );
126
- return node;
127
- }
128
-
129
- static value(node) {
130
- let tokenStyle = {};
131
- const styleAttr = node.getAttribute("data-token-style");
132
- if (styleAttr) {
133
- try {
134
- const parsed = JSON.parse(styleAttr);
135
- if (parsed && typeof parsed === "object") {
136
- tokenStyle = parsed;
137
- }
138
- } catch (error) {
139
- tokenStyle = {};
140
- }
141
- }
142
- return {
143
- value: node.getAttribute("data-token") || "",
144
- label: node.getAttribute("data-label") || "",
145
- group: node.getAttribute("data-group") || "",
146
- separator: node.getAttribute("data-separator") || "→",
147
- style: tokenStyle,
148
- };
149
- }
150
- }
151
-
152
- TokenBlot.blotName = "token";
153
- TokenBlot.tagName = "span";
154
- // Scope the blot to spans carrying the token marker class. Without this, the
155
- // blot matches EVERY <span> (Quill registers globally by tagName), so plain
156
- // spans in other editors (e.g. CEditor content) get swallowed and rendered as
157
- // generic "Variable →" chips.
158
- TokenBlot.className = "ql-token-chip";
159
-
160
- const existingTokenFormat = Quill.import("formats/token");
161
- if (!existingTokenFormat || typeof existingTokenFormat.create !== "function") {
162
- Quill.register({ "formats/token": TokenBlot }, true);
163
- }
164
- export default {
165
- name: "CTokenEditor",
166
- components: { quillEditor },
167
- props: {
168
- id: {
169
- type: String,
170
- },
171
- content: {
172
- type: String,
173
- },
174
- label: {
175
- type: String,
176
- },
177
- hint: {
178
- type: String,
179
- },
180
- helpText: {
181
- type: String,
182
- },
183
- toolbarOptions: {
184
- type: Array,
185
- },
186
- onEditorChange: {
187
- type: Function,
188
- },
189
- placeholder: {
190
- type: String,
191
- },
192
- disabled: {
193
- type: Boolean,
194
- default: false,
195
- },
196
- templateVariables: {
197
- type: Array,
198
- default: () => [],
199
- },
200
- tokenChipStyles: {
201
- type: Object,
202
- default: () => ({}),
203
- },
204
- tokenSeparator: {
205
- type: String,
206
- default: "→",
207
- },
208
- enableTokenChips: {
209
- type: Boolean,
210
- default: false,
211
- },
212
- containerMinHeight: {
213
- type: [String, Number],
214
- default: "5rem",
215
- },
216
- editorMaxHeight: {
217
- type: [String, Number],
218
- default: "200px",
219
- },
220
- },
221
- data() {
222
- return {
223
- editorInstance: null,
224
- isRenderingTokens: false,
225
- rootClickHandler: null,
226
- pasteHandler: null,
227
- renderedRawContent: "",
228
- renderedHtmlContent: "",
229
- lastSelectionRange: null,
230
- selectionChangeHandler: null,
231
- templateVariablesSignature: "",
232
- };
233
- },
234
- watch: {
235
- content: {
236
- immediate: true,
237
- handler(newValue) {
238
- const normalized = newValue || "";
239
- if (!this.editorInstance) return;
240
- if (this.enableTokenChips) {
241
- if (this.hasHtmlTags(normalized)) {
242
- if (
243
- normalized === this.renderedHtmlContent &&
244
- !this.shouldRetokenizeHtml(normalized)
245
- ) {
246
- return;
247
- }
248
- this.renderTokensFromHtml(normalized);
249
- return;
250
- }
251
- if (normalized === this.renderedRawContent) return;
252
- this.renderTokensFromRaw(normalized);
253
- return;
254
- }
255
- if (normalized === this.renderedHtmlContent) return;
256
- this.renderHtmlContent(normalized);
257
- },
258
- },
259
- tokenChipStyles: {
260
- deep: true,
261
- handler() {
262
- if (!this.editorInstance || !this.enableTokenChips) return;
263
- this.renderCurrentTokenContent();
264
- },
265
- },
266
- tokenSeparator() {
267
- if (!this.editorInstance || !this.enableTokenChips) return;
268
- this.renderCurrentTokenContent();
269
- },
270
- templateVariables: {
271
- deep: true,
272
- handler(newValue) {
273
- if (this.isRenderingTokens) return;
274
- const nextSignature = this.buildTemplateVariablesSignature(newValue);
275
- this.templateVariablesSignature = nextSignature;
276
- if (!this.editorInstance || !this.enableTokenChips) return;
277
- this.$nextTick(() => {
278
- this.renderCurrentTokenContent();
279
- this.$nextTick(() => this.refreshTokenNodes());
280
- });
281
- },
282
- },
283
- containerMinHeight() {
284
- this.$nextTick(() => this.applyEditorHeights());
285
- },
286
- editorMaxHeight() {
287
- this.$nextTick(() => this.applyEditorHeights());
288
- },
289
- },
290
- methods: {
291
- normalizeCssSize(value, fallback) {
292
- if (value === null || value === undefined || value === "") {
293
- return fallback;
294
- }
295
- if (typeof value === "number" || !isNaN(Number(value))) {
296
- return `${value}px`;
297
- }
298
- return String(value);
299
- },
300
- buildTemplateVariablesSignature(variables = []) {
301
- return (variables || [])
302
- .map((item) => {
303
- const value = item && item.value ? String(item.value) : "";
304
- const label = item && item.label ? String(item.label) : "";
305
- const group = item && item.group ? String(item.group) : "";
306
- return `${value}|${label}|${group}`;
307
- })
308
- .sort()
309
- .join("||");
310
- },
311
- hasHtmlTags(value) {
312
- return /<(p|div|span|strong|em|u|s|ol|ul|li|h[1-6]|br|img|a)\b[^>]*>/i.test(value || "");
313
- },
314
- hasMustacheTokens(value) {
315
- TOKEN_PATTERN.lastIndex = 0;
316
- return TOKEN_PATTERN.test(value || "");
317
- },
318
- shouldRetokenizeHtml(htmlContent) {
319
- if (!this.hasMustacheTokens(htmlContent)) {
320
- return false;
321
- }
322
- const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
323
- if (!quill || !quill.root) {
324
- return false;
325
- }
326
- const tokenCount = quill.root.querySelectorAll("[data-token]").length;
327
- return tokenCount === 0;
328
- },
329
- applyEditorHeights() {
330
- const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
331
- if (!quill) return;
332
- quill.container.style.minHeight = this.normalizeCssSize(
333
- this.containerMinHeight,
334
- "5rem",
335
- );
336
- quill.root.style.maxHeight = this.normalizeCssSize(
337
- this.editorMaxHeight,
338
- "200px",
339
- );
340
- },
341
- renderHtmlContent(htmlContent) {
342
- const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
343
- if (!quill) return;
344
- this.isRenderingTokens = true;
345
- const contentToRender = htmlContent || "";
346
- const previousSelection = quill.getSelection() || this.lastSelectionRange;
347
- quill.clipboard.dangerouslyPasteHTML(contentToRender, "silent");
348
- const length = Math.max(quill.getLength() - 1, 0);
349
- const nextIndex = previousSelection
350
- ? Math.min(previousSelection.index, length)
351
- : length;
352
- quill.setSelection(nextIndex, 0, "silent");
353
- this.lastSelectionRange = { index: nextIndex, length: 0 };
354
- this.renderedHtmlContent = contentToRender;
355
- this.renderedRawContent = this.extractRawText(quill);
356
- this.isRenderingTokens = false;
357
- },
358
- renderCurrentTokenContent() {
359
- const contentToRender =
360
- this.content ||
361
- this.renderedHtmlContent ||
362
- this.renderedRawContent ||
363
- "";
364
- if (this.hasHtmlTags(contentToRender)) {
365
- this.renderTokensFromHtml(contentToRender);
366
- return;
367
- }
368
- this.renderTokensFromRaw(contentToRender);
369
- },
370
- renderTokensFromHtml(htmlContent) {
371
- const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
372
- if (!quill) return;
373
-
374
- const contentToRender = htmlContent || "";
375
- const previousSelection = quill.getSelection() || this.lastSelectionRange;
376
- const converted = quill.clipboard.convert(contentToRender);
377
- const nextDelta = new Delta();
378
-
379
- (converted.ops || []).forEach((op) => {
380
- if (typeof op.insert !== "string") {
381
- nextDelta.push(op);
382
- return;
383
- }
384
-
385
- TOKEN_PATTERN.lastIndex = 0;
386
- const tokenRegex = TOKEN_PATTERN;
387
- const text = op.insert;
388
- let lastIndex = 0;
389
- let match = null;
390
-
391
- while ((match = tokenRegex.exec(text)) !== null) {
392
- const plainText = text.slice(lastIndex, match.index);
393
- if (plainText) {
394
- nextDelta.insert(plainText, op.attributes || undefined);
395
- }
396
-
397
- const tokenKey = (match[1] || "").trim();
398
- if (tokenKey) {
399
- const isKnownToken = this.isKnownToken(tokenKey);
400
- nextDelta.insert({
401
- token: {
402
- value: tokenKey,
403
- label: this.getTokenLabel(tokenKey),
404
- group: this.getTokenGroup(tokenKey),
405
- style: this.getTokenStyleConfig({ isInvalid: !isKnownToken }),
406
- separator: this.tokenSeparator,
407
- },
408
- });
409
- } else {
410
- nextDelta.insert(match[0], op.attributes || undefined);
411
- }
412
-
413
- lastIndex = tokenRegex.lastIndex;
414
- }
415
-
416
- const remainingText = text.slice(lastIndex);
417
- if (remainingText) {
418
- nextDelta.insert(remainingText, op.attributes || undefined);
419
- }
420
- });
421
-
422
- this.isRenderingTokens = true;
423
- try {
424
- quill.setContents(nextDelta, "silent");
425
-
426
- const length = Math.max(quill.getLength() - 1, 0);
427
- const nextIndex = previousSelection
428
- ? Math.min(previousSelection.index, length)
429
- : length;
430
- quill.setSelection(nextIndex, 0, "silent");
431
- this.lastSelectionRange = { index: nextIndex, length: 0 };
432
- this.renderedHtmlContent = contentToRender;
433
- this.renderedRawContent = this.extractRawText(quill);
434
- } finally {
435
- this.isRenderingTokens = false;
436
- }
437
- this.$nextTick(() => this.refreshTokenNodes());
438
- },
439
- onChange(params) {
440
- if (this.isRenderingTokens) return;
441
- if (!this.enableTokenChips) {
442
- const htmlText =
443
- params && typeof params.html === "string" ? params.html : "";
444
- this.renderedHtmlContent = htmlText;
445
- this.renderedRawContent = htmlText;
446
- this.$emit("input", htmlText);
447
- this.$emit("update:content", htmlText);
448
- this.$emit("onEditorChange", {
449
- ...params,
450
- rawText: this.extractRawText(params.quill),
451
- });
452
- return;
453
- }
454
- const rawText = this.extractRawText(params.quill);
455
- const htmlText =
456
- params && typeof params.html === "string" ? params.html : rawText;
457
- this.renderedRawContent = rawText;
458
- this.renderedHtmlContent = htmlText;
459
- this.$emit("input", rawText);
460
- this.$emit("update:content", rawText);
461
- this.$emit("onEditorChange", { ...params, rawText });
462
- },
463
- onReady(params) {
464
- this.editorInstance = params;
465
- this.templateVariablesSignature = this.buildTemplateVariablesSignature(
466
- this.templateVariables,
467
- );
468
- this.attachSelectionTracking();
469
- this.attachTokenRemoveHandler();
470
- this.attachPlainTextPasteHandler();
471
- const initialContent = this.content || "";
472
- if (this.enableTokenChips && !this.hasHtmlTags(initialContent)) {
473
- this.renderTokensFromRaw(initialContent);
474
- } else if (this.enableTokenChips) {
475
- this.renderTokensFromHtml(initialContent);
476
- } else {
477
- this.renderHtmlContent(initialContent);
478
- }
479
- this.applyEditorHeights();
480
- this.$emit("onEditorReady", params);
481
- },
482
- handleFocus() {
483
- let quill = this.$refs.myQuillEditor.quill;
484
- if (quill) {
485
- this.$emit("handleFocus", quill);
486
- }
487
- },
488
- onEditorBlur() {
489
- this.$emit("onEditorBlur");
490
- },
491
- insertToken({ value, label, group }) {
492
- if (!this.enableTokenChips) return;
493
- const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
494
- if (!quill || !value) return;
495
-
496
- const selection = quill.getSelection(true) || this.lastSelectionRange;
497
- const index = selection ? selection.index : quill.getLength();
498
- quill.insertEmbed(
499
- index,
500
- "token",
501
- {
502
- value,
503
- label: label || this.getTokenLabel(value),
504
- group: group || this.getTokenGroup(value),
505
- style: this.getTokenStyleConfig(),
506
- separator: this.tokenSeparator,
507
- },
508
- "user",
509
- );
510
- quill.setSelection(index + 1, 0, "silent");
511
- this.lastSelectionRange = { index: index + 1, length: 0 };
512
- },
513
- extractRawText(quill) {
514
- if (!quill) return "";
515
- const delta = quill.getContents();
516
- const raw = (delta.ops || [])
517
- .map((op) => {
518
- if (typeof op.insert === "string") return op.insert;
519
- if (op.insert && op.insert.token) {
520
- return `{{${op.insert.token.value}}}`;
521
- }
522
- return "";
523
- })
524
- .join("");
525
-
526
- return raw.endsWith("\n") ? raw.slice(0, -1) : raw;
527
- },
528
- renderTokensFromRaw(rawText) {
529
- const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
530
- if (!quill) return;
531
-
532
- const source = rawText || "";
533
- const previousSelection = quill.getSelection() || this.lastSelectionRange;
534
- const tokenRegex = /\{\{([^{}]+)\}\}/g;
535
- let currentIndex = 0;
536
- let lastIndex = 0;
537
- let match = null;
538
-
539
- this.isRenderingTokens = true;
540
- quill.setContents([], "silent");
541
-
542
- while ((match = tokenRegex.exec(source)) !== null) {
543
- const plainText = source.slice(lastIndex, match.index);
544
- if (plainText) {
545
- quill.insertText(currentIndex, plainText, "silent");
546
- currentIndex += plainText.length;
547
- }
548
-
549
- const tokenKey = (match[1] || "").trim();
550
- if (!tokenKey) {
551
- quill.insertText(currentIndex, match[0], "silent");
552
- currentIndex += match[0].length;
553
- lastIndex = tokenRegex.lastIndex;
554
- continue;
555
- }
556
- const isKnownToken = this.isKnownToken(tokenKey);
557
- quill.insertEmbed(
558
- currentIndex,
559
- "token",
560
- {
561
- value: tokenKey,
562
- label: this.getTokenLabel(tokenKey),
563
- group: this.getTokenGroup(tokenKey),
564
- style: this.getTokenStyleConfig({ isInvalid: !isKnownToken }),
565
- separator: this.tokenSeparator,
566
- },
567
- "silent",
568
- );
569
- currentIndex += 1;
570
- lastIndex = tokenRegex.lastIndex;
571
- }
572
-
573
- const remainingText = source.slice(lastIndex);
574
- if (remainingText) {
575
- quill.insertText(currentIndex, remainingText, "silent");
576
- currentIndex += remainingText.length;
577
- }
578
-
579
- const nextIndex = previousSelection
580
- ? Math.min(previousSelection.index, currentIndex)
581
- : currentIndex;
582
- quill.setSelection(nextIndex, 0, "silent");
583
- this.lastSelectionRange = { index: nextIndex, length: 0 };
584
- this.renderedRawContent = source;
585
- this.renderedHtmlContent = quill.root.innerHTML;
586
- this.isRenderingTokens = false;
587
- this.$nextTick(() => this.refreshTokenNodes());
588
- },
589
- attachSelectionTracking() {
590
- const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
591
- if (!quill || this.selectionChangeHandler) return;
592
- this.selectionChangeHandler = (range) => {
593
- if (range) {
594
- this.lastSelectionRange = range;
595
- }
596
- };
597
- quill.on("selection-change", this.selectionChangeHandler);
598
- },
599
- attachPlainTextPasteHandler() {
600
- if (!this.enableTokenChips) return;
601
- const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
602
- if (!quill || this.pasteHandler) return;
603
- this.pasteHandler = (event) => {
604
- const clipboardText =
605
- event.clipboardData && event.clipboardData.getData("text/plain");
606
- if (typeof clipboardText !== "string") return;
607
- event.preventDefault();
608
- const selection = quill.getSelection(true) || this.lastSelectionRange;
609
- const index = selection ? selection.index : quill.getLength();
610
- const length = selection ? selection.length || 0 : 0;
611
-
612
- const pasteDelta = new Delta().retain(index);
613
- if (length > 0) {
614
- pasteDelta.delete(length);
615
- }
616
-
617
- TOKEN_PATTERN.lastIndex = 0;
618
- const tokenRegex = TOKEN_PATTERN;
619
- let lastIndex = 0;
620
- let match = null;
621
- let insertedLength = 0;
622
-
623
- while ((match = tokenRegex.exec(clipboardText)) !== null) {
624
- const plainText = clipboardText.slice(lastIndex, match.index);
625
- if (plainText) {
626
- pasteDelta.insert(plainText);
627
- insertedLength += plainText.length;
628
- }
629
- const tokenKey = (match[1] || "").trim();
630
- if (tokenKey) {
631
- const isKnownToken = this.isKnownToken(tokenKey);
632
- pasteDelta.insert({
633
- token: {
634
- value: tokenKey,
635
- label: this.getTokenLabel(tokenKey),
636
- group: this.getTokenGroup(tokenKey),
637
- style: this.getTokenStyleConfig({ isInvalid: !isKnownToken }),
638
- separator: this.tokenSeparator,
639
- },
640
- });
641
- insertedLength += 1;
642
- } else {
643
- pasteDelta.insert(match[0]);
644
- insertedLength += match[0].length;
645
- }
646
- lastIndex = tokenRegex.lastIndex;
647
- }
648
-
649
- const remainingText = clipboardText.slice(lastIndex);
650
- if (remainingText) {
651
- pasteDelta.insert(remainingText);
652
- insertedLength += remainingText.length;
653
- }
654
-
655
- quill.updateContents(pasteDelta, "user");
656
- const nextIndex = index + insertedLength;
657
- quill.setSelection(nextIndex, 0, "silent");
658
- this.lastSelectionRange = {
659
- index: nextIndex,
660
- length: 0,
661
- };
662
- };
663
- quill.root.addEventListener("paste", this.pasteHandler);
664
- },
665
- getTokenLabel(key) {
666
- const variable = this.templateVariablesMap.get(key);
667
- return variable && variable.label ? variable.label : key;
668
- },
669
- getTokenGroup(key) {
670
- const variable = this.templateVariablesMap.get(key);
671
- return variable && variable.group ? variable.group : "Variable";
672
- },
673
- isKnownToken(key) {
674
- return this.templateVariablesMap.has(key);
675
- },
676
- getTokenStyleConfig({ isInvalid = false } = {}) {
677
- const baseStyle = isInvalid ? INVALID_TOKEN_STYLE : DEFAULT_TOKEN_STYLE;
678
- return {
679
- ...baseStyle,
680
- ...this.tokenChipStyles,
681
- };
682
- },
683
- refreshTokenNodes() {
684
- if (!this.enableTokenChips) return;
685
- const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
686
- if (!quill || !quill.root) return;
687
- const tokenNodes = quill.root.querySelectorAll("[data-token]");
688
- tokenNodes.forEach((node) => {
689
- const value = (node.getAttribute("data-token") || "").trim();
690
- if (!value) return;
691
- const variable = (this.templateVariables || []).find(
692
- (item) => item.value === value,
693
- );
694
- const isInvalid = !variable;
695
- const separator =
696
- node.getAttribute("data-separator") || this.tokenSeparator;
697
- const group =
698
- (variable && variable.group) ||
699
- node.getAttribute("data-group") ||
700
- "Variable";
701
- const label =
702
- (variable && variable.label) ||
703
- node.getAttribute("data-label") ||
704
- value;
705
- const tokenStyle = this.getTokenStyleConfig({ isInvalid });
706
-
707
- node.setAttribute("data-label", label);
708
- node.setAttribute("data-group", group);
709
- node.setAttribute("data-separator", separator);
710
- node.setAttribute("data-token-style", JSON.stringify(tokenStyle));
711
-
712
- node.innerHTML = "";
713
- node.appendChild(
714
- createTokenChipContent({
715
- tokenStyle,
716
- tokenGroup: group,
717
- tokenSeparator: separator,
718
- tokenLabel: label,
719
- }),
720
- );
721
- });
722
- },
723
- attachTokenRemoveHandler() {
724
- if (!this.enableTokenChips) return;
725
- const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
726
- if (!quill || this.rootClickHandler) return;
727
-
728
- this.rootClickHandler = (event) => {
729
- const removeTarget = event.target.closest("[data-token-remove='true']");
730
- if (!removeTarget) return;
731
- event.preventDefault();
732
- event.stopPropagation();
733
-
734
- const tokenNode = removeTarget.closest("[data-token]");
735
- if (!tokenNode) return;
736
-
737
- const blot = Quill.find(tokenNode);
738
- if (!blot) return;
739
-
740
- const index = quill.getIndex(blot);
741
- quill.deleteText(index, 1, "user");
742
- };
743
-
744
- quill.root.addEventListener("click", this.rootClickHandler);
745
- },
746
- },
747
- beforeDestroy() {
748
- this.isRenderingTokens = false;
749
- const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
750
- if (quill && this.rootClickHandler) {
751
- quill.root.removeEventListener("click", this.rootClickHandler);
752
- }
753
- if (quill && this.pasteHandler) {
754
- quill.root.removeEventListener("paste", this.pasteHandler);
755
- }
756
- if (quill && this.selectionChangeHandler) {
757
- quill.off("selection-change", this.selectionChangeHandler);
758
- }
759
- },
760
- computed: {
761
- templateVariablesMap() {
762
- const map = new Map();
763
- (this.templateVariables || []).forEach((v) => {
764
- if (v && v.value) map.set(v.value, v);
765
- });
766
- return map;
767
- },
768
- editorOption() {
769
- const hasToolbar =
770
- Array.isArray(this.toolbarOptions) && this.toolbarOptions.length > 0;
771
- return {
772
- modules: {
773
- toolbar: hasToolbar
774
- ? {
775
- container: this.toolbarOptions,
776
- }
777
- : false,
778
- },
779
- placeholder: this.placeholder,
780
- };
781
- },
782
- },
783
- };
784
- </script>
785
- <style>
786
- .ql-toolbar.ql-snow {
787
- border-top-left-radius: 0.375rem;
788
- border-top-right-radius: 0.375rem;
789
- }
790
-
791
- .ql-container.ql-snow {
792
- border-bottom-left-radius: 0.375rem;
793
- border-bottom-right-radius: 0.375rem;
794
- min-height: var(--test-editor-container-min-height, 5rem);
795
- }
796
-
797
- .ql-editor {
798
- @apply text-sm text-gray-900;
799
- max-height: var(--test-editor-max-height, 200px);
800
- }
801
- .ql-editor.ql-blank::before {
802
- @apply not-italic text-gray-500;
803
- }
804
- </style>
1
+ <template>
2
+ <div class="text-sm">
3
+ <div v-if="label || hint" class="flex justify-between">
4
+ <p v-if="label" class="font-medium text-gray-900">{{ label }}</p>
5
+ <p v-if="hint" class="text-gray-500">{{ hint }}</p>
6
+ </div>
7
+ <quill-editor
8
+ ref="myQuillEditor"
9
+ :class="`mt-1 rounded-md bg-white ${
10
+ disabled ? 'cursor-not-allowed opacity-50' : ''
11
+ } shadow-sm`"
12
+ :value="content"
13
+ :options="editorOption"
14
+ :id="id"
15
+ @change="onChange($event)"
16
+ @ready="onReady($event)"
17
+ @focus="handleFocus"
18
+ @blur="onEditorBlur"
19
+ :disabled="disabled"
20
+ ></quill-editor>
21
+ <p v-if="helpText" class="mt-2 text-gray-500">
22
+ {{ helpText }}
23
+ </p>
24
+ </div>
25
+ </template>
26
+ <script>
27
+ import "quill/dist/quill.core.css";
28
+ import "quill/dist/quill.snow.css";
29
+ import "quill/dist/quill.bubble.css";
30
+ import { quillEditor } from "vue-quill-editor";
31
+ import Quill from "quill";
32
+
33
+ const Embed = Quill.import("blots/embed");
34
+ const Delta = Quill.import("delta");
35
+ const TOKEN_PATTERN = /\{\{([^{}]+)\}\}/g;
36
+ const DEFAULT_TOKEN_STYLE = {
37
+ wrapper:
38
+ "inline-flex overflow-hidden rounded-md border border-gray-200 text-sm leading-5",
39
+ group: "p-1 text-sm text-gray-500 bg-gray-50",
40
+ separator: "bg-white py-1 pr-1 text-sm text-gray-400 bg-gray-50",
41
+ label: "bg-indigo-50 px-2 py-1 text-sm text-indigo-600",
42
+ remove: "token-remove bg-indigo-50 py-1 pr-2 text-indigo-600",
43
+ };
44
+ const INVALID_TOKEN_STYLE = {
45
+ wrapper:
46
+ "inline-flex overflow-hidden rounded-md border border-red-300 text-sm leading-5",
47
+ group: "p-1 text-sm text-gray-500 bg-gray-50",
48
+ separator: "bg-white py-1 pr-1 text-sm text-gray-400 bg-gray-50",
49
+ label: "bg-red-50 px-2 py-1 text-sm text-red-600",
50
+ remove: "token-remove bg-red-50 py-1 pr-2 text-red-600",
51
+ };
52
+
53
+ function createTokenChipContent({
54
+ tokenStyle,
55
+ tokenGroup,
56
+ tokenSeparator,
57
+ tokenLabel,
58
+ }) {
59
+ const wrapper = document.createElement("span");
60
+ wrapper.className = tokenStyle.wrapper;
61
+
62
+ const groupEl = document.createElement("span");
63
+ groupEl.className = tokenStyle.group;
64
+ groupEl.textContent = tokenGroup;
65
+
66
+ const separatorEl = document.createElement("span");
67
+ separatorEl.className = tokenStyle.separator;
68
+ separatorEl.textContent = tokenSeparator;
69
+
70
+ const labelEl = document.createElement("span");
71
+ labelEl.className = tokenStyle.label;
72
+ labelEl.textContent = tokenLabel;
73
+
74
+ const removeBtn = document.createElement("button");
75
+ removeBtn.type = "button";
76
+ removeBtn.className = tokenStyle.remove;
77
+ removeBtn.setAttribute("data-token-remove", "true");
78
+ removeBtn.setAttribute("aria-label", "Remove token");
79
+ removeBtn.textContent = "✕";
80
+
81
+ wrapper.appendChild(groupEl);
82
+ wrapper.appendChild(separatorEl);
83
+ wrapper.appendChild(labelEl);
84
+ wrapper.appendChild(removeBtn);
85
+
86
+ return wrapper;
87
+ }
88
+
89
+ class TokenBlot extends Embed {
90
+ static create(value) {
91
+ const node = super.create();
92
+ let tokenValue = "";
93
+ let tokenLabel = "";
94
+ let tokenGroup = "Variable";
95
+ let tokenSeparator = "→";
96
+ let tokenStyle = { ...DEFAULT_TOKEN_STYLE };
97
+
98
+ if (typeof value === "string") {
99
+ tokenValue = value;
100
+ tokenLabel = value;
101
+ } else if (value && typeof value === "object") {
102
+ tokenValue = value.value || "";
103
+ tokenLabel = value.label || tokenValue;
104
+ tokenGroup = value.group || "Variable";
105
+ tokenSeparator = value.separator || "→";
106
+ if (value.style) {
107
+ tokenStyle = { ...tokenStyle, ...value.style };
108
+ }
109
+ }
110
+
111
+ node.setAttribute("contenteditable", "false");
112
+ node.setAttribute("data-token", tokenValue);
113
+ node.setAttribute("data-label", tokenLabel);
114
+ node.setAttribute("data-group", tokenGroup);
115
+ node.setAttribute("data-separator", tokenSeparator);
116
+ node.setAttribute("data-token-style", JSON.stringify(tokenStyle));
117
+ node.className = "ql-token-chip inline-flex align-middle";
118
+ node.appendChild(
119
+ createTokenChipContent({
120
+ tokenStyle,
121
+ tokenGroup,
122
+ tokenSeparator,
123
+ tokenLabel,
124
+ }),
125
+ );
126
+ return node;
127
+ }
128
+
129
+ static value(node) {
130
+ let tokenStyle = {};
131
+ const styleAttr = node.getAttribute("data-token-style");
132
+ if (styleAttr) {
133
+ try {
134
+ const parsed = JSON.parse(styleAttr);
135
+ if (parsed && typeof parsed === "object") {
136
+ tokenStyle = parsed;
137
+ }
138
+ } catch (error) {
139
+ tokenStyle = {};
140
+ }
141
+ }
142
+ return {
143
+ value: node.getAttribute("data-token") || "",
144
+ label: node.getAttribute("data-label") || "",
145
+ group: node.getAttribute("data-group") || "",
146
+ separator: node.getAttribute("data-separator") || "→",
147
+ style: tokenStyle,
148
+ };
149
+ }
150
+ }
151
+
152
+ TokenBlot.blotName = "token";
153
+ TokenBlot.tagName = "span";
154
+ // Scope the blot to spans carrying the token marker class. Without this, the
155
+ // blot matches EVERY <span> (Quill registers globally by tagName), so plain
156
+ // spans in other editors (e.g. CEditor content) get swallowed and rendered as
157
+ // generic "Variable →" chips.
158
+ TokenBlot.className = "ql-token-chip";
159
+
160
+ const existingTokenFormat = Quill.import("formats/token");
161
+ if (!existingTokenFormat || typeof existingTokenFormat.create !== "function") {
162
+ Quill.register({ "formats/token": TokenBlot }, true);
163
+ }
164
+ export default {
165
+ name: "CTokenEditor",
166
+ components: { quillEditor },
167
+ props: {
168
+ id: {
169
+ type: String,
170
+ },
171
+ content: {
172
+ type: String,
173
+ },
174
+ label: {
175
+ type: String,
176
+ },
177
+ hint: {
178
+ type: String,
179
+ },
180
+ helpText: {
181
+ type: String,
182
+ },
183
+ toolbarOptions: {
184
+ type: Array,
185
+ },
186
+ onEditorChange: {
187
+ type: Function,
188
+ },
189
+ placeholder: {
190
+ type: String,
191
+ },
192
+ disabled: {
193
+ type: Boolean,
194
+ default: false,
195
+ },
196
+ templateVariables: {
197
+ type: Array,
198
+ default: () => [],
199
+ },
200
+ tokenChipStyles: {
201
+ type: Object,
202
+ default: () => ({}),
203
+ },
204
+ tokenSeparator: {
205
+ type: String,
206
+ default: "→",
207
+ },
208
+ enableTokenChips: {
209
+ type: Boolean,
210
+ default: false,
211
+ },
212
+ containerMinHeight: {
213
+ type: [String, Number],
214
+ default: "5rem",
215
+ },
216
+ editorMaxHeight: {
217
+ type: [String, Number],
218
+ default: "200px",
219
+ },
220
+ },
221
+ data() {
222
+ return {
223
+ editorInstance: null,
224
+ isRenderingTokens: false,
225
+ rootClickHandler: null,
226
+ pasteHandler: null,
227
+ renderedRawContent: "",
228
+ renderedHtmlContent: "",
229
+ lastSelectionRange: null,
230
+ selectionChangeHandler: null,
231
+ templateVariablesSignature: "",
232
+ };
233
+ },
234
+ watch: {
235
+ content: {
236
+ immediate: true,
237
+ handler(newValue) {
238
+ const normalized = newValue || "";
239
+ if (!this.editorInstance) return;
240
+ if (this.enableTokenChips) {
241
+ if (this.hasHtmlTags(normalized)) {
242
+ if (
243
+ normalized === this.renderedHtmlContent &&
244
+ !this.shouldRetokenizeHtml(normalized)
245
+ ) {
246
+ return;
247
+ }
248
+ this.renderTokensFromHtml(normalized);
249
+ return;
250
+ }
251
+ if (normalized === this.renderedRawContent) return;
252
+ this.renderTokensFromRaw(normalized);
253
+ return;
254
+ }
255
+ if (normalized === this.renderedHtmlContent) return;
256
+ this.renderHtmlContent(normalized);
257
+ },
258
+ },
259
+ tokenChipStyles: {
260
+ deep: true,
261
+ handler() {
262
+ if (!this.editorInstance || !this.enableTokenChips) return;
263
+ this.renderCurrentTokenContent();
264
+ },
265
+ },
266
+ tokenSeparator() {
267
+ if (!this.editorInstance || !this.enableTokenChips) return;
268
+ this.renderCurrentTokenContent();
269
+ },
270
+ templateVariables: {
271
+ deep: true,
272
+ handler(newValue) {
273
+ if (this.isRenderingTokens) return;
274
+ const nextSignature = this.buildTemplateVariablesSignature(newValue);
275
+ this.templateVariablesSignature = nextSignature;
276
+ if (!this.editorInstance || !this.enableTokenChips) return;
277
+ this.$nextTick(() => {
278
+ this.renderCurrentTokenContent();
279
+ this.$nextTick(() => this.refreshTokenNodes());
280
+ });
281
+ },
282
+ },
283
+ containerMinHeight() {
284
+ this.$nextTick(() => this.applyEditorHeights());
285
+ },
286
+ editorMaxHeight() {
287
+ this.$nextTick(() => this.applyEditorHeights());
288
+ },
289
+ },
290
+ methods: {
291
+ normalizeCssSize(value, fallback) {
292
+ if (value === null || value === undefined || value === "") {
293
+ return fallback;
294
+ }
295
+ if (typeof value === "number" || !isNaN(Number(value))) {
296
+ return `${value}px`;
297
+ }
298
+ return String(value);
299
+ },
300
+ buildTemplateVariablesSignature(variables = []) {
301
+ return (variables || [])
302
+ .map((item) => {
303
+ const value = item && item.value ? String(item.value) : "";
304
+ const label = item && item.label ? String(item.label) : "";
305
+ const group = item && item.group ? String(item.group) : "";
306
+ return `${value}|${label}|${group}`;
307
+ })
308
+ .sort()
309
+ .join("||");
310
+ },
311
+ hasHtmlTags(value) {
312
+ return /<(p|div|span|strong|em|u|s|ol|ul|li|h[1-6]|br|img|a)\b[^>]*>/i.test(value || "");
313
+ },
314
+ hasMustacheTokens(value) {
315
+ TOKEN_PATTERN.lastIndex = 0;
316
+ return TOKEN_PATTERN.test(value || "");
317
+ },
318
+ shouldRetokenizeHtml(htmlContent) {
319
+ if (!this.hasMustacheTokens(htmlContent)) {
320
+ return false;
321
+ }
322
+ const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
323
+ if (!quill || !quill.root) {
324
+ return false;
325
+ }
326
+ const tokenCount = quill.root.querySelectorAll("[data-token]").length;
327
+ return tokenCount === 0;
328
+ },
329
+ applyEditorHeights() {
330
+ const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
331
+ if (!quill) return;
332
+ quill.container.style.minHeight = this.normalizeCssSize(
333
+ this.containerMinHeight,
334
+ "5rem",
335
+ );
336
+ quill.root.style.maxHeight = this.normalizeCssSize(
337
+ this.editorMaxHeight,
338
+ "200px",
339
+ );
340
+ },
341
+ renderHtmlContent(htmlContent) {
342
+ const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
343
+ if (!quill) return;
344
+ this.isRenderingTokens = true;
345
+ const contentToRender = htmlContent || "";
346
+ const previousSelection = quill.getSelection() || this.lastSelectionRange;
347
+ quill.clipboard.dangerouslyPasteHTML(contentToRender, "silent");
348
+ const length = Math.max(quill.getLength() - 1, 0);
349
+ const nextIndex = previousSelection
350
+ ? Math.min(previousSelection.index, length)
351
+ : length;
352
+ quill.setSelection(nextIndex, 0, "silent");
353
+ this.lastSelectionRange = { index: nextIndex, length: 0 };
354
+ this.renderedHtmlContent = contentToRender;
355
+ this.renderedRawContent = this.extractRawText(quill);
356
+ this.isRenderingTokens = false;
357
+ },
358
+ renderCurrentTokenContent() {
359
+ const contentToRender =
360
+ this.content ||
361
+ this.renderedHtmlContent ||
362
+ this.renderedRawContent ||
363
+ "";
364
+ if (this.hasHtmlTags(contentToRender)) {
365
+ this.renderTokensFromHtml(contentToRender);
366
+ return;
367
+ }
368
+ this.renderTokensFromRaw(contentToRender);
369
+ },
370
+ renderTokensFromHtml(htmlContent) {
371
+ const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
372
+ if (!quill) return;
373
+
374
+ const contentToRender = htmlContent || "";
375
+ const previousSelection = quill.getSelection() || this.lastSelectionRange;
376
+ const converted = quill.clipboard.convert(contentToRender);
377
+ const nextDelta = new Delta();
378
+
379
+ (converted.ops || []).forEach((op) => {
380
+ if (typeof op.insert !== "string") {
381
+ nextDelta.push(op);
382
+ return;
383
+ }
384
+
385
+ TOKEN_PATTERN.lastIndex = 0;
386
+ const tokenRegex = TOKEN_PATTERN;
387
+ const text = op.insert;
388
+ let lastIndex = 0;
389
+ let match = null;
390
+
391
+ while ((match = tokenRegex.exec(text)) !== null) {
392
+ const plainText = text.slice(lastIndex, match.index);
393
+ if (plainText) {
394
+ nextDelta.insert(plainText, op.attributes || undefined);
395
+ }
396
+
397
+ const tokenKey = (match[1] || "").trim();
398
+ if (tokenKey) {
399
+ const isKnownToken = this.isKnownToken(tokenKey);
400
+ nextDelta.insert({
401
+ token: {
402
+ value: tokenKey,
403
+ label: this.getTokenLabel(tokenKey),
404
+ group: this.getTokenGroup(tokenKey),
405
+ style: this.getTokenStyleConfig({ isInvalid: !isKnownToken }),
406
+ separator: this.tokenSeparator,
407
+ },
408
+ });
409
+ } else {
410
+ nextDelta.insert(match[0], op.attributes || undefined);
411
+ }
412
+
413
+ lastIndex = tokenRegex.lastIndex;
414
+ }
415
+
416
+ const remainingText = text.slice(lastIndex);
417
+ if (remainingText) {
418
+ nextDelta.insert(remainingText, op.attributes || undefined);
419
+ }
420
+ });
421
+
422
+ this.isRenderingTokens = true;
423
+ try {
424
+ quill.setContents(nextDelta, "silent");
425
+
426
+ const length = Math.max(quill.getLength() - 1, 0);
427
+ const nextIndex = previousSelection
428
+ ? Math.min(previousSelection.index, length)
429
+ : length;
430
+ quill.setSelection(nextIndex, 0, "silent");
431
+ this.lastSelectionRange = { index: nextIndex, length: 0 };
432
+ this.renderedHtmlContent = contentToRender;
433
+ this.renderedRawContent = this.extractRawText(quill);
434
+ } finally {
435
+ this.isRenderingTokens = false;
436
+ }
437
+ this.$nextTick(() => this.refreshTokenNodes());
438
+ },
439
+ onChange(params) {
440
+ if (this.isRenderingTokens) return;
441
+ if (!this.enableTokenChips) {
442
+ const htmlText =
443
+ params && typeof params.html === "string" ? params.html : "";
444
+ this.renderedHtmlContent = htmlText;
445
+ this.renderedRawContent = htmlText;
446
+ this.$emit("input", htmlText);
447
+ this.$emit("update:content", htmlText);
448
+ this.$emit("onEditorChange", {
449
+ ...params,
450
+ rawText: this.extractRawText(params.quill),
451
+ });
452
+ return;
453
+ }
454
+ const rawText = this.extractRawText(params.quill);
455
+ const htmlText =
456
+ params && typeof params.html === "string" ? params.html : rawText;
457
+ this.renderedRawContent = rawText;
458
+ this.renderedHtmlContent = htmlText;
459
+ this.$emit("input", rawText);
460
+ this.$emit("update:content", rawText);
461
+ this.$emit("onEditorChange", { ...params, rawText });
462
+ },
463
+ onReady(params) {
464
+ this.editorInstance = params;
465
+ this.templateVariablesSignature = this.buildTemplateVariablesSignature(
466
+ this.templateVariables,
467
+ );
468
+ this.attachSelectionTracking();
469
+ this.attachTokenRemoveHandler();
470
+ this.attachPlainTextPasteHandler();
471
+ const initialContent = this.content || "";
472
+ if (this.enableTokenChips && !this.hasHtmlTags(initialContent)) {
473
+ this.renderTokensFromRaw(initialContent);
474
+ } else if (this.enableTokenChips) {
475
+ this.renderTokensFromHtml(initialContent);
476
+ } else {
477
+ this.renderHtmlContent(initialContent);
478
+ }
479
+ this.applyEditorHeights();
480
+ this.$emit("onEditorReady", params);
481
+ },
482
+ handleFocus() {
483
+ let quill = this.$refs.myQuillEditor.quill;
484
+ if (quill) {
485
+ this.$emit("handleFocus", quill);
486
+ }
487
+ },
488
+ onEditorBlur() {
489
+ this.$emit("onEditorBlur");
490
+ },
491
+ insertToken({ value, label, group }) {
492
+ if (!this.enableTokenChips) return;
493
+ const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
494
+ if (!quill || !value) return;
495
+
496
+ const selection = quill.getSelection(true) || this.lastSelectionRange;
497
+ const index = selection ? selection.index : quill.getLength();
498
+ quill.insertEmbed(
499
+ index,
500
+ "token",
501
+ {
502
+ value,
503
+ label: label || this.getTokenLabel(value),
504
+ group: group || this.getTokenGroup(value),
505
+ style: this.getTokenStyleConfig(),
506
+ separator: this.tokenSeparator,
507
+ },
508
+ "user",
509
+ );
510
+ quill.setSelection(index + 1, 0, "silent");
511
+ this.lastSelectionRange = { index: index + 1, length: 0 };
512
+ },
513
+ extractRawText(quill) {
514
+ if (!quill) return "";
515
+ const delta = quill.getContents();
516
+ const raw = (delta.ops || [])
517
+ .map((op) => {
518
+ if (typeof op.insert === "string") return op.insert;
519
+ if (op.insert && op.insert.token) {
520
+ return `{{${op.insert.token.value}}}`;
521
+ }
522
+ return "";
523
+ })
524
+ .join("");
525
+
526
+ return raw.endsWith("\n") ? raw.slice(0, -1) : raw;
527
+ },
528
+ renderTokensFromRaw(rawText) {
529
+ const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
530
+ if (!quill) return;
531
+
532
+ const source = rawText || "";
533
+ const previousSelection = quill.getSelection() || this.lastSelectionRange;
534
+ const tokenRegex = /\{\{([^{}]+)\}\}/g;
535
+ let currentIndex = 0;
536
+ let lastIndex = 0;
537
+ let match = null;
538
+
539
+ this.isRenderingTokens = true;
540
+ quill.setContents([], "silent");
541
+
542
+ while ((match = tokenRegex.exec(source)) !== null) {
543
+ const plainText = source.slice(lastIndex, match.index);
544
+ if (plainText) {
545
+ quill.insertText(currentIndex, plainText, "silent");
546
+ currentIndex += plainText.length;
547
+ }
548
+
549
+ const tokenKey = (match[1] || "").trim();
550
+ if (!tokenKey) {
551
+ quill.insertText(currentIndex, match[0], "silent");
552
+ currentIndex += match[0].length;
553
+ lastIndex = tokenRegex.lastIndex;
554
+ continue;
555
+ }
556
+ const isKnownToken = this.isKnownToken(tokenKey);
557
+ quill.insertEmbed(
558
+ currentIndex,
559
+ "token",
560
+ {
561
+ value: tokenKey,
562
+ label: this.getTokenLabel(tokenKey),
563
+ group: this.getTokenGroup(tokenKey),
564
+ style: this.getTokenStyleConfig({ isInvalid: !isKnownToken }),
565
+ separator: this.tokenSeparator,
566
+ },
567
+ "silent",
568
+ );
569
+ currentIndex += 1;
570
+ lastIndex = tokenRegex.lastIndex;
571
+ }
572
+
573
+ const remainingText = source.slice(lastIndex);
574
+ if (remainingText) {
575
+ quill.insertText(currentIndex, remainingText, "silent");
576
+ currentIndex += remainingText.length;
577
+ }
578
+
579
+ const nextIndex = previousSelection
580
+ ? Math.min(previousSelection.index, currentIndex)
581
+ : currentIndex;
582
+ quill.setSelection(nextIndex, 0, "silent");
583
+ this.lastSelectionRange = { index: nextIndex, length: 0 };
584
+ this.renderedRawContent = source;
585
+ this.renderedHtmlContent = quill.root.innerHTML;
586
+ this.isRenderingTokens = false;
587
+ this.$nextTick(() => this.refreshTokenNodes());
588
+ },
589
+ attachSelectionTracking() {
590
+ const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
591
+ if (!quill || this.selectionChangeHandler) return;
592
+ this.selectionChangeHandler = (range) => {
593
+ if (range) {
594
+ this.lastSelectionRange = range;
595
+ }
596
+ };
597
+ quill.on("selection-change", this.selectionChangeHandler);
598
+ },
599
+ attachPlainTextPasteHandler() {
600
+ if (!this.enableTokenChips) return;
601
+ const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
602
+ if (!quill || this.pasteHandler) return;
603
+ this.pasteHandler = (event) => {
604
+ const clipboardText =
605
+ event.clipboardData && event.clipboardData.getData("text/plain");
606
+ if (typeof clipboardText !== "string") return;
607
+ event.preventDefault();
608
+ const selection = quill.getSelection(true) || this.lastSelectionRange;
609
+ const index = selection ? selection.index : quill.getLength();
610
+ const length = selection ? selection.length || 0 : 0;
611
+
612
+ const pasteDelta = new Delta().retain(index);
613
+ if (length > 0) {
614
+ pasteDelta.delete(length);
615
+ }
616
+
617
+ TOKEN_PATTERN.lastIndex = 0;
618
+ const tokenRegex = TOKEN_PATTERN;
619
+ let lastIndex = 0;
620
+ let match = null;
621
+ let insertedLength = 0;
622
+
623
+ while ((match = tokenRegex.exec(clipboardText)) !== null) {
624
+ const plainText = clipboardText.slice(lastIndex, match.index);
625
+ if (plainText) {
626
+ pasteDelta.insert(plainText);
627
+ insertedLength += plainText.length;
628
+ }
629
+ const tokenKey = (match[1] || "").trim();
630
+ if (tokenKey) {
631
+ const isKnownToken = this.isKnownToken(tokenKey);
632
+ pasteDelta.insert({
633
+ token: {
634
+ value: tokenKey,
635
+ label: this.getTokenLabel(tokenKey),
636
+ group: this.getTokenGroup(tokenKey),
637
+ style: this.getTokenStyleConfig({ isInvalid: !isKnownToken }),
638
+ separator: this.tokenSeparator,
639
+ },
640
+ });
641
+ insertedLength += 1;
642
+ } else {
643
+ pasteDelta.insert(match[0]);
644
+ insertedLength += match[0].length;
645
+ }
646
+ lastIndex = tokenRegex.lastIndex;
647
+ }
648
+
649
+ const remainingText = clipboardText.slice(lastIndex);
650
+ if (remainingText) {
651
+ pasteDelta.insert(remainingText);
652
+ insertedLength += remainingText.length;
653
+ }
654
+
655
+ quill.updateContents(pasteDelta, "user");
656
+ const nextIndex = index + insertedLength;
657
+ quill.setSelection(nextIndex, 0, "silent");
658
+ this.lastSelectionRange = {
659
+ index: nextIndex,
660
+ length: 0,
661
+ };
662
+ };
663
+ quill.root.addEventListener("paste", this.pasteHandler);
664
+ },
665
+ getTokenLabel(key) {
666
+ const variable = this.templateVariablesMap.get(key);
667
+ return variable && variable.label ? variable.label : key;
668
+ },
669
+ getTokenGroup(key) {
670
+ const variable = this.templateVariablesMap.get(key);
671
+ return variable && variable.group ? variable.group : "Variable";
672
+ },
673
+ isKnownToken(key) {
674
+ return this.templateVariablesMap.has(key);
675
+ },
676
+ getTokenStyleConfig({ isInvalid = false } = {}) {
677
+ const baseStyle = isInvalid ? INVALID_TOKEN_STYLE : DEFAULT_TOKEN_STYLE;
678
+ return {
679
+ ...baseStyle,
680
+ ...this.tokenChipStyles,
681
+ };
682
+ },
683
+ refreshTokenNodes() {
684
+ if (!this.enableTokenChips) return;
685
+ const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
686
+ if (!quill || !quill.root) return;
687
+ const tokenNodes = quill.root.querySelectorAll("[data-token]");
688
+ tokenNodes.forEach((node) => {
689
+ const value = (node.getAttribute("data-token") || "").trim();
690
+ if (!value) return;
691
+ const variable = (this.templateVariables || []).find(
692
+ (item) => item.value === value,
693
+ );
694
+ const isInvalid = !variable;
695
+ const separator =
696
+ node.getAttribute("data-separator") || this.tokenSeparator;
697
+ const group =
698
+ (variable && variable.group) ||
699
+ node.getAttribute("data-group") ||
700
+ "Variable";
701
+ const label =
702
+ (variable && variable.label) ||
703
+ node.getAttribute("data-label") ||
704
+ value;
705
+ const tokenStyle = this.getTokenStyleConfig({ isInvalid });
706
+
707
+ node.setAttribute("data-label", label);
708
+ node.setAttribute("data-group", group);
709
+ node.setAttribute("data-separator", separator);
710
+ node.setAttribute("data-token-style", JSON.stringify(tokenStyle));
711
+
712
+ node.innerHTML = "";
713
+ node.appendChild(
714
+ createTokenChipContent({
715
+ tokenStyle,
716
+ tokenGroup: group,
717
+ tokenSeparator: separator,
718
+ tokenLabel: label,
719
+ }),
720
+ );
721
+ });
722
+ },
723
+ attachTokenRemoveHandler() {
724
+ if (!this.enableTokenChips) return;
725
+ const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
726
+ if (!quill || this.rootClickHandler) return;
727
+
728
+ this.rootClickHandler = (event) => {
729
+ const removeTarget = event.target.closest("[data-token-remove='true']");
730
+ if (!removeTarget) return;
731
+ event.preventDefault();
732
+ event.stopPropagation();
733
+
734
+ const tokenNode = removeTarget.closest("[data-token]");
735
+ if (!tokenNode) return;
736
+
737
+ const blot = Quill.find(tokenNode);
738
+ if (!blot) return;
739
+
740
+ const index = quill.getIndex(blot);
741
+ quill.deleteText(index, 1, "user");
742
+ };
743
+
744
+ quill.root.addEventListener("click", this.rootClickHandler);
745
+ },
746
+ },
747
+ beforeDestroy() {
748
+ this.isRenderingTokens = false;
749
+ const quill = this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill;
750
+ if (quill && this.rootClickHandler) {
751
+ quill.root.removeEventListener("click", this.rootClickHandler);
752
+ }
753
+ if (quill && this.pasteHandler) {
754
+ quill.root.removeEventListener("paste", this.pasteHandler);
755
+ }
756
+ if (quill && this.selectionChangeHandler) {
757
+ quill.off("selection-change", this.selectionChangeHandler);
758
+ }
759
+ },
760
+ computed: {
761
+ templateVariablesMap() {
762
+ const map = new Map();
763
+ (this.templateVariables || []).forEach((v) => {
764
+ if (v && v.value) map.set(v.value, v);
765
+ });
766
+ return map;
767
+ },
768
+ editorOption() {
769
+ const hasToolbar =
770
+ Array.isArray(this.toolbarOptions) && this.toolbarOptions.length > 0;
771
+ return {
772
+ modules: {
773
+ toolbar: hasToolbar
774
+ ? {
775
+ container: this.toolbarOptions,
776
+ }
777
+ : false,
778
+ },
779
+ placeholder: this.placeholder,
780
+ };
781
+ },
782
+ },
783
+ };
784
+ </script>
785
+ <style>
786
+ .ql-toolbar.ql-snow {
787
+ border-top-left-radius: 0.375rem;
788
+ border-top-right-radius: 0.375rem;
789
+ }
790
+
791
+ .ql-container.ql-snow {
792
+ border-bottom-left-radius: 0.375rem;
793
+ border-bottom-right-radius: 0.375rem;
794
+ min-height: var(--test-editor-container-min-height, 5rem);
795
+ }
796
+
797
+ .ql-editor {
798
+ @apply text-sm text-gray-900;
799
+ max-height: var(--test-editor-max-height, 200px);
800
+ }
801
+ .ql-editor.ql-blank::before {
802
+ @apply not-italic text-gray-500;
803
+ }
804
+ </style>