@ssml-builder-js/ssml-editor-elements 2.8.1 → 2.9.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @ssml-builder-js/ssml-editor-elements
2
2
 
3
+ ## 2.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Release v2.9.0 with Azure SSML validation and migration utilities, voice catalog support, and visual/editor improvements.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @ssml-builder-js/ssml-core@2.9.0
13
+
3
14
  ## 2.8.1
4
15
 
5
16
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -15,6 +15,7 @@ declare class SsmlEditorElement extends HTMLElementBase {
15
15
  private toolbarActions;
16
16
  private display;
17
17
  private editorContainer;
18
+ private visualContainer;
18
19
  private helpPanel;
19
20
  private openMenu;
20
21
  private openMenuTrigger;
@@ -29,6 +30,8 @@ declare class SsmlEditorElement extends HTMLElementBase {
29
30
  private documentState;
30
31
  private decorationsVisible;
31
32
  private helpOpen;
33
+ private visualSelectedPath;
34
+ private visualSelection;
32
35
  get value(): string;
33
36
  set value(value: string);
34
37
  get theme(): SsmlEditorTheme;
@@ -37,12 +40,15 @@ declare class SsmlEditorElement extends HTMLElementBase {
37
40
  set readonly(readonly: boolean);
38
41
  get locale(): SsmlEditorLocale;
39
42
  set locale(locale: SsmlEditorLocale);
43
+ get editMode(): "code" | "visual";
44
+ set editMode(mode: "code" | "visual");
40
45
  private prepareDocument;
41
46
  connectedCallback(): void;
42
47
  disconnectedCallback(): void;
43
48
  attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void;
44
49
  private render;
45
50
  private renderToolbar;
51
+ private createModeButton;
46
52
  private createActionButton;
47
53
  private createDecorationsSwitch;
48
54
  private createInsertionButton;
@@ -59,6 +65,8 @@ declare class SsmlEditorElement extends HTMLElementBase {
59
65
  private updateDecorations;
60
66
  private renderHelp;
61
67
  private updateTheme;
68
+ private updateEditMode;
69
+ private renderVisualEditor;
62
70
  private updateActiveButtons;
63
71
  private initialize;
64
72
  private readonly handleDocumentPointerDown;
@@ -67,6 +75,7 @@ declare class SsmlEditorElement extends HTMLElementBase {
67
75
  private disposeEditor;
68
76
  }
69
77
 
78
+ type SsmlEditorEditMode = "code" | "visual";
70
79
  declare function defineSsmlEditorElement(): void;
71
80
 
72
- export { type SsmlEditorChangeDetail, SsmlEditorElement, type SsmlEditorLocale, type SsmlEditorTheme, defineSsmlEditorElement };
81
+ export { type SsmlEditorChangeDetail, type SsmlEditorEditMode, SsmlEditorElement, type SsmlEditorLocale, type SsmlEditorTheme, defineSsmlEditorElement };
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ declare class SsmlEditorElement extends HTMLElementBase {
15
15
  private toolbarActions;
16
16
  private display;
17
17
  private editorContainer;
18
+ private visualContainer;
18
19
  private helpPanel;
19
20
  private openMenu;
20
21
  private openMenuTrigger;
@@ -29,6 +30,8 @@ declare class SsmlEditorElement extends HTMLElementBase {
29
30
  private documentState;
30
31
  private decorationsVisible;
31
32
  private helpOpen;
33
+ private visualSelectedPath;
34
+ private visualSelection;
32
35
  get value(): string;
33
36
  set value(value: string);
34
37
  get theme(): SsmlEditorTheme;
@@ -37,12 +40,15 @@ declare class SsmlEditorElement extends HTMLElementBase {
37
40
  set readonly(readonly: boolean);
38
41
  get locale(): SsmlEditorLocale;
39
42
  set locale(locale: SsmlEditorLocale);
43
+ get editMode(): "code" | "visual";
44
+ set editMode(mode: "code" | "visual");
40
45
  private prepareDocument;
41
46
  connectedCallback(): void;
42
47
  disconnectedCallback(): void;
43
48
  attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void;
44
49
  private render;
45
50
  private renderToolbar;
51
+ private createModeButton;
46
52
  private createActionButton;
47
53
  private createDecorationsSwitch;
48
54
  private createInsertionButton;
@@ -59,6 +65,8 @@ declare class SsmlEditorElement extends HTMLElementBase {
59
65
  private updateDecorations;
60
66
  private renderHelp;
61
67
  private updateTheme;
68
+ private updateEditMode;
69
+ private renderVisualEditor;
62
70
  private updateActiveButtons;
63
71
  private initialize;
64
72
  private readonly handleDocumentPointerDown;
@@ -67,6 +75,7 @@ declare class SsmlEditorElement extends HTMLElementBase {
67
75
  private disposeEditor;
68
76
  }
69
77
 
78
+ type SsmlEditorEditMode = "code" | "visual";
70
79
  declare function defineSsmlEditorElement(): void;
71
80
 
72
- export { type SsmlEditorChangeDetail, SsmlEditorElement, type SsmlEditorLocale, type SsmlEditorTheme, defineSsmlEditorElement };
81
+ export { type SsmlEditorChangeDetail, type SsmlEditorEditMode, SsmlEditorElement, type SsmlEditorLocale, type SsmlEditorTheme, defineSsmlEditorElement };
package/dist/index.js CHANGED
@@ -1178,6 +1178,11 @@ var SSML_ATTRIBUTE_PRESETS = {
1178
1178
  "mstts:audioduration": {
1179
1179
  value: AUDIO_DURATION_PRESETS
1180
1180
  },
1181
+ "mstts:backgroundaudio": {
1182
+ volume: PROSODY_VOLUME_PRESETS,
1183
+ fadein: AUDIO_DURATION_PRESETS,
1184
+ fadeout: AUDIO_DURATION_PRESETS
1185
+ },
1181
1186
  silence: {
1182
1187
  type: SILENCE_TYPE_PRESETS,
1183
1188
  value: SILENCE_VALUE_PRESETS
@@ -1669,6 +1674,24 @@ var SSML_COMPLETION_SNIPPETS = [
1669
1674
  label: "mstts:audioduration",
1670
1675
  insertText: '<mstts:audioduration value="10s" />'
1671
1676
  },
1677
+ {
1678
+ label: "mstts:dialog",
1679
+ insertText: `<mstts:dialog>
1680
+ <mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>
1681
+ </mstts:dialog>`
1682
+ },
1683
+ {
1684
+ label: "mstts:turn",
1685
+ insertText: `<mstts:turn voice="\${1:en-US-JennyNeural}">\${2:text}</mstts:turn>`
1686
+ },
1687
+ {
1688
+ label: "mstts:backgroundaudio",
1689
+ insertText: `<mstts:backgroundaudio src="\${1:https://example.com/audio.mp3}" volume="\${2:-3dB}" />`
1690
+ },
1691
+ {
1692
+ label: "mstts:ttsembedding",
1693
+ insertText: `<mstts:ttsembedding>\${1:text}</mstts:ttsembedding>`
1694
+ },
1672
1695
  {
1673
1696
  label: "sub",
1674
1697
  insertText: `<sub alias="\${1:\u8AAD\u307F}">\${2:\u6F22\u5B57}</sub>`
@@ -2414,6 +2437,63 @@ var SSML_TAG_DEFINITIONS = [
2414
2437
  example: "10s"
2415
2438
  }
2416
2439
  ]
2440
+ },
2441
+ {
2442
+ name: "mstts:dialog",
2443
+ description: "Groups multiple Azure dialog turns that can use different voices.",
2444
+ parameters: []
2445
+ },
2446
+ {
2447
+ name: "mstts:turn",
2448
+ description: "Adds one dialog turn using the required Azure voice name.",
2449
+ parameters: [
2450
+ {
2451
+ name: "voice",
2452
+ description: "The Azure voice used for this turn, such as `en-US-JennyNeural`.",
2453
+ example: "en-US-JennyNeural"
2454
+ }
2455
+ ]
2456
+ },
2457
+ {
2458
+ name: "mstts:backgroundaudio",
2459
+ description: "Plays background audio while speech is synthesized.",
2460
+ parameters: [
2461
+ {
2462
+ name: "src",
2463
+ description: "An absolute HTTP(S) URL for the background audio file.",
2464
+ example: "https://example.com/music.mp3"
2465
+ },
2466
+ {
2467
+ name: "volume",
2468
+ description: "The background audio volume, for example `-3dB` or `medium`.",
2469
+ example: "-3dB"
2470
+ },
2471
+ {
2472
+ name: "fadein",
2473
+ description: "The fade-in duration, for example `1s`.",
2474
+ example: "1s"
2475
+ },
2476
+ {
2477
+ name: "fadeout",
2478
+ description: "The fade-out duration, for example `500ms`.",
2479
+ example: "500ms"
2480
+ }
2481
+ ]
2482
+ },
2483
+ {
2484
+ name: "mstts:ttsembedding",
2485
+ description: "Embeds custom voice or speaker profile metadata for Azure Speech.",
2486
+ parameters: []
2487
+ },
2488
+ {
2489
+ name: "mstts:embedding",
2490
+ description: "Specifies embedding metadata for custom voice scenarios.",
2491
+ parameters: []
2492
+ },
2493
+ {
2494
+ name: "mstts:voiceconversion",
2495
+ description: "Specifies voice conversion metadata for custom voice scenarios.",
2496
+ parameters: []
2417
2497
  }
2418
2498
  ];
2419
2499
  var definitionsByName = /* @__PURE__ */ new Map();
@@ -3219,6 +3299,60 @@ section[data-ssml-editor] {
3219
3299
  border-radius: 0.25rem;
3220
3300
  overflow: visible;
3221
3301
  }
3302
+ [data-ssml-editor] .ssml-editor-visual {
3303
+ display: grid;
3304
+ gap: 0.75rem;
3305
+ min-height: 8rem;
3306
+ padding: 0.75rem;
3307
+ border: 1px solid var(--ssml-editor-control-border);
3308
+ border-radius: 0.25rem;
3309
+ }
3310
+ [data-ssml-editor] .ssml-editor-visual-layout {
3311
+ display: grid;
3312
+ grid-template-columns: minmax(12rem, 0.35fr) minmax(16rem, 1fr);
3313
+ gap: 1rem;
3314
+ }
3315
+ [data-ssml-editor] .ssml-editor-visual-tree ul {
3316
+ margin: 0;
3317
+ padding-left: 1.25rem;
3318
+ }
3319
+ [data-ssml-editor] .ssml-editor-visual button {
3320
+ padding: 0.35rem 0.5rem;
3321
+ border: 1px solid var(--ssml-editor-control-border);
3322
+ border-radius: 0.25rem;
3323
+ color: var(--ssml-editor-color);
3324
+ background: var(--ssml-editor-control-bg);
3325
+ cursor: pointer;
3326
+ }
3327
+ [data-ssml-editor] .ssml-editor-visual button[data-selected="true"] {
3328
+ border-color: var(--ssml-editor-active-border);
3329
+ background: var(--ssml-editor-active-bg);
3330
+ }
3331
+ [data-ssml-editor] .ssml-editor-visual textarea {
3332
+ box-sizing: border-box;
3333
+ width: 100%;
3334
+ min-height: 6rem;
3335
+ padding: 0.5rem;
3336
+ color: var(--ssml-editor-color);
3337
+ background: var(--ssml-editor-control-bg);
3338
+ border: 1px solid var(--ssml-editor-control-border);
3339
+ border-radius: 0.25rem;
3340
+ font: inherit;
3341
+ }
3342
+ [data-ssml-editor] .ssml-editor-visual-actions,
3343
+ [data-ssml-editor] .ssml-editor-visual-breadcrumb {
3344
+ display: flex;
3345
+ flex-wrap: wrap;
3346
+ gap: 0.4rem;
3347
+ align-items: center;
3348
+ }
3349
+ [data-ssml-editor] .ssml-editor-visual-errors {
3350
+ padding: 0.5rem;
3351
+ color: #b91c1c;
3352
+ background: #fef2f2;
3353
+ border: 1px solid #b91c1c;
3354
+ border-radius: 0.25rem;
3355
+ }
3222
3356
  `.trim();
3223
3357
  function injectStyles() {
3224
3358
  if (typeof document === "undefined" || document.getElementById(STYLE_ID)) {
@@ -3232,6 +3366,55 @@ function injectStyles() {
3232
3366
  function isDarkTheme(theme) {
3233
3367
  return theme === "vs-dark" || theme.toLowerCase().includes("dark");
3234
3368
  }
3369
+ function isElement(node) {
3370
+ return typeof node !== "string" && node.type !== "text";
3371
+ }
3372
+ function visualElementName(element) {
3373
+ return element.type === "custom" || element.type === "element" ? element.name : element.type;
3374
+ }
3375
+ function collectVisualTextLeaves(nodes, path = [], ancestors = []) {
3376
+ return nodes.flatMap((node, index) => {
3377
+ const currentPath = [...path, index];
3378
+ if (typeof node === "string") return [{ path: currentPath, value: node, ancestors }];
3379
+ if (node.type === "text") return [{ path: currentPath, value: node.value, ancestors }];
3380
+ return collectVisualTextLeaves(node.children ?? [], currentPath, [...ancestors, visualElementName(node)]);
3381
+ });
3382
+ }
3383
+ function updateVisualNodes(nodes, path, update) {
3384
+ if (path.length === 0) return nodes;
3385
+ const [index, ...rest] = path;
3386
+ return nodes.flatMap((node, nodeIndex) => {
3387
+ if (nodeIndex !== index) return [node];
3388
+ if (rest.length === 0) {
3389
+ const next = update(node);
3390
+ return Array.isArray(next) ? next : [next];
3391
+ }
3392
+ if (!isElement(node)) return [node];
3393
+ return [{ ...node, children: updateVisualNodes(node.children ?? [], rest, update) }];
3394
+ });
3395
+ }
3396
+ function updateVisualText(document2, path, value) {
3397
+ return { ...document2, children: updateVisualNodes(document2.children ?? [], path, () => value) };
3398
+ }
3399
+ function wrapVisualText(document2, path, start, end, type, attributes) {
3400
+ return {
3401
+ ...document2,
3402
+ children: updateVisualNodes(document2.children ?? [], path, (node) => {
3403
+ const value = typeof node === "string" ? node : node.type === "text" ? node.value : "";
3404
+ if (!value || start === end) return node;
3405
+ if (type === "break") {
3406
+ return [value.slice(0, start), { type, attributes, children: [] }, value.slice(start)].filter(
3407
+ (part) => typeof part === "string" ? part.length > 0 : true
3408
+ );
3409
+ }
3410
+ const selected = value.slice(start, end);
3411
+ const wrapper = { type, attributes, children: [selected] };
3412
+ return [value.slice(0, start), wrapper, value.slice(end)].filter(
3413
+ (part) => typeof part === "string" ? part.length > 0 : true
3414
+ );
3415
+ })
3416
+ };
3417
+ }
3235
3418
  function getMenuPosition(trigger, menu) {
3236
3419
  const bounds = trigger.getBoundingClientRect();
3237
3420
  const margin = 8;
@@ -3253,6 +3436,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
3253
3436
  this.toolbarActions = null;
3254
3437
  this.display = null;
3255
3438
  this.editorContainer = null;
3439
+ this.visualContainer = null;
3256
3440
  this.helpPanel = null;
3257
3441
  this.openMenu = null;
3258
3442
  this.openMenuTrigger = null;
@@ -3266,6 +3450,8 @@ var SsmlEditorElement = class extends HTMLElementBase {
3266
3450
  this.documentState = null;
3267
3451
  this.decorationsVisible = false;
3268
3452
  this.helpOpen = false;
3453
+ this.visualSelectedPath = null;
3454
+ this.visualSelection = { start: 0, end: 0 };
3269
3455
  this.handleDocumentPointerDown = (event) => {
3270
3456
  const target = event.target;
3271
3457
  if (this.openMenu && target instanceof Node && !this.openMenu.contains(target) && !this.openMenuTrigger?.contains(target)) {
@@ -3307,6 +3493,12 @@ var SsmlEditorElement = class extends HTMLElementBase {
3307
3493
  set locale(locale) {
3308
3494
  this.setAttribute("locale", locale);
3309
3495
  }
3496
+ get editMode() {
3497
+ return this.getAttribute("edit-mode") === "visual" ? "visual" : "code";
3498
+ }
3499
+ set editMode(mode) {
3500
+ this.setAttribute("edit-mode", mode);
3501
+ }
3310
3502
  prepareDocument(value) {
3311
3503
  try {
3312
3504
  this.documentState = (0, import_ssml_core2.parseSsml)(value);
@@ -3349,6 +3541,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
3349
3541
  }
3350
3542
  }
3351
3543
  }
3544
+ this.renderVisualEditor();
3352
3545
  return;
3353
3546
  }
3354
3547
  if (name === "theme" && this.monaco) {
@@ -3366,6 +3559,9 @@ var SsmlEditorElement = class extends HTMLElementBase {
3366
3559
  this.renderHelp();
3367
3560
  return;
3368
3561
  }
3562
+ if (name === "edit-mode") {
3563
+ this.updateEditMode();
3564
+ }
3369
3565
  if (name === "show-decorations") {
3370
3566
  this.decorationsVisible = newValue !== null;
3371
3567
  this.updateDecorations();
@@ -3390,7 +3586,9 @@ var SsmlEditorElement = class extends HTMLElementBase {
3390
3586
  display.dataset.ssmlEditorDisplay = "";
3391
3587
  const editorContainer = document.createElement("div");
3392
3588
  editorContainer.className = "ssml-editor-editor";
3393
- display.append(editorContainer);
3589
+ const visualContainer = document.createElement("div");
3590
+ visualContainer.className = "ssml-editor-visual";
3591
+ display.append(editorContainer, visualContainer);
3394
3592
  root.append(toolbar, display);
3395
3593
  this.replaceChildren(root);
3396
3594
  this.root = root;
@@ -3398,8 +3596,11 @@ var SsmlEditorElement = class extends HTMLElementBase {
3398
3596
  this.toolbarActions = toolbarActions;
3399
3597
  this.display = display;
3400
3598
  this.editorContainer = editorContainer;
3599
+ this.visualContainer = visualContainer;
3401
3600
  this.renderToolbar();
3402
3601
  this.renderHelp();
3602
+ this.updateEditMode();
3603
+ this.renderVisualEditor();
3403
3604
  }
3404
3605
  renderToolbar() {
3405
3606
  const toolbar = this.toolbar;
@@ -3443,10 +3644,10 @@ var SsmlEditorElement = class extends HTMLElementBase {
3443
3644
  for (const id of toolbarIds) {
3444
3645
  const group = groupByButtonId.get(id);
3445
3646
  if (previousGroup !== void 0 && group !== previousGroup) {
3446
- const separator = document.createElement("span");
3447
- separator.className = "ssml-editor-toolbar-separator";
3448
- separator.setAttribute("aria-hidden", "true");
3449
- toolbarActions.append(separator);
3647
+ const separator2 = document.createElement("span");
3648
+ separator2.className = "ssml-editor-toolbar-separator";
3649
+ separator2.setAttribute("aria-hidden", "true");
3650
+ toolbarActions.append(separator2);
3450
3651
  }
3451
3652
  previousGroup = group;
3452
3653
  const insertion = insertionById.get(id);
@@ -3458,8 +3659,24 @@ var SsmlEditorElement = class extends HTMLElementBase {
3458
3659
  toolbarActions.append(this.createActionButton(id));
3459
3660
  }
3460
3661
  }
3662
+ const separator = document.createElement("span");
3663
+ separator.className = "ssml-editor-toolbar-separator";
3664
+ separator.setAttribute("aria-hidden", "true");
3665
+ toolbarActions.append(separator, this.createModeButton("visual", "Visual"), this.createModeButton("code", "Code"));
3461
3666
  this.updateActiveButtons();
3462
3667
  }
3668
+ createModeButton(mode, label) {
3669
+ const button = document.createElement("button");
3670
+ button.type = "button";
3671
+ button.className = "ssml-editor-toolbar-button";
3672
+ button.dataset.ssmlEditorButton = `edit-mode-${mode}`;
3673
+ button.setAttribute("aria-pressed", String(this.editMode === mode));
3674
+ button.textContent = label;
3675
+ button.addEventListener("click", () => {
3676
+ this.editMode = mode;
3677
+ });
3678
+ return button;
3679
+ }
3463
3680
  createActionButton(id) {
3464
3681
  const copy = EDITOR_COPY[this.locale];
3465
3682
  const labels = {
@@ -3839,6 +4056,150 @@ var SsmlEditorElement = class extends HTMLElementBase {
3839
4056
  this.root.dataset.theme = isDarkTheme(this.theme) ? "dark" : "light";
3840
4057
  }
3841
4058
  }
4059
+ updateEditMode() {
4060
+ if (this.editorContainer && this.visualContainer) {
4061
+ const visual = this.editMode === "visual";
4062
+ this.editorContainer.hidden = visual;
4063
+ this.visualContainer.hidden = !visual;
4064
+ }
4065
+ for (const mode of ["visual", "code"]) {
4066
+ const button = this.toolbarActions?.querySelector(
4067
+ `[data-ssml-editor-button="edit-mode-${mode}"]`
4068
+ );
4069
+ button?.setAttribute("aria-pressed", String(this.editMode === mode));
4070
+ button?.toggleAttribute("data-active", this.editMode === mode);
4071
+ }
4072
+ this.renderVisualEditor();
4073
+ }
4074
+ renderVisualEditor() {
4075
+ const container = this.visualContainer;
4076
+ if (!container) return;
4077
+ container.replaceChildren();
4078
+ if (!this.documentState) {
4079
+ const error = document.createElement("p");
4080
+ error.className = "ssml-editor-visual-errors";
4081
+ error.textContent = "SSML syntax must be valid before visual editing is available.";
4082
+ container.append(error);
4083
+ return;
4084
+ }
4085
+ const documentState = this.documentState;
4086
+ const leaves = collectVisualTextLeaves(documentState.children ?? []);
4087
+ const selectedLeaf = leaves.find((leaf) => leaf.path.join(".") === this.visualSelectedPath?.join(".")) ?? leaves[0];
4088
+ const breadcrumb = document.createElement("div");
4089
+ breadcrumb.className = "ssml-editor-visual-breadcrumb";
4090
+ breadcrumb.textContent = `<speak>${selectedLeaf ? ` / ${selectedLeaf.ancestors.map((name) => `<${name}>`).join(" / ")}` : ""}`;
4091
+ const clear = document.createElement("button");
4092
+ clear.type = "button";
4093
+ clear.textContent = "Clear parent";
4094
+ clear.disabled = !this.visualSelectedPath;
4095
+ clear.addEventListener("click", () => {
4096
+ this.visualSelectedPath = null;
4097
+ this.renderVisualEditor();
4098
+ });
4099
+ breadcrumb.append(clear);
4100
+ container.append(breadcrumb);
4101
+ const diagnostics = (0, import_ssml_core2.validateAzureSsml)((0, import_ssml_core2.buildSsml)(documentState));
4102
+ if (diagnostics.length > 0) {
4103
+ const errors = document.createElement("div");
4104
+ errors.className = "ssml-editor-visual-errors";
4105
+ errors.setAttribute("role", "alert");
4106
+ for (const diagnostic of diagnostics) {
4107
+ const message = document.createElement("div");
4108
+ message.textContent = diagnostic.message;
4109
+ errors.append(message);
4110
+ }
4111
+ container.append(errors);
4112
+ }
4113
+ const layout = document.createElement("div");
4114
+ layout.className = "ssml-editor-visual-layout";
4115
+ const tree = document.createElement("nav");
4116
+ tree.className = "ssml-editor-visual-tree";
4117
+ tree.setAttribute("aria-label", "SSML structure tree");
4118
+ tree.append(document.createTextNode("Structure"));
4119
+ const treeList = document.createElement("ul");
4120
+ const renderTree = (nodes, parent, parentPath = []) => {
4121
+ nodes.forEach((node, index) => {
4122
+ if (!isElement(node)) return;
4123
+ const path = [...parentPath, index];
4124
+ const item = document.createElement("li");
4125
+ const button = document.createElement("button");
4126
+ button.type = "button";
4127
+ button.textContent = `<${visualElementName(node)}>`;
4128
+ button.dataset.selected = String(path.join(".") === this.visualSelectedPath?.join("."));
4129
+ button.addEventListener("click", () => {
4130
+ this.visualSelectedPath = path;
4131
+ this.renderVisualEditor();
4132
+ });
4133
+ item.append(button);
4134
+ if ((node.children ?? []).some(isElement)) {
4135
+ const childList = document.createElement("ul");
4136
+ renderTree(node.children ?? [], childList, path);
4137
+ item.append(childList);
4138
+ }
4139
+ parent.append(item);
4140
+ });
4141
+ };
4142
+ renderTree(documentState.children ?? [], treeList);
4143
+ tree.append(treeList);
4144
+ layout.append(tree);
4145
+ const form = document.createElement("div");
4146
+ form.className = "ssml-editor-visual-form";
4147
+ if (selectedLeaf) {
4148
+ const label = document.createElement("label");
4149
+ label.append(document.createTextNode("Text"));
4150
+ const textarea = document.createElement("textarea");
4151
+ textarea.value = selectedLeaf.value;
4152
+ textarea.readOnly = this.readonly;
4153
+ textarea.addEventListener("select", () => {
4154
+ this.visualSelection = { start: textarea.selectionStart, end: textarea.selectionEnd };
4155
+ });
4156
+ textarea.addEventListener("input", () => {
4157
+ if (!this.readonly) this.replaceDocument(updateVisualText(documentState, selectedLeaf.path, textarea.value));
4158
+ });
4159
+ label.append(textarea);
4160
+ form.append(label);
4161
+ const actions = document.createElement("div");
4162
+ actions.className = "ssml-editor-visual-actions";
4163
+ const wrappers = [
4164
+ ["Rate", "prosody", { rate: "slow" }],
4165
+ ["Pitch", "prosody", { pitch: "high" }],
4166
+ ["Emotion", "mstts:express-as", { style: "cheerful" }],
4167
+ ["Pause", "break", { time: "500ms" }],
4168
+ ["Pronunciation", "phoneme", { alphabet: "ipa", ph: selectedLeaf.value }]
4169
+ ];
4170
+ for (const [labelText, type, attributes] of wrappers) {
4171
+ const button = document.createElement("button");
4172
+ button.type = "button";
4173
+ button.textContent = labelText;
4174
+ button.disabled = this.readonly;
4175
+ button.addEventListener("click", () => {
4176
+ const start = this.visualSelection.start === this.visualSelection.end ? 0 : this.visualSelection.start;
4177
+ const end = this.visualSelection.start === this.visualSelection.end ? selectedLeaf.value.length : this.visualSelection.end;
4178
+ this.replaceDocument(wrapVisualText(documentState, selectedLeaf.path, start, end, type, attributes));
4179
+ this.renderVisualEditor();
4180
+ });
4181
+ actions.append(button);
4182
+ }
4183
+ const preview = document.createElement("button");
4184
+ preview.type = "button";
4185
+ preview.textContent = "Preview selection";
4186
+ preview.addEventListener("click", () => {
4187
+ this.dispatchEvent(
4188
+ new CustomEvent("preview-selection", {
4189
+ detail: { ssml: (0, import_ssml_core2.buildSsml)({ ...documentState, children: [selectedLeaf.value] }) },
4190
+ bubbles: true,
4191
+ composed: true
4192
+ })
4193
+ );
4194
+ });
4195
+ actions.append(preview);
4196
+ form.append(actions);
4197
+ } else {
4198
+ form.textContent = "Select an element or text node to edit it.";
4199
+ }
4200
+ layout.append(form);
4201
+ container.append(layout);
4202
+ }
3842
4203
  updateActiveButtons() {
3843
4204
  const editor = this.editor;
3844
4205
  const model = this.model;
@@ -3866,6 +4227,7 @@ var SsmlEditorElement = class extends HTMLElementBase {
3866
4227
  return;
3867
4228
  }
3868
4229
  const model = monacoModule.editor.createModel(this.prepareDocument(this.value), "xml");
4230
+ this.renderVisualEditor();
3869
4231
  const editor = monacoModule.editor.create(container, {
3870
4232
  model,
3871
4233
  theme: this.theme,
@@ -3964,7 +4326,8 @@ SsmlEditorElement.observedAttributes = [
3964
4326
  "locale",
3965
4327
  "show-toolbar",
3966
4328
  "show-toolbar-labels",
3967
- "show-decorations"
4329
+ "show-decorations",
4330
+ "edit-mode"
3968
4331
  ];
3969
4332
 
3970
4333
  // src/index.ts