@webitel/ui-sdk 26.8.117 → 26.8.119

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 (38) hide show
  1. package/dist/{clients-DWcx-Mfk.js → clients-CjVXyqY8.js} +1502 -170
  2. package/dist/{components-CeOlovz8.js → components-jZ2GzWbv.js} +741 -747
  3. package/dist/ui-sdk.css +1 -1
  4. package/dist/ui-sdk.js +1 -1
  5. package/dist/ui-sdk.umd.cjs +2152 -2113
  6. package/dist/{wt-chat-emoji-v9gpIuHe.js → wt-chat-emoji-RHMx_k1y.js} +1 -1
  7. package/dist/{wt-display-chip-items-Bob7miL_.js → wt-display-chip-items-D_iKQXSB.js} +1 -1
  8. package/dist/wt-label-unpxAKnu.js +28 -0
  9. package/dist/wt-rich-text-editor-B-iHNpqD.js +119 -0
  10. package/dist/{wt-send-message-popup-9O69XY0L.js → wt-send-message-popup-B2lqrtGF.js} +2 -2
  11. package/dist/{wt-type-extension-value-input-55YmFcBp.js → wt-type-extension-value-input-CJWynqhC.js} +2 -2
  12. package/dist/{wt-vidstack-player-ETPSioGH.js → wt-vidstack-player-BL6xL6yo.js} +1 -1
  13. package/package.json +3 -1
  14. package/src/components/index.js +14 -0
  15. package/src/components/on-demand/wt-rich-text-editor/__tests__/WtRichTextEditor.compat.spec.js +121 -0
  16. package/src/components/on-demand/wt-rich-text-editor/__tests__/WtRichTextEditor.spec.js +154 -0
  17. package/src/components/on-demand/wt-rich-text-editor/_internals/wt-rich-text-editor-skeleton.vue +54 -0
  18. package/src/components/on-demand/wt-rich-text-editor/wt-rich-text-editor.vue +225 -0
  19. package/src/utils/__tests__/findTreePath.spec.ts +83 -0
  20. package/src/utils/findTreePath.ts +33 -0
  21. package/src/utils/index.ts +1 -0
  22. package/types/.tsbuildinfo +1 -1
  23. package/types/components/index.d.ts +7 -0
  24. package/types/components/on-demand/wt-call-media-action/wt-call-media-action.vue.d.ts +2 -2
  25. package/types/components/on-demand/wt-rich-text-editor/__tests__/WtRichTextEditor.compat.spec.d.ts +1 -0
  26. package/types/components/on-demand/wt-rich-text-editor/__tests__/WtRichTextEditor.spec.d.ts +1 -0
  27. package/types/components/on-demand/wt-rich-text-editor/_internals/wt-rich-text-editor-skeleton.vue.d.ts +3 -0
  28. package/types/components/on-demand/wt-rich-text-editor/wt-rich-text-editor.vue.d.ts +78 -0
  29. package/types/components/transitions/cases/wt-replace-transition.vue.d.ts +1 -1
  30. package/types/components/wt-datepicker/wt-datepicker.vue.d.ts +2 -2
  31. package/types/components/wt-input-text/wt-input-text.vue.d.ts +1 -1
  32. package/types/components/wt-player/wt-player.vue.d.ts +1 -1
  33. package/types/components/wt-search-bar/wt-search-bar.vue.d.ts +2 -2
  34. package/types/components/wt-table/wt-table.vue.d.ts +1 -1
  35. package/types/components/wt-textarea/wt-textarea.vue.d.ts +1 -1
  36. package/types/components/wt-time-input/wt-time-input.vue.d.ts +1 -1
  37. package/types/utils/findTreePath.d.ts +10 -0
  38. package/types/utils/index.d.ts +1 -0
