diff --git a/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs index 55fa7b2c5f..274d7919b1 100644 --- a/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs @@ -8,14 +8,13 @@ namespace Umbraco.Cms.Core.Configuration.Models; public class RichTextEditorSettings { internal const string StaticValidElements = - "+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption"; + "+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption,video[*],audio[*],picture[*],source[*],canvas[*]"; internal const string StaticInvalidElements = "font"; private static readonly string[] Default_plugins = { - "paste", "anchor", "charmap", "table", "lists", "advlist", "hr", "autolink", "directionality", "tabfocus", - "searchreplace", + "anchor", "charmap", "table", "lists", "advlist", "autolink", "directionality", "searchreplace", }; private static readonly RichTextEditorCommand[] Default_commands = @@ -35,7 +34,7 @@ public class RichTextEditorSettings new RichTextEditorCommand { Alias = "paste", Name = "Paste", Mode = RichTextEditorCommandMode.All }, new RichTextEditorCommand { - Alias = "styleselect", Name = "Style select", Mode = RichTextEditorCommandMode.All, + Alias = "styles", Name = "Style select", Mode = RichTextEditorCommandMode.All, }, new RichTextEditorCommand { Alias = "bold", Name = "Bold", Mode = RichTextEditorCommandMode.Selection }, new RichTextEditorCommand { Alias = "italic", Name = "Italic", Mode = RichTextEditorCommandMode.Selection }, diff --git a/src/Umbraco.Core/Extensions/StringExtensions.cs b/src/Umbraco.Core/Extensions/StringExtensions.cs index 6842cc6681..a404daedf9 100644 --- a/src/Umbraco.Core/Extensions/StringExtensions.cs +++ b/src/Umbraco.Core/Extensions/StringExtensions.cs @@ -1326,8 +1326,6 @@ public static class StringExtensions /// /// /// - // From: http://stackoverflow.com/a/35046453/5018 - // Updated from .NET 2.1+: https://stackoverflow.com/a/58250915 public static bool IsFullPath(this string path) => Path.IsPathFullyQualified(path); // FORMAT STRINGS diff --git a/src/Umbraco.Infrastructure/WebAssets/Resources.Designer.cs b/src/Umbraco.Infrastructure/WebAssets/Resources.Designer.cs index 0923f23f4c..319a8b09ef 100644 --- a/src/Umbraco.Infrastructure/WebAssets/Resources.Designer.cs +++ b/src/Umbraco.Infrastructure/WebAssets/Resources.Designer.cs @@ -152,13 +152,11 @@ namespace Umbraco.Cms.Infrastructure.WebAssets { /// Looks up a localized string similar to [ /// 'lib/tinymce/tinymce.min.js', /// - /// 'lib/tinymce/plugins/paste/plugin.min.js', /// 'lib/tinymce/plugins/anchor/plugin.min.js', /// 'lib/tinymce/plugins/charmap/plugin.min.js', /// 'lib/tinymce/plugins/table/plugin.min.js', /// 'lib/tinymce/plugins/lists/plugin.min.js', /// 'lib/tinymce/plugins/advlist/plugin.min.js', - /// 'lib/tinymce/plugins/hr/plugin.min.js', /// 'lib/tinymce/plugins/autolink/plugin.min.js', /// 'lib/tinymce/plugins/directionality/plugin.min.js', /// 'lib/tinymce/plugins/t [rest of string was truncated]";. diff --git a/src/Umbraco.Infrastructure/WebAssets/TinyMceInitialize.js b/src/Umbraco.Infrastructure/WebAssets/TinyMceInitialize.js index 6cb9cf6277..d172fdadf8 100644 --- a/src/Umbraco.Infrastructure/WebAssets/TinyMceInitialize.js +++ b/src/Umbraco.Infrastructure/WebAssets/TinyMceInitialize.js @@ -1,17 +1,12 @@ [ - 'lib/tinymce/tinymce.min.js', + 'lib/tinymce/tinymce.min.js', - 'lib/tinymce/plugins/paste/plugin.min.js', - 'lib/tinymce/plugins/anchor/plugin.min.js', - 'lib/tinymce/plugins/charmap/plugin.min.js', - 'lib/tinymce/plugins/table/plugin.min.js', - 'lib/tinymce/plugins/lists/plugin.min.js', - 'lib/tinymce/plugins/advlist/plugin.min.js', - 'lib/tinymce/plugins/hr/plugin.min.js', - 'lib/tinymce/plugins/autolink/plugin.min.js', - 'lib/tinymce/plugins/directionality/plugin.min.js', - 'lib/tinymce/plugins/tabfocus/plugin.min.js', - 'lib/tinymce/plugins/searchreplace/plugin.min.js', - 'lib/tinymce/plugins/fullscreen/plugin.min.js', - 'lib/tinymce/plugins/noneditable/plugin.min.js' + 'lib/tinymce/plugins/anchor/plugin.min.js', + 'lib/tinymce/plugins/charmap/plugin.min.js', + 'lib/tinymce/plugins/table/plugin.min.js', + 'lib/tinymce/plugins/lists/plugin.min.js', + 'lib/tinymce/plugins/advlist/plugin.min.js', + 'lib/tinymce/plugins/autolink/plugin.min.js', + 'lib/tinymce/plugins/directionality/plugin.min.js', + 'lib/tinymce/plugins/searchreplace/plugin.min.js' ] diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js b/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js index 5b4029386f..2f711245e3 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js @@ -240,7 +240,7 @@ function dependencies() { { "name": "signalr", "src": [ - "./node_modules/@microsoft/signalr/dist/browser/signalr.min.js", + "./node_modules/@microsoft/signalr/dist/browser/signalr.min.js" ], "base": "./node_modules/@microsoft/signalr/dist/browser" }, @@ -258,7 +258,9 @@ function dependencies() { "./node_modules/tinymce/tinymce.min.js", "./node_modules/tinymce/plugins/**", "./node_modules/tinymce/skins/**", - "./node_modules/tinymce/themes/**" + "./node_modules/tinymce/themes/**", + "./node_modules/tinymce/models/**", + "./node_modules/tinymce/icons/**" ], "base": "./node_modules/tinymce" }, diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 81fa1d79cd..8cbd6752de 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -35,7 +35,8 @@ exports.buildDev = series(setDevelopmentMode, coreBuild); exports.coreBuild = coreBuild; exports.dev = series(setDevelopmentMode, coreBuild, runUnitTestServer, watchTask); -exports.watch = series(watchTask); +exports.fastdev = series(setDevelopmentMode, coreBuild, watchTask); +exports.watch = series(setDevelopmentMode, watchTask); // exports.runTests = series(setTestMode, series(js, testUnit)); exports.runUnit = series(setTestMode, series(js, runUnitTestServer), watchTask); diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index d472f50561..7cad29e790 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -39,7 +39,7 @@ "ng-file-upload": "12.2.13", "nouislider": "15.6.1", "spectrum-colorpicker2": "2.0.9", - "tinymce": "4.9.11", + "tinymce": "6.2.0", "typeahead.js": "0.11.1", "underscore": "1.13.4", "wicg-inert": "3.1.2" @@ -5086,9 +5086,13 @@ "optional": true, "dependencies": { "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, "node_modules/css-select-base-adapter": { @@ -5098,6 +5102,75 @@ "dev": true, "optional": true }, + "node_modules/css-select/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "optional": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "optional": true + }, + "node_modules/css-select/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "optional": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "optional": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/css-select/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "optional": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -5123,11 +5196,10 @@ } }, "node_modules/css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true, - "optional": true, "engines": { "node": ">= 6" }, @@ -6142,15 +6214,15 @@ "get-intrinsic": "^1.1.1", "get-symbol-description": "^1.0.0", "has": "^1.0.3", - "has-symbols": "^1.0.2", + "has-symbols": "^1.0.3", "internal-slot": "^1.0.3", "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", + "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", + "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", "object-keys": "^1.1.1", "object.assign": "^4.1.2", "string.prototype.trimend": "^1.0.4", @@ -9053,9 +9125,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true, "engines": { "node": ">= 0.4" @@ -9927,9 +9999,9 @@ } }, "node_modules/is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", "dev": true, "optional": true, "engines": { @@ -10088,11 +10160,14 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", "dev": true, "optional": true, + "dependencies": { + "call-bind": "^1.0.2" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -10183,13 +10258,13 @@ } }, "node_modules/is-weakref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz", - "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", "dev": true, "optional": true, "dependencies": { - "call-bind": "^1.0.0" + "call-bind": "^1.0.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12305,13 +12380,16 @@ } }, "node_modules/nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, "optional": true, "dependencies": { - "boolbase": "~1.0.0" + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" } }, "node_modules/number-is-nan": { @@ -12424,9 +12502,9 @@ } }, "node_modules/object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", "dev": true, "optional": true, "funding": { @@ -13626,18 +13704,6 @@ "node": ">=8.0.0" } }, - "node_modules/postcss-svgo/node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/postcss-svgo/node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -15968,9 +16034,9 @@ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, "node_modules/tinymce": { - "version": "4.9.11", - "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-4.9.11.tgz", - "integrity": "sha512-nkSLsax+VY5DBRjMFnHFqPwTnlLEGHCco82FwJF2JNH6W+5/ClvNC1P4uhD5lXPDNiDykSHR0XJdEh7w/ICHzA==" + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.2.0.tgz", + "integrity": "sha512-zLjbFrg0hbtJ6PxmZUjQY6zyIOM/mLrWGTvhBec7XwYwoW1E0xXMQzy2tgMTh3OvJpsclgqf2ZMjmwcv4Cludw==" }, "node_modules/tmp": { "version": "0.0.33", @@ -21234,9 +21300,60 @@ "optional": true, "requires": { "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "dependencies": { + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "optional": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "optional": true + }, + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "optional": true, + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "optional": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "optional": true + } } }, "css-select-base-adapter": { @@ -21267,11 +21384,10 @@ } }, "css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", - "dev": true, - "optional": true + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true }, "cssesc": { "version": "3.0.0", @@ -22089,15 +22205,15 @@ "get-intrinsic": "^1.1.1", "get-symbol-description": "^1.0.0", "has": "^1.0.3", - "has-symbols": "^1.0.2", + "has-symbols": "^1.0.3", "internal-slot": "^1.0.3", "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", + "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", + "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", "object-keys": "^1.1.1", "object.assign": "^4.1.2", "string.prototype.trimend": "^1.0.4", @@ -24406,9 +24522,9 @@ "optional": true }, "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true }, "has-to-string-tag-x": { @@ -25050,9 +25166,9 @@ "dev": true }, "is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", "dev": true, "optional": true }, @@ -25165,11 +25281,14 @@ "optional": true }, "is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", "dev": true, - "optional": true + "optional": true, + "requires": { + "call-bind": "^1.0.2" + } }, "is-stream": { "version": "1.1.0", @@ -25230,13 +25349,13 @@ "dev": true }, "is-weakref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz", - "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", "dev": true, "optional": true, "requires": { - "call-bind": "^1.0.0" + "call-bind": "^1.0.2" } }, "is-what": { @@ -26890,13 +27009,13 @@ } }, "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, "optional": true, "requires": { - "boolbase": "~1.0.0" + "boolbase": "^1.0.0" } }, "number-is-nan": { @@ -26986,9 +27105,9 @@ } }, "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", "dev": true, "optional": true }, @@ -27816,12 +27935,6 @@ "source-map": "^0.6.1" } }, - "css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true - }, "dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -29681,9 +29794,9 @@ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, "tinymce": { - "version": "4.9.11", - "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-4.9.11.tgz", - "integrity": "sha512-nkSLsax+VY5DBRjMFnHFqPwTnlLEGHCco82FwJF2JNH6W+5/ClvNC1P4uhD5lXPDNiDykSHR0XJdEh7w/ICHzA==" + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.2.0.tgz", + "integrity": "sha512-zLjbFrg0hbtJ6PxmZUjQY6zyIOM/mLrWGTvhBec7XwYwoW1E0xXMQzy2tgMTh3OvJpsclgqf2ZMjmwcv4Cludw==" }, "tmp": { "version": "0.0.33", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 8d8334e3a7..969fa65292 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -50,7 +50,7 @@ "ng-file-upload": "12.2.13", "nouislider": "15.6.1", "spectrum-colorpicker2": "2.0.9", - "tinymce": "4.9.11", + "tinymce": "6.2.0", "typeahead.js": "0.11.1", "underscore": "1.13.4", "wicg-inert": "3.1.2" diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index 7b690e5863..9090195d08 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -29,10 +29,6 @@ angular.module("umbraco.directives") //for the grid by default, we don't want to include the macro toolbar editorConfig.toolbar = _.without(editorConfig, "umbmacro"); } - //make sure there's a max image size - if (!scope.configuration.maxImageSize && scope.configuration.maxImageSize !== 0) { - editorConfig.maxImageSize = tinyMceService.defaultPrevalues().maxImageSize; - } //ensure the grid's global config is being passed up to the RTE, these 2 properties need to be in this format //since below we are just passing up `scope` as the actual model and for 2 way binding to work with `value` that @@ -56,23 +52,14 @@ angular.module("umbraco.directives") mode: editorConfig.mode })); - // pin toolbar to top of screen if we have focus and it scrolls off the screen - function pinToolbar() { - tinyMceService.pinToolbar(tinyMceEditor); - } - - // unpin toolbar to top of screen - function unpinToolbar() { - tinyMceService.unpinToolbar(tinyMceEditor); - } - $q.all(promises).then(function (result) { var standardConfig = result[promises.length - 1]; //create a baseline Config to extend upon var baseLineConfigObj = { - maxImageSize: editorConfig.maxImageSize + maxImageSize: editorConfig.maxImageSize, + toolbar_sticky: true }; Utilities.extend(baseLineConfigObj, standardConfig); @@ -85,6 +72,7 @@ angular.module("umbraco.directives") //initialize the standard editor functionality for Umbraco tinyMceService.initializeEditor({ editor: editor, + toolbar: editorConfig.toolbar, model: scope, // Form is found in the scope of the grid controller above us, not in our isolated scope // https://github.com/umbraco/Umbraco-CMS/issues/7461 @@ -108,35 +96,6 @@ angular.module("umbraco.directives") }, 400); }); - - // TODO: Perhaps we should pin the toolbar for the rte always, regardless of if it's in the grid or not? - // this would mean moving this code into the tinyMceService.initializeEditor - - //when we leave the editor (maybe) - editor.on('blur', function (e) { - angularHelper.safeApply(scope, function () { - unpinToolbar(); - $('.umb-panel-body').off('scroll', pinToolbar); - }); - }); - - // Focus on editor - editor.on('focus', function (e) { - angularHelper.safeApply(scope, function () { - pinToolbar(); - $('.umb-panel-body').on('scroll', pinToolbar); - }); - }); - - // Click on editor - editor.on('click', function (e) { - angularHelper.safeApply(scope, function () { - pinToolbar(); - $('.umb-panel-body').on('scroll', pinToolbar); - }); - }); - - }; /** Loads in the editor */ @@ -173,7 +132,6 @@ angular.module("umbraco.directives") scope.$on('$destroy', function () { eventsService.unsubscribe(tabShownListener); //ensure we unbind this in case the blur doesn't fire above - $('.umb-panel-body').off('scroll', pinToolbar); if (tinyMceEditor !== undefined && tinyMceEditor != null) { tinyMceEditor.destroy() } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 850a173f8d..65c4a50e43 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -4,1760 +4,1618 @@ * * * @description - * A service containing all logic for all of the Umbraco TinyMCE plugins + * A service containing all logic for all of the Umbraco TinyMCE v6 plugins + * + * @doc https://www.tiny.cloud/docs/tinymce/6/ */ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, stylesheetResource, macroResource, macroService, - $routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService, localStorageService, mediaHelper) { + $routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService, localStorageService, mediaHelper) { - //These are absolutely required in order for the macros to render inline - //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce + //These are absolutely required in order for the macros to render inline + //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style|lang],figure,figcaption"; - var fallbackStyles = [{ title: "Page header", block: "h2" }, { title: "Section header", block: "h3" }, { title: "Paragraph header", block: "h4" }, { title: "Normal", block: "p" }, { title: "Quote", block: "blockquote" }, { title: "Code", block: "code" }]; - // these languages are available for localization - var availableLanguages = [ - 'ar', - 'ar_SA', - 'hy', - 'az', - 'eu', - 'be', - 'bn_BD', - 'bs', - 'bg_BG', - 'ca', - 'zh_CN', - 'zh_TW', - 'hr', - 'cs', - 'da', - 'dv', - 'nl', - 'en_CA', - 'en_GB', - 'et', - 'fo', - 'fi', - 'fr_FR', - 'gd', - 'gl', - 'ka_GE', - 'de', - 'de_AT', - 'el', - 'he_IL', - 'hi_IN', - 'hu_HU', - 'is_IS', - 'id', - 'it', - 'ja', - 'kab', - 'kk', - 'km_KH', - 'ko_KR', - 'ku', - 'ku_IQ', - 'lv', - 'lt', - 'lb', - 'ml', - 'ml_IN', - 'mn_MN', - 'nb_NO', - 'fa', - 'fa_IR', - 'pl', - 'pt_BR', - 'pt_PT', - 'ro', - 'ru', - 'sr', - 'si_LK', - 'sk', - 'sl_SI', - 'es', - 'es_MX', - 'sv_SE', - 'tg', - 'ta', - 'ta_IN', - 'tt', - 'th_TH', - 'tr', - 'tr_TR', - 'ug', - 'uk', - 'uk_UA', - 'vi', - 'vi_VN', - 'cy' - ]; - //define fallback language - var defaultLanguage = 'en_US'; + var fallbackStyles = [ + { + title: 'Headers', items: [ + { title: "Page header", block: "h2" }, + { title: "Section header", block: "h3" }, + { title: "Paragraph header", block: "h4" } + ] + }, + { + title: 'Blocks', items: [ + { title: "Normal", block: "p" } + ] + }, + { + title: 'Containers', items: [ + { title: "Quote", block: "blockquote" }, + { title: "Code", block: "code" } + ] + }]; + // these languages are available for localization + var availableLanguages = [ + 'ar', + 'ar_SA', + 'hy', + 'az', + 'eu', + 'be', + 'bn_BD', + 'bs', + 'bg_BG', + 'ca', + 'zh_CN', + 'zh_TW', + 'hr', + 'cs', + 'da', + 'dv', + 'nl', + 'en_CA', + 'en_GB', + 'et', + 'fo', + 'fi', + 'fr_FR', + 'gd', + 'gl', + 'ka_GE', + 'de', + 'de_AT', + 'el', + 'he_IL', + 'hi_IN', + 'hu_HU', + 'is_IS', + 'id', + 'it', + 'ja', + 'kab', + 'kk', + 'km_KH', + 'ko_KR', + 'ku', + 'ku_IQ', + 'lv', + 'lt', + 'lb', + 'ml', + 'ml_IN', + 'mn_MN', + 'nb_NO', + 'fa', + 'fa_IR', + 'pl', + 'pt_BR', + 'pt_PT', + 'ro', + 'ru', + 'sr', + 'si_LK', + 'sk', + 'sl_SI', + 'es', + 'es_MX', + 'sv_SE', + 'tg', + 'ta', + 'ta_IN', + 'tt', + 'th_TH', + 'tr', + 'tr_TR', + 'ug', + 'uk', + 'uk_UA', + 'vi', + 'vi_VN', + 'cy' + ]; + //define fallback language + var defaultLanguage = 'en_US'; - /** - * Returns a promise of an object containing the stylesheets and styleFormats collections - * @param {any} configuredStylesheets - */ - function getStyles(configuredStylesheets) { + /** + * Returns a promise of an object containing the stylesheets and styleFormats collections + * @param {any} configuredStylesheets + */ + function getStyles(configuredStylesheets) { - var stylesheets = []; - var styleFormats = []; - var promises = [$q.when(true)]; //a collection of promises, the first one is an empty promise + var stylesheets = []; + var styleFormats = []; + var promises = [$q.when(true)]; //a collection of promises, the first one is an empty promise - //queue rules loading - if (configuredStylesheets) { - configuredStylesheets.forEach(function (val, key) { + //queue rules loading + if (configuredStylesheets?.length) { + configuredStylesheets.forEach(function (val, key) { - if (val.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/") === 0) { - // current format (full path to stylesheet) - stylesheets.push(val); - } - else { - // legacy format (stylesheet name only) - must prefix with stylesheet folder and postfix with ".css" - stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + val + ".css"); - } - - promises.push(stylesheetResource.getRulesByName(val).then(function (rules) { - rules.forEach(function (rule) { - var r = {}; - r.title = rule.name; - if (rule.selector[0] == ".") { - r.inline = "span"; - r.classes = rule.selector.substring(1); - } - else if (rule.selector[0] === "#") { - r.inline = "span"; - r.attributes = { id: rule.selector.substring(1) }; - } - else if (rule.selector[0] !== "." && rule.selector.indexOf(".") > -1) { - var split = rule.selector.split("."); - r.block = split[0]; - r.classes = rule.selector.substring(rule.selector.indexOf(".") + 1).replace(/\./g, " "); - } - else if (rule.selector[0] != "#" && rule.selector.indexOf("#") > -1) { - var split = rule.selector.split("#"); - r.block = split[0]; - r.classes = rule.selector.substring(rule.selector.indexOf("#") + 1); - } - else { - r.block = rule.selector; - } - - styleFormats.push(r); - }); - })); - }); + if (val.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/") === 0) { + // current format (full path to stylesheet) + stylesheets.push(val); } else { - styleFormats = fallbackStyles; + // legacy format (stylesheet name only) - must prefix with stylesheet folder and postfix with ".css" + stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + val + ".css"); } - return $q.all(promises).then(function() { - // Always push our Umbraco RTE stylesheet - // So we can style macros, embed items etc... - stylesheets.push(`${Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath}/assets/css/rte-content.css`); - - return $q.when({ stylesheets: stylesheets, styleFormats: styleFormats}); - }); - } - - /** Returns the language to use for TinyMCE */ - function getLanguage() { - var language = defaultLanguage; - //get locale from angular and match tinymce format. Angular localization is always in the format of ru-ru, de-de, en-gb, etc. - //wheras tinymce is in the format of ru, de, en, en_us, etc. - var localeId = $locale.id.replace('-', '_'); - //try matching the language using full locale format - var languageMatch = _.find(availableLanguages, function (o) { return o.toLowerCase() === localeId; }); - //if no matches, try matching using only the language - if (languageMatch === undefined) { - var localeParts = localeId.split('_'); - languageMatch = _.find(availableLanguages, function (o) { return o === localeParts[0]; }); - } - //if a match was found - set the language - if (languageMatch !== undefined) { - language = languageMatch; - } - return language; - } - - /** - * Gets toolbars for the inlite theme - * @param {any} configuredToolbar - * @param {any} tinyMceConfig - */ - function getToolbars(configuredToolbar, tinyMceConfig) { - - //the commands for selection/all - var allowedSelectionToolbar = _.map(_.filter(tinyMceConfig.commands, - function(f) { - return f.mode === "Selection" || f.mode === "All"; - }), - function(f) { - return f.alias; - }); - - //the commands for insert/all - var allowedInsertToolbar = _.map(_.filter(tinyMceConfig.commands, - function(f) { - return f.mode === "Insert" || f.mode === "All"; - }), - function(f) { - return f.alias; - }); - - var insertToolbar = _.filter(configuredToolbar, function (t) { - return allowedInsertToolbar.indexOf(t) !== -1; - }).join(" | "); - - var selectionToolbar = _.filter(configuredToolbar, function (t) { - return allowedSelectionToolbar.indexOf(t) !== -1; - }).join(" | "); - - return { - insertToolbar: insertToolbar, - selectionToolbar: selectionToolbar - } - } - - function uploadImageHandler(blobInfo, success, failure, progress){ - const xhr = new XMLHttpRequest(); - xhr.open('POST', Umbraco.Sys.ServerVariables.umbracoUrls.tinyMceApiBaseUrl + 'UploadImage'); - - xhr.onloadstart = function(e) { - angularHelper.safeApply($rootScope, function() { - eventsService.emit("rte.file.uploading"); - }); - }; - - xhr.onloadend = function(e) { - angularHelper.safeApply($rootScope, function() { - eventsService.emit("rte.file.uploaded"); - }); - }; - - xhr.upload.onprogress = function (e) { - progress(e.loaded / e.total * 100); - }; - - xhr.onerror = function () { - failure('Image upload failed due to a XHR Transport error. Code: ' + xhr.status); - }; - - xhr.onload = function () { - if (xhr.status < 200 || xhr.status >= 300) { - failure('HTTP Error: ' + xhr.status); - return; + promises.push(stylesheetResource.getRulesByName(val).then(function (rules) { + rules.forEach(function (rule) { + var r = {}; + r.title = rule.name; + if (rule.selector[0] == ".") { + r.inline = "span"; + r.classes = rule.selector.substring(1); } - - let data = xhr.responseText; - - // The response is fitted as an AngularJS resource response and needs to be cleaned of the AngularJS metadata - data = data.split("\n"); - - if (!data.length > 1) { - failure('Unrecognized text string: ' + data); - return; + else if (rule.selector[0] === "#") { + r.inline = "span"; + r.attributes = { id: rule.selector.substring(1) }; } - - let json = {}; - - try { - json = JSON.parse(data[1]); - } catch (e) { - failure('Invalid JSON: ' + data + ' - ' + e.message); - return; + else if (rule.selector[0] !== "." && rule.selector.indexOf(".") > -1) { + var split = rule.selector.split("."); + r.block = split[0]; + r.classes = rule.selector.substring(rule.selector.indexOf(".") + 1).replace(/\./g, " "); } - - if (!json || typeof json.tmpLocation !== 'string') { - failure('Invalid JSON: ' + data); - return; - } - - // Put temp location into localstorage (used to update the img with data-tmpimg later on) - localStorageService.set(`tinymce__${blobInfo.blobUri()}`, json.tmpLocation); - - // We set the img src url to be the same as we started - // The Blob URI is stored in TinyMce's cache - // so the img still shows in the editor - success(blobInfo.blobUri()); - }; - - const formData = new FormData(); - formData.append('file', blobInfo.blob(), blobInfo.blob().name); - - xhr.send(formData); - } - - function cleanupPasteData(plugin, args) { - - // Remove spans - args.content = args.content.replace(/<\s*span[^>]*>(.*?)<\s*\/\s*span>/g, "$1"); - - // Convert b to strong. - args.content = args.content.replace(/<\s*b([^>]*)>(.*?)<\s*\/\s*b([^>]*)>/g, "$2"); - - // convert i to em - args.content = args.content.replace(/<\s*i([^>]*)>(.*?)<\s*\/\s*i([^>]*)>/g, "$2"); - - - } - - function sizeImageInEditor(editor, imageDomElement, imgUrl) { - var size = editor.dom.getSize(imageDomElement); - - if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) { - var newSize = imageHelper.scaleToMaxSize(editor.settings.maxImageSize, size.w, size.h); - - editor.dom.setAttrib(imageDomElement, 'width', newSize.width); - editor.dom.setAttrib(imageDomElement, 'height', newSize.height); - - // Images inserted via Media Picker will have a URL we can use for ImageResizer QueryStrings - // Images pasted/dragged in are not persisted to media until saved & thus will need to be added - if (imgUrl) { - mediaHelper.getProcessedImageUrl(imgUrl, - { - width: newSize.width, - height: newSize.height - }) - .then(function (resizedImgUrl) { - editor.dom.setAttrib(imageDomElement, 'data-mce-src', resizedImgUrl); - }); - } - - editor.execCommand("mceAutoResize", false, null, null); - } - } - - function isMediaPickerEnabled(toolbarItemArray){ - var insertMediaButtonFound = false; - toolbarItemArray.forEach(toolbarItem => { - if(toolbarItem.indexOf("umbmediapicker") > -1){ - insertMediaButtonFound = true; - } - }); - return insertMediaButtonFound; - } - - return { - - /** - * Returns a promise of the configuration object to initialize the TinyMCE editor - * @param {} args - * @returns {} - */ - getTinyMceEditorConfig: function (args) { - - //global defaults, called before/during init - tinymce.DOM.events.domLoaded = true; - tinymce.baseURL = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/lib/tinymce/"; // trailing slash important - - var promises = [ - this.configuration(), - getStyles(args.stylesheets) - ]; - - return $q.all(promises).then(function(result) { - - var tinyMceConfig = result[0]; - var styles = result[1]; - - var toolbars = getToolbars(args.toolbar, tinyMceConfig); - - var plugins = _.map(tinyMceConfig.plugins, function (plugin) { - return plugin.name; - }); - - // Plugins that must always be active - plugins.push("autoresize"); - plugins.push("noneditable"); - - // Table plugin use color picker plugin in table properties - if (plugins.includes("table")) { - plugins.push("colorpicker"); - } - - var modeTheme = ''; - var modeInline = false; - - // Based on mode set - // classic = Theme: modern, inline: false - // inline = Theme: modern, inline: true, - // distraction-free = Theme: inlite, inline: true - switch (args.mode) { - case "classic": - modeTheme = "modern"; - modeInline = false; - break; - - case "distraction-free": - modeTheme = "inlite"; - modeInline = true; - break; - - default: - //Will default to 'classic' - modeTheme = "modern"; - modeInline = false; - break; - } - - - - //create a baseline Config to exten upon - var config = { - theme: modeTheme, - inline: modeInline, - plugins: plugins, - valid_elements: tinyMceConfig.validElements, - invalid_elements: tinyMceConfig.inValidElements, - extended_valid_elements: extendedValidElements, - menubar: false, - statusbar: false, - relative_urls: false, - autoresize_bottom_margin: 10, - content_css: styles.stylesheets, - style_formats: styles.styleFormats, - language: getLanguage(), - - //this would be for a theme other than inlite - toolbar: args.toolbar.join(" "), - - //these are for the inlite theme to work - insert_toolbar: toolbars.insertToolbar, - selection_toolbar: toolbars.selectionToolbar, - - body_class: "umb-rte", - - //see http://archive.tinymce.com/wiki.php/Configuration:cache_suffix - cache_suffix: "?umb__rnd=" + Umbraco.Sys.ServerVariables.application.cacheBuster - }; - - // Need to check if we are allowed to UPLOAD images - // This is done by checking if the insert image toolbar button is available - if(isMediaPickerEnabled(args.toolbar)){ - // Update the TinyMCE Config object to allow pasting - config.images_upload_handler = uploadImageHandler; - config.automatic_uploads = false; - config.images_replace_blob_uris = false; - - // This allows images to be pasted in & stored as Base64 until they get uploaded to server - config.paste_data_images = true; - } - - - if (args.htmlId) { - config.selector = `[id="${args.htmlId}"]`; - } else if (args.target) { - config.target = args.target; - } - - /* - // We are not ready to limit the pasted elements further than default, we will return to this feature. ( TODO: Make this feature an option. ) - // We keep spans here, cause removing spans here also removes b-tags inside of them, instead we strip them out later. (TODO: move this definition to the config file... ) - var validPasteElements = "-strong/b,-em/i,-u,-span,-p,-ol,-ul,-li,-p/div,-a[href|name],sub,sup,strike,br,del,table[width],tr,td[colspan|rowspan|width],th[colspan|rowspan|width],thead,tfoot,tbody,img[src|alt|width|height],ul,ol,li,hr,pre,dl,dt,figure,figcaption,wbr" - - // add elements from user configurated styleFormats to our list of validPasteElements. - // (This means that we only allow H3-element if its configured as a styleFormat on this specific propertyEditor.) - var style, i = 0; - for(; i < styles.styleFormats.length; i++) { - style = styles.styleFormats[i]; - if(style.block) { - validPasteElements += "," + style.block; - } - } - */ - - /** - The default paste config can be overwritten by defining these properties in the customConfig. - */ - var pasteConfig = { - - paste_remove_styles: true, - paste_text_linebreaktype: true, //Converts plaintext linebreaks to br or p elements. - paste_strip_class_attributes: "none", - - //paste_word_valid_elements: validPasteElements, - - paste_preprocess: cleanupPasteData - - }; - - Utilities.extend(config, pasteConfig); - - if (tinyMceConfig.customConfig) { - - //if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to - // convert it to json instead of having it as a string since this is what tinymce requires - for (var i in tinyMceConfig.customConfig) { - var val = tinyMceConfig.customConfig[i]; - if (val) { - val = val.toString().trim(); - if (val.detectIsJson()) { - try { - tinyMceConfig.customConfig[i] = JSON.parse(val); - //now we need to check if this custom config key is defined in our baseline, if it is we don't want to - //overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise - //if it's an object it will overwrite the baseline - if (Utilities.isArray(config[i]) && Utilities.isArray(tinyMceConfig.customConfig[i])) { - //concat it and below this concat'd array will overwrite the baseline in Utilities.extend - tinyMceConfig.customConfig[i] = config[i].concat(tinyMceConfig.customConfig[i]); - } - } - catch (e) { - //cannot parse, we'll just leave it - } - } - if (val === "true") { - tinyMceConfig.customConfig[i] = true; - } - if (val === "false") { - tinyMceConfig.customConfig[i] = false; - } - } - } - - Utilities.extend(config, tinyMceConfig.customConfig); - } - - return config; - - }); - - }, - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#configuration - * @methodOf umbraco.services.tinyMceService - * - * @description - * Returns a collection of plugins available to the tinyMCE editor - * - */ - configuration: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "rteApiBaseUrl", - "GetConfiguration"), { - cache: true - }), - 'Failed to retrieve tinymce configuration'); - }, - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#defaultPrevalues - * @methodOf umbraco.services.tinyMceService - * - * @description - * Returns a default configration to fallback on in case none is provided - * - */ - defaultPrevalues: function () { - var cfg = {}; - cfg.toolbar = ["ace", "styleselect", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "outdent", "indent", "link", "umbmediapicker", "umbmacro", "umbembeddialog"]; - cfg.stylesheets = []; - cfg.maxImageSize = 500; - return cfg; - }, - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#createInsertEmbeddedMedia - * @methodOf umbraco.services.tinyMceService - * - * @description - * Creates the umbraco insert embedded media tinymce plugin - * - * @param {Object} editor the TinyMCE editor instance - */ - createInsertEmbeddedMedia: function (editor, callback) { - editor.addButton('umbembeddialog', { - icon: 'custom icon-tv', - tooltip: 'Embed', - stateSelector: 'div[data-embed-url]', - onclick: function () { - - // Get the selected element - // Check nodename is a DIV and the claslist contains 'embeditem' - var selectedElm = editor.selection.getNode(); - var nodeName = selectedElm.nodeName; - var modify = null; - - if(nodeName.toUpperCase() === "DIV" && selectedElm.classList.contains("embeditem")){ - // See if we can go and get the attributes - var embedUrl = editor.dom.getAttrib(selectedElm, "data-embed-url"); - var embedWidth = editor.dom.getAttrib(selectedElm, "data-embed-width"); - var embedHeight = editor.dom.getAttrib(selectedElm, "data-embed-height"); - var embedConstrain = editor.dom.getAttrib(selectedElm, "data-embed-constrain"); - - modify = { - url: embedUrl, - width: parseInt(embedWidth) || 0, - height: parseInt(embedHeight) || 0, - constrain: embedConstrain - }; - } - - if (callback) { - angularHelper.safeApply($rootScope, function() { - // pass the active element along so we can retrieve it later - callback(selectedElm, modify); - }); - } - } - }); - }, - - insertEmbeddedMediaInEditor: function (editor, embed, activeElement) { - // Wrap HTML preview content here in a DIV with non-editable class of .mceNonEditable - // This turns it into a selectable/cutable block to move about - var wrapper = tinymce.activeEditor.dom.create('div', - { - 'class': 'mceNonEditable embeditem', - 'data-embed-url': embed.url, - 'data-embed-height': embed.height, - 'data-embed-width': embed.width, - 'data-embed-constrain': embed.constrain, - 'contenteditable': false - }, - embed.preview); - - // Only replace if activeElement is an Embed element. - if (activeElement && activeElement.nodeName.toUpperCase() === "DIV" && activeElement.classList.contains("embeditem")){ - activeElement.replaceWith(wrapper); // directly replaces the html node - } else { - editor.selection.setNode(wrapper); - } - }, - - - createAceCodeEditor: function(editor, callback){ - - editor.addButton("ace", { - icon: "code", - tooltip: "View Source Code", - onclick: function(){ - if (callback) { - angularHelper.safeApply($rootScope, function() { - callback(); - }); - } - } - }); - - }, - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#createMediaPicker - * @methodOf umbraco.services.tinyMceService - * - * @description - * Creates the umbraco insert media tinymce plugin - * - * @param {Object} editor the TinyMCE editor instance - */ - createMediaPicker: function (editor, callback) { - editor.addButton('umbmediapicker', { - icon: 'custom icon-picture', - tooltip: 'Media Picker', - stateSelector: 'img[data-udi]', - onclick: function () { - - var selectedElm = editor.selection.getNode(), - currentTarget; - - if (selectedElm.nodeName === 'IMG') { - var img = $(selectedElm); - - var hasUdi = img.attr("data-udi") ? true : false; - var hasDataTmpImg = img.attr("data-tmpimg") ? true : false; - - currentTarget = { - altText: img.attr("alt"), - url: img.attr("src"), - caption: img.attr('data-caption') - }; - - if (hasUdi) { - currentTarget["udi"] = img.attr("data-udi"); - } else { - currentTarget["id"] = img.attr("rel"); - } - - if(hasDataTmpImg){ - currentTarget["tmpimg"] = img.attr("data-tmpimg"); - } - } - - userService.getCurrentUser().then(function (userData) { - if (callback) { - angularHelper.safeApply($rootScope, function() { - callback(currentTarget, userData); - }); - } - }); - } - }); - }, - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#insetMediaInEditor - * @methodOf umbraco.services.tinyMceService - * - * @description - * Inserts the image element in tinymce plugin - * - * @param {Object} editor the TinyMCE editor instance - */ - insertMediaInEditor: function (editor, img) { - if (img) { - // We need to create a NEW DOM element to insert - // setting an attribute of ID to __mcenew, so we can gather a reference to the node, to be able to update its size accordingly to the size of the image. - var data = { - alt: img.altText || "", - src: (img.url) ? img.url : "nothing.jpg", - id: "__mcenew", - "data-udi": img.udi, - "data-caption": img.caption - }; - var newImage = editor.dom.createHTML('img', data); - var parentElement = editor.selection.getNode().parentElement; - - if (img.caption) { - var figCaption = editor.dom.createHTML('figcaption', {}, img.caption); - var combined = newImage + figCaption; - - if (parentElement.nodeName !== 'FIGURE') { - var fragment = editor.dom.createHTML('figure', {}, combined); - editor.selection.setContent(fragment); - } - else { - parentElement.innerHTML = combined; - } - } - else { - //if caption is removed, remove the figure element - if (parentElement.nodeName === 'FIGURE') { - parentElement.parentElement.innerHTML = newImage; - } - else { - editor.selection.setContent(newImage); - } - } - - // Using settimeout to wait for a DoM-render, so we can find the new element by ID. - $timeout(function () { - - var imgElm = editor.dom.get("__mcenew"); - editor.dom.setAttrib(imgElm, "id", null); - - // When image is loaded we are ready to call sizeImageInEditor. - var onImageLoaded = function() { - sizeImageInEditor(editor, imgElm, img.url); - editor.fire("Change"); - } - - // Check if image already is loaded. - if(imgElm.complete === true) { - onImageLoaded(); - } else { - imgElm.onload = onImageLoaded; - } - - }); - - } - }, - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#createUmbracoMacro - * @methodOf umbraco.services.tinyMceService - * - * @description - * Creates the insert umbraco macro tinymce plugin - * - * @param {Object} editor the TinyMCE editor instance - */ - createInsertMacro: function (editor, callback) { - - let self = this; - let activeMacroElement = null; //track an active macro element - - /** Adds custom rules for the macro plugin and custom serialization */ - editor.on('preInit', function (args) { - //this is requires so that we tell the serializer that a 'div' is actually allowed in the root, otherwise the cleanup will strip it out - editor.serializer.addRules('div'); - - /** This checks if the div is a macro container, if so, checks if its wrapped in a p tag and then unwraps it (removes p tag) */ - editor.serializer.addNodeFilter('div', function (nodes, name) { - for (var i = 0; i < nodes.length; i++) { - if (nodes[i].attr("class") === "umb-macro-holder" && nodes[i].parent && nodes[i].parent.name.toUpperCase() === "P") { - nodes[i].parent.unwrap(); - } - } - }); - - }); - - /** when the contents load we need to find any macros declared and load in their content */ - editor.on("SetContent", function (o) { - - //get all macro divs and load their content - $(editor.dom.select(".umb-macro-holder.mceNonEditable")).each(function () { - self.loadMacroContent($(this), null, editor); - }); - - }); - - /** - * Because the macro gets wrapped in a P tag because of the way 'enter' works, this - * method will return the macro element if not wrapped in a p, or the p if the macro - * element is the only one inside of it even if we are deep inside an element inside the macro - */ - function getRealMacroElem(element) { - var e = $(element).closest(".umb-macro-holder"); - if (e.length > 0) { - if (e.get(0).parentNode.nodeName === "P") { - //now check if we're the only element - if (element.parentNode.childNodes.length === 1) { - return e.get(0).parentNode; - } - } - return e.get(0); - } - return null; - } - - /** Adds the button instance */ - editor.addButton('umbmacro', { - icon: 'custom icon-settings-alt', - tooltip: 'Insert macro', - onPostRender: function () { - - let ctrl = this; - - /** - * Check if the macro is currently selected and toggle the menu button - */ - function onNodeChanged(evt) { - - //set our macro button active when on a node of class umb-macro-holder - activeMacroElement = getRealMacroElem(evt.element); - - //set the button active/inactive - ctrl.active(activeMacroElement !== null); - } - - //NOTE: This could be another way to deal with the active/inactive state - //editor.on('ObjectSelected', function (e) {}); - - //set onNodeChanged event listener - editor.on('NodeChange', onNodeChanged); - - }, - - /** The insert macro button click event handler */ - onclick: function () { - - var dialogData = { - //flag for use in rte so we only show macros flagged for the editor - richTextEditor: true - }; - - //when we click we could have a macro already selected and in that case we'll want to edit the current parameters - //so we'll need to extract them and submit them to the dialog. - if (activeMacroElement) { - //we have a macro selected so we'll need to parse it's alias and parameters - var contents = $(activeMacroElement).contents(); - var comment = _.find(contents, function (item) { - return item.nodeType === 8; - }); - if (!comment) { - throw "Cannot parse the current macro, the syntax in the editor is invalid"; - } - var syntax = comment.textContent.trim(); - var parsed = macroService.parseMacroSyntax(syntax); - dialogData = { - macroData: parsed, - activeMacroElement: activeMacroElement //pass the active element along so we can retrieve it later - }; - } - - if (callback) { - angularHelper.safeApply($rootScope, function () { - callback(dialogData); - }); - } - } - }); - }, - - insertMacroInEditor: function (editor, macroObject, activeMacroElement) { - - //Important note: the TinyMce plugin "noneditable" is used here so that the macro cannot be edited, - // for this to work the mceNonEditable class needs to come last and we also need to use the attribute contenteditable = false - // (even though all the docs and examples say that is not necessary) - - //put the macro syntax in comments, we will parse this out on the server side to be used - //for persisting. - var macroSyntaxComment = ""; - //create an id class for this element so we can re-select it after inserting - var uniqueId = "umb-macro-" + editor.dom.uniqueId(); - var macroDiv = editor.dom.create('div', - { - 'class': 'umb-macro-holder ' + macroObject.macroAlias + " " + uniqueId + ' mceNonEditable', - 'contenteditable': 'false' - }, - macroSyntaxComment + 'Macro alias: ' + macroObject.macroAlias + ''); - - //if there's an activeMacroElement then replace it, otherwise set the contents of the selected node - if (activeMacroElement) { - activeMacroElement.replaceWith(macroDiv); //directly replaces the html node + else if (rule.selector[0] != "#" && rule.selector.indexOf("#") > -1) { + var split = rule.selector.split("#"); + r.block = split[0]; + r.classes = rule.selector.substring(rule.selector.indexOf("#") + 1); } else { - editor.selection.setNode(macroDiv); + r.block = rule.selector; } - var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId)); - editor.setDirty(true); + styleFormats.push(r); + }); + })); + }); + } + else { + styleFormats = fallbackStyles; + } - //async load the macro content - this.loadMacroContent($macroDiv, macroObject, editor); + return $q.all(promises).then(function () { + // Always push our Umbraco RTE stylesheet + // So we can style macros, embed items etc... + stylesheets.push(`${Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath}/assets/css/rte-content.css`); - }, + return $q.when({ stylesheets: stylesheets, styleFormats: styleFormats }); + }); + } - /** loads in the macro content async from the server */ - loadMacroContent: function ($macroDiv, macroData, editor) { + /** Returns the language to use for TinyMCE */ + function getLanguage() { + var language = defaultLanguage; + //get locale from angular and match tinymce format. Angular localization is always in the format of ru-ru, de-de, en-gb, etc. + //wheras tinymce is in the format of ru, de, en, en_us, etc. + var localeId = $locale.id.replace('-', '_'); + //try matching the language using full locale format + var languageMatch = _.find(availableLanguages, function (o) { return o.toLowerCase() === localeId; }); + //if no matches, try matching using only the language + if (languageMatch === undefined) { + var localeParts = localeId.split('_'); + languageMatch = _.find(availableLanguages, function (o) { return o === localeParts[0]; }); + } + //if a match was found - set the language + if (languageMatch !== undefined) { + language = languageMatch; + } + return language; + } - //if we don't have the macroData, then we'll need to parse it from the macro div - if (!macroData) { - var contents = $macroDiv.contents(); - var comment = _.find(contents, function (item) { - return item.nodeType === 8; - }); - if (!comment) { - throw "Cannot parse the current macro, the syntax in the editor is invalid"; + function uploadImageHandler(blobInfo, progress) { + return new Promise(function (resolve, reject) { + const xhr = new XMLHttpRequest(); + xhr.open('POST', Umbraco.Sys.ServerVariables.umbracoUrls.tinyMceApiBaseUrl + 'UploadImage'); + + xhr.onloadstart = function () { + angularHelper.safeApply($rootScope, function () { + eventsService.emit("rte.file.uploading"); + }); + }; + + xhr.onloadend = function (e) { + angularHelper.safeApply($rootScope, function () { + eventsService.emit("rte.file.uploaded"); + }); + }; + + xhr.upload.onprogress = function (e) { + progress(e.loaded / e.total * 100); + }; + + xhr.onerror = function () { + reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status); + }; + + xhr.onload = function () { + if (xhr.status < 200 || xhr.status >= 300) { + reject('HTTP Error: ' + xhr.status); + return; + } + + let data = xhr.responseText; + + // The response is fitted as an AngularJS resource response and needs to be cleaned of the AngularJS metadata + data = data.split("\n"); + + if (!data.length > 1) { + reject('Unrecognized text string: ' + data); + return; + } + + let json = {}; + + try { + json = JSON.parse(data[1]); + } catch (e) { + reject('Invalid JSON: ' + data + ' - ' + e.message); + return; + } + + if (!json || typeof json.tmpLocation !== 'string') { + reject('Invalid JSON: ' + data); + return; + } + + // Put temp location into localstorage (used to update the img with data-tmpimg later on) + localStorageService.set(`tinymce__${blobInfo.blobUri()}`, json.tmpLocation); + + // We set the img src url to be the same as we started + // The Blob URI is stored in TinyMce's cache + // so the img still shows in the editor + resolve(blobInfo.blobUri()); + }; + + const formData = new FormData(); + formData.append('file', blobInfo.blob(), blobInfo.blob().name); + + xhr.send(formData); + }); + } + + function cleanupPasteData(_editor, args) { + // Remove spans + args.content = args.content.replace(/<\s*span[^>]*>(.*?)<\s*\/\s*span>/g, "$1"); + + // Convert b to strong. + args.content = args.content.replace(/<\s*b([^>]*)>(.*?)<\s*\/\s*b([^>]*)>/g, "$2"); + + // convert i to em + args.content = args.content.replace(/<\s*i([^>]*)>(.*?)<\s*\/\s*i([^>]*)>/g, "$2"); + + + } + + function sizeImageInEditor(editor, imageDomElement, imgUrl) { + var size = editor.dom.getSize(imageDomElement); + var maxImageSize = editor.options.get('maxImageSize'); + + if (maxImageSize && maxImageSize > 0) { + var newSize = imageHelper.scaleToMaxSize(maxImageSize, size.w, size.h); + + editor.dom.setAttribs(imageDomElement, { 'width': Math.round(newSize.width), 'height': Math.round(newSize.height) }); + + // Images inserted via Media Picker will have a URL we can use for ImageResizer QueryStrings + // Images pasted/dragged in are not persisted to media until saved & thus will need to be added + if (imgUrl) { + mediaHelper.getProcessedImageUrl(imgUrl, + { + width: newSize.width, + height: newSize.height + }) + .then(function (resizedImgUrl) { + editor.dom.setAttrib(imageDomElement, 'data-mce-src', resizedImgUrl); + }); + } + + editor.execCommand("mceAutoResize", false, null, null); + } + } + + function isMediaPickerEnabled(toolbarItemArray) { + return toolbarItemArray.includes('umbmediapicker'); + } + + return { + + /** + * Returns a promise of the configuration object to initialize the TinyMCE editor + * @param {} args + * @returns {} + */ + getTinyMceEditorConfig: function (args) { + + //global defaults, called before/during init + tinymce.DOM.events.domLoaded = true; + tinymce.baseURL = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/lib/tinymce/"; // trailing slash important + + var promises = [ + this.configuration(), + getStyles(args.stylesheets) + ]; + + return $q.all(promises).then(function (result) { + + var tinyMceConfig = result[0]; + var styles = result[1]; + + var plugins = _.map(tinyMceConfig.plugins, function (plugin) { + return plugin.name; + }); + + // Plugins that must always be active + plugins.push("autoresize"); + + var modeInline = false; + var toolbar = args.toolbar.join(" "); + + // Based on mode set + // classic = Theme: modern, inline: false + // inline = Theme: modern, inline: true, + // distraction-free = Theme: inlite, inline: true + if (args.mode === "inline") { + modeInline = true; + } + else if (args.mode === "distraction-free") { + modeInline = true; + toolbar = false; + } + + //create a baseline Config to extend upon + var config = { + inline: modeInline, + plugins: plugins, + valid_elements: tinyMceConfig.validElements, + invalid_elements: tinyMceConfig.inValidElements, + extended_valid_elements: extendedValidElements, + menubar: false, + statusbar: false, + relative_urls: false, + autoresize_bottom_margin: 10, + content_css: styles.stylesheets, + style_formats: styles.styleFormats, + style_formats_autohide: true, + language: getLanguage(), + + //this would be for a theme other than inlite + toolbar: toolbar, + + body_class: "umb-rte", + + //see https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#cache_suffix + cache_suffix: "?umb__rnd=" + Umbraco.Sys.ServerVariables.application.cacheBuster + }; + + // Need to check if we are allowed to UPLOAD images + // This is done by checking if the insert image toolbar button is available + if (isMediaPickerEnabled(args.toolbar)) { + // Update the TinyMCE Config object to allow pasting + config.images_upload_handler = uploadImageHandler; + config.automatic_uploads = false; + config.images_replace_blob_uris = false; + + // This allows images to be pasted in & stored as Base64 until they get uploaded to server + config.paste_data_images = true; + } + + + if (args.htmlId) { + config.selector = `[id="${args.htmlId}"]`; + } else if (args.target) { + config.target = args.target; + } + + /** + The default paste config can be overwritten by defining these properties in the customConfig. + */ + var pasteConfig = { + paste_remove_styles_if_webkit: true, + paste_preprocess: cleanupPasteData + }; + + Utilities.extend(config, pasteConfig); + + if (tinyMceConfig.customConfig) { + + //if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to + // convert it to json instead of having it as a string since this is what tinymce requires + for (var i in tinyMceConfig.customConfig) { + var val = tinyMceConfig.customConfig[i]; + if (val) { + val = val.toString().trim(); + if (val.detectIsJson()) { + try { + tinyMceConfig.customConfig[i] = JSON.parse(val); + //now we need to check if this custom config key is defined in our baseline, if it is we don't want to + //overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise + //if it's an object it will overwrite the baseline + if (Utilities.isArray(config[i]) && Utilities.isArray(tinyMceConfig.customConfig[i])) { + //concat it and below this concat'd array will overwrite the baseline in Utilities.extend + tinyMceConfig.customConfig[i] = config[i].concat(tinyMceConfig.customConfig[i]); + } } - var syntax = comment.textContent.trim(); - var parsed = macroService.parseMacroSyntax(syntax); - macroData = parsed; + catch (e) { + //cannot parse, we'll just leave it + } + } + if (val === "true") { + tinyMceConfig.customConfig[i] = true; + } + if (val === "false") { + tinyMceConfig.customConfig[i] = false; + } } + } - var $ins = $macroDiv.find("ins"); + Utilities.extend(config, tinyMceConfig.customConfig); + } - //show the throbber - $macroDiv.addClass("loading"); + return config; - // Add the contenteditable="false" attribute - // As just the CSS class of .mceNonEditable is not working by itself?! - // TODO: At later date - use TinyMCE editor DOM manipulation as opposed to jQuery - $macroDiv.attr("contenteditable", "false"); + }); - var contentId = $routeParams.id; + }, - //need to wrap in safe apply since this might be occuring outside of angular + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#configuration + * @methodOf umbraco.services.tinyMceService + * + * @description + * Returns a collection of plugins available to the tinyMCE editor + * + */ + configuration: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "rteApiBaseUrl", + "GetConfiguration"), { + cache: true + }), + 'Failed to retrieve tinymce configuration'); + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#defaultPrevalues + * @methodOf umbraco.services.tinyMceService + * + * @description + * Returns a default configration to fallback on in case none is provided + * + */ + defaultPrevalues: function () { + var cfg = {}; + cfg.toolbar = ["ace", "styles", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "outdent", "indent", "link", "umbmediapicker", "umbmacro", "umbembeddialog"]; + cfg.stylesheets = []; + cfg.maxImageSize = 500; + return cfg; + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createInsertEmbeddedMedia + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the umbraco insert embedded media tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + */ + createInsertEmbeddedMedia: function (editor, callback) { + editor.ui.registry.addButton('umbembeddialog', { + icon: 'embed', + tooltip: 'Embed', + stateSelector: 'div[data-embed-url]', + onAction: function () { + + // Get the selected element + // Check nodename is a DIV and the claslist contains 'embeditem' + var selectedElm = editor.selection.getNode(); + var nodeName = selectedElm.nodeName; + var modify = null; + + if (nodeName.toUpperCase() === "DIV" && selectedElm.classList.contains("embeditem")) { + // See if we can go and get the attributes + var embedUrl = editor.dom.getAttrib(selectedElm, "data-embed-url"); + var embedWidth = editor.dom.getAttrib(selectedElm, "data-embed-width"); + var embedHeight = editor.dom.getAttrib(selectedElm, "data-embed-height"); + var embedConstrain = editor.dom.getAttrib(selectedElm, "data-embed-constrain"); + + modify = { + url: embedUrl, + width: parseInt(embedWidth) || 0, + height: parseInt(embedHeight) || 0, + constrain: embedConstrain + }; + } + + if (callback) { angularHelper.safeApply($rootScope, function () { - macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary) - .then(function (htmlResult) { - - $macroDiv.removeClass("loading"); - htmlResult = htmlResult.trim(); - if (htmlResult !== "") { - var wasDirty = editor.isDirty(); - $ins.html(htmlResult); - if (!wasDirty) { - editor.undoManager.clear(); - } - } - }); + // pass the active element along so we can retrieve it later + callback(selectedElm, modify); }); + } + } + }); + }, + insertEmbeddedMediaInEditor: function (editor, embed, activeElement) { + // Wrap HTML preview content here in a DIV with non-editable class of .mceNonEditable + // This turns it into a selectable/cutable block to move about + var wrapper = tinymce.activeEditor.dom.create('div', + { + 'class': 'mceNonEditable embeditem', + 'data-embed-url': embed.url, + 'data-embed-height': embed.height, + 'data-embed-width': embed.width, + 'data-embed-constrain': embed.constrain, + 'contenteditable': false }, + embed.preview); - createLinkPicker: function (editor, onClick) { + // Only replace if activeElement is an Embed element. + if (activeElement && activeElement.nodeName.toUpperCase() === "DIV" && activeElement.classList.contains("embeditem")) { + activeElement.replaceWith(wrapper); // directly replaces the html node + } else { + editor.selection.setNode(wrapper); + } + }, - function createLinkList(callback) { - return function () { - var linkList = editor.settings.link_list; - if (typeof (linkList) === "string") { - tinymce.util.XHR.send({ - url: linkList, - success: function (text) { - callback(tinymce.util.JSON.parse(text)); - } - }); - } else { - callback(linkList); - } - }; - } + createAceCodeEditor: function (editor, callback) { - function showDialog(linkList) { - var data = {}, - selection = editor.selection, - dom = editor.dom, - selectedElm, anchorElm, initialText; - var win, linkListCtrl, relListCtrl, targetListCtrl; - - function linkListChangeHandler(e) { - var textCtrl = win.find('#text'); - - if (!textCtrl.value() || (e.lastControl && textCtrl.value() === e.lastControl.text())) { - textCtrl.value(e.control.text()); - } - - win.find('#href').value(e.control.value()); - } - - function buildLinkList() { - var linkListItems = [{ - text: 'None', - value: '' - }]; - - tinymce.each(linkList, function (link) { - linkListItems.push({ - text: link.text || link.title, - value: link.value || link.url, - menu: link.menu - }); - }); - - return linkListItems; - } - - function buildRelList(relValue) { - var relListItems = [{ - text: 'None', - value: '' - }]; - - tinymce.each(editor.settings.rel_list, function (rel) { - relListItems.push({ - text: rel.text || rel.title, - value: rel.value, - selected: relValue === rel.value - }); - }); - - return relListItems; - } - - function buildTargetList(targetValue) { - var targetListItems = [{ - text: 'None', - value: '' - }]; - - if (!editor.settings.target_list) { - targetListItems.push({ - text: 'New window', - value: '_blank' - }); - } - - tinymce.each(editor.settings.target_list, function (target) { - targetListItems.push({ - text: target.text || target.title, - value: target.value, - selected: targetValue === target.value - }); - }); - - return targetListItems; - } - - function buildAnchorListControl(url) { - var anchorList = []; - - tinymce.each(editor.dom.select('a:not([href])'), function (anchor) { - var id = anchor.name || anchor.id; - - if (id) { - anchorList.push({ - text: id, - value: '#' + id, - selected: url.indexOf('#' + id) !== -1 - }); - } - }); - - if (anchorList.length) { - anchorList.unshift({ - text: 'None', - value: '' - }); - - return { - name: 'anchor', - type: 'listbox', - label: 'Anchors', - values: anchorList, - onselect: linkListChangeHandler - }; - } - } - - function updateText() { - if (!initialText && data.text.length === 0) { - this.parent().parent().find('#text')[0].value(this.value()); - } - } - - selectedElm = selection.getNode(); - anchorElm = dom.getParent(selectedElm, 'a[href]'); - - data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({ - format: 'text' - }); - data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : ''; - data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : ''; - data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : ''; - - if (selectedElm.nodeName === "IMG") { - data.text = initialText = " "; - } - - if (linkList) { - linkListCtrl = { - type: 'listbox', - label: 'Link list', - values: buildLinkList(), - onselect: linkListChangeHandler - }; - } - - if (editor.settings.target_list !== false) { - targetListCtrl = { - name: 'target', - type: 'listbox', - label: 'Target', - values: buildTargetList(data.target) - }; - } - - if (editor.settings.rel_list) { - relListCtrl = { - name: 'rel', - type: 'listbox', - label: 'Rel', - values: buildRelList(data.rel) - }; - } - - var currentTarget = null; - - //if we already have a link selected, we want to pass that data over to the dialog - if (anchorElm) { - var anchor = $(anchorElm); - currentTarget = { - name: anchor.attr("title"), - url: anchor.attr("href"), - target: anchor.attr("target") - }; - - // drop the lead char from the anchor text, if it has a value - var anchorVal = anchor[0].dataset.anchor; - if (anchorVal) { - currentTarget.anchor = anchorVal.substring(1); - } - - //locallink detection, we do this here, to avoid poluting the editorService - //so the editor service can just expect to get a node-like structure - if (currentTarget.url.indexOf("localLink:") > 0) { - // if the current link has an anchor, it needs to be considered when getting the udi/id - // if an anchor exists, reduce the substring max by its length plus two to offset the removed prefix and trailing curly brace - var linkId = currentTarget.url.substring(currentTarget.url.indexOf(":") + 1, currentTarget.url.lastIndexOf("}")); - - //we need to check if this is an INT or a UDI - var parsedIntId = parseInt(linkId, 10); - if (isNaN(parsedIntId)) { - //it's a UDI - currentTarget.udi = linkId; - } else { - currentTarget.id = linkId; - } - } - } - - angularHelper.safeApply($rootScope, - function () { - if (onClick) { - onClick(currentTarget, anchorElm); - } - }); - } - - editor.addButton('link', { - icon: 'link', - tooltip: 'Insert/edit link', - shortcut: 'Ctrl+K', - onclick: createLinkList(showDialog), - stateSelector: 'a[href]' + editor.ui.registry.addButton("ace", { + icon: "sourcecode", + tooltip: "View Source Code", + onAction: function () { + if (callback) { + angularHelper.safeApply($rootScope, function () { + callback(); }); + } + } + }); - editor.addButton('unlink', { - icon: 'unlink', - tooltip: 'Remove link', - cmd: 'unlink', - stateSelector: 'a[href]' - }); + }, - editor.addShortcut('Ctrl+K', '', createLinkList(showDialog)); - this.showDialog = showDialog; + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createMediaPicker + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the umbraco insert media tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + */ + createMediaPicker: function (editor, callback) { + editor.ui.registry.addButton('umbmediapicker', { + icon: 'image', + tooltip: 'Media Picker', + stateSelector: 'img[data-udi]', + onAction: function () { - editor.addMenuItem('link', { - icon: 'link', - text: 'Insert link', - shortcut: 'Ctrl+K', - onclick: createLinkList(showDialog), - stateSelector: 'a[href]', - context: 'insert', - prependToContext: true - }); + var selectedElm = editor.selection.getNode(), + currentTarget; - // the editor frame catches Ctrl+S and handles it with the system save dialog - // - we want to handle it in the content controller, so we'll emit an event instead - editor.addShortcut('Ctrl+S', '', function () { - angularHelper.safeApply($rootScope, function() { - eventsService.emit("rte.shortcut.save"); - }); - }); + if (selectedElm.nodeName === 'IMG') { + var img = $(selectedElm); - editor.addShortcut('Ctrl+P', '', function () { - angularHelper.safeApply($rootScope, function () { - eventsService.emit("rte.shortcut.saveAndPublish"); - }); - }); + var hasUdi = img.attr("data-udi") ? true : false; + var hasDataTmpImg = img.attr("data-tmpimg") ? true : false; - }, + currentTarget = { + altText: img.attr("alt"), + url: img.attr("src"), + caption: img.attr('data-caption') + }; - insertLinkInEditor: function (editor, target, anchorElm) { - - var href = target.url; - // We want to use the Udi. If it is set, we use it, else fallback to id, and finally to null - var hasUdi = target.udi ? true : false; - var id = hasUdi ? target.udi : (target.id ? target.id : null); - - // if an anchor exists, check that it is appropriately prefixed - if (target.anchor && target.anchor[0] !== '?' && target.anchor[0] !== '#') { - target.anchor = (target.anchor.indexOf('=') === -1 ? '#' : '?') + target.anchor; - } - - // the href might be an external url, so check the value for an anchor/qs - // href has the anchor re-appended later, hence the reset here to avoid duplicating the anchor - if (!target.anchor) { - var urlParts = href.split(/(#|\?)/); - if (urlParts.length === 3) { - href = urlParts[0]; - target.anchor = urlParts[1] + urlParts[2]; - } - } - - //Create a json obj used to create the attributes for the tag - function createElemAttributes() { - var a = { - href: href, - title: target.name, - target: target.target ? target.target : null, - rel: target.rel ? target.rel : null - }; - - if (target.anchor) { - a["data-anchor"] = target.anchor; - a.href = a.href + target.anchor; - } else { - a["data-anchor"] = null; - } - - return a; - } - - function insertLink() { - if (anchorElm) { - editor.dom.setAttribs(anchorElm, createElemAttributes()); - editor.selection.select(anchorElm); - editor.execCommand('mceEndTyping'); - } else { - var selectedContent = editor.selection.getContent(); - // If there is no selected content, we can't insert a link - // as TinyMCE needs selected content for this, so instead we - // create a new dom element and insert it, using the chosen - // link name as the content. - if (selectedContent !== "") { - editor.execCommand('mceInsertLink', false, createElemAttributes()); - } else { - // Using the target url as a fallback, as href might be confusing with a local link - var linkContent = typeof target.name !== "undefined" && target.name !== "" ? target.name : target.url - var domElement = editor.dom.createHTML("a", createElemAttributes(), linkContent); - editor.execCommand('mceInsertContent', false, domElement); - } - } - } - - if (!href && !target.anchor) { - editor.execCommand('unlink'); - return; - } - - //if we have an id, it must be a locallink:id - if (id) { - - href = "/{localLink:" + id + "}"; - - insertLink(); - return; - } - - if (!href) { - href = ""; - } - - // Is email and not //user@domain.com and protocol (e.g. mailto:, sip:) is not specified - if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf(':') === -1) { - // assume it's a mailto link - href = 'mailto:' + href; - insertLink(); - return; - } - - // Is www. prefixed - if (/^\s*www\./i.test(href)) { - href = 'http://' + href; - insertLink(); - return; - } - - insertLink(); - - }, - - pinToolbar : function (editor) { - - //we can't pin the toolbar if this doesn't exist (i.e. when in distraction free mode) - if (!editor.editorContainer) { - return; - } - - var tinyMce = $(editor.editorContainer); - var toolbar = tinyMce.find(".mce-toolbar"); - var toolbarHeight = toolbar.height(); - var tinyMceRect = editor.editorContainer.getBoundingClientRect(); - var tinyMceTop = tinyMceRect.top; - var tinyMceBottom = tinyMceRect.bottom; - var tinyMceWidth = tinyMceRect.width; - - var tinyMceEditArea = tinyMce.find(".mce-edit-area"); - - // set padding in top of mce so the content does not "jump" up - tinyMceEditArea.css("padding-top", toolbarHeight); - - if (tinyMceTop < 177 && ((177 + toolbarHeight) < tinyMceBottom)) { - toolbar - .css("position", "fixed") - .css("top", "177px") - .css("left", "auto") - .css("right", "auto") - .css("width", tinyMceWidth); + if (hasUdi) { + currentTarget["udi"] = img.attr("data-udi"); } else { - toolbar - .css("position", "absolute") - .css("left", "") - .css("right", "") - .css("top", "") - .css("width", ""); + currentTarget["id"] = img.attr("rel"); } + if (hasDataTmpImg) { + currentTarget["tmpimg"] = img.attr("data-tmpimg"); + } + } + + userService.getCurrentUser().then(function (userData) { + if (callback) { + angularHelper.safeApply($rootScope, function () { + callback(currentTarget, userData); + }); + } + }); + } + }); + }, + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#insetMediaInEditor + * @methodOf umbraco.services.tinyMceService + * + * @description + * Inserts the image element in tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + */ + insertMediaInEditor: function (editor, img) { + if (img) { + // We need to create a NEW DOM element to insert + // setting an attribute of ID to __mcenew, so we can gather a reference to the node, to be able to update its size accordingly to the size of the image. + var data = { + alt: img.altText || "", + src: (img.url) ? img.url : "nothing.jpg", + id: "__mcenew", + "data-udi": img.udi, + "data-caption": img.caption + }; + var newImage = editor.dom.createHTML('img', data); + var parentElement = editor.selection.getNode().parentElement; + + if (img.caption) { + var figCaption = editor.dom.createHTML('figcaption', {}, img.caption); + var combined = newImage + figCaption; + + if (parentElement.nodeName !== 'FIGURE') { + var fragment = editor.dom.createHTML('figure', {}, combined); + editor.selection.setContent(fragment); + } + else { + parentElement.innerHTML = combined; + } + } + else { + //if caption is removed, remove the figure element + if (parentElement.nodeName === 'FIGURE') { + parentElement.parentElement.innerHTML = newImage; + } + else { + editor.selection.setContent(newImage); + } + } + + // Using settimeout to wait for a DoM-render, so we can find the new element by ID. + $timeout(function () { + + var imgElm = editor.dom.get("__mcenew"); + editor.dom.setAttrib(imgElm, "id", null); + + // When image is loaded we are ready to call sizeImageInEditor. + var onImageLoaded = function () { + sizeImageInEditor(editor, imgElm, img.url); + editor.dispatch("Change"); + } + + // Check if image already is loaded. + if (imgElm.complete === true) { + onImageLoaded(); + } else { + imgElm.onload = onImageLoaded; + } + + }); + + } + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createUmbracoMacro + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the insert umbraco macro tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + */ + createInsertMacro: function (editor, callback) { + + let self = this; + let activeMacroElement = null; //track an active macro element + + /** Adds custom rules for the macro plugin and custom serialization */ + editor.on('preInit', function (args) { + //this is requires so that we tell the serializer that a 'div' is actually allowed in the root, otherwise the cleanup will strip it out + editor.serializer.addRules('div'); + + /** This checks if the div is a macro container, if so, checks if its wrapped in a p tag and then unwraps it (removes p tag) */ + editor.serializer.addNodeFilter('div', function (nodes, name) { + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].attr("class") === "umb-macro-holder" && nodes[i].parent && nodes[i].parent.name.toUpperCase() === "P") { + nodes[i].parent.unwrap(); + } + } + }); + + }); + + /** when the contents load we need to find any macros declared and load in their content */ + editor.on("SetContent", function () { + + //get all macro divs and load their content + $(editor.dom.select(".umb-macro-holder.mceNonEditable")).each(function () { + self.loadMacroContent($(this), null, editor); + }); + + }); + + /** + * Because the macro gets wrapped in a P tag because of the way 'enter' works, this + * method will return the macro element if not wrapped in a p, or the p if the macro + * element is the only one inside of it even if we are deep inside an element inside the macro + */ + function getRealMacroElem(element) { + var e = $(element).closest(".umb-macro-holder"); + if (e.length > 0) { + if (e.get(0).parentNode.nodeName === "P") { + //now check if we're the only element + if (element.parentNode.childNodes.length === 1) { + return e.get(0).parentNode; + } + } + return e.get(0); + } + return null; + } + + /** Adds the button instance */ + editor.ui.registry.addButton('umbmacro', { + icon: 'preferences', + tooltip: 'Insert macro', + onSetup: function (buttonApi) { + /** + * Check if the macro is currently selected and toggle the menu button + */ + function onNodeChanged(evt) { + + //set our macro button active when on a node of class umb-macro-holder + activeMacroElement = getRealMacroElem(evt.element); + + //set the button active/inactive + buttonApi.setEnabled(activeMacroElement === null); + } + + //set onNodeChanged event listener + editor.on('NodeChange', onNodeChanged); + + return function () { + //remove the event listener + editor.off('NodeChange', onNodeChanged); + } + }, - unpinToolbar: function (editor) { + /** The insert macro button click event handler */ + onAction: function () { - var tinyMce = $(editor.editorContainer); - var toolbar = tinyMce.find(".mce-toolbar"); - var tinyMceEditArea = tinyMce.find(".mce-edit-area"); - // reset padding in top of mce so the content does not "jump" up - tinyMceEditArea.css("padding-top", "0"); - toolbar.css("position", "static"); + var dialogData = { + //flag for use in rte so we only show macros flagged for the editor + richTextEditor: true + }; + + //when we click we could have a macro already selected and in that case we'll want to edit the current parameters + //so we'll need to extract them and submit them to the dialog. + if (activeMacroElement) { + //we have a macro selected so we'll need to parse it's alias and parameters + var contents = $(activeMacroElement).contents(); + var comment = _.find(contents, function (item) { + return item.nodeType === 8; + }); + if (!comment) { + throw "Cannot parse the current macro, the syntax in the editor is invalid"; + } + var syntax = comment.textContent.trim(); + var parsed = macroService.parseMacroSyntax(syntax); + dialogData = { + macroData: parsed, + activeMacroElement: activeMacroElement //pass the active element along so we can retrieve it later + }; + } + + if (callback) { + angularHelper.safeApply($rootScope, function () { + callback(dialogData); + }); + } + } + }); + }, + + insertMacroInEditor: function (editor, macroObject, activeMacroElement) { + + //Important note: the TinyMce plugin "noneditable" is used here so that the macro cannot be edited, + // for this to work the mceNonEditable class needs to come last and we also need to use the attribute contenteditable = false + // (even though all the docs and examples say that is not necessary) + + //put the macro syntax in comments, we will parse this out on the server side to be used + //for persisting. + var macroSyntaxComment = ""; + //create an id class for this element so we can re-select it after inserting + var uniqueId = "umb-macro-" + editor.dom.uniqueId(); + var macroDiv = editor.dom.create('div', + { + 'class': 'umb-macro-holder ' + macroObject.macroAlias + " " + uniqueId + ' mceNonEditable', + 'contenteditable': 'false' }, + macroSyntaxComment + 'Macro alias: ' + macroObject.macroAlias + ''); - /** Helper method to initialize the tinymce editor within Umbraco */ - initializeEditor: function (args) { + //if there's an activeMacroElement then replace it, otherwise set the contents of the selected node + if (activeMacroElement) { + activeMacroElement.replaceWith(macroDiv); //directly replaces the html node + } + else { + editor.selection.setNode(macroDiv); + } - if (!args.editor) { - throw "args.editor is required"; + var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId)); + editor.setDirty(true); + + //async load the macro content + this.loadMacroContent($macroDiv, macroObject, editor); + + }, + + /** loads in the macro content async from the server */ + loadMacroContent: function ($macroDiv, macroData, editor) { + + //if we don't have the macroData, then we'll need to parse it from the macro div + if (!macroData) { + var contents = $macroDiv.contents(); + var comment = _.find(contents, function (item) { + return item.nodeType === 8; + }); + if (!comment) { + throw "Cannot parse the current macro, the syntax in the editor is invalid"; + } + var syntax = comment.textContent.trim(); + var parsed = macroService.parseMacroSyntax(syntax); + macroData = parsed; + } + + var $ins = $macroDiv.find("ins"); + + //show the throbber + $macroDiv.addClass("loading"); + + // Add the contenteditable="false" attribute + // As just the CSS class of .mceNonEditable is not working by itself?! + // TODO: At later date - use TinyMCE editor DOM manipulation as opposed to jQuery + $macroDiv.attr("contenteditable", "false"); + + var contentId = $routeParams.id; + + //need to wrap in safe apply since this might be occuring outside of angular + angularHelper.safeApply($rootScope, function () { + macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary) + .then(function (htmlResult) { + + $macroDiv.removeClass("loading"); + htmlResult = htmlResult.trim(); + if (htmlResult !== "") { + var wasDirty = editor.isDirty(); + $ins.html(htmlResult); + if (!wasDirty) { + editor.undoManager.clear(); + } } - //if (!args.model.value) { - // throw "args.model.value is required"; - //} + }); + }); - // force TinyMCE to load plugins/themes from minified files (see http://archive.tinymce.com/wiki.php/api4:property.tinymce.suffix.static) - args.editor.suffix = ".min"; + }, - var unwatch = null; + createLinkPicker: function (editor, onClick) { - //Starts a watch on the model value so that we can update TinyMCE if the model changes behind the scenes or from the server - function startWatch() { - unwatch = $rootScope.$watch(() => args.model.value, function (newVal, oldVal) { - if (newVal !== oldVal) { - //update the display val again if it has changed from the server; - //uses an empty string in the editor when the value is null - args.editor.setContent(newVal || "", { format: 'raw' }); + function createLinkList(callback) { + return function () { + var linkList = editor.options.get('link_list'); - //we need to manually fire this event since it is only ever fired based on loading from the DOM, this - // is required for our plugins listening to this event to execute - args.editor.fire('LoadContent', null); - } - }); + if (linkList && typeof linkList === "string") { + fetch(linkList).then(function (response) { + callback(response.json()); + }).catch(function (error) { + console.log(error); + }); + } else { + callback(linkList); + } + }; + } + + function showDialog(linkList) { + var data = {}, + selection = editor.selection, + dom = editor.dom, + selectedElm, anchorElm, initialText; + var win, linkListCtrl, relListCtrl, targetListCtrl; + + function linkListChangeHandler(e) { + var textCtrl = win.find('#text'); + + if (!textCtrl.value() || (e.lastControl && textCtrl.value() === e.lastControl.text())) { + textCtrl.value(e.control.text()); + } + + win.find('#href').value(e.control.value()); + } + + function buildLinkList() { + var linkListItems = [{ + text: 'None', + value: '' + }]; + + tinymce.each(linkList, function (link) { + linkListItems.push({ + text: link.text || link.title, + value: link.value || link.url, + menu: link.menu + }); + }); + + return linkListItems; + } + + function buildRelList(relValue) { + var relListItems = [{ + text: 'None', + value: '' + }]; + + var linkRelList = editor.options.get('link_rel_list'); + if (linkRelList) { + tinymce.each(linkRelList, function (rel) { + relListItems.push({ + text: rel.text || rel.title, + value: rel.value, + selected: relValue === rel.value + }); + }); + } + + return relListItems; + } + + function buildTargetList(targetValue) { + var targetListItems = [{ + text: 'None', + value: '' + }]; + + var linkList = editor.options.get('link_list'); + if (linkList) { + tinymce.each(linkList, function (target) { + targetListItems.push({ + text: target.text || target.title, + value: target.value, + selected: targetValue === target.value + }); + }); + } else { + targetListItems.push({ + text: 'New window', + value: '_blank' + }); + } + + return targetListItems; + } + + selectedElm = selection.getNode(); + anchorElm = dom.getParent(selectedElm, 'a[href]'); + + data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({ + format: 'text' + }); + data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : ''; + data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : ''; + data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : ''; + + if (selectedElm.nodeName === "IMG") { + data.text = initialText = " "; + } + + if (linkList) { + linkListCtrl = { + type: 'listbox', + label: 'Link list', + values: buildLinkList(), + onselect: linkListChangeHandler + }; + } + + var optionsLinkList = editor.options.get('link_list'); + if (optionsLinkList !== false) { + targetListCtrl = { + name: 'target', + type: 'listbox', + label: 'Target', + values: buildTargetList(data.target) + }; + } + + var linkRelList = editor.options.get('link_rel_list'); + if (linkRelList) { + relListCtrl = { + name: 'rel', + type: 'listbox', + label: 'Rel', + values: buildRelList(data.rel) + }; + } + + var currentTarget = null; + + //if we already have a link selected, we want to pass that data over to the dialog + if (anchorElm) { + var anchor = $(anchorElm); + currentTarget = { + name: anchor.attr("title"), + url: anchor.attr("href"), + target: anchor.attr("target") + }; + + // drop the lead char from the anchor text, if it has a value + var anchorVal = anchor[0].dataset.anchor; + if (anchorVal) { + currentTarget.anchor = anchorVal.substring(1); + } + + //locallink detection, we do this here, to avoid poluting the editorService + //so the editor service can just expect to get a node-like structure + if (currentTarget.url.indexOf("localLink:") > 0) { + // if the current link has an anchor, it needs to be considered when getting the udi/id + // if an anchor exists, reduce the substring max by its length plus two to offset the removed prefix and trailing curly brace + var linkId = currentTarget.url.substring(currentTarget.url.indexOf(":") + 1, currentTarget.url.lastIndexOf("}")); + + //we need to check if this is an INT or a UDI + var parsedIntId = parseInt(linkId, 10); + if (isNaN(parsedIntId)) { + //it's a UDI + currentTarget.udi = linkId; + } else { + currentTarget.id = linkId; } + } + } - //Stops the watch on model.value which is done anytime we are manually updating the model.value - function stopWatch() { - if (unwatch) { - unwatch(); - } + angularHelper.safeApply($rootScope, + function () { + if (onClick) { + onClick(currentTarget, anchorElm); } + }); + } - function syncContent() { + editor.ui.registry.addButton('link', { + icon: 'link', + tooltip: 'Insert/edit link', + shortcut: 'Ctrl+K', + onAction: createLinkList(showDialog), + stateSelector: 'a[href]' + }); - if(args.model.value === args.editor.getContent()) { - return; - } + editor.ui.registry.addButton('unlink', { + icon: 'unlink', + tooltip: 'Remove link', + onAction: () => { + editor.execCommand('unlink'); + }, + stateSelector: 'a[href]' + }); - //stop watching before we update the value - stopWatch(); - angularHelper.safeApply($rootScope, function () { - args.model.value = args.editor.getContent(); + editor.addShortcut('Ctrl+K', '', createLinkList(showDialog)); + this.showDialog = showDialog; - //make the form dirty manually so that the track changes works, setting our model doesn't trigger - // the angular bits because tinymce replaces the textarea. - if (args.currentForm) { - args.currentForm.$setDirty(); - } - // With complex validation we need to set a input field to dirty, not the form. but we will keep the old code for backwards compatibility. - if (args.currentFormInput) { - args.currentFormInput.$setDirty(); - } - }); + editor.ui.registry.addMenuItem('link', { + icon: 'link', + text: 'Insert link', + shortcut: 'Ctrl+K', + onAction: createLinkList(showDialog), + stateSelector: 'a[href]', + context: 'insert', + prependToContext: true + }); - //re-watch the value - startWatch(); - } + // the editor frame catches Ctrl+S and handles it with the system save dialog + // - we want to handle it in the content controller, so we'll emit an event instead + editor.addShortcut('Ctrl+S', '', function () { + angularHelper.safeApply($rootScope, function () { + eventsService.emit("rte.shortcut.save"); + }); + }); - // If we can not find the insert image/media toolbar button - // Then we need to add an event listener to the editor - // That will update native browser drag & drop events - // To update the icon to show you can NOT drop something into the editor - - var toolbarItems = args.editor.settings.toolbar === false ? [] : args.editor.settings.toolbar.split(" "); - if(isMediaPickerEnabled(toolbarItems) === false){ - // Wire up the event listener - args.editor.on('dragend dragover draggesture dragdrop drop drag', function (e) { - e.preventDefault(); - e.dataTransfer.effectAllowed = "none"; - e.dataTransfer.dropEffect = "none"; - e.stopPropagation(); - }); - } + editor.addShortcut('Ctrl+P', '', function () { + angularHelper.safeApply($rootScope, function () { + eventsService.emit("rte.shortcut.saveAndPublish"); + }); + }); - args.editor.on('SetContent', function (e) { - var content = e.content; + }, - // Upload BLOB images (dragged/pasted ones) - // find src attribute where value starts with `blob:` - // search is case-insensitive and allows single or double quotes - if(content.search(/src=["']blob:.*?["']/gi) !== -1){ - args.editor.uploadImages(function(data) { - // Once all images have been uploaded - data.forEach(function(item) { - // Select img element - var img = item.element; + insertLinkInEditor: function (editor, target, anchorElm) { - // Get img src - var imgSrc = img.getAttribute("src"); - var tmpLocation = localStorageService.get(`tinymce__${imgSrc}`) + var href = target.url; + // We want to use the Udi. If it is set, we use it, else fallback to id, and finally to null + var hasUdi = target.udi ? true : false; + var id = hasUdi ? target.udi : (target.id ? target.id : null); - // Select the img & add new attr which we can search for - // When its being persisted in RTE property editor - // To create a media item & delete this tmp one etc - tinymce.activeEditor.$(img).attr({ "data-tmpimg": tmpLocation }); + // if an anchor exists, check that it is appropriately prefixed + if (target.anchor && target.anchor[0] !== '?' && target.anchor[0] !== '#') { + target.anchor = (target.anchor.indexOf('=') === -1 ? '#' : '?') + target.anchor; + } - // Resize the image to the max size configured - // NOTE: no imagesrc passed into func as the src is blob://... - // We will append ImageResizing Querystrings on perist to DB with node save - sizeImageInEditor(args.editor, img); - }); + // the href might be an external url, so check the value for an anchor/qs + // href has the anchor re-appended later, hence the reset here to avoid duplicating the anchor + if (!target.anchor) { + var urlParts = href.split(/(#|\?)/); + if (urlParts.length === 3) { + href = urlParts[0]; + target.anchor = urlParts[1] + urlParts[2]; + } + } + //Create a json obj used to create the attributes for the tag + function createElemAttributes() { + var a = { + href: href, + title: target.name, + target: target.target ? target.target : null, + rel: target.rel ? target.rel : null + }; - }); + if (target.anchor) { + a["data-anchor"] = target.anchor; + a.href = a.href + target.anchor; + } else { + a["data-anchor"] = null; + } - // Get all img where src starts with blob: AND does NOT have a data=tmpimg attribute - // This is most likely seen as a duplicate image that has already been uploaded - // editor.uploadImages() does not give us any indiciation that the image been uploaded already - var blobImageWithNoTmpImgAttribute = args.editor.dom.select("img[src^='blob:']:not([data-tmpimg])"); + return a; + } - //For each of these selected items - blobImageWithNoTmpImgAttribute.forEach(imageElement => { - var blobSrcUri = args.editor.dom.getAttrib(imageElement, "src"); + function insertLink() { + if (anchorElm) { + editor.dom.setAttribs(anchorElm, createElemAttributes()); + editor.selection.select(anchorElm); + editor.execCommand('mceEndTyping'); + } else { + var selectedContent = editor.selection.getContent(); + // If there is no selected content, we can't insert a link + // as TinyMCE needs selected content for this, so instead we + // create a new dom element and insert it, using the chosen + // link name as the content. + if (selectedContent !== "") { + editor.execCommand('mceInsertLink', false, createElemAttributes()); + } else { + // Using the target url as a fallback, as href might be confusing with a local link + var linkContent = typeof target.name !== "undefined" && target.name !== "" ? target.name : target.url + var domElement = editor.dom.createHTML("a", createElemAttributes(), linkContent); + editor.execCommand('mceInsertContent', false, domElement); + } + } + } - // Find the same image uploaded (Should be in LocalStorage) - // May already exist in the editor as duplicate image - // OR added to the RTE, deleted & re-added again - // So lets fetch the tempurl out of localstorage for that blob URI item - var tmpLocation = localStorageService.get(`tinymce__${blobSrcUri}`) + if (!href && !target.anchor) { + editor.execCommand('unlink'); + return; + } - if(tmpLocation){ - sizeImageInEditor(args.editor, imageElement); - args.editor.dom.setAttrib(imageElement, "data-tmpimg", tmpLocation); - } - }); + //if we have an id, it must be a locallink:id + if (id) { - } + href = "/{localLink:" + id + "}"; - if(Umbraco.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce === true){ - /** prevent injecting arbitrary JavaScript execution in on-attributes. */ - const allNodes = Array.prototype.slice.call(args.editor.dom.doc.getElementsByTagName("*")); - allNodes.forEach(node => { - for (var i = 0; i < node.attributes.length; i++) { - if(node.attributes[i].name.indexOf("on") === 0) { - node.removeAttribute(node.attributes[i].name) - } - } - }); - } + insertLink(); + return; + } + if (!href) { + href = ""; + } + + // Is email and not //user@domain.com and protocol (e.g. mailto:, sip:) is not specified + if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf(':') === -1) { + // assume it's a mailto link + href = 'mailto:' + href; + insertLink(); + return; + } + + // Is www. prefixed + if (/^\s*www\./i.test(href)) { + href = 'http://' + href; + insertLink(); + return; + } + + insertLink(); + + }, + + /** Helper method to initialize the tinymce editor within Umbraco */ + initializeEditor: function (args) { + + if (!args.editor) { + throw "args.editor is required"; + } + //if (!args.model.value) { + // throw "args.model.value is required"; + //} + + // force TinyMCE to load plugins/themes from minified files (see http://archive.tinymce.com/wiki.php/api4:property.tinymce.suffix.static) + args.editor.suffix = ".min"; + + // Register custom option maxImageSize + args.editor.options.register('maxImageSize', { processor: 'number', default: 500 }); + + var unwatch = null; + + //Starts a watch on the model value so that we can update TinyMCE if the model changes behind the scenes or from the server + function startWatch() { + unwatch = $rootScope.$watch(() => args.model.value, function (newVal, oldVal) { + if (newVal !== oldVal) { + //update the display val again if it has changed from the server; + //uses an empty string in the editor when the value is null + args.editor.setContent(newVal || "", { format: 'raw' }); + + //we need to manually dispatch this event since it is only ever dispatchd based on loading from the DOM, this + // is required for our plugins listening to this event to execute + args.editor.dispatch('LoadContent', null); + } + }); + } + + //Stops the watch on model.value which is done anytime we are manually updating the model.value + function stopWatch() { + if (unwatch) { + unwatch(); + } + } + + function syncContent() { + + if (args.model.value === args.editor.getContent()) { + return; + } + + //stop watching before we update the value + stopWatch(); + angularHelper.safeApply($rootScope, function () { + args.model.value = args.editor.getContent(); + + //make the form dirty manually so that the track changes works, setting our model doesn't trigger + // the angular bits because tinymce replaces the textarea. + if (args.currentForm) { + args.currentForm.$setDirty(); + } + // With complex validation we need to set a input field to dirty, not the form. but we will keep the old code for backwards compatibility. + if (args.currentFormInput) { + args.currentFormInput.$setDirty(); + } + }); + + //re-watch the value + startWatch(); + } + + // If we can not find the insert image/media toolbar button + // Then we need to add an event listener to the editor + // That will update native browser drag & drop events + // To update the icon to show you can NOT drop something into the editor + if (args.toolbar && isMediaPickerEnabled(args.toolbar) === false) { + // Wire up the event listener + args.editor.on('dragstart dragend dragover draggesture dragdrop drop drag', function (e) { + e.preventDefault(); + e.dataTransfer.effectAllowed = "none"; + e.dataTransfer.dropEffect = "none"; + e.stopPropagation(); + }); + } + + args.editor.on('SetContent', function (e) { + var content = e.content; + + // Upload BLOB images (dragged/pasted ones) + // find src attribute where value starts with `blob:` + // search is case-insensitive and allows single or double quotes + if (content.search(/src=["']blob:.*?["']/gi) !== -1) { + args.editor.uploadImages().then(function (data) { + // Once all images have been uploaded + data.forEach(function (item) { + // Skip items that failed upload + if (item.status === false) { + return; + } + + // Select img element + var img = item.element; + + // Get img src + var imgSrc = img.getAttribute("src"); + var tmpLocation = localStorageService.get(`tinymce__${imgSrc}`) + + // Select the img & add new attr which we can search for + // When its being persisted in RTE property editor + // To create a media item & delete this tmp one etc + args.editor.dom.setAttrib(img, "data-tmpimg", tmpLocation); + + // Resize the image to the max size configured + // NOTE: no imagesrc passed into func as the src is blob://... + // We will append ImageResizing Querystrings on perist to DB with node save + sizeImageInEditor(args.editor, img); }); - args.editor.on('init', function (e) { + // Get all img where src starts with blob: AND does NOT have a data=tmpimg attribute + // This is most likely seen as a duplicate image that has already been uploaded + // editor.uploadImages() does not give us any indiciation that the image been uploaded already + var blobImageWithNoTmpImgAttribute = args.editor.dom.select("img[src^='blob:']:not([data-tmpimg])"); - if (args.model.value) { - args.editor.setContent(args.model.value); - } + //For each of these selected items + blobImageWithNoTmpImgAttribute.forEach(imageElement => { + var blobSrcUri = args.editor.dom.getAttrib(imageElement, "src"); - //enable browser based spell checking - args.editor.getBody().setAttribute('spellcheck', true); + // Find the same image uploaded (Should be in LocalStorage) + // May already exist in the editor as duplicate image + // OR added to the RTE, deleted & re-added again + // So lets fetch the tempurl out of localstorage for that blob URI item + var tmpLocation = localStorageService.get(`tinymce__${blobSrcUri}`) - - /** Setup sanitization for preventing injecting arbitrary JavaScript execution in attributes: - * https://github.com/advisories/GHSA-w7jx-j77m-wp65 - * https://github.com/advisories/GHSA-5vm8-hhgr-jcjp - */ - const uriAttributesToSanitize = ['src', 'href', 'data', 'background', 'action', 'formaction', 'poster', 'xlink:href']; - const parseUri = function() { - // Encapsulated JS logic. - const safeSvgDataUrlElements = [ 'img', 'video' ]; - const scriptUriRegExp = /((java|vb)script|mhtml):/i; - const trimRegExp = /[\s\u0000-\u001F]+/g; - const isInvalidUri = (uri, tagName) => { - if (/^data:image\//i.test(uri)) { - return safeSvgDataUrlElements.indexOf(tagName) !== -1 && /^data:image\/svg\+xml/i.test(uri); - } else { - return /^data:/i.test(uri); - } - }; - - return function parseUri(uri, tagName) { - uri = uri.replace(trimRegExp, ''); - try { - // Might throw malformed URI sequence - uri = decodeURIComponent(uri); - } catch (ex) { - // Fallback to non UTF-8 decoder - uri = unescape(uri); - } - - if (scriptUriRegExp.test(uri)) { - return; - } - - if (isInvalidUri(uri, tagName)) { - return; - } - - return uri; - } - }(); - - if(Umbraco.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce === true){ - args.editor.serializer.addAttributeFilter(uriAttributesToSanitize, function (nodes) { - nodes.forEach(function(node) { - node.attributes.forEach(function(attr) { - const attrName = attr.name.toLowerCase(); - if(uriAttributesToSanitize.indexOf(attrName) !== -1) { - attr.value = parseUri(attr.value, node.name); - } - }); - }); - }); - } - - //start watching the value - startWatch(); - }); - - args.editor.on('Change', function (e) { - syncContent(); - }); - args.editor.on('Keyup', function (e) { - syncContent(); - }); - - //when we leave the editor (maybe) - args.editor.on('blur', function (e) { - syncContent(); - }); - - // When the element is removed from the DOM, we need to terminate - // any active watchers to ensure scopes are disposed and do not leak. - // No need to sync content as that has already happened. - args.editor.on('remove', () => stopWatch()); - - args.editor.on('ObjectResized', function (e) { - var srcAttr = $(e.target).attr("src"); - var path = srcAttr.split("?")[0]; - mediaHelper.getProcessedImageUrl(path, { - width: e.width, - height: e.height, - mode: "max" - }).then(function (resizedPath) { - $(e.target).attr("data-mce-src", resizedPath); - }); - - syncContent(); - }); - - args.editor.on('Dirty', function (e) { - syncContent(); // Set model.value to the RTE's content - }); - - let self = this; - - //create link picker - self.createLinkPicker(args.editor, function (currentTarget, anchorElement) { - - entityResource.getAnchors(args.model.value).then(anchorValues => { - - const linkPicker = { - currentTarget: currentTarget, - dataTypeKey: args.model.dataTypeKey, - ignoreUserStartNodes: args.model.config.ignoreUserStartNodes, - anchors: anchorValues, - size: args.model.config.overlaySize, - submit: model => { - self.insertLinkInEditor(args.editor, model.target, anchorElement); - editorService.close(); - }, - close: () => { - editorService.close(); - } - }; - - editorService.linkPicker(linkPicker); - }); - - }); - - //Create the insert media plugin - self.createMediaPicker(args.editor, function (currentTarget, userData, imgDomElement) { - - var startNodeId, startNodeIsVirtual; - if (!args.model.config.startNodeId) { - if (args.model.config.ignoreUserStartNodes === true) { - startNodeId = -1; - startNodeIsVirtual = true; - } - else { - startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; - startNodeIsVirtual = userData.startMediaIds.length !== 1; - } - } - - var mediaPicker = { - currentTarget: currentTarget, - onlyImages: true, - showDetails: true, - disableFolderSelect: true, - disableFocalPoint: true, - startNodeId: startNodeId, - startNodeIsVirtual: startNodeIsVirtual, - dataTypeKey: args.model.dataTypeKey, - submit: function (model) { - self.insertMediaInEditor(args.editor, model.selection[0], imgDomElement); - editorService.close(); - }, - close: function () { - editorService.close(); - } - }; - editorService.mediaPicker(mediaPicker); - }); - - //Create the embedded plugin - self.createInsertEmbeddedMedia(args.editor, function (activeElement, modify) { - var embed = { - modify: modify, - submit: function (model) { - self.insertEmbeddedMediaInEditor(args.editor, model.embed, activeElement); - editorService.close(); - }, - close: function () { - editorService.close(); - } - }; - editorService.embed(embed); - }); - - //Create the insert macro plugin - self.createInsertMacro(args.editor, function (dialogData) { - var macroPicker = { - dialogData: dialogData, - submit: function (model) { - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); - self.insertMacroInEditor(args.editor, macroObject, dialogData.activeMacroElement); - editorService.close(); - }, - close: function () { - editorService.close(); - } - }; - editorService.macroPicker(macroPicker); - }); - - self.createAceCodeEditor(args.editor, function () { - - // TODO: CHECK TO SEE WHAT WE NEED TO DO WIT MACROS (See code block?) - /* - var html = editor.getContent({source_view: true}); - html = html.replace(/]*)>([^<]*)<\/span>/gm, String.fromCharCode(chr)); - editor.dom.remove(editor.dom.select('.CmCaReT')); - html = html.replace(/(
*)[\s\S]*?(<\/ins> *<\/div>)/ig, "$1Macro alias: $2$3"); - */ - - var aceEditor = { - content: args.editor.getContent(), - view: 'views/propertyeditors/rte/codeeditor.html', - submit: function (model) { - args.editor.setContent(model.content); - args.editor.fire('Change'); - editorService.close(); - }, - close: function () { - editorService.close(); - } - }; - - editorService.open(aceEditor); + if (tmpLocation) { + sizeImageInEditor(args.editor, imageElement); + args.editor.dom.setAttrib(imageElement, "data-tmpimg", tmpLocation); + } }); + }); } - }; + if (Umbraco.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce === true) { + /** prevent injecting arbitrary JavaScript execution in on-attributes. */ + const allNodes = Array.prototype.slice.call(args.editor.dom.doc.getElementsByTagName("*")); + allNodes.forEach(node => { + for (var i = 0; i < node.attributes.length; i++) { + if (node.attributes[i].name.indexOf("on") === 0) { + node.removeAttribute(node.attributes[i].name) + } + } + }); + } + + }); + + args.editor.on('init', function () { + + if (args.model.value) { + args.editor.setContent(args.model.value); + } + + //enable browser based spell checking + args.editor.getBody().setAttribute('spellcheck', true); + + /** Setup sanitization for preventing injecting arbitrary JavaScript execution in attributes: + * https://github.com/advisories/GHSA-w7jx-j77m-wp65 + * https://github.com/advisories/GHSA-5vm8-hhgr-jcjp + */ + const uriAttributesToSanitize = ['src', 'href', 'data', 'background', 'action', 'formaction', 'poster', 'xlink:href']; + const parseUri = function () { + // Encapsulated JS logic. + const safeSvgDataUrlElements = ['img', 'video']; + const scriptUriRegExp = /((java|vb)script|mhtml):/i; + const trimRegExp = /[\s\u0000-\u001F]+/g; + const isInvalidUri = (uri, tagName) => { + if (/^data:image\//i.test(uri)) { + return safeSvgDataUrlElements.indexOf(tagName) !== -1 && /^data:image\/svg\+xml/i.test(uri); + } else { + return /^data:/i.test(uri); + } + }; + + return function parseUri(uri, tagName) { + uri = uri.replace(trimRegExp, ''); + try { + // Might throw malformed URI sequence + uri = decodeURIComponent(uri); + } catch (ex) { + // Fallback to non UTF-8 decoder + uri = unescape(uri); + } + + if (scriptUriRegExp.test(uri)) { + return; + } + + if (isInvalidUri(uri, tagName)) { + return; + } + + return uri; + } + }(); + + if (Umbraco.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce === true) { + args.editor.serializer.addAttributeFilter(uriAttributesToSanitize, function (nodes) { + nodes.forEach(function (node) { + node.attributes.forEach(function (attr) { + const attrName = attr.name.toLowerCase(); + if (uriAttributesToSanitize.indexOf(attrName) !== -1) { + attr.value = parseUri(attr.value, node.name); + } + }); + }); + }); + } + + //start watching the value + startWatch(); + }); + + args.editor.on('Change', function (e) { + syncContent(); + }); + args.editor.on('Keyup', function (e) { + syncContent(); + }); + + //when we leave the editor (maybe) + args.editor.on('blur', function (e) { + syncContent(); + }); + + // When the element is removed from the DOM, we need to terminate + // any active watchers to ensure scopes are disposed and do not leak. + // No need to sync content as that has already happened. + args.editor.on('remove', () => stopWatch()); + + args.editor.on('ObjectResized', function (e) { + var srcAttr = $(e.target).attr("src"); + + if (!srcAttr) { + return; + } + + var path = srcAttr.split("?")[0]; + mediaHelper.getProcessedImageUrl(path, { + width: e.width, + height: e.height, + mode: "max" + }).then(function (resizedPath) { + $(e.target).attr("data-mce-src", resizedPath); + }); + + syncContent(); + }); + + args.editor.on('Dirty', function (e) { + syncContent(); // Set model.value to the RTE's content + }); + + let self = this; + + //create link picker + self.createLinkPicker(args.editor, function (currentTarget, anchorElement) { + + entityResource.getAnchors(args.model.value).then(anchorValues => { + + const linkPicker = { + currentTarget: currentTarget, + dataTypeKey: args.model.dataTypeKey, + ignoreUserStartNodes: args.model.config.ignoreUserStartNodes, + anchors: anchorValues, + size: args.model.config.overlaySize, + submit: model => { + self.insertLinkInEditor(args.editor, model.target, anchorElement); + editorService.close(); + }, + close: () => { + editorService.close(); + } + }; + + editorService.linkPicker(linkPicker); + }); + + }); + + //Create the insert media plugin + self.createMediaPicker(args.editor, function (currentTarget, userData, imgDomElement) { + + var startNodeId, startNodeIsVirtual; + if (!args.model.config.startNodeId) { + if (args.model.config.ignoreUserStartNodes === true) { + startNodeId = -1; + startNodeIsVirtual = true; + } + else { + startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + startNodeIsVirtual = userData.startMediaIds.length !== 1; + } + } + + var mediaPicker = { + currentTarget: currentTarget, + onlyImages: true, + showDetails: true, + disableFolderSelect: true, + disableFocalPoint: true, + startNodeId: startNodeId, + startNodeIsVirtual: startNodeIsVirtual, + dataTypeKey: args.model.dataTypeKey, + submit: function (model) { + self.insertMediaInEditor(args.editor, model.selection[0], imgDomElement); + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.mediaPicker(mediaPicker); + }); + + //Create the embedded plugin + self.createInsertEmbeddedMedia(args.editor, function (activeElement, modify) { + var embed = { + modify: modify, + submit: function (model) { + self.insertEmbeddedMediaInEditor(args.editor, model.embed, activeElement); + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.embed(embed); + }); + + //Create the insert macro plugin + self.createInsertMacro(args.editor, function (dialogData) { + var macroPicker = { + dialogData: dialogData, + submit: function (model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); + self.insertMacroInEditor(args.editor, macroObject, dialogData.activeMacroElement); + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.macroPicker(macroPicker); + }); + + self.createAceCodeEditor(args.editor, function () { + + // TODO: CHECK TO SEE WHAT WE NEED TO DO WIT MACROS (See code block?) + /* + var html = editor.getContent({source_view: true}); + html = html.replace(/]*)>([^<]*)<\/span>/gm, String.fromCharCode(chr)); + editor.dom.remove(editor.dom.select('.CmCaReT')); + html = html.replace(/(
*)[\s\S]*?(<\/ins> *<\/div>)/ig, "$1Macro alias: $2$3"); + */ + + var aceEditor = { + content: args.editor.getContent(), + view: 'views/propertyeditors/rte/codeeditor.html', + submit: function (model) { + args.editor.setContent(model.content); + args.editor.dispatch('Change'); + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + + editorService.open(aceEditor); + }); + + } + + }; } angular.module('umbraco.services').factory('tinyMceService', tinyMceService); diff --git a/src/Umbraco.Web.UI.Client/src/less/rte.less b/src/Umbraco.Web.UI.Client/src/less/rte.less index f9406c72b8..8ff4d128ed 100644 --- a/src/Umbraco.Web.UI.Client/src/less/rte.less +++ b/src/Umbraco.Web.UI.Client/src/less/rte.less @@ -140,6 +140,13 @@ } } +.tox-tinymce-inline { + z-index: 999; +} + +.tox-tinymce-fullscreen { + position: absolute; +} .mce-menu { border-radius: 3px; @@ -170,16 +177,3 @@ border: 1px solid @gray-8; max-width: none; } - -.mce-fullscreen { - position: absolute; - - .mce-in { - position: fixed; - top: 35px !important; - } - - umb-editor__overlay, .umb-editor { - position: fixed; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index de93b8470a..765a5164b0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -15,10 +15,6 @@ angular.module("umbraco") if (!editorConfig || Utilities.isString(editorConfig)) { editorConfig = tinyMceService.defaultPrevalues(); } - //make sure there's a max image size - if (!editorConfig.maxImageSize && editorConfig.maxImageSize !== 0) { - editorConfig.maxImageSize = tinyMceService.defaultPrevalues().maxImageSize; - } var width = editorConfig.dimensions ? parseInt(editorConfig.dimensions.width, 10) || null : null; var height = editorConfig.dimensions ? parseInt(editorConfig.dimensions.height, 10) || null : null; @@ -81,6 +77,7 @@ angular.module("umbraco") //initialize the standard editor functionality for Umbraco tinyMceService.initializeEditor({ editor: editor, + toolbar: editorConfig.toolbar, model: $scope.model, currentFormInput: $scope.rteForm.modelValue }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html index 96de0cd040..0926efd6a4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html @@ -40,8 +40,9 @@
diff --git a/src/Umbraco.Web.UI/appsettings.Development.template.json b/src/Umbraco.Web.UI/appsettings.Development.template.json index 33c5dd2fae..b2bde5bf2d 100644 --- a/src/Umbraco.Web.UI/appsettings.Development.template.json +++ b/src/Umbraco.Web.UI/appsettings.Development.template.json @@ -41,18 +41,6 @@ "RuntimeMinification": { "useInMemoryCache": true, "cacheBuster": "Timestamp" - }, - "RichTextEditor": { - "Commands": [ - { - "Alias": "fullscreen", - "Name": "Full Screen", - "Mode": "All" - } - ], - "Plugins": [ - "fullscreen" - ] } } } diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts index 9288dd6f72..649d1835f9 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts @@ -675,7 +675,7 @@ context('Content', () => { cy.umbracoTreeItem("content", [viewMacroName]).click(); // Insert macro - cy.get('#mceu_13-button').click(); + cy.get('button[title="Insert macro"]').click(); cy.get('.umb-card-grid-item').contains(viewMacroName).click(); // Save and publish diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index e0572c1cb3..223f83c12b 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -2339,10 +2339,16 @@ } }, "dependencies": { + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true + }, "@cypress/request": { - "version": "2.88.7", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.7.tgz", - "integrity": "sha512-FTULIP2rnDJvZDT9t6B4nSfYR40ue19tVmv3wUcY05R9/FPCoMl1nAPJkzWzBCo7ltVn5ThQTbxiMoGBN7k0ig==", + "version": "2.88.10", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz", + "integrity": "sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==", "dev": true, "requires": { "aws-sign2": "~0.7.0", @@ -2352,8 +2358,7 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", + "http-signature": "~1.3.6", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", @@ -2459,18 +2464,6 @@ } } }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -2535,9 +2528,9 @@ "dev": true }, "async": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.2.tgz", - "integrity": "sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", "dev": true }, "asynckit": { @@ -2683,13 +2676,12 @@ } }, "cli-table3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz", - "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", + "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==", "dev": true, "requires": { - "colors": "^1.1.2", - "object-assign": "^4.1.0", + "@colors/colors": "1.5.0", "string-width": "^4.2.0" } }, @@ -2730,12 +2722,6 @@ "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", "dev": true }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true - }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2790,9 +2776,9 @@ } }, "cy-verify-downloads": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/cy-verify-downloads/-/cy-verify-downloads-0.0.5.tgz", - "integrity": "sha512-aRK7VvKG5rmDJK4hjZ27KM2oOOz0cMO7z/j4zX8qCc4ffXZS1XRJkofUY0w5u6MCB/wUsNMs03VuvkeR2tNPoQ==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/cy-verify-downloads/-/cy-verify-downloads-0.1.5.tgz", + "integrity": "sha512-8iviQ+LhZ9z7bUEfN5YOGqYy292tSDHVDNsz9eaGZ97dVybgx9NhkSyk//2rVXIV97JBIdx8GIeGBBD+JBB27w==", "dev": true }, "cycle": { @@ -2802,9 +2788,9 @@ "dev": true }, "cypress": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-8.4.1.tgz", - "integrity": "sha512-itJXq0Vx3sXCUrDyBi2IUrkxVu/gTTp1VhjB5tzGgkeCR8Ae+/T8WV63rsZ7fS8Tpq7LPPXiyoM/sEdOX7cR6A==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-8.7.0.tgz", + "integrity": "sha512-b1bMC3VQydC6sXzBMFnSqcvwc9dTZMgcaOzT0vpSD+Gq1yFc+72JDWi55sfUK5eIeNLAtWOGy1NNb6UlhMvB+Q==", "dev": true, "requires": { "@cypress/request": "^2.88.6", @@ -2841,6 +2827,7 @@ "minimist": "^1.2.5", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "proxy-from-env": "1.0.0", "ramda": "~0.27.1", "request-progress": "^3.0.0", "supports-color": "^8.1.1", @@ -3021,12 +3008,6 @@ "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=", "dev": true }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, "fast-glob": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", @@ -3040,12 +3021,6 @@ "micromatch": "^4.0.4" } }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, "fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -3196,22 +3171,6 @@ "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", "dev": true }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "dev": true, - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3219,14 +3178,14 @@ "dev": true }, "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", "dev": true, "requires": { "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" } }, "human-signals": { @@ -3358,15 +3317,9 @@ "dev": true }, "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "dev": true }, "json-stringify-safe": { @@ -3386,14 +3339,14 @@ } }, "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" } }, @@ -3500,18 +3453,18 @@ } }, "mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true }, "mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "requires": { - "mime-db": "1.51.0" + "mime-db": "1.52.0" } }, "mimic-fn": { @@ -3562,12 +3515,6 @@ "path-key": "^3.0.0" } }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3650,26 +3597,24 @@ "dev": true }, "prompt": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.2.0.tgz", - "integrity": "sha512-iGerYRpRUg5ZyC+FJ/25G5PUKuWAGRjW1uOlhX7Pi3O5YygdK6R+KEaBjRbHSkU5vfS5PZCltSPZdDtUYwRCZA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.3.0.tgz", + "integrity": "sha512-ZkaRWtaLBZl7KKAKndKYUL8WqNT+cQHKRZnT4RYYms48jQkFw3rrBL+/N5K/KtdEveHkxs982MX2BkDKub2ZMg==", "dev": true, "requires": { - "async": "~0.9.0", - "colors": "^1.1.2", + "@colors/colors": "1.5.0", + "async": "3.2.3", "read": "1.0.x", "revalidator": "0.1.x", "winston": "2.x" - }, - "dependencies": { - "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", - "dev": true - } } }, + "proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=", + "dev": true + }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -3693,9 +3638,9 @@ "dev": true }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", "dev": true }, "querystring": { @@ -3711,9 +3656,9 @@ "dev": true }, "ramda": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", - "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz", + "integrity": "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==", "dev": true }, "read": { @@ -3834,9 +3779,9 @@ } }, "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", "dev": true, "requires": { "asn1": "~0.2.3", @@ -3959,9 +3904,9 @@ "dev": true }, "typescript": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", - "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==" + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==" }, "umbraco-cypress-testhelpers": { "version": "1.0.0-beta-73", @@ -3985,15 +3930,6 @@ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, "url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 45ef9bef67..34c82f4441 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -7,16 +7,20 @@ "test": "npx cypress run", "ui": "npx cypress open" }, + "engines": { + "node": ">=14.0.0 <17", + "npm": ">=8.0.0 < 9" + }, "devDependencies": { - "cross-env": "^7.0.2", - "cypress": "8.4.1", - "cy-verify-downloads": "0.0.5", + "cross-env": "^7.0.3", + "cypress": "8.7.0", + "cy-verify-downloads": "0.1.5", "del": "^6.0.0", "ncp": "^2.0.0", "prompt": "^1.2.0", "umbraco-cypress-testhelpers": "^1.0.0-beta-73" }, "dependencies": { - "typescript": "^3.9.2" + "typescript": "^4.6.3" } } diff --git a/tests/Umbraco.Tests.AcceptanceTest/tsconfig.json b/tests/Umbraco.Tests.AcceptanceTest/tsconfig.json index 96178bfc54..2c71b50b6a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tsconfig.json +++ b/tests/Umbraco.Tests.AcceptanceTest/tsconfig.json @@ -1,37 +1,36 @@ { - "compileOnSave": false, - "compilerOptions": { - "baseUrl": "./", - "outDir": "./lib", - "sourceMap": false, - "declaration": true, - "module": "CommonJS", - "moduleResolution": "node", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "esModuleInterop": true, - "importHelpers": true, - "target": "es5", - - "types": [ - "cypress", - "cy-verify-downloads" - ], - "lib": [ - "es5", - "dom" - ], - "plugins": [ - { - "name": "typescript-tslint-plugin", - "alwaysShowRuleFailuresAsWarnings": false, - "ignoreDefinitionFiles": true, - "configFile": "tslint.json", - "suppressWhileTypeErrorsPresent": false - } - ] - }, - "include": [ - "src/**/*.ts" + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./lib", + "sourceMap": false, + "declaration": true, + "module": "CommonJS", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "esModuleInterop": true, + "importHelpers": true, + "target": "es5", + "types": [ + "cypress", + "cy-verify-downloads" + ], + "lib": [ + "es5", + "dom" + ], + "plugins": [ + { + "name": "typescript-tslint-plugin", + "alwaysShowRuleFailuresAsWarnings": false, + "ignoreDefinitionFiles": true, + "configFile": "tslint.json", + "suppressWhileTypeErrorsPresent": false + } ] + }, + "include": [ + "cypress/**/*.ts" + ] }