@redvars/peacock 3.5.1 → 3.6.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 (225) hide show
  1. package/dist/{BaseButton-DuASuVth.js → BaseButton-BNFAYn-S.js} +2 -2
  2. package/dist/{BaseButton-DuASuVth.js.map → BaseButton-BNFAYn-S.js.map} +1 -1
  3. package/dist/BaseInput-14YmcfK7.js +27 -0
  4. package/dist/BaseInput-14YmcfK7.js.map +1 -0
  5. package/dist/banner.js +2 -3
  6. package/dist/banner.js.map +1 -1
  7. package/dist/{button-DouvOfEU.js → button-colors-Ccys3hvS.js} +5 -294
  8. package/dist/button-colors-Ccys3hvS.js.map +1 -0
  9. package/dist/button-group.js +226 -6
  10. package/dist/button-group.js.map +1 -1
  11. package/dist/button.js +294 -8
  12. package/dist/button.js.map +1 -1
  13. package/dist/calendar-column-view.js +634 -0
  14. package/dist/calendar-column-view.js.map +1 -0
  15. package/dist/calendar-event-BrQ_SEKD.js +199 -0
  16. package/dist/calendar-event-BrQ_SEKD.js.map +1 -0
  17. package/dist/calendar-month-view.js +376 -0
  18. package/dist/calendar-month-view.js.map +1 -0
  19. package/dist/calendar.js +339 -0
  20. package/dist/calendar.js.map +1 -0
  21. package/dist/canvas.js +361 -0
  22. package/dist/canvas.js.map +1 -0
  23. package/dist/cb-compound-expression.js +125 -0
  24. package/dist/cb-compound-expression.js.map +1 -0
  25. package/dist/cb-divider.js +150 -0
  26. package/dist/cb-divider.js.map +1 -0
  27. package/dist/cb-expression.js +75 -0
  28. package/dist/cb-expression.js.map +1 -0
  29. package/dist/cb-predicate.js +137 -0
  30. package/dist/cb-predicate.js.map +1 -0
  31. package/dist/code-editor.js +2 -1
  32. package/dist/code-editor.js.map +1 -1
  33. package/dist/code-highlighter.js +1 -1
  34. package/dist/code-highlighter.js.map +1 -1
  35. package/dist/condition-builder.js +58 -0
  36. package/dist/condition-builder.js.map +1 -0
  37. package/dist/custom-elements-jsdocs.json +8479 -3965
  38. package/dist/custom-elements.json +15228 -7544
  39. package/dist/dropdown-button.js +216 -0
  40. package/dist/dropdown-button.js.map +1 -0
  41. package/dist/event-manager-D-QCmUgR.js +113 -0
  42. package/dist/event-manager-D-QCmUgR.js.map +1 -0
  43. package/dist/fab.js +1 -1
  44. package/dist/flow-designer-DvTUrDp5.js +1656 -0
  45. package/dist/flow-designer-DvTUrDp5.js.map +1 -0
  46. package/dist/flow-designer-node-BWrPuxAR.js +548 -0
  47. package/dist/flow-designer-node-BWrPuxAR.js.map +1 -0
  48. package/dist/flow-designer-node.js +4 -0
  49. package/dist/flow-designer-node.js.map +1 -0
  50. package/dist/flow-designer.js +16 -0
  51. package/dist/flow-designer.js.map +1 -0
  52. package/dist/html-editor.js +27516 -0
  53. package/dist/html-editor.js.map +1 -0
  54. package/dist/icon-button-CK1ZuE-2.js +247 -0
  55. package/dist/icon-button-CK1ZuE-2.js.map +1 -0
  56. package/dist/index.js +29 -6
  57. package/dist/index.js.map +1 -1
  58. package/dist/{is-dark-mode-DicqGkCJ.js → is-dark-mode-DOcaw4Yq.js} +2 -27
  59. package/dist/is-dark-mode-DOcaw4Yq.js.map +1 -0
  60. package/dist/modal.js +412 -0
  61. package/dist/modal.js.map +1 -0
  62. package/dist/{navigation-rail-Lxetd5-Z.js → navigation-rail-DTTkqohi.js} +1049 -2391
  63. package/dist/navigation-rail-DTTkqohi.js.map +1 -0
  64. package/dist/notification-manager.js +268 -0
  65. package/dist/notification-manager.js.map +1 -0
  66. package/dist/peacock-loader.js +93 -8
  67. package/dist/peacock-loader.js.map +1 -1
  68. package/dist/popover-NC7b1lTq.js +1971 -0
  69. package/dist/popover-NC7b1lTq.js.map +1 -0
  70. package/dist/popover-content.js +125 -0
  71. package/dist/popover-content.js.map +1 -0
  72. package/dist/popover.js +4 -0
  73. package/dist/popover.js.map +1 -0
  74. package/dist/split-button.js +388 -0
  75. package/dist/split-button.js.map +1 -0
  76. package/dist/src/__controllers/floating-controller.d.ts +35 -0
  77. package/dist/src/calendar/base-event.d.ts +10 -0
  78. package/dist/src/calendar/calendar-column-view.d.ts +41 -0
  79. package/dist/src/calendar/calendar-event.d.ts +7 -0
  80. package/dist/src/calendar/calendar-month-view.d.ts +31 -0
  81. package/dist/src/calendar/calendar.d.ts +65 -0
  82. package/dist/src/calendar/event-manager.d.ts +17 -0
  83. package/dist/src/calendar/index.d.ts +4 -0
  84. package/dist/src/calendar/types.d.ts +13 -0
  85. package/dist/src/calendar/utils.d.ts +31 -0
  86. package/dist/src/canvas/canvas.d.ts +92 -0
  87. package/dist/src/canvas/index.d.ts +2 -0
  88. package/dist/src/condition-builder/cb-compound-expression.d.ts +31 -0
  89. package/dist/src/condition-builder/cb-divider.d.ts +26 -0
  90. package/dist/src/condition-builder/cb-expression.d.ts +31 -0
  91. package/dist/src/condition-builder/cb-predicate.d.ts +30 -0
  92. package/dist/src/condition-builder/condition-builder.d.ts +27 -0
  93. package/dist/src/condition-builder/index.d.ts +5 -0
  94. package/dist/src/dropdown-button/dropdown-button.d.ts +68 -0
  95. package/dist/src/dropdown-button/index.d.ts +1 -0
  96. package/dist/src/flow-designer/commands.d.ts +66 -0
  97. package/dist/src/flow-designer/flow-designer-node.d.ts +46 -0
  98. package/dist/src/flow-designer/flow-designer.d.ts +133 -0
  99. package/dist/src/flow-designer/index.d.ts +7 -0
  100. package/dist/src/flow-designer/layout.d.ts +30 -0
  101. package/dist/src/flow-designer/types.d.ts +142 -0
  102. package/dist/src/flow-designer/validation.d.ts +43 -0
  103. package/dist/src/flow-designer/workflow-utils.d.ts +40 -0
  104. package/dist/src/html-editor/html-editor.d.ts +89 -0
  105. package/dist/src/html-editor/index.d.ts +2 -0
  106. package/dist/src/index.d.ts +15 -0
  107. package/dist/src/list/index.d.ts +2 -0
  108. package/dist/src/list/list-item.d.ts +35 -0
  109. package/dist/src/list/list.d.ts +28 -0
  110. package/dist/src/menu/menu/menu.d.ts +5 -7
  111. package/dist/src/menu/menu-item/menu-item.d.ts +14 -13
  112. package/dist/src/modal/index.d.ts +1 -0
  113. package/dist/src/modal/modal.d.ts +57 -0
  114. package/dist/src/navigation-rail/navigation-rail.d.ts +3 -7
  115. package/dist/src/notification-manager/index.d.ts +1 -0
  116. package/dist/src/notification-manager/notification-manager.d.ts +44 -0
  117. package/dist/src/number-field/number-field.d.ts +2 -2
  118. package/dist/src/popover/index.d.ts +2 -0
  119. package/dist/src/popover/popover-content.d.ts +29 -0
  120. package/dist/src/popover/popover.d.ts +62 -0
  121. package/dist/src/split-button/index.d.ts +1 -0
  122. package/dist/src/split-button/split-button.d.ts +72 -0
  123. package/dist/src/svg/index.d.ts +1 -0
  124. package/dist/src/svg/svg.d.ts +38 -0
  125. package/dist/src/toolbar/toolbar.d.ts +3 -3
  126. package/dist/src/tooltip/tooltip.d.ts +2 -15
  127. package/dist/test/flow-designer.test.d.ts +1 -0
  128. package/dist/toolbar.js +3 -3
  129. package/dist/toolbar.js.map +1 -1
  130. package/dist/tsconfig.tsbuildinfo +1 -1
  131. package/package.json +10 -2
  132. package/readme.md +3 -3
  133. package/src/__controllers/floating-controller.ts +237 -0
  134. package/src/banner/banner.scss +2 -3
  135. package/src/button/button/button.ts +1 -0
  136. package/src/calendar/base-event.ts +49 -0
  137. package/src/calendar/calendar-column-view.scss +326 -0
  138. package/src/calendar/calendar-column-view.ts +392 -0
  139. package/src/calendar/calendar-event.ts +20 -0
  140. package/src/calendar/calendar-month-view.scss +192 -0
  141. package/src/calendar/calendar-month-view.ts +244 -0
  142. package/src/calendar/calendar.scss +71 -0
  143. package/src/calendar/calendar.ts +298 -0
  144. package/src/calendar/event-manager.ts +117 -0
  145. package/src/calendar/index.ts +4 -0
  146. package/src/calendar/types.ts +14 -0
  147. package/src/calendar/utils.ts +180 -0
  148. package/src/canvas/canvas.scss +60 -0
  149. package/src/canvas/canvas.ts +391 -0
  150. package/src/canvas/index.ts +2 -0
  151. package/src/code-highlighter/code-highlighter.ts +1 -1
  152. package/src/condition-builder/cb-compound-expression.scss +37 -0
  153. package/src/condition-builder/cb-compound-expression.ts +80 -0
  154. package/src/condition-builder/cb-divider.scss +93 -0
  155. package/src/condition-builder/cb-divider.ts +56 -0
  156. package/src/condition-builder/cb-expression.scss +14 -0
  157. package/src/condition-builder/cb-expression.ts +49 -0
  158. package/src/condition-builder/cb-predicate.scss +35 -0
  159. package/src/condition-builder/cb-predicate.ts +102 -0
  160. package/src/condition-builder/condition-builder.scss +13 -0
  161. package/src/condition-builder/condition-builder.ts +38 -0
  162. package/src/condition-builder/index.ts +5 -0
  163. package/src/dropdown-button/demo/index.html +110 -0
  164. package/src/dropdown-button/dropdown-button.scss +22 -0
  165. package/src/dropdown-button/dropdown-button.ts +206 -0
  166. package/src/dropdown-button/index.ts +1 -0
  167. package/src/flow-designer/DEMO.md +239 -0
  168. package/src/flow-designer/commands.ts +278 -0
  169. package/src/flow-designer/flow-designer-node.ts +172 -0
  170. package/src/flow-designer/flow-designer.scss +457 -0
  171. package/src/flow-designer/flow-designer.ts +611 -0
  172. package/src/flow-designer/index.ts +41 -0
  173. package/src/flow-designer/layout.ts +357 -0
  174. package/src/flow-designer/types.ts +166 -0
  175. package/src/flow-designer/validation.ts +284 -0
  176. package/src/flow-designer/workflow-utils.ts +282 -0
  177. package/src/html-editor/html-editor.scss +188 -0
  178. package/src/html-editor/html-editor.ts +491 -0
  179. package/src/html-editor/index.ts +3 -0
  180. package/src/index.ts +27 -1
  181. package/src/list/index.ts +2 -0
  182. package/src/list/list-item.scss +111 -0
  183. package/src/list/list-item.ts +175 -0
  184. package/src/list/list.scss +24 -0
  185. package/src/list/list.ts +51 -0
  186. package/src/menu/menu/menu.scss +2 -2
  187. package/src/menu/menu/menu.ts +91 -101
  188. package/src/menu/menu-item/menu-item.scss +4 -0
  189. package/src/menu/menu-item/menu-item.ts +82 -78
  190. package/src/modal/index.ts +1 -0
  191. package/src/modal/modal.scss +206 -0
  192. package/src/modal/modal.ts +195 -0
  193. package/src/navigation-rail/navigation-rail-item.scss +7 -38
  194. package/src/navigation-rail/navigation-rail-item.ts +1 -2
  195. package/src/navigation-rail/navigation-rail.scss +17 -21
  196. package/src/navigation-rail/navigation-rail.ts +6 -9
  197. package/src/notification-manager/index.ts +1 -0
  198. package/src/notification-manager/notification-manager.scss +113 -0
  199. package/src/notification-manager/notification-manager.ts +199 -0
  200. package/src/number-field/number-field.ts +2 -2
  201. package/src/peacock-loader.ts +83 -0
  202. package/src/popover/index.ts +2 -0
  203. package/src/popover/popover-content.scss +69 -0
  204. package/src/popover/popover-content.ts +51 -0
  205. package/src/popover/popover.scss +7 -0
  206. package/src/popover/popover.ts +170 -0
  207. package/src/split-button/index.ts +1 -0
  208. package/src/split-button/split-button-colors.scss +56 -0
  209. package/src/split-button/split-button-sizes.scss +28 -0
  210. package/src/split-button/split-button.scss +79 -0
  211. package/src/split-button/split-button.ts +236 -0
  212. package/src/svg/index.ts +1 -0
  213. package/src/svg/svg.scss +91 -0
  214. package/src/svg/svg.ts +160 -0
  215. package/src/table/table.ts +2 -2
  216. package/src/toolbar/toolbar.ts +3 -3
  217. package/src/tooltip/tooltip.scss +4 -3
  218. package/src/tooltip/tooltip.ts +46 -104
  219. package/dist/button-DouvOfEU.js.map +0 -1
  220. package/dist/button-group-CEdMwvJJ.js +0 -464
  221. package/dist/button-group-CEdMwvJJ.js.map +0 -1
  222. package/dist/is-dark-mode-DicqGkCJ.js.map +0 -1
  223. package/dist/navigation-rail-Lxetd5-Z.js.map +0 -1
  224. package/dist/src/menu/menu/MenuSurfaceController.d.ts +0 -18
  225. package/src/menu/menu/MenuSurfaceController.ts +0 -61
