+
+
+
+
+ Add between {{model.config.minNumber}} and {{model.config.maxNumber}} items
+
+ You can only have {{model.config.maxNumber}} items selected
+
+
+
+
+
+ Add {{model.config.minNumber - renderModel.length}} item(s)
+
+ You can only have {{model.config.maxNumber}} items selected
+
+
+
+
+
+ Add up to {{model.config.maxNumber}} items
+
+ You can only have {{model.config.maxNumber}} items selected
+
+
+
+
+
+ Add at least {{model.config.minNumber}} item(s)
+
+
+
+
+
+
+
+
+
+
+ You need to add at least {{model.config.minNumber}} items
+
+
+
+
+ You can only have {{model.config.maxNumber}} items selected
+
+
+
+
diff --git a/src/Umbraco.Web/Models/ContentEditing/LinkDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/LinkDisplay.cs
new file mode 100644
index 0000000000..857ae2c318
--- /dev/null
+++ b/src/Umbraco.Web/Models/ContentEditing/LinkDisplay.cs
@@ -0,0 +1,36 @@
+using System.Runtime.Serialization;
+using Umbraco.Core;
+
+namespace Umbraco.Web.Models.ContentEditing
+{
+ [DataContract(Name = "link", Namespace = "")]
+ internal class LinkDisplay
+ {
+ [DataMember(Name = "icon")]
+ public string Icon { get; set; }
+
+ [DataMember(Name = "isMedia")]
+ public bool IsMedia { get; set; }
+
+ [DataMember(Name = "name")]
+ public string Name { get; set; }
+
+ [DataMember(Name = "published")]
+ public bool Published { get; set; }
+
+ [DataMember(Name = "queryString")]
+ public string QueryString { get; set; }
+
+ [DataMember(Name = "target")]
+ public string Target { get; set; }
+
+ [DataMember(Name = "trashed")]
+ public bool Trashed { get; set; }
+
+ [DataMember(Name = "udi")]
+ public GuidUdi Udi { get; set; }
+
+ [DataMember(Name = "url")]
+ public string Url { get; set; }
+ }
+}
diff --git a/src/Umbraco.Web/Models/Link.cs b/src/Umbraco.Web/Models/Link.cs
new file mode 100644
index 0000000000..74ad4ad2af
--- /dev/null
+++ b/src/Umbraco.Web/Models/Link.cs
@@ -0,0 +1,13 @@
+using Umbraco.Core;
+
+namespace Umbraco.Web.Models
+{
+ public class Link
+ {
+ public string Name { get; set; }
+ public string Target { get; set; }
+ public LinkType Type { get; set; }
+ public Udi Udi { get; set; }
+ public string Url { get; set; }
+ }
+}
diff --git a/src/Umbraco.Web/Models/LinkType.cs b/src/Umbraco.Web/Models/LinkType.cs
new file mode 100644
index 0000000000..3db3165d7f
--- /dev/null
+++ b/src/Umbraco.Web/Models/LinkType.cs
@@ -0,0 +1,9 @@
+namespace Umbraco.Web.Models
+{
+ public enum LinkType
+ {
+ Content,
+ Media,
+ External
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs
new file mode 100644
index 0000000000..515512eff8
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs
@@ -0,0 +1,13 @@
+using Umbraco.Core.PropertyEditors;
+
+namespace Umbraco.Web.PropertyEditors
+{
+ public class MultiUrlPickerConfiguration
+ {
+ [ConfigurationField("minNumber", "Minimum number of items", "number")]
+ public int MinNumber { get; set; }
+
+ [ConfigurationField("maxNumber", "Maximum number of items", "number")]
+ public int MaxNumber { get; set; }
+ }
+}
diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfigurationEditor.cs
new file mode 100644
index 0000000000..e780e410a7
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfigurationEditor.cs
@@ -0,0 +1,8 @@
+using Umbraco.Core.PropertyEditors;
+
+namespace Umbraco.Web.PropertyEditors
+{
+ public class MultiUrlPickerConfigurationEditor : ConfigurationEditor
+ {
+ }
+}
diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs
new file mode 100644
index 0000000000..54bf5c4d15
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs
@@ -0,0 +1,26 @@
+using System;
+using Umbraco.Core;
+using Umbraco.Core.PropertyEditors;
+using Umbraco.Core.Logging;
+using Umbraco.Core.Services;
+using Umbraco.Web.PublishedCache;
+
+namespace Umbraco.Web.PropertyEditors
+{
+ [DataEditor(Constants.PropertyEditors.Aliases.MultiUrlPicker, EditorType.PropertyValue|EditorType.MacroParameter, "Multi Url Picker", "multiurlpicker", ValueType = "JSON", Group = "pickers", Icon = "icon-link")]
+ public class MultiUrlPickerPropertyEditor : DataEditor
+ {
+ private readonly IEntityService _entityService;
+ private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
+
+ public MultiUrlPickerPropertyEditor(ILogger logger, IEntityService entityService, IPublishedSnapshotAccessor publishedSnapshotAccessor) : base(logger, EditorType.PropertyValue|EditorType.MacroParameter)
+ {
+ _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService));
+ _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor));
+ }
+
+ protected override IConfigurationEditor CreateConfigurationEditor() => new MultiUrlPickerConfigurationEditor();
+
+ protected override IDataValueEditor CreateValueEditor() => new MultiUrlPickerValueEditor(_entityService, _publishedSnapshotAccessor, Logger, Attribute);
+ }
+}
diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs
new file mode 100644
index 0000000000..381627eac2
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs
@@ -0,0 +1,176 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+using Umbraco.Core;
+using Umbraco.Core.Logging;
+using Umbraco.Core.Models;
+using Umbraco.Core.Models.Editors;
+using Umbraco.Core.Models.Entities;
+using Umbraco.Core.PropertyEditors;
+using Umbraco.Core.Services;
+using Umbraco.Web.Models.ContentEditing;
+using Umbraco.Web.PublishedCache;
+
+namespace Umbraco.Web.PropertyEditors
+{
+ public class MultiUrlPickerValueEditor : DataValueEditor
+ {
+ private readonly IEntityService _entityService;
+ private readonly ILogger _logger;
+ private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
+
+ public MultiUrlPickerValueEditor(IEntityService entityService, IPublishedSnapshotAccessor publishedSnapshotAccessor, ILogger logger, DataEditorAttribute attribute) : base(attribute)
+ {
+ _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService));
+ _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null)
+ {
+ var value = property.GetValue(culture, segment)?.ToString();
+
+ if (string.IsNullOrEmpty(value))
+ {
+ return Enumerable.Empty