├── .editorconfig ├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RJP.MultiUrlPicker.sln ├── build.cmd ├── build.fsx ├── build ├── nuspec │ └── RJP.UmbracoMultiUrlPicker.nuspec └── version.txt └── src ├── RJP.MultiUrlPicker.Web.UI ├── MultiUrlPicker.html └── MultiUrlPicker.js └── RJP.MultiUrlPicker ├── Information.cs ├── Models ├── Link.cs ├── LinkDisplay.cs ├── LinkDto.cs ├── LinkType.cs └── MultiUrls.cs ├── MultiUrlPickerPropertyEditor.cs ├── MultiUrlPickerValueConverter.cs ├── Properties └── AssemblyInfo.cs ├── RJP.MultiUrlPicker.csproj ├── app.config └── packages.config /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 4 10 | end_of_line = crlf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [*.{js,css,yml}] 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .fake/ 2 | .nuget/ 3 | .vs/ 4 | .idea/ 5 | 6 | /packages/ 7 | /artifacts/ 8 | 9 | bin/ 10 | obj/ 11 | node_modules/ 12 | 13 | .DS_Store 14 | *.user 15 | *.suo 16 | *.log 17 | *.swp 18 | *.bak 19 | *.log 20 | *.orig 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | # 2.2.1 6 | ### Bugfixes 7 | * #88 - Can't edit anchor tag links 8 | 9 | # 2.2.0 10 | ### Features 11 | 12 | * #83 - Umbraco version 7.12.0 breaks UI 13 | * #82 - Moved `ng-repeat` to the `umb-node-preview` directive 14 | * Localized validation messages 15 | * Added helper messages for min/max number of items 16 | 17 | # 2.1.0 18 | ### Features 19 | 20 | * #45 - Feature Request : Querystring Parameters. Thanks to @mattbrailsford 21 | * No more "Could not find persisted pre-value for field (minNumberOfItems|maxNumberOfItems)" warnings in the Umbraco log 22 | 23 | ### Bugfixes 24 | 25 | * #60 - Only returns first item on startup 26 | 27 | # 2.0.1 28 | ### Bugfixes 29 | 30 | * #54 - Multi Url Picker cant add nodes 31 | 32 | # 2.0.0 33 | ### Features 34 | 35 | * Updated styling to match Umbraco v7.6 36 | * Added Udi support 37 | * New data types with `max number of items` set to 1 will return a single `Link` or `null`. 38 | Existing data types will continue to return `IEnumerable` 39 | 40 | ### Breaking 41 | 42 | * Upgraded Umbraco to v7.6 43 | 44 | # 1.3.2 45 | ### Bugfixes 46 | 47 | * #48 - Editing a media item breaks the link 48 | * #51 - Picked nodes are not resolved when deleted 49 | 50 | # 1.3.1 51 | ### Bugfixes 52 | 53 | * Fixed bug introduced when cleaning code 54 | 55 | # 1.3.0 56 | ### Features 57 | 58 | * #22 - Usage in macro parameter 59 | 60 | ### Bugfixes 61 | 62 | * #28 - Not working in combination with Doc Type Grid Editor Package 63 | 64 | # 1.2.0 65 | ### Features 66 | 67 | * #14 - Use url as name when Page Title is left empty 68 | 69 | # 1.1.0 70 | ### Features 71 | 72 | * #8 - Hide handle when only one item. 73 | 74 | ### Bugfixes 75 | 76 | * #11 - Validation problem with min/max and external links. 77 | * #13 - Gracefully handle when the recycling bin is chosen 78 | 79 | # 1.0.0 80 | ### Features 81 | 82 | * Added min/max items field 83 | * Added document icons to the editor 84 | 85 | ### Breaking 86 | 87 | * Upgraded to Umbraco v7.1.2 88 | * Removed Pick multiple items field 89 | 90 | # 0.2.0 91 | ### Features 92 | 93 | * Added property value converter 94 | 95 | # 0.1.1 96 | ### Bugfixes 97 | 98 | * Data is now stored in the ntext column 99 | 100 | # 0.1.0 101 | 102 | * Initial release 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Rasmus John Pedersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi Url Picker for Umbraco 7 2 | 3 | [![NuGet release](https://img.shields.io/nuget/v/RJP.UmbracoMultiUrlPicker.svg)](https://www.nuget.org/packages/RJP.UmbracoMultiUrlPicker) 4 | [![Our Umbraco project page](https://img.shields.io/badge/our-umbraco-orange.svg)](https://our.umbraco.org/projects/backoffice-extensions/multi-url-picker) 5 | 6 | Allows editors to pick and sort multiple urls, it uses Umbraco's link picker which supports internal and external links and media. 7 | 8 | ## Installation 9 | 10 | Install the NuGet [package](https://www.nuget.org/packages/RJP.UmbracoMultiUrlPicker). 11 | 12 | or 13 | 14 | Install the [package](http://our.umbraco.org/projects/backoffice-extensions/multi-url-picker) from the Umbraco package repository. 15 | 16 | ## Usage 17 | 18 | Add a new property to your document type and select the `Multi Url Picker` property editor in the `pickers` category. 19 | 20 | If you're using the models builder, you can access the property on your model e.g. `Model.Links` if your property alias is `links`. 21 | 22 | ```csharp 23 | @{ var links = Model.Links.ToList(); } 24 | 25 | @if (links.Count > 0) 26 | { 27 | 33 | } 34 | ``` 35 | 36 | If `Max number of items` is configured to 1 37 | 38 | ```csharp 39 | @if(Model.Link != null) 40 | { 41 | @Model.Link.Name 42 | } 43 | 44 | ``` 45 | 46 | ## Changelog 47 | See the [changelog here](CHANGELOG.md) 48 | -------------------------------------------------------------------------------- /RJP.MultiUrlPicker.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Files", "Files", "{F960CEAB-2C15-4617-9BA9-68E8CEBD9920}" 7 | ProjectSection(SolutionItems) = preProject 8 | CHANGELOG.md = CHANGELOG.md 9 | LICENSE = LICENSE 10 | README.md = README.md 11 | EndProjectSection 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RJP.MultiUrlPicker", "src\RJP.MultiUrlPicker\RJP.MultiUrlPicker.csproj", "{2553C499-3593-43ED-95D6-39B03CC61ED1}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {2553C499-3593-43ED-95D6-39B03CC61ED1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {2553C499-3593-43ED-95D6-39B03CC61ED1}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {2553C499-3593-43ED-95D6-39B03CC61ED1}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {2553C499-3593-43ED-95D6-39B03CC61ED1}.Release|Any CPU.Build.0 = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(SolutionProperties) = preSolution 27 | HideSolutionNode = FALSE 28 | EndGlobalSection 29 | EndGlobal 30 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | if not exist ".\.nuget" ( 4 | mkdir ".\.nuget" 5 | attrib +h ".\.nuget" /s /d 6 | ) 7 | SET "release=" 8 | SET "buildVersion=" 9 | FOR /F %%i IN (build/version.txt) DO IF NOT DEFINED release SET "release=%%i" 10 | if defined APPVEYOR_BUILD_NUMBER ( 11 | SET "buildVersion=build%APPVEYOR_BUILD_NUMBER%" 12 | ) else ( 13 | FOR /F "skip=1" %%i IN (build/version.txt) DO IF NOT DEFINED buildVersion SET "buildVersion=%%i" 14 | ) 15 | 16 | if not exist ".\.nuget\nuget.exe" powershell -Command "Invoke-WebRequest https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile .\.nuget\nuget.exe" 17 | if not exist ".\packages\Fake" ".nuget\NuGet.exe" "Install" "FAKE" "-OutputDirectory" "packages" "-ExcludeVersion" 18 | "packages\FAKE\tools\Fake.exe" build.fsx "%*" -ev release=%release% -ev buildVersion=%buildVersion% 19 | exit /b %errorlevel% 20 | -------------------------------------------------------------------------------- /build.fsx: -------------------------------------------------------------------------------- 1 | // include Fake lib 2 | #r @"packages/FAKE/tools/FakeLib.dll" 3 | open Fake 4 | 5 | // Properties 6 | let solutionFile = FindFirstMatchingFile "*.sln" currentDirectory 7 | let csprojFiles = filesInDirMatchingRecursive "*.csproj" (directoryInfo "./src/") 8 | let nuspecFiles = filesInDirMatching "*.nuspec" (directoryInfo "./build/nuspec/") 9 | let packageFiles = 10 | subDirectories (directoryInfo "./src") 11 | |> Seq.map (filesInDirMatching "package.json") 12 | |> Seq.concat 13 | 14 | let version = environVarOrFail "release" 15 | let informationalVersion = 16 | if hasBuildParam "buildVersion" then sprintf "%s-%s" version (environVar "buildVersion") 17 | else version 18 | 19 | let nugetPath = FullName "./.nuget/nuget.exe" 20 | let assemblyInfos = !! "./src/**/AssemblyInfo.cs" 21 | 22 | let artifactsDir = FullName "./artifacts/" 23 | let appPluginsDir = artifactsDir @@ "App_Plugins" @@ (fileNameWithoutExt solutionFile) 24 | 25 | let Exec command args workingDir = 26 | let result = Shell.Exec(command, args, workingDir) 27 | if result <> 0 then failwithf "%s exited with error %d" command result 28 | 29 | let yarnOrNpm = 30 | match tryFindFileOnPath "yarnpkg.cmd" with 31 | | Some path -> path 32 | | None -> 33 | match tryFindFileOnPath "npm.cmd" with 34 | | Some path -> path 35 | | None -> failwith "yarn or npm could not be found" 36 | 37 | // Targets 38 | Target "Clean" (fun _ -> 39 | CleanDirs [artifactsDir] 40 | ) 41 | 42 | Target "RestorePackages" (fun _ -> 43 | solutionFile 44 | |> RestoreMSSolutionPackages(fun p -> 45 | { p with 46 | Retries = 4 }) 47 | ) 48 | 49 | Target "RestoreUiPackages" (fun _ -> 50 | packageFiles 51 | |> Seq.iter(fun f -> Exec yarnOrNpm "install" f.DirectoryName) 52 | ) 53 | 54 | Target "AssemblyInfo" (fun _ -> 55 | ReplaceAssemblyInfoVersionsBulk assemblyInfos (fun f -> 56 | { f with 57 | AssemblyVersion = sprintf "%s.*" version 58 | AssemblyInformationalVersion = informationalVersion }) 59 | ) 60 | 61 | Target "Build" (fun _ -> 62 | csprojFiles 63 | |> Seq.iter (fun path -> 64 | let buildDir = artifactsDir @@ path.Directory.Name 65 | !! path.FullName 66 | |> MSBuildRelease buildDir "Build" 67 | |> ignore 68 | ) 69 | ) 70 | 71 | Target "BuildUI" (fun _ -> 72 | packageFiles 73 | |> Seq.iter(fun f -> Exec yarnOrNpm (sprintf "run build -- --output-path %s" appPluginsDir) f.DirectoryName) 74 | ) 75 | 76 | Target "Package" (fun _ -> 77 | nuspecFiles 78 | |> Seq.iter (fun path -> 79 | path.FullName 80 | |> NuGetPackDirectly (fun p -> 81 | { p with 82 | ToolPath = nugetPath 83 | WorkingDir = currentDirectory 84 | OutputPath = artifactsDir 85 | Version = informationalVersion })) 86 | ) 87 | 88 | Target "Default" DoNothing 89 | 90 | // Dependencies 91 | "Clean" 92 | ==> "RestorePackages" <=> "RestoreUiPackages" <=> "AssemblyInfo" 93 | ==> "Build" <=> "BuildUI" 94 | ==> "Default" 95 | 96 | "Build" <=> "BuildUi" 97 | ==> "Package" 98 | 99 | // start build 100 | RunTargetOrDefault "Default" 101 | -------------------------------------------------------------------------------- /build/nuspec/RJP.UmbracoMultiUrlPicker.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | RJP.UmbracoMultiUrlPicker 5 | $version$ 6 | Umbraco Multi Url Picker 7 | Rasmus John Pedersen 8 | Rasmus John Pedersen 9 | https://github.com/rasmusjp/umbraco-multi-url-picker/raw/master/LICENSE 10 | https://github.com/rasmusjp/umbraco-multi-url-picker 11 | false 12 | Allows editors to pick and sort multiple urls, it uses Umbraco's link picker which supports internal and external links and media. 13 | 14 | Copyright 2014 Rasmus John Pedersen 15 | umbraco, urlpicker, multiurlpicker 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /build/version.txt: -------------------------------------------------------------------------------- 1 | 2.2.1 -------------------------------------------------------------------------------- /src/RJP.MultiUrlPicker.Web.UI/MultiUrlPicker.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 14 | 15 |
16 | 17 | 22 | Add 23 | 24 | 25 |
26 | 27 | 28 | 29 | Add between {{model.config.minNumberOfItems}} and {{model.config.maxNumberOfItems}} items 30 | 31 | You can only have {{model.config.maxNumberOfItems}} items selected 32 | 33 | 34 | 35 | 36 | 37 | Add {{model.config.minNumberOfItems - ctrl.renderModel.length}} item(s) 38 | 39 | You can only have {{model.config.maxNumberOfItems}} items selected 40 | 41 | 42 | 43 | 44 | 45 | Add up to {{model.config.maxNumberOfItems}} items 46 | 47 | You can only have {{model.config.maxNumberOfItems}} items selected 48 | 49 | 50 | 51 | 52 | 53 | Add at least {{model.config.minNumberOfItems}} item(s) 54 | 55 | 56 |
57 | 58 | 59 | 60 | 61 | 62 |
63 | You need to add at least {{model.config.minNumberOfItems}} items 64 |
65 |
66 | You can only have {{model.config.maxNumberOfItems}} items selected 67 |
68 |
69 | 70 | 74 | 75 |
76 | -------------------------------------------------------------------------------- /src/RJP.MultiUrlPicker.Web.UI/MultiUrlPicker.js: -------------------------------------------------------------------------------- 1 | ; (function () { 2 | 'use strict' 3 | 4 | var MultiUrlPickerController = function ($scope, angularHelper, entityResource, iconHelper) { 5 | this.renderModel = [] 6 | 7 | if ($scope.preview) { 8 | return 9 | } 10 | 11 | if (!Array.isArray($scope.model.value)) { 12 | $scope.model.value = [] 13 | } 14 | 15 | $scope.model.value.forEach(function (link) { 16 | link.icon = iconHelper.convertFromLegacyIcon(link.icon) 17 | this.renderModel.push(link) 18 | }.bind(this)) 19 | 20 | $scope.$on('formSubmitting', function () { 21 | $scope.model.value = this.renderModel 22 | }.bind(this)) 23 | 24 | $scope.$watch(function () { 25 | return this.renderModel.length 26 | }.bind(this), function () { 27 | if ($scope.model.config && $scope.model.config.minNumberOfItems) { 28 | $scope.multiUrlPickerForm.minCount.$setValidity('minCount', +$scope.model.config.minNumberOfItems <= this.renderModel.length) 29 | } 30 | if ($scope.model.config && $scope.model.config.maxNumberOfItems) { 31 | $scope.multiUrlPickerForm.maxCount.$setValidity('maxCount', +$scope.model.config.maxNumberOfItems >= this.renderModel.length) 32 | } 33 | this.sortableOptions.disabled = this.renderModel.length === 1 34 | }.bind(this)) 35 | 36 | this.sortableOptions = { 37 | distance: 10, 38 | tolerance: 'pointer', 39 | scroll: true, 40 | zIndex: 6000, 41 | update: function () { 42 | angularHelper.getCurrentForm($scope).$setDirty() 43 | } 44 | } 45 | 46 | this.remove = function ($index) { 47 | this.renderModel.splice($index, 1) 48 | 49 | angularHelper.getCurrentForm($scope).$setDirty() 50 | } 51 | 52 | this.openLinkPicker = function (link, $index) { 53 | var target = link ? { 54 | name: link.name, 55 | // the linkPicker breaks if it get an id or udi for media 56 | id: link.isMedia ? null : link.id, 57 | udi: link.isMedia ? null : link.udi, 58 | url: link.url || '', 59 | anchor: link.querystring, 60 | target: link.target 61 | } : null 62 | 63 | // 7.12.0 and 7.12.1 removes the url in the linkPicker if there's no # or ? at the end 64 | // Hopefully this PR https://github.com/umbraco/Umbraco-CMS/pull/2868 will be merged in 7.12.2 65 | if (link && link.url && (Umbraco.Sys.ServerVariables.application.version == '7.12.0' || Umbraco.Sys.ServerVariables.application.version == '7.12.1')) { 66 | target.url = target.url + '#' 67 | } 68 | 69 | this.linkPickerOverlay = { 70 | view: 'linkpicker', 71 | currentTarget: target, 72 | show: true, 73 | hideQuerystring: $scope.model.config.hideQuerystring === '1', 74 | hideTarget: $scope.model.config.hideTarget === '1', 75 | submit: function (model) { 76 | // Parse query string: add missing ? or truncate to null 77 | var querystring = model.target.anchor 78 | if (querystring) { 79 | if (querystring[0] !== '?' && querystring[0] !== '#') querystring = '?' + querystring 80 | if (querystring.length === 1) querystring = null 81 | } 82 | 83 | if (model.target.url || querystring) { 84 | if (link) { 85 | if (link.isMedia && link.url === model.target.url) { 86 | // we can assume the existing media item is changed and no new file has been selected 87 | // so we don't need to update the id, udi and isMedia fields 88 | } else { 89 | link.id = model.target.id 90 | link.udi = model.target.udi 91 | link.isMedia = model.target.isMedia 92 | } 93 | 94 | link.name = model.target.name || model.target.url || querystring 95 | link.target = model.target.target 96 | link.url = model.target.url || '' 97 | link.querystring = querystring 98 | } else { 99 | link = { 100 | id: model.target.id, 101 | isMedia: model.target.isMedia, 102 | name: model.target.name || model.target.url || querystring, 103 | target: model.target.target, 104 | udi: model.target.udi, 105 | url: model.target.url, 106 | querystring: querystring 107 | } 108 | this.renderModel.push(link) 109 | } 110 | 111 | if (link.udi) { 112 | var entityType = link.isMedia ? 'media' : 'document' 113 | 114 | entityResource.getById(link.udi, entityType) 115 | .then(function (data) { 116 | link.icon = iconHelper.convertFromLegacyIcon(data.icon) 117 | link.published = !(data.metaData && data.metaData.IsPublished === false && entityType === 'document') 118 | }) 119 | } else { 120 | link.published = true 121 | link.icon = 'icon-link' 122 | } 123 | 124 | angularHelper.getCurrentForm($scope).$setDirty() 125 | } 126 | 127 | this.linkPickerOverlay.show = false 128 | this.linkPickerOverlay = null 129 | }.bind(this) 130 | } 131 | } 132 | } 133 | 134 | var mupHttpProvider = function ($httpProvider) { 135 | $httpProvider.interceptors.push(function ($q) { 136 | return { 137 | response: function (response) { 138 | if (response.config.url.indexOf('views/common/overlays/linkpicker/linkpicker.html') !== -1) { 139 | // Inject the querystring field 140 | var $markup = $(response.data) 141 | var $anchorField = $markup.find('[label="@defaultdialogs_anchorLinkPicker"]') 142 | // Umbraco 7.12 added it's own anchor/querystring field, so only inject if it doesn't exist 143 | if (!$anchorField.length) { 144 | var $urlField = $markup.find('[label="@defaultdialogs_urlLinkPicker"],[label="@content_urls"]') 145 | $urlField.after('') 146 | response.data = $markup[0] 147 | } 148 | } 149 | return response 150 | } 151 | } 152 | }) 153 | } 154 | 155 | angular.module('umbraco').controller('RJP.MultiUrlPickerController', MultiUrlPickerController) 156 | .config(['$httpProvider', mupHttpProvider]) 157 | })() 158 | -------------------------------------------------------------------------------- /src/RJP.MultiUrlPicker/Information.cs: -------------------------------------------------------------------------------- 1 | namespace RJP.MultiUrlPicker 2 | { 3 | using System; 4 | using System.Reflection; 5 | 6 | internal static class Information 7 | { 8 | private static readonly Lazy _version; 9 | 10 | static Information() 11 | { 12 | _version = new Lazy(() => Assembly.GetExecutingAssembly().GetName().Version); 13 | } 14 | 15 | public static Version Version 16 | { 17 | get 18 | { 19 | return _version.Value; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/RJP.MultiUrlPicker/Models/Link.cs: -------------------------------------------------------------------------------- 1 | namespace RJP.MultiUrlPicker.Models 2 | { 3 | using System; 4 | 5 | using Newtonsoft.Json.Linq; 6 | 7 | using Umbraco.Core.Models; 8 | using Umbraco.Core; 9 | 10 | using Umbraco.Web; 11 | using Umbraco.Web.Extensions; 12 | using Umbraco.Core.Models.PublishedContent; 13 | 14 | public class Link 15 | { 16 | private readonly JToken _linkItem; 17 | private bool _publishedContentInitialized = false; 18 | private string _name; 19 | private string _url; 20 | private string _target; 21 | private bool? _deleted; 22 | private LinkType? _linkType; 23 | private IPublishedContent _content; 24 | private Udi _udi; 25 | private int? _id; 26 | 27 | public Link(JToken linkItem) 28 | { 29 | _linkItem = linkItem; 30 | } 31 | 32 | private IPublishedContent PublishedContent 33 | { 34 | get 35 | { 36 | InitPublishedContent(); 37 | return _content; 38 | } 39 | } 40 | 41 | [Obsolete("Use Udi instead")] 42 | public int? Id 43 | { 44 | get 45 | { 46 | if (_id == null) 47 | { 48 | _id = _linkItem.Value("id"); 49 | if (!_id.HasValue) 50 | { 51 | InitPublishedContent(); 52 | } 53 | } 54 | return _id; 55 | } 56 | 57 | } 58 | 59 | public Udi Udi 60 | { 61 | get 62 | { 63 | if (_udi == null) 64 | { 65 | if (!Udi.TryParse(_linkItem.Value("udi"), out _udi)) 66 | { 67 | InitPublishedContent(); 68 | } 69 | } 70 | return _udi; 71 | } 72 | } 73 | 74 | public string Name 75 | { 76 | get 77 | { 78 | if (string.IsNullOrEmpty(_name)) 79 | { 80 | _name = _linkItem.Value("name"); 81 | } 82 | return _name; 83 | } 84 | } 85 | 86 | internal bool Deleted 87 | { 88 | get 89 | { 90 | if (_deleted == null) 91 | { 92 | if (Id.HasValue || Udi != null) 93 | { 94 | _deleted = PublishedContent == null; 95 | } 96 | else 97 | { 98 | _deleted = false; 99 | } 100 | } 101 | return (bool)_deleted; 102 | } 103 | } 104 | 105 | public string Url 106 | { 107 | get 108 | { 109 | if (string.IsNullOrEmpty(_url)) 110 | { 111 | _url = PublishedContent?.Url ?? _linkItem.Value("url"); 112 | 113 | var qs = _linkItem.Value("querystring"); 114 | if (!string.IsNullOrWhiteSpace(qs)) 115 | { 116 | _url += qs; 117 | } 118 | } 119 | return _url; 120 | } 121 | } 122 | 123 | public string Target 124 | { 125 | get 126 | { 127 | if (string.IsNullOrEmpty(_target)) 128 | { 129 | _target = _linkItem.Value("target"); 130 | } 131 | return _target == string.Empty ? null : _target; 132 | } 133 | } 134 | 135 | public LinkType Type 136 | { 137 | get 138 | { 139 | if (_linkType == null) 140 | { 141 | if (Udi != null) 142 | { 143 | if (Udi.EntityType == Constants.UdiEntityType.Media) 144 | { 145 | _linkType = LinkType.Media; 146 | } 147 | else 148 | { 149 | _linkType = LinkType.Content; 150 | } 151 | } 152 | else 153 | { 154 | _linkType = LinkType.External; 155 | } 156 | } 157 | return _linkType.Value; 158 | } 159 | } 160 | 161 | 162 | private void InitPublishedContent() 163 | { 164 | if (!_publishedContentInitialized) 165 | { 166 | _publishedContentInitialized = true; 167 | 168 | if (UmbracoContext.Current == null) 169 | { 170 | return; 171 | } 172 | 173 | if (Udi.TryParse(_linkItem.Value("udi"), out _udi)) 174 | { 175 | _content = _udi.ToPublishedContent(); 176 | _id = _content?.Id; 177 | } 178 | else 179 | { 180 | // there were no Udi so let's try the legacy way 181 | _id = _linkItem.Value("id"); 182 | if (_id.HasValue) 183 | { 184 | var helper = new UmbracoHelper(UmbracoContext.Current); 185 | 186 | if (_linkItem.Value("isMedia")) 187 | { 188 | _content = helper.TypedMedia(_id.Value); 189 | } 190 | else 191 | { 192 | _content = helper.TypedContent(_id.Value); 193 | } 194 | 195 | SetUdi(); 196 | } 197 | } 198 | } 199 | } 200 | 201 | private void SetUdi() 202 | { 203 | if (_content != null && _udi == null) 204 | { 205 | Guid? key = _content.GetKey(); 206 | if (key == Guid.Empty) 207 | { 208 | // if the key is Guid.Empty the model might be created by the ModelsBuilder, 209 | // if so it, by default, derives from PublishedContentModel. 210 | // By calling UnWrap() we get the original content, which probably implements 211 | // IPublishedContentWithKey, so we can get the key 212 | key = (_content as PublishedContentWrapped)?.Unwrap().GetKey(); 213 | } 214 | 215 | if (key.HasValue && key != Guid.Empty) 216 | { 217 | string udiType = _content.ItemType == PublishedItemType.Media ? 218 | Constants.UdiEntityType.Media : 219 | Constants.UdiEntityType.Document; 220 | 221 | _udi = Udi.Create(udiType, key.Value); 222 | } 223 | } 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/RJP.MultiUrlPicker/Models/LinkDisplay.cs: -------------------------------------------------------------------------------- 1 | namespace RJP.MultiUrlPicker.Models 2 | { 3 | using System.Runtime.Serialization; 4 | 5 | using Umbraco.Core; 6 | 7 | [DataContract] 8 | internal class LinkDisplay 9 | { 10 | [DataMember(Name = "icon")] 11 | public string Icon { get; set; } 12 | 13 | [DataMember(Name = "id")] 14 | public int? Id { get; set; } 15 | 16 | [DataMember(Name = "isMedia")] 17 | public bool IsMedia { get; set; } 18 | 19 | [DataMember(Name = "name")] 20 | public string Name { get; set; } 21 | 22 | [DataMember(Name = "published")] 23 | public bool Published { get; set; } 24 | 25 | [DataMember(Name = "target")] 26 | public string Target { get; set; } 27 | 28 | [DataMember(Name = "udi")] 29 | public GuidUdi Udi { get; set; } 30 | 31 | [DataMember(Name = "url")] 32 | public string Url { get; set; } 33 | 34 | [DataMember(Name = "querystring")] 35 | public string Querystring { get; set; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/RJP.MultiUrlPicker/Models/LinkDto.cs: -------------------------------------------------------------------------------- 1 | namespace RJP.MultiUrlPicker.Models 2 | { 3 | using System.Runtime.Serialization; 4 | 5 | using Umbraco.Core; 6 | 7 | [DataContract] 8 | internal class LinkDto 9 | { 10 | [DataMember(Name = "icon")] 11 | public string Icon { get; set; } 12 | 13 | [DataMember(Name = "id")] 14 | public int? Id { get; set; } 15 | 16 | [DataMember(Name = "isMedia")] 17 | public bool? IsMedia { get; set; } 18 | 19 | [DataMember(Name = "name")] 20 | public string Name { get; set; } 21 | 22 | [DataMember(Name = "target")] 23 | public string Target { get; set; } 24 | 25 | [DataMember(Name = "udi")] 26 | public GuidUdi Udi { get; set; } 27 | 28 | [DataMember(Name = "url")] 29 | public string Url { get; set; } 30 | 31 | [DataMember(Name = "querystring")] 32 | public string Querystring { get; set; } 33 | 34 | internal bool Published { get; set; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/RJP.MultiUrlPicker/Models/LinkType.cs: -------------------------------------------------------------------------------- 1 | namespace RJP.MultiUrlPicker.Models 2 | { 3 | public enum LinkType 4 | { 5 | Content, 6 | Media, 7 | External 8 | } 9 | } -------------------------------------------------------------------------------- /src/RJP.MultiUrlPicker/Models/MultiUrls.cs: -------------------------------------------------------------------------------- 1 | namespace RJP.MultiUrlPicker.Models 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | using Newtonsoft.Json; 8 | using Newtonsoft.Json.Linq; 9 | 10 | using Umbraco.Core.Logging; 11 | 12 | [Obsolete("Use IEnumerable instead")] 13 | public class MultiUrls : IEnumerable 14 | { 15 | private readonly string _propertyData; 16 | private readonly List _multiUrls = new List(); 17 | 18 | internal MultiUrls() 19 | { 20 | } 21 | 22 | internal MultiUrls(JArray propertyData) 23 | { 24 | _propertyData = propertyData.ToString(); 25 | 26 | Initialize(propertyData); 27 | } 28 | 29 | public MultiUrls(string propertyData) 30 | { 31 | _propertyData = propertyData; 32 | 33 | if (!string.IsNullOrEmpty(propertyData)) 34 | { 35 | var relatedLinks = JsonConvert.DeserializeObject(propertyData); 36 | Initialize(relatedLinks); 37 | } 38 | } 39 | 40 | private void Initialize(JArray data) 41 | { 42 | foreach (var item in data) 43 | { 44 | var newLink = new Link(item); 45 | if (!newLink.Deleted) 46 | { 47 | _multiUrls.Add(new Link(item)); 48 | } 49 | else 50 | { 51 | LogHelper.Warn( 52 | string.Format("MultiUrlPicker value converter skipped a link as the node has been unpublished/deleted (Id: {0}), ", newLink.Id)); 53 | } 54 | } 55 | } 56 | 57 | public string PropertyData 58 | { 59 | get 60 | { 61 | return _propertyData; 62 | } 63 | } 64 | 65 | // Although this method seems unnecessary it makes .Any() available in Dynamics 66 | public bool Any() 67 | { 68 | return Enumerable.Any(this); 69 | } 70 | 71 | public IEnumerator GetEnumerator() 72 | { 73 | return _multiUrls.GetEnumerator(); 74 | } 75 | 76 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 77 | { 78 | return GetEnumerator(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/RJP.MultiUrlPicker/MultiUrlPickerPropertyEditor.cs: -------------------------------------------------------------------------------- 1 | namespace RJP.MultiUrlPicker 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | using ClientDependency.Core; 8 | 9 | using Newtonsoft.Json; 10 | 11 | using Umbraco.Core; 12 | using Umbraco.Core.Logging; 13 | using Umbraco.Core.Models; 14 | using Umbraco.Core.Models.Editors; 15 | using Umbraco.Core.Models.EntityBase; 16 | using Umbraco.Core.PropertyEditors; 17 | using Umbraco.Core.Services; 18 | using Umbraco.Web; 19 | using Umbraco.Web.PropertyEditors; 20 | 21 | using Models; 22 | 23 | using Constants = Umbraco.Core.Constants; 24 | using Umbraco.Core.Configuration; 25 | 26 | [PropertyEditorAsset(ClientDependencyType.Javascript, "~/App_Plugins/RJP.MultiUrlPicker/MultiUrlPicker.js")] 27 | [PropertyEditor("RJP.MultiUrlPicker", "Multi Url Picker", "JSON", 28 | "~/App_Plugins/RJP.MultiUrlPicker/MultiUrlPicker.html", 29 | Group = "pickers", Icon = "icon-link", IsParameterEditor = true)] 30 | public class MultiUrlPickerPropertyEditor : PropertyEditor 31 | { 32 | private IDictionary _defaultPreValues; 33 | 34 | public MultiUrlPickerPropertyEditor() 35 | { 36 | _defaultPreValues = new Dictionary 37 | { 38 | {"minNumberOfItems", null}, 39 | {"maxNumberOfItems", null}, 40 | {"hideQuerystring", "0"}, 41 | {"hideTarget", "0"}, 42 | {"version", Information.Version.ToString(3)}, 43 | }; 44 | } 45 | 46 | public override IDictionary DefaultPreValues 47 | { 48 | get { return _defaultPreValues; } 49 | set { _defaultPreValues = value; } 50 | } 51 | 52 | protected override PreValueEditor CreatePreValueEditor() 53 | { 54 | return new MultiUrlPickerPreValueEditor(); 55 | } 56 | 57 | protected override PropertyValueEditor CreateValueEditor() 58 | { 59 | return new MultiUrlPickerPropertyValueEditor(base.CreateValueEditor()); 60 | } 61 | 62 | private class MultiUrlPickerPreValueEditor : PreValueEditor 63 | { 64 | public MultiUrlPickerPreValueEditor() 65 | { 66 | Fields.AddRange(new[] 67 | { 68 | new PreValueField 69 | { 70 | Key = "minNumberOfItems", 71 | Name = "Min number of items", 72 | View = "number" 73 | }, 74 | new PreValueField 75 | { 76 | Key = "maxNumberOfItems", 77 | Name = "Max number of items", 78 | View = "number" 79 | }, 80 | new PreValueField 81 | { 82 | Key = "hideTarget", 83 | Name = "Hide target/open in new window or tab checkbox", 84 | View = "boolean" 85 | }, 86 | new PreValueField 87 | { 88 | Key = "version", 89 | Name = "Multi Url Picker version", 90 | View = "hidden", 91 | HideLabel = true 92 | } 93 | }); 94 | 95 | if (UmbracoVersion.Current < new Version(7, 12, 0)) 96 | { 97 | // Umbraco 7.12 has it's own query string field which is not possible to hide at the time of writing 98 | // so only add the field in older versions 99 | Fields.Insert(2, new PreValueField 100 | { 101 | Key = "hideQuerystring", 102 | Name = "Hide query string input", 103 | View = "boolean" 104 | }); 105 | } 106 | } 107 | 108 | public override IDictionary ConvertDbToEditor(IDictionary defaultPreVals, PreValueCollection persistedPreVals) 109 | { 110 | // if there isn't a version stored set it to 0 for backwards compatibility 111 | if (!persistedPreVals.PreValuesAsDictionary.ContainsKey("version")) 112 | { 113 | persistedPreVals.PreValuesAsDictionary["version"] = new PreValue("0"); 114 | } 115 | 116 | return base.ConvertDbToEditor(defaultPreVals, persistedPreVals); 117 | } 118 | } 119 | 120 | private class MultiUrlPickerPropertyValueEditor : PropertyValueEditorWrapper 121 | { 122 | public MultiUrlPickerPropertyValueEditor(PropertyValueEditor wrapped) : base(wrapped) 123 | { 124 | } 125 | 126 | public override object ConvertDbToEditor(Property property, PropertyType propertyType, IDataTypeService dataTypeService) 127 | { 128 | string value = property.Value?.ToString(); 129 | 130 | if (string.IsNullOrEmpty(value)) 131 | { 132 | return Enumerable.Empty(); 133 | } 134 | 135 | try 136 | { 137 | ServiceContext services = ApplicationContext.Current.Services; 138 | IEntityService entityService = services.EntityService; 139 | IContentTypeService contentTypeService = services.ContentTypeService; 140 | 141 | var links = JsonConvert.DeserializeObject>(value); 142 | 143 | var documentLinks = links.FindAll(link => 144 | link.Id.HasValue && false == link.IsMedia.GetValueOrDefault() || 145 | link.Udi != null && link.Udi.EntityType == Constants.UdiEntityType.Document 146 | ); 147 | 148 | var mediaLinks = links.FindAll(link => 149 | link.Id.HasValue && true == link.IsMedia.GetValueOrDefault() || 150 | link.Udi != null && link.Udi.EntityType == Constants.UdiEntityType.Media 151 | ); 152 | 153 | List entities = new List(); 154 | if (documentLinks.Count > 0) 155 | { 156 | if (documentLinks[0].Id.HasValue) 157 | { 158 | entities.AddRange( 159 | entityService.GetAll(UmbracoObjectTypes.Document, documentLinks.Select(link => link.Id.Value).ToArray()) 160 | ); 161 | } 162 | else 163 | { 164 | entities.AddRange( 165 | entityService.GetAll(UmbracoObjectTypes.Document, documentLinks.Select(link => link.Udi.Guid).ToArray()) 166 | ); 167 | } 168 | } 169 | 170 | if (mediaLinks.Count > 0) 171 | { 172 | if (mediaLinks[0].Id.HasValue) 173 | { 174 | entities.AddRange( 175 | entityService.GetAll(UmbracoObjectTypes.Media, mediaLinks.Select(link => link.Id.Value).ToArray()) 176 | ); 177 | } 178 | else 179 | { 180 | entities.AddRange( 181 | entityService.GetAll(UmbracoObjectTypes.Media, mediaLinks.Select(link => link.Udi.Guid).ToArray()) 182 | ); 183 | } 184 | } 185 | 186 | var result = new List(); 187 | foreach (LinkDto dto in links) 188 | { 189 | if (dto.Id.HasValue || dto.Udi != null) 190 | { 191 | IUmbracoEntity entity = entities.Find(e => e.Key == dto.Udi?.Guid || e.Id == dto.Id); 192 | if (entity == null) 193 | { 194 | continue; 195 | } 196 | 197 | string entityType = Equals(entity.AdditionalData["NodeObjectTypeId"], Constants.ObjectTypes.MediaGuid) ? 198 | Constants.UdiEntityType.Media : 199 | Constants.UdiEntityType.Document; 200 | 201 | var udi = new GuidUdi(entityType, entity.Key); 202 | 203 | string contentTypeAlias = entity.AdditionalData["ContentTypeAlias"] as string; 204 | string icon; 205 | bool isMedia = false; 206 | bool published = Equals(entity.AdditionalData["IsPublished"], true); 207 | string url = dto.Url; 208 | 209 | if (string.IsNullOrEmpty(contentTypeAlias)) 210 | { 211 | continue; 212 | } 213 | 214 | if (udi.EntityType == Constants.UdiEntityType.Document) 215 | { 216 | IContentType contentType = contentTypeService.GetContentType(contentTypeAlias); 217 | 218 | if (contentType == null) 219 | { 220 | continue; 221 | } 222 | 223 | icon = contentType.Icon; 224 | 225 | if (UmbracoContext.Current != null) 226 | { 227 | url = UmbracoContext.Current.UrlProvider.GetUrl(entity.Id); 228 | } 229 | } 230 | else 231 | { 232 | IMediaType mediaType = contentTypeService.GetMediaType(contentTypeAlias); 233 | 234 | if (mediaType == null) 235 | { 236 | continue; 237 | } 238 | 239 | icon = mediaType.Icon; 240 | isMedia = true; 241 | published = true; 242 | 243 | if (UmbracoContext.Current != null) 244 | { 245 | url = UmbracoContext.Current.MediaCache.GetById(entity.Id)?.Url; 246 | } 247 | } 248 | 249 | result.Add(new LinkDisplay 250 | { 251 | Icon = icon, 252 | Id = entity.Id, 253 | IsMedia = isMedia, 254 | Name = dto.Name, 255 | Target = dto.Target, 256 | Published = published, 257 | Udi = udi, 258 | Url = url, 259 | Querystring = dto.Querystring 260 | }); 261 | } 262 | else 263 | { 264 | result.Add(new LinkDisplay 265 | { 266 | Icon = "icon-link", 267 | Name = dto.Name, 268 | Published = true, 269 | Target = dto.Target, 270 | Url = dto.Url, 271 | Querystring = dto.Querystring 272 | }); 273 | } 274 | } 275 | return result; 276 | } 277 | catch (Exception ex) 278 | { 279 | ApplicationContext.Current.ProfilingLogger.Logger.Error("Error getting links", ex); 280 | } 281 | 282 | return base.ConvertDbToEditor(property, propertyType, dataTypeService); 283 | } 284 | 285 | public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) 286 | { 287 | string value = editorValue.Value?.ToString(); 288 | 289 | if (string.IsNullOrEmpty(value)) 290 | { 291 | return string.Empty; 292 | } 293 | 294 | try 295 | { 296 | return JsonConvert.SerializeObject( 297 | from link in JsonConvert.DeserializeObject>(value) 298 | select new LinkDto 299 | { 300 | Name = link.Name, 301 | Target = link.Target, 302 | Udi = link.Udi, 303 | Url = link.Udi == null ? link.Url : null, // only save the url for external links 304 | Querystring = link.Querystring 305 | }, 306 | new JsonSerializerSettings 307 | { 308 | NullValueHandling = NullValueHandling.Ignore 309 | }); 310 | } 311 | catch (Exception ex) 312 | { 313 | ApplicationContext.Current.ProfilingLogger.Logger.Error("Error saving links", ex); 314 | } 315 | return base.ConvertEditorToDb(editorValue, currentValue); 316 | } 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/RJP.MultiUrlPicker/MultiUrlPickerValueConverter.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Core.Logging; 2 | using Umbraco.Core.PropertyEditors.ValueConverters; 3 | 4 | namespace RJP.MultiUrlPicker 5 | { 6 | using System; 7 | using System.Linq; 8 | using System.Collections.Generic; 9 | 10 | using Newtonsoft.Json.Linq; 11 | 12 | using Umbraco.Core; 13 | using Umbraco.Core.Models; 14 | using Umbraco.Core.Models.PublishedContent; 15 | using Umbraco.Core.PropertyEditors; 16 | using Umbraco.Core.Services; 17 | 18 | using Models; 19 | 20 | public class MultiUrlPickerValueConverter : PropertyValueConverterBase, IPropertyValueConverterMeta 21 | { 22 | private readonly IDataTypeService _dataTypeService; 23 | 24 | public MultiUrlPickerValueConverter(IDataTypeService dataTypeService) 25 | { 26 | _dataTypeService = dataTypeService; 27 | } 28 | 29 | public MultiUrlPickerValueConverter() : this(ApplicationContext.Current.Services.DataTypeService) 30 | { 31 | } 32 | 33 | public override bool IsConverter(PublishedPropertyType propertyType) 34 | { 35 | return propertyType.PropertyEditorAlias.Equals("RJP.MultiUrlPicker"); 36 | } 37 | 38 | public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) 39 | { 40 | if (string.IsNullOrWhiteSpace(source?.ToString())) 41 | { 42 | return null; 43 | } 44 | 45 | if (source.ToString().Trim().StartsWith("[")) 46 | { 47 | try 48 | { 49 | return JArray.Parse(source.ToString()); 50 | } 51 | catch (Exception ex) 52 | { 53 | LogHelper.Error("Error parsing JSON", ex); 54 | } 55 | } 56 | return null; 57 | } 58 | 59 | public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) 60 | { 61 | bool isMultiple = IsMultipleDataType(propertyType.DataTypeId, out int maxNumberOfItems); 62 | if (source == null) 63 | { 64 | return isMultiple ? new MultiUrls() : null; 65 | } 66 | 67 | var urls = new MultiUrls((JArray)source); 68 | if(isMultiple) 69 | { 70 | if(maxNumberOfItems > 0) 71 | { 72 | return urls.Take(maxNumberOfItems); 73 | } 74 | return urls; 75 | } 76 | return urls.FirstOrDefault(); 77 | } 78 | 79 | public Type GetPropertyValueType(PublishedPropertyType propertyType) 80 | { 81 | if (IsMultipleDataType(propertyType.DataTypeId, out int maxNumberOfItems)) 82 | { 83 | return typeof(IEnumerable); 84 | } 85 | return typeof(Link); 86 | } 87 | 88 | public PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType, PropertyCacheValue cacheValue) 89 | { 90 | switch (cacheValue) 91 | { 92 | case PropertyCacheValue.Source: 93 | return PropertyCacheLevel.Content; 94 | case PropertyCacheValue.Object: 95 | case PropertyCacheValue.XPath: 96 | return PropertyCacheLevel.ContentCache; 97 | } 98 | 99 | return PropertyCacheLevel.None; 100 | } 101 | 102 | private bool IsMultipleDataType(int dataTypeId, out int maxNumberOfItems) 103 | { 104 | IDictionary preValues = _dataTypeService 105 | .GetPreValuesCollectionByDataTypeId(dataTypeId) 106 | .PreValuesAsDictionary; 107 | 108 | if (preValues.TryGetValue("maxNumberOfItems", out PreValue maxNumberOfItemsPreValue) && 109 | int.TryParse(maxNumberOfItemsPreValue.Value, out maxNumberOfItems)) 110 | { 111 | PreValue versionPreValue; 112 | Version version; 113 | // for backwards compatibility, always return true if version 114 | // is less than 2.0.0 115 | if (preValues.TryGetValue("version", out versionPreValue) && 116 | Version.TryParse(versionPreValue.Value, out version) 117 | && version >= new Version(2, 0, 0)) 118 | { 119 | return maxNumberOfItems != 1; 120 | } 121 | } 122 | 123 | maxNumberOfItems = 0; 124 | return true; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/RJP.MultiUrlPicker/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("RJP.MultiUrlPicker")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("RJP.MultiUrlPicker")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("d6b0bd2f-b2da-4b8f-a33d-d2a13161b766")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | [assembly: AssemblyVersion("2.2.0.*")] 35 | [assembly: AssemblyInformationalVersion("2.2.0")] 36 | -------------------------------------------------------------------------------- /src/RJP.MultiUrlPicker/RJP.MultiUrlPicker.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {2553C499-3593-43ED-95D6-39B03CC61ED1} 8 | Library 9 | Properties 10 | RJP.MultiUrlPicker 11 | RJP.MultiUrlPicker 12 | v4.5 13 | 512 14 | 15 | ..\ 16 | true 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | true 35 | 36 | 37 | 38 | ..\..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.dll 39 | True 40 | 41 | 42 | ..\..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll 43 | True 44 | 45 | 46 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\businesslogic.dll 47 | True 48 | 49 | 50 | ..\..\packages\ClientDependency.1.9.2\lib\net45\ClientDependency.Core.dll 51 | True 52 | 53 | 54 | ..\..\packages\ClientDependency-Mvc5.1.8.0.0\lib\net45\ClientDependency.Core.Mvc.dll 55 | True 56 | 57 | 58 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\cms.dll 59 | True 60 | 61 | 62 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\controls.dll 63 | True 64 | 65 | 66 | ..\..\packages\xmlrpcnet.2.5.0\lib\net20\CookComputing.XmlRpcV2.dll 67 | False 68 | 69 | 70 | ..\..\packages\Examine.0.1.82\lib\net45\Examine.dll 71 | True 72 | 73 | 74 | ..\..\packages\HtmlAgilityPack.1.4.9.5\lib\Net45\HtmlAgilityPack.dll 75 | True 76 | 77 | 78 | ..\..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll 79 | False 80 | 81 | 82 | ..\..\packages\ImageProcessor.2.5.3\lib\net45\ImageProcessor.dll 83 | True 84 | 85 | 86 | ..\..\packages\ImageProcessor.Web.4.8.3\lib\net45\ImageProcessor.Web.dll 87 | True 88 | 89 | 90 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\interfaces.dll 91 | True 92 | 93 | 94 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\log4net.dll 95 | True 96 | 97 | 98 | ..\..\packages\Log4Net.Async.2.0.4\lib\net40\Log4Net.Async.dll 99 | True 100 | 101 | 102 | False 103 | ..\..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll 104 | False 105 | 106 | 107 | ..\..\packages\Markdown.1.14.7\lib\net45\MarkdownSharp.dll 108 | True 109 | 110 | 111 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\Microsoft.ApplicationBlocks.Data.dll 112 | True 113 | 114 | 115 | ..\..\packages\Microsoft.AspNet.Identity.Core.2.2.1\lib\net45\Microsoft.AspNet.Identity.Core.dll 116 | True 117 | 118 | 119 | ..\..\packages\Microsoft.AspNet.Identity.Owin.2.2.1\lib\net45\Microsoft.AspNet.Identity.Owin.dll 120 | True 121 | 122 | 123 | ..\..\packages\Microsoft.AspNet.SignalR.Core.2.2.1\lib\net45\Microsoft.AspNet.SignalR.Core.dll 124 | True 125 | 126 | 127 | ..\..\packages\Microsoft.IO.RecyclableMemoryStream.1.2.1\lib\net45\Microsoft.IO.RecyclableMemoryStream.dll 128 | True 129 | 130 | 131 | ..\..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll 132 | True 133 | 134 | 135 | ..\..\packages\Microsoft.Owin.Host.SystemWeb.3.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll 136 | True 137 | 138 | 139 | ..\..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll 140 | True 141 | 142 | 143 | ..\..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll 144 | True 145 | 146 | 147 | ..\..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll 148 | True 149 | 150 | 151 | False 152 | ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll 153 | 154 | 155 | ..\..\packages\MiniProfiler.2.1.0\lib\net40\MiniProfiler.dll 156 | False 157 | 158 | 159 | ..\..\packages\MySql.Data.6.9.9\lib\net45\MySql.Data.dll 160 | True 161 | 162 | 163 | ..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll 164 | True 165 | 166 | 167 | ..\..\packages\Owin.1.0\lib\net40\Owin.dll 168 | True 169 | 170 | 171 | ..\..\packages\Semver.2.0.4\lib\netstandard1.1\Semver.dll 172 | True 173 | 174 | 175 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\SQLCE4Umbraco.dll 176 | True 177 | 178 | 179 | 180 | 181 | 182 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\System.Data.SqlServerCe.dll 183 | True 184 | 185 | 186 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\System.Data.SqlServerCe.Entity.dll 187 | True 188 | 189 | 190 | 191 | ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll 192 | True 193 | 194 | 195 | 196 | ..\..\packages\System.Threading.Tasks.Dataflow.4.7.0\lib\portable-net45+win8+wpa81\System.Threading.Tasks.Dataflow.dll 197 | True 198 | 199 | 200 | 201 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll 202 | True 203 | 204 | 205 | ..\..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll 206 | True 207 | 208 | 209 | ..\..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.3\lib\net45\System.Web.Http.WebHost.dll 210 | True 211 | 212 | 213 | ..\..\packages\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll 214 | True 215 | 216 | 217 | ..\..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll 218 | True 219 | 220 | 221 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll 222 | True 223 | 224 | 225 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll 226 | True 227 | 228 | 229 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll 230 | True 231 | 232 | 233 | 234 | 235 | False 236 | 237 | 238 | 239 | 240 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\TidyNet.dll 241 | True 242 | 243 | 244 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\umbraco.dll 245 | True 246 | 247 | 248 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\Umbraco.Core.dll 249 | True 250 | 251 | 252 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\umbraco.DataLayer.dll 253 | True 254 | 255 | 256 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\umbraco.editorControls.dll 257 | True 258 | 259 | 260 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\umbraco.MacroEngines.dll 261 | True 262 | 263 | 264 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\umbraco.providers.dll 265 | True 266 | 267 | 268 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\Umbraco.Web.UI.dll 269 | True 270 | 271 | 272 | ..\..\packages\UmbracoCms.Core.7.6.0\lib\net45\UmbracoExamine.dll 273 | True 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 303 | -------------------------------------------------------------------------------- /src/RJP.MultiUrlPicker/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/RJP.MultiUrlPicker/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | --------------------------------------------------------------------------------