@webitel/ui-sdk 26.8.118 → 26.8.120

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 (37) hide show
  1. package/dist/{clients-DWcx-Mfk.js → clients-CwGTZ_pb.js} +1502 -170
  2. package/dist/{components-CeOlovz8.js → components-Bl2fDyms.js} +933 -948
  3. package/dist/ui-sdk.css +1 -1
  4. package/dist/ui-sdk.js +1 -1
  5. package/dist/ui-sdk.umd.cjs +2158 -2119
  6. package/dist/{wt-chat-emoji-v9gpIuHe.js → wt-chat-emoji-CQ-ZUYcm.js} +1 -1
  7. package/dist/{wt-display-chip-items-Bob7miL_.js → wt-display-chip-items-yuaxgXrU.js} +1 -1
  8. package/dist/wt-label-CiLpqe61.js +34 -0
  9. package/dist/wt-rich-text-editor-DyGnGabB.js +130 -0
  10. package/dist/{wt-send-message-popup-9O69XY0L.js → wt-send-message-popup-Ig3Uq6So.js} +2 -2
  11. package/dist/{wt-type-extension-value-input-55YmFcBp.js → wt-type-extension-value-input-B4fjVEic.js} +2 -2
  12. package/dist/{wt-vidstack-player-ETPSioGH.js → wt-vidstack-player-DitE8XQx.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 +127 -0
  16. package/src/components/on-demand/wt-rich-text-editor/__tests__/WtRichTextEditor.spec.js +157 -0
  17. package/src/components/on-demand/wt-rich-text-editor/__tests__/loadTinyMce.spec.js +54 -0
  18. package/src/components/on-demand/wt-rich-text-editor/_internals/loadTinyMce.ts +31 -0
  19. package/src/components/on-demand/wt-rich-text-editor/_internals/wt-rich-text-editor-skeleton.vue +54 -0
  20. package/src/components/on-demand/wt-rich-text-editor/wt-rich-text-editor.vue +223 -0
  21. package/types/.tsbuildinfo +1 -1
  22. package/types/components/index.d.ts +7 -0
  23. package/types/components/on-demand/wt-call-media-action/wt-call-media-action.vue.d.ts +2 -2
  24. package/types/components/on-demand/wt-rich-text-editor/__tests__/WtRichTextEditor.compat.spec.d.ts +1 -0
  25. package/types/components/on-demand/wt-rich-text-editor/__tests__/WtRichTextEditor.spec.d.ts +1 -0
  26. package/types/components/on-demand/wt-rich-text-editor/__tests__/loadTinyMce.spec.d.ts +1 -0
  27. package/types/components/on-demand/wt-rich-text-editor/_internals/loadTinyMce.d.ts +1 -0
  28. package/types/components/on-demand/wt-rich-text-editor/_internals/wt-rich-text-editor-skeleton.vue.d.ts +3 -0
  29. package/types/components/on-demand/wt-rich-text-editor/wt-rich-text-editor.vue.d.ts +65 -0
  30. package/types/components/transitions/cases/wt-replace-transition.vue.d.ts +1 -1
  31. package/types/components/wt-datepicker/wt-datepicker.vue.d.ts +2 -2
  32. package/types/components/wt-input-text/wt-input-text.vue.d.ts +1 -1
  33. package/types/components/wt-player/wt-player.vue.d.ts +1 -1
  34. package/types/components/wt-search-bar/wt-search-bar.vue.d.ts +2 -2
  35. package/types/components/wt-table/wt-table.vue.d.ts +1 -1
  36. package/types/components/wt-textarea/wt-textarea.vue.d.ts +1 -1
  37. package/types/components/wt-time-input/wt-time-input.vue.d.ts +1 -1
@@ -1,4 +1,4 @@
1
- import "./components-CeOlovz8.js";
1
+ import "./components-Bl2fDyms.js";
2
2
  import { t as e } from "./_plugin-vue_export-helper-B3ysoDQm.js";
3
3
  import { m as t } from "./enums-CsINFPBu.js";
4
4
  import { t as n } from "./wt-button-rUZKbswL.js";
@@ -1,4 +1,4 @@
1
- import { C as e, i as t } from "./components-CeOlovz8.js";
1
+ import { C as e, i as t } from "./components-Bl2fDyms.js";
2
2
  import { t as n } from "./_plugin-vue_export-helper-B3ysoDQm.js";
3
3
  import { t as r } from "./utils-DHSDTqJG.js";
4
4
  import { Fragment as i, computed as a, createCommentVNode as o, createElementBlock as s, createTextVNode as c, createVNode as l, defineComponent as u, openBlock as d, renderList as f, renderSlot as p, toDisplayString as m, unref as h, withCtx as g } from "vue";
@@ -0,0 +1,34 @@
1
+ import { t as e } from "./_plugin-vue_export-helper-B3ysoDQm.js";
2
+ import { createBlock as t, createCommentVNode as n, createElementBlock as r, createStaticVNode as i, createTextVNode as a, defineComponent as o, normalizeClass as s, openBlock as c, renderSlot as l, resolveComponent as u, toDisplayString as d, withCtx as f } from "vue";
3
+ //#region src/components/on-demand/wt-rich-text-editor/_internals/wt-rich-text-editor-skeleton.vue
4
+ var p = {}, m = {
5
+ "aria-busy": "true",
6
+ class: "wt-rich-text-editor-skeleton"
7
+ };
8
+ function h(e, t) {
9
+ return c(), r("div", m, [...t[0] ||= [i("<div class=\"wt-rich-text-editor-skeleton__toolbar\" data-v-0802d94d></div><div class=\"wt-rich-text-editor-skeleton__body\" data-v-0802d94d><div class=\"wt-rich-text-editor-skeleton__line\" data-v-0802d94d></div><div class=\"wt-rich-text-editor-skeleton__line\" data-v-0802d94d></div><div class=\"wt-rich-text-editor-skeleton__line wt-rich-text-editor-skeleton__line--short\" data-v-0802d94d></div></div>", 2)]]);
10
+ }
11
+ var g = /*#__PURE__*/ e(p, [["render", h], ["__scopeId", "data-v-0802d94d"]]), _ = /*#__PURE__*/ e(/* @__PURE__ */ o({
12
+ __name: "wt-label",
13
+ props: {
14
+ disabled: { type: Boolean },
15
+ invalid: { type: Boolean },
16
+ required: { type: Boolean },
17
+ hint: {}
18
+ },
19
+ setup(e) {
20
+ return (i, o) => {
21
+ let p = u("wt-hint");
22
+ return c(), r("label", { class: s([{
23
+ "wt-label--invalid": e.invalid,
24
+ "wt-label--disabled": e.disabled,
25
+ "wt-label--required": e.required
26
+ }, "wt-label typo-subtitle-2"]) }, [l(i.$slots, "default", {}, void 0, !0), e.hint ? (c(), t(p, { key: 0 }, {
27
+ default: f(() => [a(d(e.hint), 1)], void 0, !0),
28
+ _: 1
29
+ })) : n("", !0)], 2);
30
+ };
31
+ }
32
+ }), [["__scopeId", "data-v-7d80b998"]]);
33
+ //#endregion
34
+ export { g as n, _ as t };
@@ -0,0 +1,130 @@
1
+ import { n as e, t } from "./wt-label-CiLpqe61.js";
2
+ import { t as n } from "./_plugin-vue_export-helper-B3ysoDQm.js";
3
+ import { computed as r, createBlock as i, createCommentVNode as a, createElementBlock as o, createTextVNode as s, defineComponent as c, guardReactiveProps as l, mergeModels as u, mergeProps as d, normalizeProps as f, onBeforeUnmount as p, openBlock as m, ref as h, renderSlot as g, toDisplayString as _, unref as v, useId as y, useModel as b, withCtx as x } from "vue";
4
+ import S from "@tinymce/tinymce-vue";
5
+ //#region src/components/on-demand/wt-rich-text-editor/_internals/loadTinyMce.ts
6
+ var C = null;
7
+ function w() {
8
+ return C ??= import("tinymce/tinymce").then(() => Promise.all([
9
+ import("tinymce/icons/default"),
10
+ import("tinymce/models/dom"),
11
+ import("tinymce/themes/silver"),
12
+ import("tinymce/plugins/advlist"),
13
+ import("tinymce/plugins/emoticons"),
14
+ import("tinymce/plugins/emoticons/js/emojis"),
15
+ import("tinymce/plugins/fullscreen"),
16
+ import("tinymce/plugins/image"),
17
+ import("tinymce/plugins/link"),
18
+ import("tinymce/plugins/lists"),
19
+ import("tinymce/plugins/table"),
20
+ import("tinymce/skins/ui/oxide/skin.css")
21
+ ]).then(() => void 0)), C;
22
+ }
23
+ //#endregion
24
+ //#region src/components/on-demand/wt-rich-text-editor/wt-rich-text-editor.vue?vue&type=script&setup=true&lang.ts
25
+ var T = { class: "wt-rich-text-editor" }, E = /*#__PURE__*/ n(/* @__PURE__ */ c({
26
+ compatConfig: { MODE: 3 },
27
+ __name: "wt-rich-text-editor",
28
+ props: /*@__PURE__*/ u({
29
+ label: {},
30
+ labelProps: {},
31
+ output: { default: "html" },
32
+ height: { default: 300 },
33
+ disabled: { type: Boolean },
34
+ placeholder: {},
35
+ toolbar: {},
36
+ plugins: {}
37
+ }, {
38
+ modelValue: {},
39
+ modelModifiers: {}
40
+ }),
41
+ emits: ["update:modelValue"],
42
+ setup(n) {
43
+ let c = {
44
+ ...S,
45
+ name: "TinyMceEditor",
46
+ compatConfig: { MODE: 3 }
47
+ }, u = n, C = b(n, "modelValue"), E = y(), D = h(!1);
48
+ w().then(() => {
49
+ D.value = !0;
50
+ });
51
+ let O = [
52
+ "advlist",
53
+ "emoticons",
54
+ "fullscreen",
55
+ "image",
56
+ "link",
57
+ "lists",
58
+ "table"
59
+ ], k = [
60
+ "undo redo",
61
+ "bold italic underline strikethrough",
62
+ "fullscreen",
63
+ "fontfamily fontsize",
64
+ "alignleft aligncenter alignright alignjustify",
65
+ "numlist bullist",
66
+ "forecolor backcolor removeformat",
67
+ "emoticons image link table"
68
+ ].join(" | "), A = r(() => u.output === "html"), j = r(() => u.plugins ?? (A.value ? O : [])), M = r(() => u.toolbar ?? (A.value ? k : "undo redo"));
69
+ function N() {
70
+ let e = getComputedStyle(document.documentElement);
71
+ return {
72
+ backgroundColor: e.getPropertyValue("--content-wrapper-color").trim(),
73
+ color: e.getPropertyValue("--wt-text-field-text-color").trim()
74
+ };
75
+ }
76
+ let P = r(() => {
77
+ let { backgroundColor: e, color: t } = N();
78
+ return {
79
+ height: Number(u.height),
80
+ menubar: !1,
81
+ statusbar: A.value,
82
+ toolbar_mode: "sliding",
83
+ toolbar_sticky: !0,
84
+ placeholder: u.placeholder,
85
+ skin: !1,
86
+ content_css: !1,
87
+ content_style: `body { background-color: ${e}; color: ${t}; }`
88
+ };
89
+ }), F = null, I = new MutationObserver(() => {
90
+ F?.getBody() && F.dom.setStyles(F.getBody(), N());
91
+ });
92
+ function L(e, t) {
93
+ F = t, I.observe(document.documentElement, {
94
+ attributes: !0,
95
+ attributeFilter: ["class"]
96
+ });
97
+ }
98
+ return p(() => {
99
+ I.disconnect(), F = null;
100
+ }), (r, u) => (m(), o("div", T, [n.label || r.$slots.label ? (m(), i(t, d({ key: 0 }, n.labelProps, {
101
+ disabled: n.disabled,
102
+ for: v(E)
103
+ }), {
104
+ default: x(() => [g(r.$slots, "label", f(l({ label: n.label })), () => [s(_(n.label), 1)], !0)], void 0, !0),
105
+ _: 3
106
+ }, 16, ["disabled", "for"])) : a("", !0), D.value ? (m(), i(c, {
107
+ key: 1,
108
+ id: v(E),
109
+ modelValue: C.value,
110
+ "onUpdate:modelValue": u[0] ||= (e) => C.value = e,
111
+ disabled: n.disabled,
112
+ init: P.value,
113
+ "output-format": n.output,
114
+ plugins: j.value,
115
+ toolbar: M.value,
116
+ "license-key": "gpl",
117
+ onInit: L
118
+ }, null, 8, [
119
+ "id",
120
+ "modelValue",
121
+ "disabled",
122
+ "init",
123
+ "output-format",
124
+ "plugins",
125
+ "toolbar"
126
+ ])) : (m(), i(e, { key: 2 }))]));
127
+ }
128
+ }), [["__scopeId", "data-v-47d8ed9a"]]);
129
+ //#endregion
130
+ export { E as default };
@@ -1,6 +1,6 @@
1
- import { n as e } from "./components-CeOlovz8.js";
1
+ import { n as e } from "./components-Bl2fDyms.js";
2
2
  import "./convertDuration-CW5qzlwH.js";
3
- import { i as t, r as n } from "./clients-DWcx-Mfk.js";
3
+ import { i as t, r as n } from "./clients-CwGTZ_pb.js";
4
4
  import { n as r } from "./vue-i18n-VBiROMZ4.js";
5
5
  import { n as i } from "./useEventBus-B-ZeklLu.js";
6
6
  import { createBlock as a, createTextVNode as o, createVNode as s, defineComponent as c, mergeProps as l, onMounted as u, openBlock as d, ref as f, resolveComponent as p, toDisplayString as m, unref as h, withCtx as g } from "vue";
@@ -1,8 +1,8 @@
1
1
  import { a as e, c as t, i as n, n as r, r as i, s as a, t as o } from "./dist-EXdgfpvN.js";
2
2
  import { n as s } from "./genClient-CvWlQ0TZ.js";
3
3
  import { a as c } from "./caseConverters-DGri4uJo.js";
4
- import { a as l, o as u, r as d, s as f } from "./components-CeOlovz8.js";
5
- import { a as p, n as m, t as h } from "./clients-DWcx-Mfk.js";
4
+ import { a as l, o as u, r as d, s as f } from "./components-Bl2fDyms.js";
5
+ import { a as p, n as m, t as h } from "./clients-CwGTZ_pb.js";
6
6
  import { a as g, b as _, d as v, f as y, g as b, i as x, n as S, o as C, r as w, s as T, t as E, y as D } from "./enums-CsINFPBu.js";
7
7
  import { n as ee, t as te } from "./vue-i18n-VBiROMZ4.js";
8
8
  import "./wt-toast-pXV44Y3v.js";
@@ -1,4 +1,4 @@
1
- import { w as e } from "./components-CeOlovz8.js";
1
+ import { w as e } from "./components-Bl2fDyms.js";
2
2
  import "./convertDuration-CW5qzlwH.js";
3
3
  import { t } from "./_plugin-vue_export-helper-B3ysoDQm.js";
4
4
  import { m as n } from "./enums-CsINFPBu.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webitel/ui-sdk",
3
- "version": "26.8.118",
3
+ "version": "26.8.120",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "dev": "npm run docs:dev",
@@ -62,6 +62,7 @@
62
62
  "@morev/vue-transitions": "^3.0.5",
63
63
  "@primeuix/themes": "^1.1.1",
64
64
  "@tailwindcss/vite": "^4.1.7",
65
+ "@tinymce/tinymce-vue": "^6.3.0",
65
66
  "@vuepic/vue-datepicker": "^4.5.1",
66
67
  "@vueuse/components": "^14.0.0",
67
68
  "autosize": "^6.0.1",
@@ -79,6 +80,7 @@
79
80
  "qs-esm": "^7.0.3",
80
81
  "sortablejs": "^1.15.3",
81
82
  "tailwindcss": "^4.1.7",
83
+ "tinymce": "^8.9.2",
82
84
  "vue-multiselect": "^3.4.0",
83
85
  "vue-observe-visibility": "^2.0.0-alpha.1",
84
86
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
@@ -1,5 +1,6 @@
1
1
  import { defineAsyncComponent } from 'vue';
2
2
 
3
+ import WtRichTextEditorSkeleton from './on-demand/wt-rich-text-editor/_internals/wt-rich-text-editor-skeleton.vue';
3
4
  import WtReplaceTransition from './transitions/cases/wt-replace-transition.vue';
4
5
  import WtAppHeader from './wt-app-header/wt-app-header.vue';
5
6
  import WtAppNavigator from './wt-app-header/wt-app-navigator.vue';
@@ -181,6 +182,17 @@ const WtNavigationMenu = defineAsyncComponent(
181
182
  () =>
182
183
  import('./on-demand/wt-navigation-menu/components/wt-navigation-menu.vue'),
183
184
  );
185
+ /**
186
+ * TinyMCE lives in this chunk alone: an app that never renders an editor never
187
+ * downloads it. The skeleton covers the download.
188
+ * @type {typeof import('./on-demand/wt-rich-text-editor/wt-rich-text-editor.vue').default}
189
+ */
190
+ const WtRichTextEditor = defineAsyncComponent({
191
+ loader: () =>
192
+ import('./on-demand/wt-rich-text-editor/wt-rich-text-editor.vue'),
193
+ loadingComponent: WtRichTextEditorSkeleton,
194
+ delay: 0,
195
+ });
184
196
  /** @type {typeof import('./on-demand/wt-selection-popup/wt-selection-popup.vue').default} */
185
197
  const WtSelectionPopup = defineAsyncComponent(
186
198
  () => import('./on-demand/wt-selection-popup/wt-selection-popup.vue'),
@@ -233,6 +245,7 @@ const Components = {
233
245
  WtLoader,
234
246
  WtRoundedAction,
235
247
  WtReplaceTransition,
248
+ WtRichTextEditor,
236
249
  WtCheckbox,
237
250
  WtDatepicker,
238
251
  WtIconBtn,
@@ -364,6 +377,7 @@ export {
364
377
  WtProgressBar,
365
378
  WtRadio,
366
379
  WtReplaceTransition,
380
+ WtRichTextEditor,
367
381
  WtRoundedAction,
368
382
  WtScreenRecordingsAction,
369
383
  WtSearchBar,
@@ -0,0 +1,127 @@
1
+ /**
2
+ * cc-workspaces runs on the @vue/compat runtime. There, a component given a
3
+ * `modelValue` prop has it rewritten to `value` / `onModelCompat:input` unless
4
+ * the component is marked as Vue 3 — and tinymce-vue binds the model only when
5
+ * `onUpdate:modelValue` is in its attrs, so it silently stops emitting
6
+ * (WTEL-4477). This runs the editor on the compat runtime (one instance of it:
7
+ * @vue/test-utils would bring the real `vue`, so mounting goes through compat's
8
+ * own createApp) to pin that the listener still arrives.
9
+ */
10
+ vi.mock('vue', async () => await import('@vue/compat'));
11
+
12
+ vi.mock('tinymce/tinymce', () => ({}));
13
+ vi.mock('tinymce/icons/default', () => ({}));
14
+ vi.mock('tinymce/models/dom', () => ({}));
15
+ vi.mock('tinymce/themes/silver', () => ({}));
16
+ vi.mock('tinymce/plugins/advlist', () => ({}));
17
+ vi.mock('tinymce/plugins/emoticons', () => ({}));
18
+ vi.mock('tinymce/plugins/emoticons/js/emojis', () => ({}));
19
+ vi.mock('tinymce/plugins/fullscreen', () => ({}));
20
+ vi.mock('tinymce/plugins/image', () => ({}));
21
+ vi.mock('tinymce/plugins/link', () => ({}));
22
+ vi.mock('tinymce/plugins/lists', () => ({}));
23
+ vi.mock('tinymce/plugins/table', () => ({}));
24
+ vi.mock('tinymce/skins/ui/oxide/skin.css', () => ({}));
25
+
26
+ // Stands in for tinymce-vue's Editor: no declared emits, model read from attrs.
27
+ const seenAttrs = [];
28
+ const seenModel = [];
29
+ let emitEdit = () => {};
30
+ vi.mock('@tinymce/tinymce-vue', async () => {
31
+ const { defineComponent, h } = await import('@vue/compat');
32
+ return {
33
+ default: defineComponent({
34
+ name: 'Editor',
35
+ props: [
36
+ 'id',
37
+ 'modelValue',
38
+ 'init',
39
+ 'plugins',
40
+ 'toolbar',
41
+ 'outputFormat',
42
+ 'disabled',
43
+ 'licenseKey',
44
+ ],
45
+ setup(props, ctx) {
46
+ seenAttrs.push(Object.keys(ctx.attrs));
47
+ seenModel.push(props.modelValue);
48
+ // lets a test type into the stand-in
49
+ emitEdit = (value) => ctx.attrs['onUpdate:modelValue']?.(value);
50
+ return () => h('div');
51
+ },
52
+ }),
53
+ };
54
+ });
55
+
56
+ const { configureCompat, createApp, h } = await import('@vue/compat');
57
+ const { default: TinyMceVueEditor } = await import('@tinymce/tinymce-vue');
58
+ const { default: WtRichTextEditor } = await import(
59
+ '../wt-rich-text-editor.vue'
60
+ );
61
+
62
+ async function render(component, props) {
63
+ const app = createApp({
64
+ render: () => h(component, props),
65
+ });
66
+ // the label is resolved globally in apps; not under test here
67
+ app.component('WtLabel', {
68
+ render: () => null,
69
+ });
70
+ app.mount(document.createElement('div'));
71
+ // the editor mounts once TinyMCE has loaded (mocked here: at once)
72
+ await new Promise((resolve) => setTimeout(resolve));
73
+ return app;
74
+ }
75
+
76
+ describe('WtRichTextEditor on @vue/compat', () => {
77
+ beforeAll(() => {
78
+ configureCompat({
79
+ MODE: 2,
80
+ });
81
+ });
82
+
83
+ afterAll(() => {
84
+ configureCompat({
85
+ MODE: 3,
86
+ });
87
+ });
88
+
89
+ beforeEach(() => {
90
+ seenAttrs.length = 0;
91
+ seenModel.length = 0;
92
+ });
93
+
94
+ it('control: compat takes the model listener away from plain tinymce-vue', async () => {
95
+ (
96
+ await render(TinyMceVueEditor, {
97
+ modelValue: '',
98
+ 'onUpdate:modelValue': () => {},
99
+ })
100
+ ).unmount();
101
+
102
+ expect(seenAttrs[0]).not.toContain('onUpdate:modelValue');
103
+ });
104
+
105
+ it('keeps the model listener on its editor', async () => {
106
+ (
107
+ await render(WtRichTextEditor, {
108
+ modelValue: '',
109
+ 'onUpdate:modelValue': () => {},
110
+ })
111
+ ).unmount();
112
+
113
+ expect(seenAttrs[0]).toContain('onUpdate:modelValue');
114
+ });
115
+
116
+ it('takes its own v-model on compat: the value in, edits out', async () => {
117
+ const onUpdate = vi.fn();
118
+ await render(WtRichTextEditor, {
119
+ modelValue: '<p>Seed</p>',
120
+ 'onUpdate:modelValue': onUpdate,
121
+ });
122
+
123
+ expect(seenModel[0]).toBe('<p>Seed</p>');
124
+ emitEdit('<p>Edited</p>');
125
+ expect(onUpdate).toHaveBeenCalledWith('<p>Edited</p>');
126
+ });
127
+ });
@@ -0,0 +1,157 @@
1
+ import { flushPromises, mount } from '@vue/test-utils';
2
+ import { nextTick } from 'vue';
3
+
4
+ // TinyMCE cannot run in jsdom: its side-effect imports are stubbed, and the
5
+ // Vue wrapper is replaced by a stub that records its props.
6
+ vi.mock('tinymce/tinymce', () => ({}));
7
+ vi.mock('tinymce/icons/default', () => ({}));
8
+ vi.mock('tinymce/models/dom', () => ({}));
9
+ vi.mock('tinymce/themes/silver', () => ({}));
10
+ vi.mock('tinymce/plugins/advlist', () => ({}));
11
+ vi.mock('tinymce/plugins/emoticons', () => ({}));
12
+ vi.mock('tinymce/plugins/emoticons/js/emojis', () => ({}));
13
+ vi.mock('tinymce/plugins/fullscreen', () => ({}));
14
+ vi.mock('tinymce/plugins/image', () => ({}));
15
+ vi.mock('tinymce/plugins/link', () => ({}));
16
+ vi.mock('tinymce/plugins/lists', () => ({}));
17
+ vi.mock('tinymce/plugins/table', () => ({}));
18
+ vi.mock('tinymce/skins/ui/oxide/skin.css', () => ({}));
19
+ // setup-based like the real Editor, so wrapping it the wrong way shows up
20
+ vi.mock('@tinymce/tinymce-vue', async () => {
21
+ const { h } = await import('vue');
22
+ return {
23
+ default: {
24
+ name: 'Editor',
25
+ props: [
26
+ 'id',
27
+ 'modelValue',
28
+ 'init',
29
+ 'plugins',
30
+ 'toolbar',
31
+ 'outputFormat',
32
+ 'disabled',
33
+ 'licenseKey',
34
+ ],
35
+ emits: [
36
+ 'update:modelValue',
37
+ 'init',
38
+ ],
39
+ setup: () => () =>
40
+ h('div', {
41
+ class: 'editor-stub',
42
+ }),
43
+ },
44
+ };
45
+ });
46
+
47
+ const { default: WtRichTextEditor } = await import(
48
+ '../wt-rich-text-editor.vue'
49
+ );
50
+
51
+ // the editor mounts once TinyMCE has loaded (mocked here: at once)
52
+ const mountEditor = async (props = {}) => {
53
+ const wrapper = mount(WtRichTextEditor, {
54
+ props,
55
+ });
56
+ await flushPromises();
57
+ return wrapper;
58
+ };
59
+
60
+ const editorStub = (wrapper) =>
61
+ wrapper.findComponent({
62
+ name: 'TinyMceEditor',
63
+ });
64
+
65
+ describe('WtRichTextEditor', () => {
66
+ afterEach(() => {
67
+ document.documentElement.classList.remove('theme--dark');
68
+ });
69
+
70
+ it('shows a label only when given one', async () => {
71
+ expect((await mountEditor()).find('.wt-label').exists()).toBe(false);
72
+ const labelled = await mountEditor({
73
+ label: 'Reply',
74
+ });
75
+ expect(labelled.text()).toContain('Reply');
76
+ });
77
+
78
+ it('edits html with the full toolbar by default', async () => {
79
+ const editor = editorStub(await mountEditor());
80
+
81
+ expect(editor.props('outputFormat')).toBe('html');
82
+ expect(editor.props('plugins')).toContain('table');
83
+ expect(editor.props('toolbar')).toContain('bold italic');
84
+ expect(editor.props('init').height).toBe(300);
85
+ expect(editor.props('licenseKey')).toBe('gpl');
86
+ });
87
+
88
+ it('drops the formatting tools for plain text', async () => {
89
+ const editor = editorStub(
90
+ await mountEditor({
91
+ output: 'text',
92
+ }),
93
+ );
94
+
95
+ expect(editor.props('plugins')).toEqual([]);
96
+ expect(editor.props('toolbar')).toBe('undo redo');
97
+ expect(editor.props('init').statusbar).toBe(false);
98
+ });
99
+
100
+ it('takes toolbar and plugin overrides', async () => {
101
+ const editor = editorStub(
102
+ await mountEditor({
103
+ toolbar: 'bold italic | link',
104
+ plugins: [
105
+ 'link',
106
+ ],
107
+ }),
108
+ );
109
+
110
+ expect(editor.props('toolbar')).toBe('bold italic | link');
111
+ expect(editor.props('plugins')).toEqual([
112
+ 'link',
113
+ ]);
114
+ });
115
+
116
+ it('passes the model through both ways', async () => {
117
+ const wrapper = await mountEditor({
118
+ modelValue: '<p>Hi</p>',
119
+ });
120
+ expect(editorStub(wrapper).props('modelValue')).toBe('<p>Hi</p>');
121
+
122
+ await editorStub(wrapper).vm.$emit('update:modelValue', '<p>Hello</p>');
123
+
124
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([
125
+ '<p>Hello</p>',
126
+ ]);
127
+ });
128
+
129
+ it('restyles the editable body when the app switches theme', async () => {
130
+ const body = {};
131
+ const setStyles = vi.fn();
132
+ const wrapper = await mountEditor();
133
+
134
+ editorStub(wrapper).vm.$emit(
135
+ 'init',
136
+ {},
137
+ {
138
+ getBody: () => body,
139
+ dom: {
140
+ setStyles,
141
+ },
142
+ },
143
+ );
144
+ document.documentElement.classList.add('theme--dark');
145
+ // MutationObserver callbacks run as microtasks
146
+ await nextTick();
147
+ await Promise.resolve();
148
+
149
+ expect(setStyles).toHaveBeenCalledWith(
150
+ body,
151
+ expect.objectContaining({
152
+ backgroundColor: expect.any(String),
153
+ color: expect.any(String),
154
+ }),
155
+ );
156
+ });
157
+ });
@@ -0,0 +1,54 @@
1
+ const { loaded, track } = vi.hoisted(() => {
2
+ const loaded = [];
3
+ return {
4
+ loaded,
5
+ track: (name) => () => {
6
+ loaded.push(name);
7
+ return {};
8
+ },
9
+ };
10
+ });
11
+
12
+ vi.mock('tinymce/tinymce', track('core'));
13
+ vi.mock('tinymce/icons/default', track('icons'));
14
+ vi.mock('tinymce/models/dom', track('model'));
15
+ vi.mock('tinymce/themes/silver', track('theme'));
16
+ vi.mock('tinymce/plugins/advlist', track('advlist'));
17
+ vi.mock('tinymce/plugins/emoticons', track('emoticons'));
18
+ vi.mock('tinymce/plugins/emoticons/js/emojis', track('emojis'));
19
+ vi.mock('tinymce/plugins/fullscreen', track('fullscreen'));
20
+ vi.mock('tinymce/plugins/image', track('image'));
21
+ vi.mock('tinymce/plugins/link', track('link'));
22
+ vi.mock('tinymce/plugins/lists', track('lists'));
23
+ vi.mock('tinymce/plugins/table', track('table'));
24
+ vi.mock('tinymce/skins/ui/oxide/skin.css', track('skin'));
25
+
26
+ const { loadTinyMce } = await import('../_internals/loadTinyMce');
27
+
28
+ describe('loadTinyMce', () => {
29
+ it('evaluates the core before any add-on that registers on it', async () => {
30
+ await loadTinyMce();
31
+
32
+ expect(loaded[0]).toBe('core');
33
+ expect(loaded).toEqual(
34
+ expect.arrayContaining([
35
+ 'icons',
36
+ 'model',
37
+ 'theme',
38
+ 'table',
39
+ 'skin',
40
+ ]),
41
+ );
42
+ });
43
+
44
+ it('loads once, however many editors ask', async () => {
45
+ const count = loaded.length;
46
+
47
+ await Promise.all([
48
+ loadTinyMce(),
49
+ loadTinyMce(),
50
+ ]);
51
+
52
+ expect(loaded).toHaveLength(count);
53
+ });
54
+ });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Loads TinyMCE (self-hosted) once, core first.
3
+ *
4
+ * The icons, model, theme and plugins register themselves on the global
5
+ * `tinymce` the core sets, so the core must run before any of them. Plain
6
+ * side-effect imports do not guarantee that: in an app's production build
7
+ * the core — it touches `module` — ends up behind a lazy CommonJS wrapper,
8
+ * while the add-ons, plain IIFEs, are hoisted and run first, failing with
9
+ * "tinymce is not defined". Awaiting the core's import pins the order.
10
+ */
11
+ let loading: Promise<void> | null = null;
12
+
13
+ export function loadTinyMce(): Promise<void> {
14
+ loading ??= import('tinymce/tinymce').then(() =>
15
+ Promise.all([
16
+ import('tinymce/icons/default'),
17
+ import('tinymce/models/dom'),
18
+ import('tinymce/themes/silver'),
19
+ import('tinymce/plugins/advlist'),
20
+ import('tinymce/plugins/emoticons'),
21
+ import('tinymce/plugins/emoticons/js/emojis'),
22
+ import('tinymce/plugins/fullscreen'),
23
+ import('tinymce/plugins/image'),
24
+ import('tinymce/plugins/link'),
25
+ import('tinymce/plugins/lists'),
26
+ import('tinymce/plugins/table'),
27
+ import('tinymce/skins/ui/oxide/skin.css'),
28
+ ]).then(() => undefined),
29
+ );
30
+ return loading;
31
+ }