@@ -0,0 +1,225 @@
1
+ <template>
2
+ <div class="wt-rich-text-editor">
3
+ <wt-label
4
+ v-if="label || $slots.label"
5
+ :disabled="disabled"
6
+ :for="editorId"
7
+ v-bind="labelProps"
8
+ >
9
+ <!-- @slot Custom editor label -->
10
+ <slot
11
+ name="label"
12
+ v-bind="{ label }"
13
+ >
14
+ {{ label }}
15
+ </slot>
16
+ </wt-label>
17
+ <tiny-mce-editor
18
+ :id="editorId"
19
+ v-model="model"
20
+ :disabled="disabled"
21
+ :init="init"
22
+ :output-format="output"
23
+ :plugins="editorPlugins"
24
+ :toolbar="editorToolbar"
25
+ license-key="gpl"
26
+ @init="handleInit"
27
+ />
28
+ </div>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ /**
33
+ * TinyMCE, self-hosted. Everything below loads with this component only:
34
+ * `components/index.js` registers it through `defineAsyncComponent`, so an
35
+ * app that never renders an editor never downloads TinyMCE.
36
+ */
37
+ import TinyMceVueEditor from '@tinymce/tinymce-vue';
38
+ import type { Editor as TinyMceInstance } from 'tinymce';
39
+ import 'tinymce/tinymce';
40
+ import 'tinymce/icons/default';
41
+ import 'tinymce/models/dom';
42
+ import 'tinymce/themes/silver';
43
+ import 'tinymce/plugins/advlist';
44
+ import 'tinymce/plugins/emoticons';
45
+ import 'tinymce/plugins/emoticons/js/emojis';
46
+ import 'tinymce/plugins/fullscreen';
47
+ import 'tinymce/plugins/image';
48
+ import 'tinymce/plugins/link';
49
+ import 'tinymce/plugins/lists';
50
+ import 'tinymce/plugins/table';
51
+ import 'tinymce/skins/ui/oxide/skin.css';
52
+ import { computed, onBeforeUnmount, useId } from 'vue';
53
+
54
+ import WtLabel from '../../wt-label/wt-label.vue';
55
+
56
+ /**
57
+ * tinymce-vue binds the model only when it finds `onUpdate:modelValue` in its
58
+ * attrs. Apps on the @vue/compat runtime (cc-workspaces) rewrite `v-model` on
59
+ * any component not marked as Vue 3 into the Vue 2 `value` / `input` pair, so
60
+ * the listener never arrives and edits are lost (WTEL-4477). Marking the
61
+ * editor as Vue 3 keeps `v-model` intact; plain Vue 3 ignores the option.
62
+ */
63
+ // A copy rather than `extends`: `extends` does not carry `setup()`, which is
64
+ // all tinymce-vue's Editor is.
65
+ const TinyMceEditor = {
66
+ ...TinyMceVueEditor,
67
+ name: 'TinyMceEditor',
68
+ compatConfig: {
69
+ MODE: 3,
70
+ },
71
+ } as typeof TinyMceVueEditor;
72
+
73
+ type RichTextOutput = 'html' | 'text';
74
+
75
+ interface Props {
76
+ /**
77
+ * Editor label
78
+ */
79
+ label?: string;
80
+ /**
81
+ * Object with props, passed down to wt-label as props
82
+ */
83
+ labelProps?: Record<string, unknown>;
84
+ /**
85
+ * `html` keeps the formatting toolbar; `text` edits (and emits) plain text
86
+ * @values html, text
87
+ */
88
+ output?: RichTextOutput;
89
+ /**
90
+ * Editor height, in px
91
+ */
92
+ height?: number | string;
93
+ disabled?: boolean;
94
+ /**
95
+ * Placeholder shown while the editor is empty
96
+ */
97
+ placeholder?: string;
98
+ /**
99
+ * TinyMCE toolbar, replacing the default for `output`
100
+ */
101
+ toolbar?: string;
102
+ /**
103
+ * TinyMCE plugins, replacing the default for `output`. A plugin outside
104
+ * the bundled set (advlist, emoticons, fullscreen, image, link, lists,
105
+ * table) must be imported by the app.
106
+ */
107
+ plugins?: string[];
108
+ }
109
+
110
+ // Vue 3 semantics on the @vue/compat runtime too (cc-workspaces): otherwise
111
+ // compat rewrites this component's own `v-model` to `value` / `input` and
112
+ // defineModel below never sees it.
113
+ defineOptions({
114
+ compatConfig: {
115
+ MODE: 3,
116
+ },
117
+ });
118
+
119
+ const props = withDefaults(defineProps<Props>(), {
120
+ output: 'html',
121
+ height: 300,
122
+ });
123
+
124
+ /**
125
+ * @model modelValue — the content, as `output` formats it
126
+ */
127
+ const model = defineModel<string>();
128
+
129
+ const editorId = useId();
130
+
131
+ const HTML_PLUGINS = [
132
+ 'advlist',
133
+ 'emoticons',
134
+ 'fullscreen',
135
+ 'image',
136
+ 'link',
137
+ 'lists',
138
+ 'table',
139
+ ];
140
+
141
+ const HTML_TOOLBAR = [
142
+ 'undo redo',
143
+ 'bold italic underline strikethrough',
144
+ 'fullscreen',
145
+ 'fontfamily fontsize',
146
+ 'alignleft aligncenter alignright alignjustify',
147
+ 'numlist bullist',
148
+ 'forecolor backcolor removeformat',
149
+ 'emoticons image link table',
150
+ ].join(' | ');
151
+
152
+ const isHtml = computed(() => props.output === 'html');
153
+
154
+ const editorPlugins = computed(
155
+ () => props.plugins ?? (isHtml.value ? HTML_PLUGINS : []),
156
+ );
157
+ const editorToolbar = computed(
158
+ () => props.toolbar ?? (isHtml.value ? HTML_TOOLBAR : 'undo redo'),
159
+ );
160
+
161
+ // The editable area lives in an iframe, out of reach of the app's CSS
162
+ // variables, so its colours are read from the page and handed over.
163
+ function readColors() {
164
+ const styles = getComputedStyle(document.documentElement);
165
+ return {
166
+ backgroundColor: styles.getPropertyValue('--content-wrapper-color').trim(),
167
+ color: styles.getPropertyValue('--wt-text-field-text-color').trim(),
168
+ };
169
+ }
170
+
171
+ const init = computed(() => {
172
+ const { backgroundColor, color } = readColors();
173
+ return {
174
+ height: Number(props.height),
175
+ menubar: false,
176
+ statusbar: isHtml.value,
177
+ toolbar_mode: 'sliding' as const,
178
+ toolbar_sticky: true,
179
+ placeholder: props.placeholder,
180
+ // the skin CSS is imported above; content styles are set here
181
+ skin: false,
182
+ content_css: false,
183
+ content_style: `body { background-color: ${backgroundColor}; color: ${color}; }`,
184
+ };
185
+ });
186
+
187
+ let editor: TinyMceInstance | null = null;
188
+
189
+ // The app switches themes by toggling `theme--dark` on <html>; follow it.
190
+ const themeObserver = new MutationObserver(() => {
191
+ if (!editor?.getBody()) return;
192
+ editor.dom.setStyles(editor.getBody(), readColors());
193
+ });
194
+
195
+ function handleInit(_event: unknown, initialized: TinyMceInstance) {
196
+ editor = initialized;
197
+ themeObserver.observe(document.documentElement, {
198
+ attributes: true,
199
+ attributeFilter: [
200
+ 'class',
201
+ ],
202
+ });
203
+ }
204
+
205
+ onBeforeUnmount(() => {
206
+ themeObserver.disconnect();
207
+ editor = null;
208
+ });
209
+ </script>
210
+
211
+ <style scoped>
212
+ .wt-rich-text-editor :deep(.tox-toolbar__primary),
213
+ .wt-rich-text-editor :deep(.tox-editor-header) {
214
+ background-color: var(--content-wrapper-color);
215
+ }
216
+
217
+ .wt-rich-text-editor :deep(.tox-tbtn svg) {
218
+ fill: var(--icon-active-color);
219
+ }
220
+
221
+ .wt-rich-text-editor :deep(.tox-tbtn--disabled svg),
222
+ .wt-rich-text-editor :deep(.tox-tbtn--disabled:hover svg) {
223
+ fill: var(--icon-color);
224
+ }
225
+ </style>
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { findTreePath } from '../findTreePath';
4
+
5
+ interface Service {
6
+ id: string;
7
+ name: string;
8
+ service?: Service[];
9
+ }
10
+
11
+ const services: Service[] = [
12
+ {
13
+ id: 'billing',
14
+ name: 'Billing',
15
+ service: [
16
+ {
17
+ id: 'refunds',
18
+ name: 'Refunds',
19
+ service: [
20
+ {
21
+ id: 'card',
22
+ name: 'Card refund',
23
+ },
24
+ ],
25
+ },
26
+ ],
27
+ },
28
+ {
29
+ id: 'support',
30
+ name: 'Support',
31
+ },
32
+ ];
33
+
34
+ const byId = (id: string) => (service: Service) => service.id === id;
35
+
36
+ describe('findTreePath', () => {
37
+ it('returns the nodes from the root down to the target', () => {
38
+ expect(
39
+ findTreePath(services, byId('card'), 'service')?.map(({ name }) => name),
40
+ ).toEqual([
41
+ 'Billing',
42
+ 'Refunds',
43
+ 'Card refund',
44
+ ]);
45
+ });
46
+
47
+ it('returns a one-node path for a root', () => {
48
+ expect(findTreePath(services, byId('support'), 'service')).toEqual([
49
+ services[1],
50
+ ]);
51
+ });
52
+
53
+ it('returns null when nothing matches, or there is no tree', () => {
54
+ expect(findTreePath(services, byId('missing'), 'service')).toBeNull();
55
+ expect(findTreePath(undefined, byId('card'), 'service')).toBeNull();
56
+ expect(findTreePath([], byId('card'), 'service')).toBeNull();
57
+ });
58
+
59
+ it('stops at the first match, depth first', () => {
60
+ const tree = [
61
+ {
62
+ id: 'a',
63
+ children: [
64
+ {
65
+ id: 'x',
66
+ },
67
+ ],
68
+ },
69
+ {
70
+ id: 'x',
71
+ },
72
+ ];
73
+
74
+ expect(
75
+ findTreePath(tree, (node) => node.id === 'x', 'children')?.map(
76
+ ({ id }) => id,
77
+ ),
78
+ ).toEqual([
79
+ 'a',
80
+ 'x',
81
+ ]);
82
+ });
83
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Walks a tree depth-first and returns the nodes from a root down to the
3
+ * first node matching `isTarget`, both ends included, or `null` when nothing
4
+ * matches. Children are read from `childrenKey` (e.g. `service` for case
5
+ * service catalogs, whose services nest under `service`).
6
+ *
7
+ * The path is what a breadcrumb, an "A / B / C" label or an ancestor check
8
+ * needs: `path.map(({ name }) => name)`, `path.slice(0, -1)`, and so on.
9
+ */
10
+ export function findTreePath<TNode extends object>(
11
+ nodes: readonly TNode[] | null | undefined,
12
+ isTarget: (node: TNode) => boolean,
13
+ // NoInfer: the node type comes from `nodes` only; inferred from the key
14
+ // literal instead, loosely typed trees narrowed to `{ service: any }`
15
+ childrenKey: NoInfer<keyof TNode>,
16
+ ): TNode[] | null {
17
+ for (const node of nodes ?? []) {
18
+ if (isTarget(node))
19
+ return [
20
+ node,
21
+ ];
22
+ const children = node[childrenKey];
23
+ if (Array.isArray(children)) {
24
+ const path = findTreePath(children as TNode[], isTarget, childrenKey);
25
+ if (path)
26
+ return [
27
+ node,
28
+ ...path,
29
+ ];
30
+ }
31
+ }
32
+ return null;
33
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './displayText';
2
+ export * from './findTreePath';
2
3
  export * from './formatDate';
3
4
  export * from './prettifyDate';