@@ -0,0 +1,188 @@
1
+ @use '../../scss/mixin';
2
+
3
+ @include mixin.base-styles;
4
+
5
+ :host {
6
+ display: block;
7
+ width: 100%;
8
+ }
9
+
10
+ // ── Field wrapper overrides ─────────────────────────────────────────────────
11
+
12
+ .html-editor-field {
13
+ // Let the field expand to fit content vertically
14
+ --field-height: auto;
15
+ --field-padding-block: 0;
16
+ --code-editor-height: var(--html-editor-min-height, 8rem);
17
+
18
+ width: 100%;
19
+ }
20
+
21
+ .mode-switcher {
22
+ display: flex;
23
+ justify-content: flex-end;
24
+ padding: var(--spacing-075, 0.375rem) var(--spacing-100, 0.5rem)
25
+ var(--spacing-050, 0.25rem);
26
+ }
27
+
28
+ // ── Toolbar ─────────────────────────────────────────────────────────────────
29
+
30
+ .html-editor-toolbar {
31
+ display: flex;
32
+ flex-wrap: wrap;
33
+ align-items: center;
34
+ gap: var(--spacing-025, 0.125rem);
35
+ padding: var(--spacing-050, 0.25rem) var(--spacing-100, 0.5rem);
36
+ background: var(
37
+ --html-editor-toolbar-background,
38
+ var(--color-surface-container, #f3edf7)
39
+ );
40
+ border-block-end: 1px solid
41
+ var(
42
+ --html-editor-toolbar-border-color,
43
+ var(--color-outline-variant, #cac4d0)
44
+ );
45
+ border-start-start-radius: inherit;
46
+ border-start-end-radius: inherit;
47
+ }
48
+
49
+ .toolbar-btn {
50
+ display: inline-flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ width: 2rem;
54
+ height: 2rem;
55
+ padding: 0;
56
+ border: none;
57
+ border-radius: var(--shape-corner-extra-small, 0.25rem);
58
+ background: transparent;
59
+ color: var(--color-on-surface-variant, #49454f);
60
+ cursor: pointer;
61
+ transition:
62
+ background 120ms ease,
63
+ color 120ms ease;
64
+
65
+ &:hover:not(:disabled) {
66
+ background: color-mix(
67
+ in srgb,
68
+ var(--color-on-surface-variant, #49454f) 8%,
69
+ transparent
70
+ );
71
+ }
72
+
73
+ &:active:not(:disabled) {
74
+ background: color-mix(
75
+ in srgb,
76
+ var(--color-primary, #6750a4) 12%,
77
+ transparent
78
+ );
79
+ color: var(--color-primary, #6750a4);
80
+ }
81
+
82
+ &:disabled {
83
+ opacity: 0.38;
84
+ cursor: not-allowed;
85
+ }
86
+
87
+ &.active {
88
+ background: color-mix(
89
+ in srgb,
90
+ var(--color-primary, #6750a4) 16%,
91
+ transparent
92
+ );
93
+ color: var(--color-primary, #6750a4);
94
+ }
95
+
96
+ wc-icon {
97
+ pointer-events: none;
98
+ }
99
+ }
100
+
101
+ .toolbar-divider {
102
+ display: inline-block;
103
+ width: 1px;
104
+ height: 1.5rem;
105
+ background: var(--color-outline-variant, #cac4d0);
106
+ margin-inline: var(--spacing-025, 0.125rem);
107
+ flex-shrink: 0;
108
+ }
109
+
110
+ // ── Editable content area ───────────────────────────────────────────────────
111
+
112
+ .html-editor-content {
113
+ min-height: var(--html-editor-min-height, 8rem);
114
+ padding: var(--spacing-200, 1rem) var(--spacing-150, 0.75rem);
115
+ outline: none;
116
+ color: var(--color-on-surface, #1c1b1f);
117
+ font-family: var(--typography-body-medium-font-family, inherit);
118
+ font-size: var(--typography-body-medium-font-size, 0.875rem);
119
+ line-height: var(--typography-body-medium-line-height, 1.5);
120
+ cursor: text;
121
+ word-break: break-word;
122
+ overflow-wrap: break-word;
123
+
124
+ &.hidden {
125
+ display: none;
126
+ }
127
+
128
+ .tiptap-root {
129
+ min-height: calc(var(--html-editor-min-height, 8rem) - 2rem);
130
+ }
131
+
132
+ .ProseMirror {
133
+ outline: none;
134
+ min-height: calc(var(--html-editor-min-height, 8rem) - 2rem);
135
+ }
136
+
137
+ // Placeholder
138
+ .ProseMirror p.is-editor-empty:first-child::before {
139
+ content: attr(data-placeholder);
140
+ color: var(--color-on-surface-variant, #49454f);
141
+ opacity: 0.6;
142
+ pointer-events: none;
143
+ float: left;
144
+ height: 0;
145
+ }
146
+
147
+ // Sensible defaults for user-generated rich content
148
+ ul,
149
+ ol {
150
+ padding-inline-start: 1.5rem;
151
+ margin-block: 0.25rem;
152
+ }
153
+
154
+ a {
155
+ color: var(--color-primary, #6750a4);
156
+ text-decoration: underline;
157
+ }
158
+
159
+ strong,
160
+ b {
161
+ font-weight: 600;
162
+ }
163
+ }
164
+
165
+ .html-source {
166
+ &.hidden {
167
+ display: none;
168
+ }
169
+ }
170
+
171
+ // ── Read-only tag ───────────────────────────────────────────────────────────
172
+
173
+ .read-only-tag {
174
+ margin: var(--spacing-050, 0.25rem);
175
+ }
176
+
177
+ // ── Disabled / readonly host states ────────────────────────────────────────
178
+
179
+ :host([disabled]) .html-editor-content,
180
+ :host([readonly]) .html-editor-content {
181
+ cursor: not-allowed;
182
+ opacity: 0.6;
183
+ }
184
+
185
+ :host([disabled]) .html-source,
186
+ :host([readonly]) .html-source {
187
+ opacity: 0.7;
188
+ }
@@ -0,0 +1,491 @@
1
+ import { html, nothing } from 'lit';
2
+ import { property, query, state } from 'lit/decorators.js';
3
+ import { classMap } from 'lit/directives/class-map.js';
4
+ import { Editor, mergeAttributes } from '@tiptap/core';
5
+ import StarterKit from '@tiptap/starter-kit';
6
+ import Underline from '@tiptap/extension-underline';
7
+ import Placeholder from '@tiptap/extension-placeholder';
8
+ import Mention from '@tiptap/extension-mention';
9
+ import { html as beautifyHtml } from 'js-beautify';
10
+
11
+ import IndividualComponent from '@/IndividualComponent.js';
12
+ import BaseInput from '../input/BaseInput.js';
13
+ import { redispatchEvent } from '../__utils/dispatch-event-utils.js';
14
+
15
+ import styles from './html-editor.scss';
16
+
17
+ /**
18
+ * @label HTML Editor
19
+ * @tag wc-html-editor
20
+ * @rawTag html-editor
21
+ *
22
+ * @summary A Tiptap-powered HTML editor with visual and source editing modes.
23
+ * @overview
24
+ * <p>The HTML Editor provides a rich-text editing experience built on Tiptap.
25
+ * It wraps the editable area in a Material 3 styled <code>wc-field</code>,
26
+ * exposes common formatting actions, and includes a segmented switch between
27
+ * <strong>Visual</strong> and <strong>HTML</strong> source modes.</p>
28
+ *
29
+ * <p>Get and set the HTML content via the <code>value</code> property. The component
30
+ * dispatches a <code>change</code> event whenever the content is modified.
31
+ * Mention suggestions are supported through the <code>mentions</code> property,
32
+ * with optional externally managed lookup via the <code>search</code> event.</p>
33
+ *
34
+ * @cssprop --html-editor-min-height - Minimum height of the editable area. Defaults to 8rem.
35
+ * @cssprop --html-editor-toolbar-background - Background color of the toolbar.
36
+ * @cssprop --html-editor-toolbar-border-color - Border color between toolbar and editing area.
37
+ *
38
+ * @fires {Event} change - Fired whenever the editable content changes.
39
+ * @fires {CustomEvent} search - Fired in managed mention mode with { query, callback } detail.
40
+ *
41
+ * @example
42
+ * ```html
43
+ * <wc-html-editor
44
+ * label="Description"
45
+ * value="<p>Hello <strong>world</strong></p>"
46
+ * .mentions="[{ label: 'Alex', value: 'alex' }]"
47
+ * ></wc-html-editor>
48
+ * ```
49
+ * @tags input editor
50
+ */
51
+ @IndividualComponent
52
+ export class HtmlEditor extends BaseInput {
53
+ static styles = [styles];
54
+
55
+ private _editor?: Editor;
56
+
57
+ private _changeTimeout?: number;
58
+
59
+ /** Current HTML value of the editor. */
60
+ @property({ type: String })
61
+ value = '';
62
+
63
+ /** Label displayed above the editor. */
64
+ @property({ type: String })
65
+ label = '';
66
+
67
+ /** Placeholder text shown when the editor is empty. */
68
+ @property({ type: String })
69
+ placeholder = 'Enter text\u2026';
70
+
71
+ /** Visual style of the wrapping field. */
72
+ @property({ type: String })
73
+ variant: 'filled' | 'outlined' | 'default' = 'default';
74
+
75
+ /** Helper text displayed below the editor. */
76
+ @property({ type: String, attribute: 'helper-text' })
77
+ helperText = '';
78
+
79
+ /** Whether to show an error state. */
80
+ @property({ type: Boolean })
81
+ error = false;
82
+
83
+ /** Error message text. */
84
+ @property({ type: String, attribute: 'error-text' })
85
+ errorText = '';
86
+
87
+ /** Whether toolbar controls should be displayed in visual mode. */
88
+ @property({ type: Boolean, attribute: 'show-toolbar' })
89
+ showToolbar = true;
90
+
91
+ /** Mention suggestions used by the mention extension. */
92
+ @property({ type: Array })
93
+ mentions: Array<{ label: string; value: string }> = [];
94
+
95
+ /** Mention filtering mode. */
96
+ @property({ type: String, attribute: 'mentions-search' })
97
+ mentionsSearch: 'contains' | 'managed' = 'contains';
98
+
99
+ /** Character that triggers mention suggestions. */
100
+ @property({ type: String, attribute: 'suggestion-character' })
101
+ suggestionCharacter = '@';
102
+
103
+ /** Whether to include the suggestion character in rendered mention text. */
104
+ @property({ type: Boolean, attribute: 'show-suggestion-character' })
105
+ showSuggestionCharacter = true;
106
+
107
+ /** Debounce in milliseconds for dispatching `change`. */
108
+ @property({ type: Number })
109
+ debounce = 250;
110
+
111
+ @state() private _focused = false;
112
+
113
+ @state() private _mode: 'visual' | 'html' = 'visual';
114
+
115
+ @query('.tiptap-root')
116
+ private _editorEl!: HTMLDivElement;
117
+
118
+ // ─── Lifecycle ─────────────────────────────────────────────────────────────
119
+
120
+ protected firstUpdated() {
121
+ this._initializeEditor();
122
+ }
123
+
124
+ disconnectedCallback() {
125
+ super.disconnectedCallback();
126
+ if (this._changeTimeout) {
127
+ window.clearTimeout(this._changeTimeout);
128
+ this._changeTimeout = undefined;
129
+ }
130
+ this._destroyEditor();
131
+ }
132
+
133
+ protected updated(changed: Map<string, unknown>) {
134
+ if (changed.has('value') && this._editor) {
135
+ const editorHtml = HtmlEditor._normalizeHtml(this._editor.getHTML());
136
+ const nextHtml = HtmlEditor._normalizeHtml(this.value);
137
+ if (editorHtml !== nextHtml) {
138
+ this._editor.commands.setContent(this.value ?? '', false);
139
+ }
140
+ }
141
+
142
+ if ((changed.has('disabled') || changed.has('readonly')) && this._editor) {
143
+ this._editor.setEditable(!(this.disabled || this.readonly));
144
+ }
145
+
146
+ if (changed.has('placeholder') && this._editor) {
147
+ this._destroyEditor();
148
+ this._initializeEditor();
149
+ }
150
+
151
+ if (changed.has('mentions') && this._editor) {
152
+ const oldMentions = changed.get('mentions') as Array<{
153
+ label: string;
154
+ value: string;
155
+ }>;
156
+ if (oldMentions !== this.mentions) {
157
+ this._destroyEditor();
158
+ this._initializeEditor();
159
+ }
160
+ }
161
+ }
162
+
163
+ // ─── Private helpers ───────────────────────────────────────────────────────
164
+
165
+ private static _normalizeHtml(value: string) {
166
+ return beautifyHtml(value ?? '', {
167
+ wrap_line_length: 120,
168
+ });
169
+ }
170
+
171
+ private _destroyEditor() {
172
+ if (!this._editor) return;
173
+ this._editorEl?.removeEventListener('click', this._focusEditorOnContainerClick);
174
+ this._editor.destroy();
175
+ this._editor = undefined;
176
+ }
177
+
178
+ private _initializeEditor() {
179
+ if (!this._editorEl || this._editor) return;
180
+
181
+ this._editor = new Editor({
182
+ element: this._editorEl,
183
+ extensions: [
184
+ StarterKit,
185
+ Underline,
186
+ Placeholder.configure({
187
+ placeholder: this.placeholder,
188
+ }),
189
+ Mention.configure({
190
+ HTMLAttributes: {
191
+ class: 'mention',
192
+ },
193
+ renderHTML: ({ options, node }) => {
194
+ const item = this._getMentionItem(node.attrs.id);
195
+ return [
196
+ 'a',
197
+ mergeAttributes({ contenteditable: false }, options.HTMLAttributes),
198
+ `${this.showSuggestionCharacter ? options.suggestion.char : ''}${
199
+ item ? item.label : node.attrs.id
200
+ }`,
201
+ ];
202
+ },
203
+ suggestion: {
204
+ allowSpaces: true,
205
+ char: this.suggestionCharacter,
206
+ items: async ({ query: mentionQuery }) => {
207
+ if (this.mentionsSearch === 'managed') {
208
+ return this._requestManagedMentions(mentionQuery);
209
+ }
210
+
211
+ return this.mentions
212
+ .filter(item =>
213
+ item.label.toLowerCase().startsWith(mentionQuery.toLowerCase()),
214
+ )
215
+ .map(item => item.value)
216
+ .slice(0, 5);
217
+ },
218
+ },
219
+ }),
220
+ ],
221
+ content: this.value,
222
+ editable: !(this.disabled || this.readonly),
223
+ onFocus: () => {
224
+ this._focused = true;
225
+ },
226
+ onBlur: () => {
227
+ this._focused = false;
228
+ },
229
+ onUpdate: () => {
230
+ if (!this._editor) return;
231
+
232
+ const nextHtml = HtmlEditor._normalizeHtml(this._editor.getHTML());
233
+ if (nextHtml !== this.value) {
234
+ this.value = nextHtml;
235
+ }
236
+
237
+ this._dispatchDebouncedChange();
238
+ },
239
+ });
240
+
241
+ this._editorEl.addEventListener('click', this._focusEditorOnContainerClick);
242
+ }
243
+
244
+ private _focusEditorOnContainerClick = (event: Event) => {
245
+ if (!this._editor) return;
246
+ if (event.target === this._editorEl) {
247
+ this._editor.commands.focus('end');
248
+ }
249
+ };
250
+
251
+ private _dispatchDebouncedChange() {
252
+ if (this._changeTimeout) {
253
+ window.clearTimeout(this._changeTimeout);
254
+ }
255
+
256
+ this._changeTimeout = window.setTimeout(() => {
257
+ redispatchEvent(this, new Event('change', { bubbles: true, composed: true }));
258
+ }, this.debounce);
259
+ }
260
+
261
+ private _requestManagedMentions(mentionQuery: string): Promise<string[]> {
262
+ return new Promise(resolve => {
263
+ this.dispatchEvent(
264
+ new CustomEvent('search', {
265
+ detail: {
266
+ query: mentionQuery,
267
+ callback: (mentions: Array<{ label: string; value: string }>) => {
268
+ this.mentions = mentions;
269
+ resolve(this.mentions.map(item => item.value));
270
+ },
271
+ },
272
+ bubbles: true,
273
+ composed: true,
274
+ }),
275
+ );
276
+ });
277
+ }
278
+
279
+ private _getMentionItem(value: string) {
280
+ return this.mentions.find(item => item.value === value);
281
+ }
282
+
283
+ private _execCommand(command: () => void) {
284
+ if (this.disabled || this.readonly || !this._editor) return;
285
+ command();
286
+ this._editor.commands.focus();
287
+ }
288
+
289
+ private _switchMode(event: CustomEvent<{ value: string }>) {
290
+ event.stopPropagation();
291
+ const nextMode = event.detail?.value === 'html' ? 'html' : 'visual';
292
+
293
+ if (nextMode === this._mode) return;
294
+
295
+ if (nextMode === 'html' && this._editor) {
296
+ this.value = HtmlEditor._normalizeHtml(this._editor.getHTML());
297
+ }
298
+
299
+ this._mode = nextMode;
300
+ }
301
+
302
+ private _handleSourceChange(event: Event) {
303
+ event.stopPropagation();
304
+ const target = event.currentTarget as { value?: string };
305
+ const nextValue = target.value ?? '';
306
+
307
+ if (nextValue === this.value) return;
308
+
309
+ this.value = nextValue;
310
+ this._dispatchDebouncedChange();
311
+ }
312
+
313
+ // ─── Toolbar button ────────────────────────────────────────────────────────
314
+
315
+ private _toolbarButton(
316
+ icon: string,
317
+ title: string,
318
+ action: () => void,
319
+ active = false,
320
+ ) {
321
+ return html`
322
+ <button
323
+ class=${classMap({
324
+ 'toolbar-btn': true,
325
+ active,
326
+ })}
327
+ title=${title}
328
+ aria-label=${title}
329
+ ?disabled=${this.disabled || this.readonly}
330
+ @mousedown=${(e: Event) => e.preventDefault()}
331
+ @click=${(e: Event) => {
332
+ e.preventDefault();
333
+ this._execCommand(action);
334
+ }}
335
+ >
336
+ <wc-icon name=${icon} size="sm"></wc-icon>
337
+ </button>
338
+ `;
339
+ }
340
+
341
+ // ─── Toolbar ───────────────────────────────────────────────────────────────
342
+
343
+ private _renderToolbar() {
344
+ if (!this._editor || !this.showToolbar || this._mode !== 'visual') {
345
+ return nothing;
346
+ }
347
+
348
+ return html`
349
+ <div
350
+ class="html-editor-toolbar"
351
+ role="toolbar"
352
+ aria-label="Formatting toolbar"
353
+ >
354
+ ${this._toolbarButton(
355
+ 'undo',
356
+ 'Undo',
357
+ () => this._editor?.commands.undo(),
358
+ )}
359
+ ${this._toolbarButton(
360
+ 'redo',
361
+ 'Redo',
362
+ () => this._editor?.commands.redo(),
363
+ )}
364
+
365
+ <span class="toolbar-divider"></span>
366
+
367
+ ${this._toolbarButton(
368
+ 'format_bold',
369
+ 'Bold',
370
+ () => this._editor?.chain().focus().toggleBold().run(),
371
+ this._editor.isActive('bold'),
372
+ )}
373
+ ${this._toolbarButton(
374
+ 'format_italic',
375
+ 'Italic',
376
+ () => this._editor?.chain().focus().toggleItalic().run(),
377
+ this._editor.isActive('italic'),
378
+ )}
379
+ ${this._toolbarButton(
380
+ 'format_underlined',
381
+ 'Underline',
382
+ () => this._editor?.chain().focus().toggleUnderline().run(),
383
+ this._editor.isActive('underline'),
384
+ )}
385
+ ${this._toolbarButton(
386
+ 'format_strikethrough',
387
+ 'Strikethrough',
388
+ () => this._editor?.chain().focus().toggleStrike().run(),
389
+ this._editor.isActive('strike'),
390
+ )}
391
+
392
+ <span class="toolbar-divider"></span>
393
+
394
+ ${this._toolbarButton(
395
+ 'format_list_bulleted',
396
+ 'Unordered list',
397
+ () => this._editor?.chain().focus().toggleBulletList().run(),
398
+ this._editor.isActive('bulletList'),
399
+ )}
400
+ ${this._toolbarButton(
401
+ 'format_list_numbered',
402
+ 'Ordered list',
403
+ () => this._editor?.chain().focus().toggleOrderedList().run(),
404
+ this._editor.isActive('orderedList'),
405
+ )}
406
+ </div>
407
+ `;
408
+ }
409
+
410
+ private _renderReadonlyTag() {
411
+ if (this.disabled) {
412
+ return html`<wc-tag class="read-only-tag" color="red">Disabled</wc-tag>`;
413
+ }
414
+ if (this.readonly) {
415
+ return html`<wc-tag class="read-only-tag" color="red">Read Only</wc-tag>`;
416
+ }
417
+ return nothing;
418
+ }
419
+
420
+ // ─── Render ────────────────────────────────────────────────────────────────
421
+
422
+ render() {
423
+ const isEmpty = !this.value || this.value === '<br>';
424
+
425
+ return html`
426
+ <wc-field
427
+ label=${this.label}
428
+ ?required=${this.required}
429
+ ?disabled=${this.disabled}
430
+ ?readonly=${this.readonly}
431
+ ?skeleton=${this.skeleton}
432
+ ?focused=${this._focused}
433
+ ?error=${this.error}
434
+ error-text=${this.errorText}
435
+ helper-text=${this.helperText}
436
+ variant=${this.variant}
437
+ ?populated=${!isEmpty}
438
+ .host=${this}
439
+ class=${classMap({
440
+ 'html-editor-field': true,
441
+ disabled: this.disabled,
442
+ readonly: this.readonly,
443
+ })}
444
+ >
445
+ <div class="mode-switcher">
446
+ <wc-segmented-button-group @change=${this._switchMode}>
447
+ <wc-segmented-button
448
+ value="visual"
449
+ ?selected=${this._mode === 'visual'}
450
+ ?disabled=${this.disabled}
451
+ >
452
+ Visual
453
+ </wc-segmented-button>
454
+ <wc-segmented-button
455
+ value="html"
456
+ ?selected=${this._mode === 'html'}
457
+ ?disabled=${this.disabled}
458
+ >
459
+ HTML
460
+ </wc-segmented-button>
461
+ </wc-segmented-button-group>
462
+ </div>
463
+
464
+ ${this._renderToolbar()}
465
+
466
+ <div
467
+ class=${classMap({
468
+ 'html-editor-content': true,
469
+ 'is-empty': isEmpty,
470
+ hidden: this._mode !== 'visual',
471
+ })}
472
+ data-placeholder=${this.placeholder}
473
+ >
474
+ <div class="tiptap-root"></div>
475
+ </div>
476
+
477
+ <div class=${classMap({ 'html-source': true, hidden: this._mode !== 'html' })}>
478
+ <wc-code-editor
479
+ language="html"
480
+ .value=${this.value}
481
+ ?readonly=${this.readonly}
482
+ ?disabled=${this.disabled}
483
+ @change=${this._handleSourceChange}
484
+ ></wc-code-editor>
485
+ </div>
486
+
487
+ ${this._renderReadonlyTag()}
488
+ </wc-field>
489
+ `;
490
+ }
491
+ }
@@ -0,0 +1,3 @@
1
+ import { HtmlEditor } from './html-editor.js';
2
+
3
+ export { HtmlEditor };