From 0d79a1f443eae3e8f31f989d3adba200b84435f1 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 10 Mar 2025 10:57:23 +0100 Subject: [PATCH 1/6] Fixed issue with max validation using default 0 value. (#18616) --- .../PropertyEditors/SliderPropertyEditor.cs | 3 +-- .../PropertyEditors/SliderValueEditorTests.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs index 5277a2f552..02e96d075a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs @@ -10,7 +10,6 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors.Validation; -using Umbraco.Cms.Core.PropertyEditors.Validators; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; @@ -261,7 +260,7 @@ public class SliderPropertyEditor : DataEditor ["value"]); } - if (sliderRange.To > sliderConfiguration.MaximumValue) + if (sliderConfiguration.MaximumValue != 0 && sliderRange.To > sliderConfiguration.MaximumValue) { yield return new ValidationResult( LocalizedTextService.Localize("validation", "outOfRangeMaximum", [sliderRange.To.ToString(), sliderConfiguration.MaximumValue.ToString()]), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs index e417eb5baa..14bbb99601 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs @@ -205,6 +205,21 @@ public class SliderValueEditorTests } } + [Test] + public void Max_Item_Validation_Respects_0_As_Unlimited() + { + var value = new JsonObject + { + { "from", 1.0m }, + { "to", 1.0m }, + }; + var editor = CreateValueEditor(); + editor.ConfigurationObject = new SliderConfiguration(); + + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + Assert.IsEmpty(result); + } + [TestCase(0.2, 1.3, 1.7, true)] [TestCase(0.2, 1.4, 1.7, false)] [TestCase(0.2, 1.3, 1.6, false)] From 4fe168f5402f030918155656d476dc8e1a7d3a6c Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 10 Mar 2025 11:19:40 +0100 Subject: [PATCH 2/6] Removed obsolete integration test. (#18604) --- .../CompatibilitySuppressions.xml | 7 +++++++ .../Services/ContentPublishingServiceTests.Publish.cs | 6 ------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml b/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml index 9241ec135e..7c6ea3140b 100644 --- a/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml +++ b/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml @@ -64,6 +64,13 @@ lib/net9.0/Umbraco.Tests.Integration.dll true + + CP0002 + M:Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.ContentPublishingServiceTests.Publish_Branch_Does_Not_Publish_Unpublished_Children_Unless_Explicitly_Instructed_To(System.Boolean) + lib/net9.0/Umbraco.Tests.Integration.dll + lib/net9.0/Umbraco.Tests.Integration.dll + true + CP0002 M:Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.TemplateServiceTests.Deleting_Master_Template_Also_Deletes_Children diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs index db0bf3658d..a8abd95f98 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs @@ -42,12 +42,6 @@ public partial class ContentPublishingServiceTests VerifyIsPublished(Subpage.Key); } - [Obsolete("Replaced by Publish_Branch_Does_Not_Publish_Unpublished_Children_Unless_Instructed_To. This will be removed in Umbraco 16.")] - public Task Publish_Branch_Does_Not_Publish_Unpublished_Children_Unless_Explicitly_Instructed_To(bool force) - { - return Task.CompletedTask; - } - [TestCase(PublishBranchFilter.Default)] [TestCase(PublishBranchFilter.IncludeUnpublished)] [TestCase(PublishBranchFilter.ForceRepublish)] From 7ef11621dd8e4197f45d83cff38c8d0b425415f1 Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Mon, 10 Mar 2025 12:40:58 +0000 Subject: [PATCH 3/6] Tiptap RTE: Character Map modal + toolbar item (#18577) * Tiptap: Character Map extension * Added missing icon! :facepalm: * Added missing manifests for the CharMap modal * CharMap dialog styles * Scoped the Character Map modal to Tiptap package Exported modal alias constant. --- .../src/assets/lang/en.ts | 9 + .../mocks/data/data-type/data-type.data.ts | 7 +- .../core/icon-registry/icon-dictionary.json | 4 + .../src/packages/core/icon-registry/icons.ts | 3 + .../core/icon-registry/icons/icon-omega.ts | 1 + .../character-map-modal.element.ts | 472 ++++++++++++++++++ .../character-map-modal.token.ts | 13 + .../components/character-map/constants.ts | 1 + .../tiptap/components/character-map/index.ts | 1 + .../components/character-map/manifests.ts | 11 + .../src/packages/tiptap/components/index.ts | 1 + .../packages/tiptap/components/manifests.ts | 5 + .../src/packages/tiptap/constants.ts | 1 + .../packages/tiptap/extensions/manifests.ts | 12 + .../character-map.tiptap-toolbar-api.ts | 20 + .../src/packages/tiptap/manifests.ts | 7 +- .../tiptap-toolbar-configuration.context.ts | 2 +- 17 files changed, 567 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-omega.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/character-map-modal.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/character-map-modal.token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/components/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/character-map.tiptap-toolbar-api.ts diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index c423039d7a..1428b961d7 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2756,6 +2756,15 @@ export default { toolbar_removeItem: 'Remove action', toolbar_emptyGroup: 'Empty', sourceCodeEdit: 'Edit source code', + charmap: 'Character map', + charmap_headline: 'Special character', + charmap_currency: 'Currency', + charmap_text: 'Text', + charmap_quotations: 'Quotations', + charmap_maths: 'Mathematical', + charmap_extlatin: 'Extended Latin', + charmap_symbols: 'Symbols', + charmap_arrows: 'Arrows', }, linkPicker: { modalSource: 'Source', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 2b858b9d23..3cf3fb770b 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -1063,7 +1063,12 @@ export const data: Array = [ 'Umb.Tiptap.Toolbar.TextAlignCenter', 'Umb.Tiptap.Toolbar.TextAlignRight', ], - ['Umb.Tiptap.Toolbar.TextDirectionRtl', 'Umb.Tiptap.Toolbar.TextDirectionLtr'], + ['Umb.Tiptap.Toolbar.Subscript', 'Umb.Tiptap.Toolbar.Superscript'], + [ + 'Umb.Tiptap.Toolbar.CharacterMap', + 'Umb.Tiptap.Toolbar.TextDirectionRtl', + 'Umb.Tiptap.Toolbar.TextDirectionLtr', + ], [ 'Umb.Tiptap.Toolbar.BulletList', 'Umb.Tiptap.Toolbar.OrderedList', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json index be201e346b..fec3dac7ac 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json @@ -1387,6 +1387,10 @@ "file": "phone.svg", "legacy": true }, + { + "name": "icon-omega", + "file": "omega.svg" + }, { "name": "icon-operator", "file": "user-cog.svg" diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons.ts index ab93598d5f..4302b267ac 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons.ts @@ -1123,6 +1123,9 @@ legacy: true, hidden: true, path: () => import("./icons/icon-old-phone.js"), },{ +name: "icon-omega", +path: () => import("./icons/icon-omega.js"), +},{ name: "icon-operator", path: () => import("./icons/icon-operator.js"), },{ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-omega.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-omega.ts new file mode 100644 index 0000000000..1579666da6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-omega.ts @@ -0,0 +1 @@ +export default ``; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/character-map-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/character-map-modal.element.ts new file mode 100644 index 0000000000..4056251ee2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/character-map-modal.element.ts @@ -0,0 +1,472 @@ +import type { UmbTiptapCharacterMapModalData, UmbTiptapCharacterMapModalValue } from './character-map-modal.token.js'; +import { css, customElement, html, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; + +@customElement('umb-character-map-modal') +export class UmbCharacterMapModalElement extends UmbModalBaseElement< + UmbTiptapCharacterMapModalData, + UmbTiptapCharacterMapModalValue +> { + /* The character mapping code has been derived from TinyMCE. + * https://github.com/tinymce/tinymce/blob/6.8.5/modules/tinymce/src/plugins/charmap/main/ts/core/CharMap.ts#L20-L362 + * SPDX-License-Identifier: MIT + * Copyright © 2022 Ephox Corporation DBA Tiny Technologies, Inc. + * Modifications are licensed under the MIT License. */ + #characterMap: Record> = { + '#general_all': [], + '#tiptap_charmap_currency': [ + [36, 'dollar sign'], + [162, 'cent sign'], + [8364, 'euro sign'], + [163, 'pound sign'], + [165, 'yen sign'], + [164, 'currency sign'], + [8352, 'euro-currency sign'], + [8353, 'colon sign'], + [8354, 'cruzeiro sign'], + [8355, 'french franc sign'], + [8356, 'lira sign'], + [8357, 'mill sign'], + [8358, 'naira sign'], + [8359, 'peseta sign'], + [8360, 'rupee sign'], + [8361, 'won sign'], + [8362, 'new sheqel sign'], + [8363, 'dong sign'], + [8365, 'kip sign'], + [8366, 'tugrik sign'], + [8367, 'drachma sign'], + [8368, 'german penny symbol'], + [8369, 'peso sign'], + [8370, 'guarani sign'], + [8371, 'austral sign'], + [8372, 'hryvnia sign'], + [8373, 'cedi sign'], + [8374, 'livre tournois sign'], + [8375, 'spesmilo sign'], + [8376, 'tenge sign'], + [8377, 'indian rupee sign'], + [8378, 'turkish lira sign'], + [8379, 'nordic mark sign'], + [8380, 'manat sign'], + [8381, 'ruble sign'], + [20870, 'yen character'], + [20803, 'yuan character'], + [22291, 'yuan character, in hong kong and taiwan'], + [22278, 'yen/yuan character variant one'], + ], + '#tiptap_charmap_text': [ + [169, 'copyright sign'], + [174, 'registered sign'], + [8482, 'trade mark sign'], + [8240, 'per mille sign'], + [181, 'micro sign'], + [183, 'middle dot'], + [8226, 'bullet'], + [8230, 'three dot leader'], + [8242, 'minutes / feet'], + [8243, 'seconds / inches'], + [167, 'section sign'], + [182, 'paragraph sign'], + [223, 'sharp s / ess-zed'], + ], + '#tiptap_charmap_quotations': [ + [8249, 'single left-pointing angle quotation mark'], + [8250, 'single right-pointing angle quotation mark'], + [171, 'left pointing guillemet'], + [187, 'right pointing guillemet'], + [8216, 'left single quotation mark'], + [8217, 'right single quotation mark'], + [8220, 'left double quotation mark'], + [8221, 'right double quotation mark'], + [8218, 'single low-9 quotation mark'], + [8222, 'double low-9 quotation mark'], + [60, 'less-than sign'], + [62, 'greater-than sign'], + [8804, 'less-than or equal to'], + [8805, 'greater-than or equal to'], + [8211, 'en dash'], + [8212, 'em dash'], + [175, 'macron'], + [8254, 'overline'], + [164, 'currency sign'], + [166, 'broken bar'], + [168, 'diaeresis'], + [161, 'inverted exclamation mark'], + [191, 'turned question mark'], + [710, 'circumflex accent'], + [732, 'small tilde'], + [176, 'degree sign'], + [8722, 'minus sign'], + [177, 'plus-minus sign'], + [247, 'division sign'], + [8260, 'fraction slash'], + [215, 'multiplication sign'], + [185, 'superscript one'], + [178, 'superscript two'], + [179, 'superscript three'], + [188, 'fraction one quarter'], + [189, 'fraction one half'], + [190, 'fraction three quarters'], + ], + '#tiptap_charmap_maths': [ + [402, 'function / florin'], + [8747, 'integral'], + [8721, 'n-ary sumation'], + [8734, 'infinity'], + [8730, 'square root'], + [8764, 'similar to'], + [8773, 'approximately equal to'], + [8776, 'almost equal to'], + [8800, 'not equal to'], + [8801, 'identical to'], + [8712, 'element of'], + [8713, 'not an element of'], + [8715, 'contains as member'], + [8719, 'n-ary product'], + [8743, 'logical and'], + [8744, 'logical or'], + [172, 'not sign'], + [8745, 'intersection'], + [8746, 'union'], + [8706, 'partial differential'], + [8704, 'for all'], + [8707, 'there exists'], + [8709, 'diameter'], + [8711, 'backward difference'], + [8727, 'asterisk operator'], + [8733, 'proportional to'], + [8736, 'angle'], + ], + '#tiptap_charmap_extlatin': [ + [192, 'A - grave'], + [193, 'A - acute'], + [194, 'A - circumflex'], + [195, 'A - tilde'], + [196, 'A - diaeresis'], + [197, 'A - ring above'], + [256, 'A - macron'], + [198, 'ligature AE'], + [199, 'C - cedilla'], + [200, 'E - grave'], + [201, 'E - acute'], + [202, 'E - circumflex'], + [203, 'E - diaeresis'], + [274, 'E - macron'], + [204, 'I - grave'], + [205, 'I - acute'], + [206, 'I - circumflex'], + [207, 'I - diaeresis'], + [298, 'I - macron'], + [208, 'ETH'], + [209, 'N - tilde'], + [210, 'O - grave'], + [211, 'O - acute'], + [212, 'O - circumflex'], + [213, 'O - tilde'], + [214, 'O - diaeresis'], + [216, 'O - slash'], + [332, 'O - macron'], + [338, 'ligature OE'], + [352, 'S - caron'], + [217, 'U - grave'], + [218, 'U - acute'], + [219, 'U - circumflex'], + [220, 'U - diaeresis'], + [362, 'U - macron'], + [221, 'Y - acute'], + [376, 'Y - diaeresis'], + [562, 'Y - macron'], + [222, 'THORN'], + [224, 'a - grave'], + [225, 'a - acute'], + [226, 'a - circumflex'], + [227, 'a - tilde'], + [228, 'a - diaeresis'], + [229, 'a - ring above'], + [257, 'a - macron'], + [230, 'ligature ae'], + [231, 'c - cedilla'], + [232, 'e - grave'], + [233, 'e - acute'], + [234, 'e - circumflex'], + [235, 'e - diaeresis'], + [275, 'e - macron'], + [236, 'i - grave'], + [237, 'i - acute'], + [238, 'i - circumflex'], + [239, 'i - diaeresis'], + [299, 'i - macron'], + [240, 'eth'], + [241, 'n - tilde'], + [242, 'o - grave'], + [243, 'o - acute'], + [244, 'o - circumflex'], + [245, 'o - tilde'], + [246, 'o - diaeresis'], + [248, 'o slash'], + [333, 'o macron'], + [339, 'ligature oe'], + [353, 's - caron'], + [249, 'u - grave'], + [250, 'u - acute'], + [251, 'u - circumflex'], + [252, 'u - diaeresis'], + [363, 'u - macron'], + [253, 'y - acute'], + [254, 'thorn'], + [255, 'y - diaeresis'], + [563, 'y - macron'], + [913, 'Alpha'], + [914, 'Beta'], + [915, 'Gamma'], + [916, 'Delta'], + [917, 'Epsilon'], + [918, 'Zeta'], + [919, 'Eta'], + [920, 'Theta'], + [921, 'Iota'], + [922, 'Kappa'], + [923, 'Lambda'], + [924, 'Mu'], + [925, 'Nu'], + [926, 'Xi'], + [927, 'Omicron'], + [928, 'Pi'], + [929, 'Rho'], + [931, 'Sigma'], + [932, 'Tau'], + [933, 'Upsilon'], + [934, 'Phi'], + [935, 'Chi'], + [936, 'Psi'], + [937, 'Omega'], + [945, 'alpha'], + [946, 'beta'], + [947, 'gamma'], + [948, 'delta'], + [949, 'epsilon'], + [950, 'zeta'], + [951, 'eta'], + [952, 'theta'], + [953, 'iota'], + [954, 'kappa'], + [955, 'lambda'], + [956, 'mu'], + [957, 'nu'], + [958, 'xi'], + [959, 'omicron'], + [960, 'pi'], + [961, 'rho'], + [962, 'final sigma'], + [963, 'sigma'], + [964, 'tau'], + [965, 'upsilon'], + [966, 'phi'], + [967, 'chi'], + [968, 'psi'], + [969, 'omega'], + ], + '#tiptap_charmap_symbols': [ + [8501, 'alef symbol'], + [982, 'pi symbol'], + [8476, 'real part symbol'], + [978, 'upsilon - hook symbol'], + [8472, 'Weierstrass p'], + [8465, 'imaginary part'], + ], + '#tiptap_charmap_arrows': [ + [8592, 'leftwards arrow'], + [8593, 'upwards arrow'], + [8594, 'rightwards arrow'], + [8595, 'downwards arrow'], + [8596, 'left right arrow'], + [8629, 'carriage return'], + [8656, 'leftwards double arrow'], + [8657, 'upwards double arrow'], + [8658, 'rightwards double arrow'], + [8659, 'downwards double arrow'], + [8660, 'left right double arrow'], + [8756, 'therefore'], + [8834, 'subset of'], + [8835, 'superset of'], + [8836, 'not a subset of'], + [8838, 'subset of or equal to'], + [8839, 'superset of or equal to'], + [8853, 'circled plus'], + [8855, 'circled times'], + [8869, 'perpendicular'], + [8901, 'dot operator'], + [8968, 'left ceiling'], + [8969, 'right ceiling'], + [8970, 'left floor'], + [8971, 'right floor'], + [9001, 'left-pointing angle bracket'], + [9002, 'right-pointing angle bracket'], + [9674, 'lozenge'], + [9824, 'black spade suit'], + [9827, 'black club suit'], + [9829, 'black heart suit'], + [9830, 'black diamond suit'], + [8194, 'en space'], + [8195, 'em space'], + [8201, 'thin space'], + [8204, 'zero width non-joiner'], + [8205, 'zero width joiner'], + [8206, 'left-to-right mark'], + [8207, 'right-to-left mark'], + ], + }; + + @state() + private _filterQuery = ''; + + @state() + private _hoverLabel: string = ''; + + @state() + private _selectedGroup: string = '#general_all'; + + #onClickCharacter(code: number) { + this.value = String.fromCharCode(code); + this._submitModal(); + } + + #onFilterInput(event: InputEvent & { target: HTMLInputElement }) { + this._filterQuery = (event.target.value ?? '').toLocaleLowerCase(); + } + + override render() { + return html` + + ${this.#renderCharacterMap()} + + + `; + } + + #filterHandler = ([, label]: [number, string]) => + !this._filterQuery || label.toLocaleLowerCase().includes(this._filterQuery); + + #renderCharacterMap() { + const characters = + this._selectedGroup && this._selectedGroup !== '#general_all' + ? this.#characterMap[this._selectedGroup].filter(this.#filterHandler) + : Object.values(this.#characterMap).flat().filter(this.#filterHandler); + return html` +
+
+ ${repeat( + Object.keys(this.#characterMap), + (group) => group, + (group) => html` + (this._selectedGroup = group)}> + `, + )} +
+
+ +
+ +
+
+ + ${when( + characters?.length, + () => html` +
+ ${repeat( + characters, + ([code]) => code, + ([code, label]) => html` + this.#onClickCharacter(code)} + @mouseover=${() => (this._hoverLabel = label)} + @mouseleave=${() => (this._hoverLabel = '')}> + ${String.fromCharCode(code)} + + `, + )} +
+ `, + () => html`

There are no items to show

`, + )} +
+
+
+
${this._hoverLabel}
+ `; + } + + static override styles = [ + css` + :host { + --umb-body-layout-color-background: var(--uui-color-surface); + --uui-menu-item-flat-structure: 1; + } + + #container { + display: grid; + grid-template-columns: var(--uui-size-48) 1fr; + gap: var(--uui-size-layout-1); + } + + #main { + display: flex; + flex-direction: column; + gap: var(--uui-size-space-4); + } + + uui-scroll-container { + height: 300px; + width: calc(450px + var(--uui-size-layout-1)); + } + + #characters { + display: grid; + grid-template-columns: repeat(auto-fill, var(--uui-size-14)); + gap: var(--uui-size-5); + padding: var(--uui-size-1); + + uui-button { + --uui-button-font-weight: normal; + + border-radius: var(--uui-border-radius); + font-size: 1.5rem; + + &:focus, + &:hover { + outline: 2px solid var(--uui-color-selected); + } + } + } + + div[slot='footer-info'] { + margin-left: var(--uui-size-layout-1); + text-transform: capitalize; + } + `, + ]; +} + +export { UmbCharacterMapModalElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-character-map-modal': UmbCharacterMapModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/character-map-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/character-map-modal.token.ts new file mode 100644 index 0000000000..402ac2a824 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/character-map-modal.token.ts @@ -0,0 +1,13 @@ +import { UMB_TIPTAP_CHARACTER_MAP_MODAL_ALIAS } from './constants.js'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export type UmbTiptapCharacterMapModalData = never; + +export type UmbTiptapCharacterMapModalValue = string; + +export const UMB_TIPTAP_CHARACTER_MAP_MODAL = new UmbModalToken< + UmbTiptapCharacterMapModalData, + UmbTiptapCharacterMapModalValue +>(UMB_TIPTAP_CHARACTER_MAP_MODAL_ALIAS, { + modal: { type: 'dialog' }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/constants.ts new file mode 100644 index 0000000000..391d3b83ec --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/constants.ts @@ -0,0 +1 @@ +export const UMB_TIPTAP_CHARACTER_MAP_MODAL_ALIAS = 'Umb.Modal.Tiptap.CharacterMap'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/index.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/index.ts new file mode 100644 index 0000000000..49e9efcc4b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/index.ts @@ -0,0 +1 @@ +export * from './character-map-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/manifests.ts new file mode 100644 index 0000000000..a238e84eb3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/manifests.ts @@ -0,0 +1,11 @@ +import { UMB_TIPTAP_CHARACTER_MAP_MODAL_ALIAS } from './constants.js'; +import type { ManifestModal } from '@umbraco-cms/backoffice/modal'; + +export const manifests: Array = [ + { + type: 'modal', + alias: UMB_TIPTAP_CHARACTER_MAP_MODAL_ALIAS, + name: 'Character Map Modal', + element: () => import('./character-map-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts index edb0192afd..8f4aa2d10d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts @@ -1,2 +1,3 @@ export * from './input-tiptap/index.js'; export * from './cascading-menu-popover/cascading-menu-popover.element.js'; +export * from './character-map/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/manifests.ts new file mode 100644 index 0000000000..99bde2c055 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/manifests.ts @@ -0,0 +1,5 @@ +import { manifests as characterMap } from './character-map/manifests.js'; + +export const manifests: Array = [ + ...characterMap, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/constants.ts index a818f164e9..12354824c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/constants.ts @@ -1 +1,2 @@ +export * from './components/character-map/constants.js'; export * from './property-editors/tiptap/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts index 4042511918..28fadd642c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts @@ -582,6 +582,18 @@ const toolbarExtensions: Array = [ ], }, }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.CharacterMap', + name: 'Character Map Tiptap Extension', + api: () => import('./toolbar/character-map.tiptap-toolbar-api.js'), + meta: { + alias: 'umbCharacterMap', + icon: 'icon-omega', + label: '#tiptap_charmap', + }, + }, ]; const extensions = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/character-map.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/character-map.tiptap-toolbar-api.ts new file mode 100644 index 0000000000..ef239b3727 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/character-map.tiptap-toolbar-api.ts @@ -0,0 +1,20 @@ +import { UmbTiptapToolbarElementApiBase } from '../base.js'; +import { UMB_TIPTAP_CHARACTER_MAP_MODAL } from '../../components/character-map/index.js'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarCharacterMapExtensionApi extends UmbTiptapToolbarElementApiBase { + override async execute(editor?: Editor) { + if (!editor) return; + + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const modal = modalManager.open(this, UMB_TIPTAP_CHARACTER_MAP_MODAL); + + if (!modal) return; + + const data = await modal.onSubmit().catch(() => undefined); + if (!data) return; + + editor?.chain().focus().insertContent(data).run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/manifests.ts index 9325da5e4f..18d1160740 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/manifests.ts @@ -1,5 +1,10 @@ +import { manifests as components } from './components/manifests.js'; import { manifests as extensions } from './extensions/manifests.js'; import { manifests as propertyEditors } from './property-editors/manifests.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; -export const manifests: Array = [...extensions, ...propertyEditors]; +export const manifests: Array = [ + ...components, + ...extensions, + ...propertyEditors, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts index 93d78d4208..7c20d0f49b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts @@ -61,7 +61,7 @@ export class UmbTiptapToolbarConfigurationContext extends UmbContextBase Date: Mon, 10 Mar 2025 13:22:57 +0000 Subject: [PATCH 4/6] Tiptap RTE: Anchor extension + toolbar item (#18575) * Tiptap: Anchor extension * Corrected import; fixed the build * Removed icon, mistakenly added to this branch it was intended for the Character Map feature. * Updated dialog style * Scoped the Anchor modal to Tiptap package Exported modal alias constant. * Tweaked selected Anchor styling * Localizes public labels * Exported Anchor constants correctly --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- .../src/assets/lang/en.ts | 2 + .../extensions/tiptap-anchor.extension.ts | 54 ++++++++++++ .../src/external/tiptap/index.ts | 1 + .../mocks/data/data-type/data-type.data.ts | 2 +- .../src/mocks/data/document/document.data.ts | 2 +- .../anchor-modal/anchor-modal.element.ts | 84 +++++++++++++++++++ .../anchor-modal/anchor-modal.token.ts | 15 ++++ .../components/anchor-modal/constants.ts | 1 + .../tiptap/components/anchor-modal/index.ts | 1 + .../components/anchor-modal/manifests.ts | 11 +++ .../src/packages/tiptap/components/index.ts | 1 + .../packages/tiptap/components/manifests.ts | 2 + .../src/packages/tiptap/constants.ts | 1 + .../core/rich-text-essentials.tiptap-api.ts | 23 ++++- .../packages/tiptap/extensions/manifests.ts | 12 +++ .../toolbar/anchor.tiptap-toolbar-api.ts | 24 ++++++ .../tiptap-toolbar-configuration.context.ts | 2 +- 17 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-anchor.extension.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/anchor-modal.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/anchor-modal.token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/anchor.tiptap-toolbar-api.ts diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 1428b961d7..def637a8b3 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2737,6 +2737,8 @@ export default { config_overlaySize_description: 'Select the width of the overlay (link picker).', }, tiptap: { + anchor: 'Anchor', + anchor_input: 'Enter an anchor ID', config_dimensions_description: 'Set the maximum width and height of the editor. This excludes the toolbar height.', config_extensions: 'Capabilities', config_toolbar: 'Toolbar', diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-anchor.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-anchor.extension.ts new file mode 100644 index 0000000000..0638768517 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-anchor.extension.ts @@ -0,0 +1,54 @@ +import { Node, mergeAttributes } from '@tiptap/core'; + +export const Anchor = Node.create({ + name: 'anchor', + + atom: true, + draggable: true, + inline: true, + group: 'inline', + marks: '', + selectable: true, + + addAttributes() { + return { + id: {}, + }; + }, + + addNodeView() { + return ({ HTMLAttributes }) => { + const dom = document.createElement('span'); + dom.setAttribute('data-umb-anchor', ''); + dom.setAttribute('title', HTMLAttributes.id); + + const icon = document.createElement('uui-icon'); + icon.setAttribute('name', 'icon-anchor'); + + dom.appendChild(icon); + + return { dom }; + }; + }, + + addOptions() { + return { + HTMLAttributes: { + id: 'id', + }, + }; + }, + + parseHTML() { + return [ + { + tag: 'a[id]', + getAttrs: (element) => (element.innerHTML === '' ? {} : false), + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts index f85780ae9d..af5bf8ba09 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts @@ -28,6 +28,7 @@ export { TextAlign } from '@tiptap/extension-text-align'; export { Underline } from '@tiptap/extension-underline'; // CUSTOM EXTENSIONS +export * from './extensions/tiptap-anchor.extension.js'; export * from './extensions/tiptap-div.extension.js'; export * from './extensions/tiptap-figcaption.extension.js'; export * from './extensions/tiptap-figure.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 3cf3fb770b..79ef5e95d9 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -1075,7 +1075,7 @@ export const data: Array = [ 'Umb.Tiptap.Toolbar.Blockquote', 'Umb.Tiptap.Toolbar.HorizontalRule', ], - ['Umb.Tiptap.Toolbar.Link', 'Umb.Tiptap.Toolbar.Unlink'], + ['Umb.Tiptap.Toolbar.Anchor', 'Umb.Tiptap.Toolbar.Link', 'Umb.Tiptap.Toolbar.Unlink'], ['Umb.Tiptap.Toolbar.Table', 'Umb.Tiptap.Toolbar.MediaPicker', 'Umb.Tiptap.Toolbar.EmbeddedMedia'], ], ], diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts index 98ea26928f..45be5334cf 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts @@ -911,10 +911,10 @@ export const data: Array = [ value: { blocks: undefined, markup: ` +

Here is a link for all HTML tags.

Some value for the RTE with an external link and an internal link.

-

All HTML tags

This is a plain old span tag. Hello world. diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/anchor-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/anchor-modal.element.ts new file mode 100644 index 0000000000..e86a99a458 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/anchor-modal.element.ts @@ -0,0 +1,84 @@ +import type { UmbTiptapAnchorModalData, UmbTiptapAnchorModalValue } from './anchor-modal.token.js'; +import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; +import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; + +@customElement('umb-tiptap-anchor-modal') +export class UmbTiptapAnchorModalElement extends UmbModalBaseElement< + UmbTiptapAnchorModalData, + UmbTiptapAnchorModalValue +> { + async #onSubmit(event: SubmitEvent) { + event.preventDefault(); + + const form = event.target as HTMLFormElement; + if (!form) return; + + const isValid = form.checkValidity(); + if (!isValid) return; + + const formData = new FormData(form); + const name = formData.get('name') as string; + + this.value = name; + this._submitModal(); + } + + override render() { + const label = this.localize.term('tiptap_anchor_input'); + return html` + + +
+ + ${label} + + +
+
+ + +
+ `; + } + + static override styles = [ + css` + :host { + --umb-body-layout-color-background: var(--uui-color-surface); + } + + uui-dialog-layout { + width: var(--uui-size-100); + } + + uui-input { + width: 100%; + } + `, + ]; +} + +export { UmbTiptapAnchorModalElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-tiptap-anchor-modal': UmbTiptapAnchorModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/anchor-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/anchor-modal.token.ts new file mode 100644 index 0000000000..68e763d376 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/anchor-modal.token.ts @@ -0,0 +1,15 @@ +import { UMB_TIPTAP_ANCHOR_MODAL_ALIAS } from './constants.js'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export type UmbTiptapAnchorModalData = { + id?: string; +}; + +export type UmbTiptapAnchorModalValue = string; + +export const UMB_TIPTAP_ANCHOR_MODAL = new UmbModalToken( + UMB_TIPTAP_ANCHOR_MODAL_ALIAS, + { + modal: { type: 'dialog' }, + }, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/constants.ts new file mode 100644 index 0000000000..16efc1e726 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/constants.ts @@ -0,0 +1 @@ +export const UMB_TIPTAP_ANCHOR_MODAL_ALIAS = 'Umb.Modal.Tiptap.Anchor'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/index.ts new file mode 100644 index 0000000000..2bdc2fe35e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/index.ts @@ -0,0 +1 @@ +export * from './anchor-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/manifests.ts new file mode 100644 index 0000000000..99bbb4a2b5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/manifests.ts @@ -0,0 +1,11 @@ +import { UMB_TIPTAP_ANCHOR_MODAL_ALIAS } from './constants.js'; +import type { ManifestModal } from '@umbraco-cms/backoffice/modal'; + +export const manifests: Array = [ + { + type: 'modal', + alias: UMB_TIPTAP_ANCHOR_MODAL_ALIAS, + name: 'Tiptap Anchor Modal', + element: () => import('./anchor-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts index 8f4aa2d10d..3f50b33fda 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts @@ -1,3 +1,4 @@ export * from './input-tiptap/index.js'; +export * from './anchor-modal/index.js'; export * from './cascading-menu-popover/cascading-menu-popover.element.js'; export * from './character-map/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/manifests.ts index 99bde2c055..a81acb72c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/manifests.ts @@ -1,5 +1,7 @@ +import { manifests as anchorModal } from './anchor-modal/manifests.js'; import { manifests as characterMap } from './character-map/manifests.js'; export const manifests: Array = [ + ...anchorModal, ...characterMap, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/constants.ts index 12354824c4..8613d3f9b4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/constants.ts @@ -1,2 +1,3 @@ +export * from './components/anchor-modal/constants.js'; export * from './components/character-map/constants.js'; export * from './property-editors/tiptap/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts index e51609936d..9a3e2fb22b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts @@ -1,10 +1,18 @@ import { UmbTiptapExtensionApiBase } from '../base.js'; import { css } from '@umbraco-cms/backoffice/external/lit'; -import { Div, HtmlGlobalAttributes, Span, StarterKit, TrailingNode } from '@umbraco-cms/backoffice/external/tiptap'; +import { + Anchor, + Div, + HtmlGlobalAttributes, + Span, + StarterKit, + TrailingNode, +} from '@umbraco-cms/backoffice/external/tiptap'; export class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionApiBase { getTiptapExtensions = () => [ StarterKit, + Anchor, Div, Span, HtmlGlobalAttributes.configure({ @@ -76,6 +84,19 @@ export class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionA padding: 0; } } + + span[data-umb-anchor] { + &.ProseMirror-selectednode { + border-radius: var(--uui-border-radius); + outline: 2px solid var(--uui-color-selected); + } + + uui-icon { + height: 1rem; + width: 1rem; + vertical-align: text-bottom; + } + } `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts index 28fadd642c..b7584dec6a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts @@ -399,6 +399,18 @@ const toolbarExtensions: Array = [ label: 'Ordered List', }, }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Anchor', + name: 'Anchor Tiptap Extension', + api: () => import('./toolbar/anchor.tiptap-toolbar-api.js'), + meta: { + alias: 'anchor', + icon: 'icon-anchor', + label: '#tiptap_anchor', + }, + }, { type: 'tiptapToolbarExtension', kind: 'button', diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/anchor.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/anchor.tiptap-toolbar-api.ts new file mode 100644 index 0000000000..42a8d5f96a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/anchor.tiptap-toolbar-api.ts @@ -0,0 +1,24 @@ +import { UmbTiptapToolbarElementApiBase } from '../base.js'; +import { UMB_TIPTAP_ANCHOR_MODAL } from '../../components/anchor-modal/index.js'; +import { Anchor } from '@umbraco-cms/backoffice/external/tiptap'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarAnchorExtensionApi extends UmbTiptapToolbarElementApiBase { + override async execute(editor?: Editor) { + const attrs = editor?.getAttributes(Anchor.name); + if (!attrs) return; + + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const modal = modalManager.open(this, UMB_TIPTAP_ANCHOR_MODAL, { data: { id: attrs?.id } }); + if (!modal) return; + + const result = await modal.onSubmit().catch(() => undefined); + if (!result) return; + + editor + ?.chain() + .insertContent({ type: Anchor.name, attrs: { id: result } }) + .run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts index 7c20d0f49b..2b8b19e043 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts @@ -56,7 +56,7 @@ export class UmbTiptapToolbarConfigurationContext extends UmbContextBase Date: Mon, 10 Mar 2025 14:50:28 +0100 Subject: [PATCH 5/6] Added description to block list properties hide content editor (#18619) * Added description to block list properties hide content editor * Update the description to use localization --------- Co-authored-by: Oskar kruger --- .../workspace/views/block-list-type-workspace-view.element.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/workspace/views/block-list-type-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/workspace/views/block-list-type-workspace-view.element.ts index 989d3d7a57..28d8241111 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/workspace/views/block-list-type-workspace-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/workspace/views/block-list-type-workspace-view.element.ts @@ -75,6 +75,7 @@ export class UmbBlockListTypeWorkspaceViewSettingsElement extends UmbLitElement `; From bc488d78e429ae4cfa9caba01529c17694bb0a32 Mon Sep 17 00:00:00 2001 From: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> Date: Mon, 10 Mar 2025 21:46:22 +0700 Subject: [PATCH 6/6] V15 QA Fixed the failing acceptance tests due to UI changes (#18617) * Fixed the failing tests * Bumped version * Fixed can allow bulk trash in the media section * Fixed notification message --- tests/Umbraco.Tests.AcceptanceTest/package-lock.json | 8 ++++---- tests/Umbraco.Tests.AcceptanceTest/package.json | 2 +- .../Content/ContentWithListViewContent.spec.ts | 12 ++++++------ .../tests/DefaultConfig/Media/ListViewMedia.spec.ts | 8 ++++---- .../tests/DefaultConfig/Media/Media.spec.ts | 7 +++---- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 4bc42e2d78..935ad9dc02 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.29", - "@umbraco/playwright-testhelpers": "^15.0.32", + "@umbraco/playwright-testhelpers": "^15.0.33", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -67,9 +67,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "15.0.32", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.32.tgz", - "integrity": "sha512-4wzLTtqbzIc0TokP+/nC/vbKfcboYQFGam6eLzZj4oMQmkBExxv5EBhI06qrpst8/rQc5OK4TTwJAGL3GCuKew==", + "version": "15.0.33", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.33.tgz", + "integrity": "sha512-EboW4KNFN5wG4UR8tsLWhjpQVZY0lkVNDbNFu9iohFE2bSfrV2CETcWAthVx8IwJja4nP3dOdwwMKb39/MUNdw==", "dependencies": { "@umbraco/json-models-builders": "2.0.30", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 71dd1e16d3..642f701cd6 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.29", - "@umbraco/playwright-testhelpers": "^15.0.32", + "@umbraco/playwright-testhelpers": "^15.0.33", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithListViewContent.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithListViewContent.spec.ts index 06e720c62d..b86bc575b2 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithListViewContent.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithListViewContent.spec.ts @@ -22,7 +22,8 @@ test.afterEach(async ({umbracoApi}) => { await umbracoApi.documentType.ensureNameNotExists(childDocumentTypeName); }); -test('can create content with the list view data type', async ({umbracoApi, umbracoUi}) => { +// Remove .fixme when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.fixme('can create content with the list view data type', async ({umbracoApi, umbracoUi}) => { // Arrange const expectedState = 'Draft'; const defaultListViewDataTypeName = 'List View - Content'; @@ -40,6 +41,7 @@ test('can create content with the list view data type', async ({umbracoApi, umbr // Assert await umbracoUi.content.isSuccessNotificationVisible(); + await umbracoUi.content.isErrorNotificationVisible(false); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.variants[0].state).toBe(expectedState); @@ -204,8 +206,7 @@ test('can publish child content from list', async ({umbracoApi, umbracoUi}) => { const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAPropertyEditorAndAnAllowedChildNode(documentTypeName, dataTypeName, dataTypeData.id, childDocumentTypeId); const documentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoApi.document.createDefaultDocumentWithParent(childContentName, childDocumentTypeId, documentId); - const publishData = {"publishSchedules": [{"culture": null}]}; - await umbracoApi.document.publish(documentId, publishData); + await umbracoApi.document.publish(documentId); await umbracoUi.content.goToSection(ConstantHelper.sections.content); await umbracoUi.content.goToContentWithName(contentName); @@ -251,9 +252,8 @@ test('can unpublish child content from list', async ({umbracoApi, umbracoUi}) => const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAPropertyEditorAndAnAllowedChildNode(documentTypeName, dataTypeName, dataTypeData.id, childDocumentTypeId); const documentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); const childDocumentId = await umbracoApi.document.createDefaultDocumentWithParent(childContentName, childDocumentTypeId, documentId); - const publishData = {"publishSchedules": [{"culture": null}]}; - await umbracoApi.document.publish(documentId, publishData); - await umbracoApi.document.publish(childDocumentId, publishData); + await umbracoApi.document.publish(documentId); + await umbracoApi.document.publish(childDocumentId); const childContentDataBeforeUnpublished = await umbracoApi.document.getByName(childContentName); expect(childContentDataBeforeUnpublished.variants[0].state).toBe('Published'); await umbracoUi.content.goToSection(ConstantHelper.sections.content); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/ListViewMedia.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/ListViewMedia.spec.ts index c55d98d240..bf0af84159 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/ListViewMedia.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/ListViewMedia.spec.ts @@ -1,5 +1,5 @@ import {expect} from '@playwright/test'; -import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; const dataTypeName = 'List View - Media'; let dataTypeDefaultData = null; @@ -105,7 +105,7 @@ test('can allow bulk trash in the media section', async ({umbracoApi, umbracoUi} await umbracoUi.media.clickConfirmTrashButton(); // Assert - await umbracoUi.media.reloadMediaTree(); + await umbracoUi.media.isSuccessNotificationVisible(); expect(await umbracoApi.media.doesNameExist(firstMediaFileName)).toBeFalsy(); expect(await umbracoApi.media.doesNameExist(secondMediaFileName)).toBeFalsy(); expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(firstMediaFileName)).toBeTruthy(); @@ -114,8 +114,8 @@ test('can allow bulk trash in the media section', async ({umbracoApi, umbracoUi} await umbracoUi.media.isItemVisibleInRecycleBin(secondMediaFileName, true, false); }); -// TODO: Remove skip when update code to select media successfully. -test.skip('can allow bulk move in the media section', async ({umbracoApi, umbracoUi}) => { +// TODO: Remove fixme when update code to select media successfully. +test.fixme('can allow bulk move in the media section', async ({umbracoApi, umbracoUi}) => { // Arrange const mediaFolderName = 'Test Folder Name'; await umbracoApi.media.ensureNameNotExists(mediaFolderName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts index a5ad42852d..8e7f878166 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts @@ -74,7 +74,6 @@ for (const mediaFileType of mediaFileTypes) { await umbracoUi.media.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created); const mediaData = await umbracoApi.media.getByName(mediaFileType.fileName); await umbracoUi.media.doesMediaHaveThumbnail(mediaData.id, mediaFileType.thumbnail, mediaData.urls[0].url); - await umbracoUi.media.reloadMediaTree(); await umbracoUi.media.isMediaTreeItemVisible(mediaFileType.fileName); expect(await umbracoApi.media.doesNameExist(mediaFileType.fileName)).toBeTruthy(); @@ -200,7 +199,7 @@ test('can restore a media item from the recycle bin', async ({umbracoApi, umbrac // Assert await umbracoUi.media.doesSuccessNotificationHaveText(NotificationConstantHelper.success.restored); - await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName, false); + await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName, false, false); await umbracoUi.media.reloadMediaTree(); await umbracoUi.media.isMediaTreeItemVisible(mediaFileName); expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeTruthy(); @@ -223,7 +222,7 @@ test('can delete a media item from the recycle bin', async ({umbracoApi, umbraco // Assert await umbracoUi.media.doesSuccessNotificationHaveText(NotificationConstantHelper.success.deleted); - await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName, false); + await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName, false, false); expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeFalsy(); expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(mediaFileName)).toBeFalsy(); }); @@ -241,7 +240,7 @@ test('can empty the recycle bin', async ({umbracoApi, umbracoUi}) => { await umbracoUi.media.clickConfirmEmptyRecycleBinButton(); // Assert - await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName, false); + await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName, false, false); await umbracoUi.media.doesSuccessNotificationHaveText(NotificationConstantHelper.success.emptiedRecycleBin); expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeFalsy(); expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(mediaFileName)).toBeFalsy();