From e45b66fb80d8523fc3abbf0f9a119dfae8b7fd02 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 10 Jan 2017 08:03:51 +0100 Subject: [PATCH] Upgrade to 7.5.7 --- .gitignore | 1 + OurUmbraco.Site/OurUmbraco.Site.csproj | 91 +- .../Partials/Grid/Bootstrap2-Fluid.cshtml | 6 +- .../Views/Partials/Grid/Bootstrap2.cshtml | 6 +- .../Partials/Grid/Bootstrap3-Fluid.cshtml | 6 +- .../Views/Partials/Grid/Bootstrap3.cshtml | 8 +- .../Partials/Grid/Editors/Textstring.cshtml | 4 +- OurUmbraco.Site/Views/Web.config | 1 + .../config/ClientDependency.config | 20 +- OurUmbraco.Site/config/Dashboard.config | 16 +- .../config/imageprocessor/cache.config | 10 + .../config/imageprocessor/processing.config | 61 + .../config/imageprocessor/security.config | 24 + OurUmbraco.Site/packages.config | 10 +- .../umbraco/js/canvasdesigner.panel.js | 3684 +-- .../umbraco/js/umbraco.controllers.js | 16777 +++++++------- .../umbraco/js/umbraco.directives.js | 19210 ++++++++-------- OurUmbraco.Site/umbraco/js/umbraco.filters.js | 82 +- .../umbraco/js/umbraco.installer.js | 908 +- .../umbraco/js/umbraco.resources.js | 1410 +- .../umbraco/js/umbraco.security.js | 204 +- .../umbraco/js/umbraco.services.js | 6958 +++--- OurUmbraco.Site/umbraco/js/umbraco.testing.js | 344 +- .../umbraco/lib/angular/angular.js | 4294 ++-- .../umbraco/lib/umbraco/Extensions.js | 16 - OurUmbraco.Site/web.template.config | 14 + 26 files changed, 27464 insertions(+), 26701 deletions(-) create mode 100644 OurUmbraco.Site/config/imageprocessor/cache.config create mode 100644 OurUmbraco.Site/config/imageprocessor/processing.config create mode 100644 OurUmbraco.Site/config/imageprocessor/security.config diff --git a/.gitignore b/.gitignore index 6cebe0fe..97464d96 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ OurUmbraco.Site/App_Data/migrations/*.txt OurUmbraco.Site/apidocs/ OurUmbraco.Site/App_Data/Documentation/ OurUmbraco.Site/App_Data/NotificationTest/ +wwwcache/ diff --git a/OurUmbraco.Site/OurUmbraco.Site.csproj b/OurUmbraco.Site/OurUmbraco.Site.csproj index 802d2f89..fdb1fd6c 100644 --- a/OurUmbraco.Site/OurUmbraco.Site.csproj +++ b/OurUmbraco.Site/OurUmbraco.Site.csproj @@ -1,6 +1,6 @@  - + Debug @@ -59,8 +59,8 @@ ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll True - - ..\packages\UmbracoCms.Core.7.5.6\lib\businesslogic.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\businesslogic.dll True @@ -71,12 +71,12 @@ ..\packages\ClientDependency-Mvc5.1.8.0.0\lib\net45\ClientDependency.Core.Mvc.dll True - - ..\packages\UmbracoCms.Core.7.5.6\lib\cms.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\cms.dll True - - ..\packages\UmbracoCms.Core.7.5.6\lib\controls.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\controls.dll True @@ -99,16 +99,16 @@ ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll True - - ..\packages\ImageProcessor.2.4.5.0\lib\net45\ImageProcessor.dll + + ..\packages\ImageProcessor.2.5.1\lib\net45\ImageProcessor.dll True - - ..\packages\ImageProcessor.Web.4.6.6.0\lib\net45\ImageProcessor.Web.dll + + ..\packages\ImageProcessor.Web.4.7.2\lib\net45\ImageProcessor.Web.dll True - - ..\packages\UmbracoCms.Core.7.5.6\lib\interfaces.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\interfaces.dll True @@ -116,7 +116,7 @@ ..\packages\JsonFx.2.0.1209.2802\lib\net40\JsonFx.dll - ..\packages\UmbracoCms.Core.7.5.6\lib\log4net.dll + ..\packages\UmbracoCms.Core.7.5.7\lib\log4net.dll True @@ -128,7 +128,7 @@ True - ..\packages\UmbracoCms.Core.7.5.6\lib\Microsoft.ApplicationBlocks.Data.dll + ..\packages\UmbracoCms.Core.7.5.7\lib\Microsoft.ApplicationBlocks.Data.dll True @@ -148,6 +148,10 @@ True + + ..\packages\Microsoft.IO.RecyclableMemoryStream.1.2.0\lib\net45\Microsoft.IO.RecyclableMemoryStream.dll + True + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll True @@ -220,8 +224,8 @@ ..\packages\semver.1.1.2\lib\net451\Semver.dll True - - ..\packages\UmbracoCms.Core.7.5.6\lib\SQLCE4Umbraco.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\SQLCE4Umbraco.dll True @@ -230,11 +234,11 @@ - ..\packages\UmbracoCms.Core.7.5.6\lib\System.Data.SqlServerCe.dll + ..\packages\UmbracoCms.Core.7.5.7\lib\System.Data.SqlServerCe.dll True - ..\packages\UmbracoCms.Core.7.5.6\lib\System.Data.SqlServerCe.Entity.dll + ..\packages\UmbracoCms.Core.7.5.7\lib\System.Data.SqlServerCe.Entity.dll True @@ -307,51 +311,51 @@ - ..\packages\UmbracoCms.Core.7.5.6\lib\TidyNet.dll + ..\packages\UmbracoCms.Core.7.5.7\lib\TidyNet.dll True ..\packages\TweetinviAPI.1.2.0\lib\net45\Tweetinvi.dll True - - ..\packages\UmbracoCms.Core.7.5.6\lib\umbraco.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\umbraco.dll True - - ..\packages\UmbracoCms.Core.7.5.6\lib\Umbraco.Core.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\Umbraco.Core.dll True - - ..\packages\UmbracoCms.Core.7.5.6\lib\umbraco.DataLayer.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\umbraco.DataLayer.dll True - - ..\packages\UmbracoCms.Core.7.5.6\lib\umbraco.editorControls.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\umbraco.editorControls.dll True ..\packages\UmbracoCms.IdentityExtensions.1.0.0\lib\net45\Umbraco.IdentityExtensions.dll True - - ..\packages\UmbracoCms.Core.7.5.6\lib\umbraco.MacroEngines.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\umbraco.MacroEngines.dll True ..\packages\Umbraco.ModelsBuilder.3.0.5\lib\Umbraco.ModelsBuilder.dll True - - ..\packages\UmbracoCms.Core.7.5.6\lib\umbraco.providers.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\umbraco.providers.dll True - - ..\packages\UmbracoCms.Core.7.5.6\lib\Umbraco.Web.UI.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\Umbraco.Web.UI.dll True - - ..\packages\UmbracoCms.Core.7.5.6\lib\UmbracoExamine.dll + + ..\packages\UmbracoCms.Core.7.5.7\lib\UmbracoExamine.dll True @@ -700,6 +704,9 @@ + + + @@ -4431,6 +4438,9 @@ + + + @@ -4441,9 +4451,6 @@ - - - web.config @@ -4521,12 +4528,12 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + + - + - - - - + + + + - - + + @@ -44,12 +45,15 @@ NOTES: --> - + - + diff --git a/OurUmbraco.Site/config/Dashboard.config b/OurUmbraco.Site/config/Dashboard.config index 3758c903..a8c7f3cb 100644 --- a/OurUmbraco.Site/config/Dashboard.config +++ b/OurUmbraco.Site/config/Dashboard.config @@ -27,8 +27,8 @@ developer - - views/dashboard/developer/developerdashboardvideos.html + + views/dashboard/developer/developerdashboardvideos.html @@ -42,8 +42,8 @@ - - views/dashboard/developer/examinemanagement.html + + views/dashboard/developer/examinemanagement.html @@ -53,8 +53,8 @@ media - - views/dashboard/media/mediafolderbrowser.html + + views/dashboard/media/mediafolderbrowser.html @@ -87,8 +87,8 @@ member - - views/dashboard/members/membersdashboardvideos.html + + views/dashboard/members/membersdashboardvideos.html diff --git a/OurUmbraco.Site/config/imageprocessor/cache.config b/OurUmbraco.Site/config/imageprocessor/cache.config new file mode 100644 index 00000000..6e8da489 --- /dev/null +++ b/OurUmbraco.Site/config/imageprocessor/cache.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/OurUmbraco.Site/config/imageprocessor/processing.config b/OurUmbraco.Site/config/imageprocessor/processing.config new file mode 100644 index 00000000..0adea47b --- /dev/null +++ b/OurUmbraco.Site/config/imageprocessor/processing.config @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OurUmbraco.Site/config/imageprocessor/security.config b/OurUmbraco.Site/config/imageprocessor/security.config new file mode 100644 index 00000000..7bce8ee7 --- /dev/null +++ b/OurUmbraco.Site/config/imageprocessor/security.config @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OurUmbraco.Site/packages.config b/OurUmbraco.Site/packages.config index 56a8b46c..9cadcda7 100644 --- a/OurUmbraco.Site/packages.config +++ b/OurUmbraco.Site/packages.config @@ -9,8 +9,9 @@ - - + + + @@ -37,6 +38,7 @@ + @@ -58,8 +60,8 @@ - - + + diff --git a/OurUmbraco.Site/umbraco/js/canvasdesigner.panel.js b/OurUmbraco.Site/umbraco/js/canvasdesigner.panel.js index 599f39ce..964137be 100644 --- a/OurUmbraco.Site/umbraco/js/canvasdesigner.panel.js +++ b/OurUmbraco.Site/umbraco/js/canvasdesigner.panel.js @@ -1,1858 +1,1858 @@ - -/*********************************************************************************************************/ -/* Global function and variable for panel/page com */ -/*********************************************************************************************************/ - -/* Called for every canvasdesigner-over click */ -var onClickCanvasdesignerItem = function (schema) { - - var scope = angular.element($("#canvasdesignerPanel")).scope(); - - //if (scope.schemaFocus != schema.toLowerCase()) { - //var notFound = true; - $.each(scope.canvasdesignerModel.configs, function (indexConfig, config) { - if (config.schema && schema.toLowerCase() == config.schema.toLowerCase()) { - scope.currentSelected = config; - } - }); - //} - - scope.clearSelectedCategory(); - - scope.closeFloatPanels(); - - scope.$apply(); - -} - -/* Called for every canvasdesigner-over rollover */ -var onMouseoverCanvasdesignerItem = function (name) { - - var scope = angular.element($("#canvasdesignerPanel")).scope(); - - $.each(scope.canvasdesignerModel.configs, function (indexConfig, config) { - config.highlighted = false; - if (config.name && name.toLowerCase() == config.name.toLowerCase()) { - config.highlighted = true; - } - }); - - scope.$apply(); - -} - -/* Called when the iframe is first loaded */ -var setFrameIsLoaded = function (canvasdesignerConfig, canvasdesignerPalette) { - - var scope = angular.element($("#canvasdesignerPanel")).scope(); - - scope.canvasdesignerModel = canvasdesignerConfig; - scope.canvasdesignerPalette = canvasdesignerPalette; - scope.enableCanvasdesigner++; - scope.$apply(); -} - -/* Iframe body click */ -var iframeBodyClick = function () { - - var scope = angular.element($("#canvasdesignerPanel")).scope(); - - scope.closeFloatPanels(); -} - -/*********************************************************************************************************/ -/* Canvasdesigner panel app and controller */ -/*********************************************************************************************************/ - -var app = angular.module("Umbraco.canvasdesigner", ['colorpicker', 'ui.slider', 'umbraco.resources', 'umbraco.services']) - -.controller("Umbraco.canvasdesignerController", function ($scope, $http, $window, $timeout, $location, dialogService) { - - $scope.isOpen = false; - $scope.frameLoaded = false; - $scope.enableCanvasdesigner = 0; - $scope.googleFontFamilies = {}; - $scope.pageId = $location.search().id; - $scope.pageUrl = "../dialogs/Preview.aspx?id=" + $location.search().id; - $scope.valueAreLoaded = false; - $scope.devices = [ - { name: "desktop", css: "desktop", icon: "icon-display", title: "Desktop" }, - { name: "laptop - 1366px", css: "laptop border", icon: "icon-laptop", title: "Laptop" }, - { name: "iPad portrait - 768px", css: "iPad-portrait border", icon: "icon-ipad", title: "Tablet portrait" }, - { name: "iPad landscape - 1024px", css: "iPad-landscape border", icon: "icon-ipad flip", title: "Tablet landscape" }, - { name: "smartphone portrait - 480px", css: "smartphone-portrait border", icon: "icon-iphone", title: "Smartphone portrait" }, - { name: "smartphone landscape - 320px", css: "smartphone-landscape border", icon: "icon-iphone flip", title: "Smartphone landscape" } - ]; - $scope.previewDevice = $scope.devices[0]; - - var apiController = "../Api/Canvasdesigner/"; - - /*****************************************************************************/ - /* Preview devices */ - /*****************************************************************************/ - - // Set preview device - $scope.updatePreviewDevice = function (device) { - $scope.previewDevice = device; - }; - - /*****************************************************************************/ - /* Exit Preview */ - /*****************************************************************************/ - - $scope.exitPreview = function () { - window.top.location.href = "../endPreview.aspx?redir=%2f" + $scope.pageId; - }; - - /*****************************************************************************/ - /* UI designer managment */ - /*****************************************************************************/ - - // Update all Canvasdesigner config's values from data - var updateConfigValue = function (data) { - - var fonts = []; - - $.each($scope.canvasdesignerModel.configs, function (indexConfig, config) { - if (config.editors) { - $.each(config.editors, function (indexItem, item) { - - /* try to get value */ - try { - - if (item.values) { - angular.forEach(Object.keys(item.values), function (key, indexKey) { - if (key != "''") { - var propertyAlias = key.toLowerCase() + item.alias.toLowerCase(); - var newValue = eval("data." + propertyAlias.replace("@", "")); - if (newValue == "''") { - newValue = ""; - } - item.values[key] = newValue; - } - }) - } - - // TODO: special init for font family picker - if (item.type == "googlefontpicker") { - if (item.values.fontType == 'google' && item.values.fontFamily + item.values.fontWeight && $.inArray(item.values.fontFamily + ":" + item.values.fontWeight, fonts) < 0) { - fonts.splice(0, 0, item.values.fontFamily + ":" + item.values.fontWeight); - } - } - - } - catch (err) { - console.info("Style parameter not found " + item.alias); - } - - }); - } - - }); - - // Load google font - $.each(fonts, function (indexFont, font) { - loadGoogleFont(font); - loadGoogleFontInFront(font); - }); - - $scope.valueAreLoaded = true; - - }; - - // Load parameters from GetLessParameters and init data of the Canvasdesigner config - $scope.initCanvasdesigner = function () { - - LazyLoad.js(['https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js']); - - $http.get(apiController + 'Load', { params: { pageId: $scope.pageId } }) - .success(function (data) { - - updateConfigValue(data); - - $timeout(function () { - $scope.frameLoaded = true; - }, 200); - - }); - - }; - - // Refresh all less parameters for every changes watching canvasdesignerModel - var refreshCanvasdesigner = function () { - - var parameters = []; - - if ($scope.canvasdesignerModel) { - - angular.forEach($scope.canvasdesignerModel.configs, function (config, indexConfig) { - - // Get currrent selected element - // TODO - //if ($scope.schemaFocus && angular.lowercase($scope.schemaFocus) == angular.lowercase(config.name)) { - // $scope.currentSelected = config.selector ? config.selector : config.schema; - //} - - if (config.editors) { - angular.forEach(config.editors, function (item, indexItem) { - - // Add new style - if (item.values) { - angular.forEach(Object.keys(item.values), function (key, indexKey) { - var propertyAlias = key.toLowerCase() + item.alias.toLowerCase(); - var value = eval("item.values." + key); - parameters.splice(parameters.length + 1, 0, "'@" + propertyAlias + "':'" + value + "'"); - }) - } - }); - } - - }); - - // Refresh page style - refreshFrontStyles(parameters); - - // Refresh layout of selected element - //$timeout(function () { - $scope.positionSelectedHide(); - if ($scope.currentSelected) { - refreshOutlineSelected($scope.currentSelected); - } - //}, 200); - - - - } - } - - $scope.createStyle = function (){ - $scope.saveLessParameters(false); - } - - $scope.saveStyle = function () { - $scope.saveLessParameters(true); - } - - // Save all parameter in CanvasdesignerParameters.less file - $scope.saveLessParameters = function (inherited) { - - var parameters = []; - $.each($scope.canvasdesignerModel.configs, function (indexConfig, config) { - if (config.editors) { - $.each(config.editors, function (indexItem, item) { - - if (item.values) { - angular.forEach(Object.keys(item.values), function (key, indexKey) { - var propertyAlias = key.toLowerCase() + item.alias.toLowerCase(); - var value = eval("item.values." + key); - parameters.splice(parameters.length + 1, 0, "@" + propertyAlias + ":" + value + ";"); - }) - - // TODO: special init for font family picker - if (item.type == "googlefontpicker" && item.values.fontFamily) { - var variant = item.values.fontWeight != "" || item.values.fontStyle != "" ? ":" + item.values.fontWeight + item.values.fontStyle : ""; - var gimport = "@import url('https://fonts.googleapis.com/css?family=" + item.values.fontFamily + variant + "');"; - if ($.inArray(gimport, parameters) < 0) { - parameters.splice(0, 0, gimport); - } - } - } - - }); - } - }); - - var resultParameters = { parameters: parameters.join(""), pageId: $scope.pageId, inherited: inherited }; - var transform = function (result) { - return $.param(result); - } - - $('.btn-default-save').attr("disabled", true); - $http.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded"; - $http.post(apiController + 'Save', resultParameters, { - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, - transformRequest: transform - }) - .success(function (data) { - $('.btn-default-save').attr("disabled", false); - $('#speechbubble').fadeIn('slow').delay(5000).fadeOut('slow'); - }); - - } - - // Delete current page Canvasdesigner - $scope.deleteCanvasdesigner = function () { - $('.btn-default-delete').attr("disabled", true); - $http.get(apiController + 'Delete', { params: { pageId: $scope.pageId } }) - .success(function (data) { - location.reload(); - }) - } - - - /*****************************************************************************/ - /* Preset design */ - /*****************************************************************************/ - - // Refresh with selected Canvasdesigner palette - $scope.refreshCanvasdesignerByPalette = function (palette) { - updateConfigValue(palette.data); - refreshCanvasdesigner(); - }; - - // Hidden botton to make preset from the current settings - $scope.makePreset = function () { - - var parameters = []; - $.each($scope.canvasdesignerModel.configs, function (indexConfig, config) { - if (config.editors) { - $.each(config.editors, function (indexItem, item) { - if (item.values) { - angular.forEach(Object.keys(item.values), function (key, indexKey) { - var propertyAlias = key.toLowerCase() + item.alias.toLowerCase(); - var value = eval("item.values." + key); - var value = (value != 0 && (value == undefined || value == "")) ? "''" : value; - parameters.splice(parameters.length + 1, 0, "\"" + propertyAlias + "\":" + " \"" + value + "\""); - }) - } - }); - } - }); - - $(".btn-group").append(""); - - }; - - /*****************************************************************************/ - /* Panel managment */ - /*****************************************************************************/ - - $scope.openPreviewDevice = function () { - $scope.showDevicesPreview = true; - $scope.closeIntelCanvasdesigner(); - } - - $scope.closePreviewDevice = function(){ - $scope.showDevicesPreview = false; - if ($scope.showStyleEditor) { - $scope.openIntelCanvasdesigner(); - } - } - - $scope.openPalettePicker = function () { - $scope.showPalettePicker = true; - $scope.showStyleEditor = false; - $scope.closeIntelCanvasdesigner(); - }; - - $scope.openStyleEditor = function () { - $scope.showStyleEditor = true; - $scope.showPalettePicker = false; - $scope.outlineSelectedHide() - $scope.openIntelCanvasdesigner() - } - - // Remove value from field - $scope.removeField = function (field) { - field.value = ""; - }; - - // Check if category existe - $scope.hasEditor = function (editors, category) { - var result = false; - angular.forEach(editors, function (item, index) { - if (item.category == category) { - result = true; - } - }); - return result; - }; - - $scope.closeFloatPanels = function () { - - /* hack to hide color picker */ - $(".spectrumcolorpicker input").spectrum("hide"); - - dialogService.close(); - $scope.showPalettePicker = false; - $scope.$apply(); - }; - - $scope.clearHighlightedItems = function () { - $.each($scope.canvasdesignerModel.configs, function (indexConfig, config) { - config.highlighted = false; - }); - } - - $scope.setCurrentHighlighted = function (item) { - $scope.clearHighlightedItems(); - item.highlighted = true; - } - - $scope.setCurrentSelected = function(item) { - $scope.currentSelected = item; - $scope.clearSelectedCategory(); - refreshOutlineSelected($scope.currentSelected); - } - - /* Editor categories */ - - $scope.getCategories = function (item) { - - var propertyCategories = []; - - $.each(item.editors, function (indexItem, editor) { - if (editor.category) { - if ($.inArray(editor.category, propertyCategories) < 0) { - propertyCategories.splice( propertyCategories.length + 1, 0, editor.category); - } - } - }); - - return propertyCategories; - - } - - $scope.setSelectedCategory = function (item) { - $scope.categoriesVisibility = $scope.categoriesVisibility || {}; - $scope.categoriesVisibility[item] = !$scope.categoriesVisibility[item]; - } - - $scope.clearSelectedCategory = function () { - $scope.categoriesVisibility = null; - } - - /*****************************************************************************/ - /* Call function into the front-end */ - /*****************************************************************************/ - - var loadGoogleFontInFront = function (font) { - if (document.getElementById("resultFrame").contentWindow.getFont) - document.getElementById("resultFrame").contentWindow.getFont(font); - }; - - var refreshFrontStyles = function (parameters) { - if (document.getElementById("resultFrame").contentWindow.refreshLayout) - document.getElementById("resultFrame").contentWindow.refreshLayout(parameters); - }; - - var hideUmbracoPreviewBadge = function () { - var iframe = (document.getElementById("resultFrame").contentWindow || document.getElementById("resultFrame").contentDocument); - if(iframe.document.getElementById("umbracoPreviewBadge")) - iframe.document.getElementById("umbracoPreviewBadge").style.display = "none"; - }; - - $scope.openIntelCanvasdesigner = function () { - if (document.getElementById("resultFrame").contentWindow.initIntelCanvasdesigner) - document.getElementById("resultFrame").contentWindow.initIntelCanvasdesigner($scope.canvasdesignerModel); - }; - - $scope.closeIntelCanvasdesigner = function () { - if (document.getElementById("resultFrame").contentWindow.closeIntelCanvasdesigner) - document.getElementById("resultFrame").contentWindow.closeIntelCanvasdesigner($scope.canvasdesignerModel); - $scope.outlineSelectedHide(); - }; - - var refreshOutlineSelected = function (config) { - var schema = config.selector ? config.selector : config.schema; - if (document.getElementById("resultFrame").contentWindow.refreshOutlineSelected) - document.getElementById("resultFrame").contentWindow.refreshOutlineSelected(schema); - } - - $scope.outlineSelectedHide = function () { - $scope.currentSelected = null; - if (document.getElementById("resultFrame").contentWindow.outlineSelectedHide) - document.getElementById("resultFrame").contentWindow.outlineSelectedHide(); - }; - - $scope.refreshOutlinePosition = function (config) { - var schema = config.selector ? config.selector : config.schema; - if (document.getElementById("resultFrame").contentWindow.refreshOutlinePosition) - document.getElementById("resultFrame").contentWindow.refreshOutlinePosition(schema); - } - - $scope.positionSelectedHide = function () { - if (document.getElementById("resultFrame").contentWindow.outlinePositionHide) - document.getElementById("resultFrame").contentWindow.outlinePositionHide(); - } - - /*****************************************************************************/ - /* Google font loader, TODO: put together from directive, front and back */ - /*****************************************************************************/ - - var webFontScriptLoaded = false; - var loadGoogleFont = function (font) { - - if (!webFontScriptLoaded) { - $.getScript('https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js') - .done(function () { - webFontScriptLoaded = true; - // Recursively call once webfont script is available. - loadGoogleFont(font); - }) - .fail(function () { - console.log('error loading webfont'); - }); - } - else { - WebFont.load({ - google: { - families: [font] - }, - loading: function () { - //console.log('loading font' + font + ' in UI designer'); - }, - active: function () { - //console.log('loaded font ' + font + ' in UI designer'); - }, - inactive: function () { - //console.log('error loading font ' + font + ' in UI designer'); - } - }); - } - - }; - - /*****************************************************************************/ - /* Init */ - /*****************************************************************************/ - - // Preload of the google font - if ($scope.showStyleEditor) { - $http.get(apiController + 'GetGoogleFont').success(function (data) { - $scope.googleFontFamilies = data; - }); - } - - // watch framLoaded, only if iframe page have enableCanvasdesigner() - $scope.$watch("enableCanvasdesigner", function () { - $timeout(function () { - if ($scope.enableCanvasdesigner > 0) { - - $scope.$watch('ngRepeatFinished', function (ngRepeatFinishedEvent) { - $timeout(function () { - $scope.initCanvasdesigner(); - }, 200); - }); - - $scope.$watch('canvasdesignerModel', function () { - refreshCanvasdesigner(); - }, true); - - } - }, 100); - }, true); - -}) - -.directive('onFinishRenderFilters', function ($timeout) { - return { - restrict: 'A', - link: function (scope, element, attr) { - if (scope.$last === true) { - $timeout(function () { - scope.$emit('ngRepeatFinished'); - }); - } - } - }; -}) - -.directive('iframeIsLoaded', function ($timeout) { - return { - restrict: 'A', - link: function (scope, element, attr) { - element.load(function () { - var iframe = (element.context.contentWindow || element.context.contentDocument); - if(iframe && iframe.document.getElementById("umbracoPreviewBadge")) - iframe.document.getElementById("umbracoPreviewBadge").style.display = "none"; - if (!document.getElementById("resultFrame").contentWindow.refreshLayout) { - scope.frameLoaded = true; - scope.$apply(); - } - }); - } - }; -}) +/*********************************************************************************************************/ +/* Global function and variable for panel/page com */ +/*********************************************************************************************************/ - -/*********************************************************************************************************/ -/* Background editor */ -/*********************************************************************************************************/ - -angular.module("Umbraco.canvasdesigner") - -.controller("Umbraco.canvasdesigner.background", function ($scope, dialogService) { - - if (!$scope.item.values) { - $scope.item.values = { - imageorpattern: '', - color: '' - }; - } - - $scope.open = function (field) { - - var config = { - template: "mediaPickerModal.html", - change: function (data) { - $scope.item.values.imageorpattern = data; - }, - callback: function (data) { - $scope.item.values.imageorpattern = data; - }, - cancel: function (data) { - $scope.item.values.imageorpattern = data; - }, - dialogData: $scope.googleFontFamilies, - dialogItem: $scope.item.values.imageorpattern - }; - - dialogService.open(config); - - }; - -}) - -.controller('canvasdesigner.mediaPickerModal', function ($scope, $http, mediaResource, umbRequestHelper, entityResource, mediaHelper) { - - if (mediaHelper && mediaHelper.registerFileResolver) { - mediaHelper.registerFileResolver("Umbraco.UploadField", function (property, entity, thumbnail) { - if (thumbnail) { - - if (mediaHelper.detectIfImageByExtension(property.value)) { - var thumbnailUrl = umbRequestHelper.getApiUrl( - "imagesApiBaseUrl", - "GetBigThumbnail", - [{ originalImagePath: property.value }]); - - return thumbnailUrl; - } - else { - return null; - } - - } - else { - return property.value; - } - }); - } - - var modalFieldvalue = $scope.dialogItem; - - $scope.currentFolder = {}; - $scope.currentFolder.children = []; - $scope.currentPath = []; - $scope.startNodeId = -1; - - $scope.options = { - url: umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostAddFile"), - formData: { - currentFolder: $scope.startNodeId - } - }; - - //preload selected item - $scope.selectedMedia = undefined; - - $scope.submitFolder = function (e) { - if (e.keyCode === 13) { - e.preventDefault(); - $scope.$parent.data.showFolderInput = false; - - if ($scope.$parent.data.newFolder && $scope.$parent.data.newFolder != "") { - mediaResource - .addFolder($scope.$parent.data.newFolder, $scope.currentFolder.id) - .then(function (data) { - $scope.$parent.data.newFolder = undefined; - $scope.gotoFolder(data); - }); - } - } - }; - - $scope.gotoFolder = function (folder) { - - if (!folder) { - folder = { id: $scope.startNodeId, name: "Media", icon: "icon-folder" }; - } - - if (folder.id > 0) { - var matches = _.filter($scope.currentPath, function (value, index) { - if (value.id == folder.id) { - value.indexInPath = index; - return value; - } - }); - - if (matches && matches.length > 0) { - $scope.currentPath = $scope.currentPath.slice(0, matches[0].indexInPath + 1); - } - else { - $scope.currentPath.push(folder); - } - } - else { - $scope.currentPath = []; - } - - //mediaResource.rootMedia() - mediaResource.getChildren(folder.id) - .then(function (data) { - folder.children = data.items ? data.items : []; - - angular.forEach(folder.children, function (child) { - child.isFolder = child.contentTypeAlias == "Folder" ? true : false; - if (!child.isFolder) { - angular.forEach(child.properties, function (property) { - if (property.alias == "umbracoFile" && property.value) - { - child.thumbnail = mediaHelper.resolveFile(child, true); - child.image = property.value; - } - }) - } - }); - - $scope.options.formData.currentFolder = folder.id; - $scope.currentFolder = folder; - }); - }; - - $scope.iconFolder = "glyphicons-icon folder-open" - - $scope.selectMedia = function (media) { - - if (!media.isFolder) { - //we have 3 options add to collection (if multi) show details, or submit it right back to the callback - $scope.selectedMedia = media; - modalFieldvalue = "url(" + $scope.selectedMedia.image + ")"; - $scope.change(modalFieldvalue); - } - else { - $scope.gotoFolder(media); - } - - }; - - //default root item - if (!$scope.selectedMedia) { - $scope.gotoFolder(); - } - - $scope.submitAndClose = function () { - if (modalFieldvalue != "") { - $scope.submit(modalFieldvalue); - } else { - $scope.cancel(); - } - - }; - - $scope.cancelAndClose = function () { - $scope.cancel(); - } - -}) +/* Called for every canvasdesigner-over click */ +var onClickCanvasdesignerItem = function (schema) { + + var scope = angular.element($("#canvasdesignerPanel")).scope(); + + //if (scope.schemaFocus != schema.toLowerCase()) { + //var notFound = true; + $.each(scope.canvasdesignerModel.configs, function (indexConfig, config) { + if (config.schema && schema.toLowerCase() == config.schema.toLowerCase()) { + scope.currentSelected = config; + } + }); + //} + + scope.clearSelectedCategory(); + + scope.closeFloatPanels(); + + scope.$apply(); + +} + +/* Called for every canvasdesigner-over rollover */ +var onMouseoverCanvasdesignerItem = function (name) { + + var scope = angular.element($("#canvasdesignerPanel")).scope(); + + $.each(scope.canvasdesignerModel.configs, function (indexConfig, config) { + config.highlighted = false; + if (config.name && name.toLowerCase() == config.name.toLowerCase()) { + config.highlighted = true; + } + }); + + scope.$apply(); + +} + +/* Called when the iframe is first loaded */ +var setFrameIsLoaded = function (canvasdesignerConfig, canvasdesignerPalette) { + + var scope = angular.element($("#canvasdesignerPanel")).scope(); + + scope.canvasdesignerModel = canvasdesignerConfig; + scope.canvasdesignerPalette = canvasdesignerPalette; + scope.enableCanvasdesigner++; + scope.$apply(); +} + +/* Iframe body click */ +var iframeBodyClick = function () { + + var scope = angular.element($("#canvasdesignerPanel")).scope(); + + scope.closeFloatPanels(); +} + + +/*********************************************************************************************************/ +/* Canvasdesigner panel app and controller */ +/*********************************************************************************************************/ + +var app = angular.module("Umbraco.canvasdesigner", ['colorpicker', 'ui.slider', 'umbraco.resources', 'umbraco.services']) + +.controller("Umbraco.canvasdesignerController", function ($scope, $http, $window, $timeout, $location, dialogService) { + + $scope.isOpen = false; + $scope.frameLoaded = false; + $scope.enableCanvasdesigner = 0; + $scope.googleFontFamilies = {}; + $scope.pageId = $location.search().id; + $scope.pageUrl = "../dialogs/Preview.aspx?id=" + $location.search().id; + $scope.valueAreLoaded = false; + $scope.devices = [ + { name: "desktop", css: "desktop", icon: "icon-display", title: "Desktop" }, + { name: "laptop - 1366px", css: "laptop border", icon: "icon-laptop", title: "Laptop" }, + { name: "iPad portrait - 768px", css: "iPad-portrait border", icon: "icon-ipad", title: "Tablet portrait" }, + { name: "iPad landscape - 1024px", css: "iPad-landscape border", icon: "icon-ipad flip", title: "Tablet landscape" }, + { name: "smartphone portrait - 480px", css: "smartphone-portrait border", icon: "icon-iphone", title: "Smartphone portrait" }, + { name: "smartphone landscape - 320px", css: "smartphone-landscape border", icon: "icon-iphone flip", title: "Smartphone landscape" } + ]; + $scope.previewDevice = $scope.devices[0]; + + var apiController = "../Api/Canvasdesigner/"; + + /*****************************************************************************/ + /* Preview devices */ + /*****************************************************************************/ + + // Set preview device + $scope.updatePreviewDevice = function (device) { + $scope.previewDevice = device; + }; + + /*****************************************************************************/ + /* Exit Preview */ + /*****************************************************************************/ + + $scope.exitPreview = function () { + window.top.location.href = "../endPreview.aspx?redir=%2f" + $scope.pageId; + }; + + /*****************************************************************************/ + /* UI designer managment */ + /*****************************************************************************/ + + // Update all Canvasdesigner config's values from data + var updateConfigValue = function (data) { + + var fonts = []; + + $.each($scope.canvasdesignerModel.configs, function (indexConfig, config) { + if (config.editors) { + $.each(config.editors, function (indexItem, item) { + + /* try to get value */ + try { + + if (item.values) { + angular.forEach(Object.keys(item.values), function (key, indexKey) { + if (key != "''") { + var propertyAlias = key.toLowerCase() + item.alias.toLowerCase(); + var newValue = eval("data." + propertyAlias.replace("@", "")); + if (newValue == "''") { + newValue = ""; + } + item.values[key] = newValue; + } + }) + } + + // TODO: special init for font family picker + if (item.type == "googlefontpicker") { + if (item.values.fontType == 'google' && item.values.fontFamily + item.values.fontWeight && $.inArray(item.values.fontFamily + ":" + item.values.fontWeight, fonts) < 0) { + fonts.splice(0, 0, item.values.fontFamily + ":" + item.values.fontWeight); + } + } + + } + catch (err) { + console.info("Style parameter not found " + item.alias); + } + + }); + } + + }); + + // Load google font + $.each(fonts, function (indexFont, font) { + loadGoogleFont(font); + loadGoogleFontInFront(font); + }); + + $scope.valueAreLoaded = true; + + }; + + // Load parameters from GetLessParameters and init data of the Canvasdesigner config + $scope.initCanvasdesigner = function () { + + LazyLoad.js(['https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js']); + + $http.get(apiController + 'Load', { params: { pageId: $scope.pageId } }) + .success(function (data) { + + updateConfigValue(data); + + $timeout(function () { + $scope.frameLoaded = true; + }, 200); + + }); + + }; + + // Refresh all less parameters for every changes watching canvasdesignerModel + var refreshCanvasdesigner = function () { + + var parameters = []; + + if ($scope.canvasdesignerModel) { + + angular.forEach($scope.canvasdesignerModel.configs, function (config, indexConfig) { + + // Get currrent selected element + // TODO + //if ($scope.schemaFocus && angular.lowercase($scope.schemaFocus) == angular.lowercase(config.name)) { + // $scope.currentSelected = config.selector ? config.selector : config.schema; + //} + + if (config.editors) { + angular.forEach(config.editors, function (item, indexItem) { + + // Add new style + if (item.values) { + angular.forEach(Object.keys(item.values), function (key, indexKey) { + var propertyAlias = key.toLowerCase() + item.alias.toLowerCase(); + var value = eval("item.values." + key); + parameters.splice(parameters.length + 1, 0, "'@" + propertyAlias + "':'" + value + "'"); + }) + } + }); + } + + }); + + // Refresh page style + refreshFrontStyles(parameters); + + // Refresh layout of selected element + //$timeout(function () { + $scope.positionSelectedHide(); + if ($scope.currentSelected) { + refreshOutlineSelected($scope.currentSelected); + } + //}, 200); + + + + } + } + + $scope.createStyle = function (){ + $scope.saveLessParameters(false); + } + + $scope.saveStyle = function () { + $scope.saveLessParameters(true); + } + + // Save all parameter in CanvasdesignerParameters.less file + $scope.saveLessParameters = function (inherited) { + + var parameters = []; + $.each($scope.canvasdesignerModel.configs, function (indexConfig, config) { + if (config.editors) { + $.each(config.editors, function (indexItem, item) { + + if (item.values) { + angular.forEach(Object.keys(item.values), function (key, indexKey) { + var propertyAlias = key.toLowerCase() + item.alias.toLowerCase(); + var value = eval("item.values." + key); + parameters.splice(parameters.length + 1, 0, "@" + propertyAlias + ":" + value + ";"); + }) + + // TODO: special init for font family picker + if (item.type == "googlefontpicker" && item.values.fontFamily) { + var variant = item.values.fontWeight != "" || item.values.fontStyle != "" ? ":" + item.values.fontWeight + item.values.fontStyle : ""; + var gimport = "@import url('https://fonts.googleapis.com/css?family=" + item.values.fontFamily + variant + "');"; + if ($.inArray(gimport, parameters) < 0) { + parameters.splice(0, 0, gimport); + } + } + } + + }); + } + }); + + var resultParameters = { parameters: parameters.join(""), pageId: $scope.pageId, inherited: inherited }; + var transform = function (result) { + return $.param(result); + } + + $('.btn-default-save').attr("disabled", true); + $http.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded"; + $http.post(apiController + 'Save', resultParameters, { + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, + transformRequest: transform + }) + .success(function (data) { + $('.btn-default-save').attr("disabled", false); + $('#speechbubble').fadeIn('slow').delay(5000).fadeOut('slow'); + }); + + } + + // Delete current page Canvasdesigner + $scope.deleteCanvasdesigner = function () { + $('.btn-default-delete').attr("disabled", true); + $http.get(apiController + 'Delete', { params: { pageId: $scope.pageId } }) + .success(function (data) { + location.reload(); + }) + } + + + /*****************************************************************************/ + /* Preset design */ + /*****************************************************************************/ + + // Refresh with selected Canvasdesigner palette + $scope.refreshCanvasdesignerByPalette = function (palette) { + updateConfigValue(palette.data); + refreshCanvasdesigner(); + }; + + // Hidden botton to make preset from the current settings + $scope.makePreset = function () { + + var parameters = []; + $.each($scope.canvasdesignerModel.configs, function (indexConfig, config) { + if (config.editors) { + $.each(config.editors, function (indexItem, item) { + if (item.values) { + angular.forEach(Object.keys(item.values), function (key, indexKey) { + var propertyAlias = key.toLowerCase() + item.alias.toLowerCase(); + var value = eval("item.values." + key); + var value = (value != 0 && (value == undefined || value == "")) ? "''" : value; + parameters.splice(parameters.length + 1, 0, "\"" + propertyAlias + "\":" + " \"" + value + "\""); + }) + } + }); + } + }); + + $(".btn-group").append(""); + + }; + + /*****************************************************************************/ + /* Panel managment */ + /*****************************************************************************/ + + $scope.openPreviewDevice = function () { + $scope.showDevicesPreview = true; + $scope.closeIntelCanvasdesigner(); + } + + $scope.closePreviewDevice = function(){ + $scope.showDevicesPreview = false; + if ($scope.showStyleEditor) { + $scope.openIntelCanvasdesigner(); + } + } + + $scope.openPalettePicker = function () { + $scope.showPalettePicker = true; + $scope.showStyleEditor = false; + $scope.closeIntelCanvasdesigner(); + }; + + $scope.openStyleEditor = function () { + $scope.showStyleEditor = true; + $scope.showPalettePicker = false; + $scope.outlineSelectedHide() + $scope.openIntelCanvasdesigner() + } + + // Remove value from field + $scope.removeField = function (field) { + field.value = ""; + }; + + // Check if category existe + $scope.hasEditor = function (editors, category) { + var result = false; + angular.forEach(editors, function (item, index) { + if (item.category == category) { + result = true; + } + }); + return result; + }; + + $scope.closeFloatPanels = function () { + + /* hack to hide color picker */ + $(".spectrumcolorpicker input").spectrum("hide"); + + dialogService.close(); + $scope.showPalettePicker = false; + $scope.$apply(); + }; + + $scope.clearHighlightedItems = function () { + $.each($scope.canvasdesignerModel.configs, function (indexConfig, config) { + config.highlighted = false; + }); + } + + $scope.setCurrentHighlighted = function (item) { + $scope.clearHighlightedItems(); + item.highlighted = true; + } + + $scope.setCurrentSelected = function(item) { + $scope.currentSelected = item; + $scope.clearSelectedCategory(); + refreshOutlineSelected($scope.currentSelected); + } + + /* Editor categories */ + + $scope.getCategories = function (item) { + + var propertyCategories = []; + + $.each(item.editors, function (indexItem, editor) { + if (editor.category) { + if ($.inArray(editor.category, propertyCategories) < 0) { + propertyCategories.splice( propertyCategories.length + 1, 0, editor.category); + } + } + }); + + return propertyCategories; + + } + + $scope.setSelectedCategory = function (item) { + $scope.categoriesVisibility = $scope.categoriesVisibility || {}; + $scope.categoriesVisibility[item] = !$scope.categoriesVisibility[item]; + } + + $scope.clearSelectedCategory = function () { + $scope.categoriesVisibility = null; + } + + /*****************************************************************************/ + /* Call function into the front-end */ + /*****************************************************************************/ + + var loadGoogleFontInFront = function (font) { + if (document.getElementById("resultFrame").contentWindow.getFont) + document.getElementById("resultFrame").contentWindow.getFont(font); + }; + + var refreshFrontStyles = function (parameters) { + if (document.getElementById("resultFrame").contentWindow.refreshLayout) + document.getElementById("resultFrame").contentWindow.refreshLayout(parameters); + }; + + var hideUmbracoPreviewBadge = function () { + var iframe = (document.getElementById("resultFrame").contentWindow || document.getElementById("resultFrame").contentDocument); + if(iframe.document.getElementById("umbracoPreviewBadge")) + iframe.document.getElementById("umbracoPreviewBadge").style.display = "none"; + }; + + $scope.openIntelCanvasdesigner = function () { + if (document.getElementById("resultFrame").contentWindow.initIntelCanvasdesigner) + document.getElementById("resultFrame").contentWindow.initIntelCanvasdesigner($scope.canvasdesignerModel); + }; + + $scope.closeIntelCanvasdesigner = function () { + if (document.getElementById("resultFrame").contentWindow.closeIntelCanvasdesigner) + document.getElementById("resultFrame").contentWindow.closeIntelCanvasdesigner($scope.canvasdesignerModel); + $scope.outlineSelectedHide(); + }; + + var refreshOutlineSelected = function (config) { + var schema = config.selector ? config.selector : config.schema; + if (document.getElementById("resultFrame").contentWindow.refreshOutlineSelected) + document.getElementById("resultFrame").contentWindow.refreshOutlineSelected(schema); + } + + $scope.outlineSelectedHide = function () { + $scope.currentSelected = null; + if (document.getElementById("resultFrame").contentWindow.outlineSelectedHide) + document.getElementById("resultFrame").contentWindow.outlineSelectedHide(); + }; + + $scope.refreshOutlinePosition = function (config) { + var schema = config.selector ? config.selector : config.schema; + if (document.getElementById("resultFrame").contentWindow.refreshOutlinePosition) + document.getElementById("resultFrame").contentWindow.refreshOutlinePosition(schema); + } + + $scope.positionSelectedHide = function () { + if (document.getElementById("resultFrame").contentWindow.outlinePositionHide) + document.getElementById("resultFrame").contentWindow.outlinePositionHide(); + } + + /*****************************************************************************/ + /* Google font loader, TODO: put together from directive, front and back */ + /*****************************************************************************/ + + var webFontScriptLoaded = false; + var loadGoogleFont = function (font) { + + if (!webFontScriptLoaded) { + $.getScript('https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js') + .done(function () { + webFontScriptLoaded = true; + // Recursively call once webfont script is available. + loadGoogleFont(font); + }) + .fail(function () { + console.log('error loading webfont'); + }); + } + else { + WebFont.load({ + google: { + families: [font] + }, + loading: function () { + //console.log('loading font' + font + ' in UI designer'); + }, + active: function () { + //console.log('loaded font ' + font + ' in UI designer'); + }, + inactive: function () { + //console.log('error loading font ' + font + ' in UI designer'); + } + }); + } + + }; + + /*****************************************************************************/ + /* Init */ + /*****************************************************************************/ + + // Preload of the google font + if ($scope.showStyleEditor) { + $http.get(apiController + 'GetGoogleFont').success(function (data) { + $scope.googleFontFamilies = data; + }); + } + + // watch framLoaded, only if iframe page have enableCanvasdesigner() + $scope.$watch("enableCanvasdesigner", function () { + $timeout(function () { + if ($scope.enableCanvasdesigner > 0) { + + $scope.$watch('ngRepeatFinished', function (ngRepeatFinishedEvent) { + $timeout(function () { + $scope.initCanvasdesigner(); + }, 200); + }); + + $scope.$watch('canvasdesignerModel', function () { + refreshCanvasdesigner(); + }, true); + + } + }, 100); + }, true); - -/*********************************************************************************************************/ -/* Background editor */ -/*********************************************************************************************************/ - -angular.module("Umbraco.canvasdesigner") - -.controller("Umbraco.canvasdesigner.border", function ($scope, dialogService) { - - $scope.defaultBorderList = ["all", "left", "right", "top", "bottom"]; - $scope.borderList = []; - - $scope.bordertypes = ["solid", "dashed", "dotted"]; - $scope.selectedBorder = { - name: "all", - size: 0, - color: '', - type: '' - }; - - $scope.setselectedBorder = function (bordertype) { - - if (bordertype == "all") { - $scope.selectedBorder.name="all"; - $scope.selectedBorder.size= $scope.item.values.bordersize; - $scope.selectedBorder.color= $scope.item.values.bordercolor; - $scope.selectedBorder.type= $scope.item.values.bordertype; - } - - if (bordertype == "left") { - $scope.selectedBorder.name= "left"; - $scope.selectedBorder.size= $scope.item.values.leftbordersize; - $scope.selectedBorder.color= $scope.item.values.leftbordercolor; - $scope.selectedBorder.type= $scope.item.values.leftbordertype; - } - - if (bordertype == "right") { - $scope.selectedBorder.name= "right"; - $scope.selectedBorder.size= $scope.item.values.rightbordersize; - $scope.selectedBorder.color= $scope.item.values.rightbordercolor; - $scope.selectedBorder.type= $scope.item.values.rightbordertype; - } - - if (bordertype == "top") { - $scope.selectedBorder.name= "top"; - $scope.selectedBorder.size= $scope.item.values.topbordersize; - $scope.selectedBorder.color= $scope.item.values.topbordercolor; - $scope.selectedBorder.type= $scope.item.values.topbordertype; - } - - if (bordertype == "bottom") { - $scope.selectedBorder.name= "bottom"; - $scope.selectedBorder.size= $scope.item.values.bottombordersize; - $scope.selectedBorder.color= $scope.item.values.bottombordercolor; - $scope.selectedBorder.type= $scope.item.values.bottombordertype; - } - - } - - if (!$scope.item.values) { - $scope.item.values = { - bordersize: '', - bordercolor: '', - bordertype: 'solid', - leftbordersize: '', - leftbordercolor: '', - leftbordertype: 'solid', - rightbordersize: '', - rightbordercolor: '', - rightbordertype: 'solid', - topbordersize: '', - topbordercolor: '', - topbordertype: 'solid', - bottombordersize: '', - bottombordercolor: '', - bottombordertype: 'solid', - }; - } - - if ($scope.item.enable) { - angular.forEach($scope.defaultBorderList, function (key, indexKey) { - if ($.inArray(key, $scope.item.enable) >= 0) { - $scope.borderList.splice($scope.borderList.length + 1, 0, key); - } - }) - } - else { - $scope.borderList = $scope.defaultBorderList; - } - - $scope.$watch("valueAreLoaded", function () { - $scope.setselectedBorder($scope.borderList[0]); - }, false); - - $scope.$watch("selectedBorder", function () { - - if ($scope.selectedBorder.name == "all") { - $scope.item.values.bordersize = $scope.selectedBorder.size; - $scope.item.values.bordertype = $scope.selectedBorder.type; - } - - if ($scope.selectedBorder.name == "left") { - $scope.item.values.leftbordersize = $scope.selectedBorder.size; - $scope.item.values.leftbordertype = $scope.selectedBorder.type; - } - - if ($scope.selectedBorder.name == "right") { - $scope.item.values.rightbordersize = $scope.selectedBorder.size; - $scope.item.values.rightbordertype = $scope.selectedBorder.type; - } - - if ($scope.selectedBorder.name == "top") { - $scope.item.values.topbordersize = $scope.selectedBorder.size; - $scope.item.values.topbordertype = $scope.selectedBorder.type; - } - - if ($scope.selectedBorder.name == "bottom") { - $scope.item.values.bottombordersize = $scope.selectedBorder.size; - $scope.item.values.bottombordertype = $scope.selectedBorder.type; - } - - }, true) - }) - -/*********************************************************************************************************/ -/* color editor */ -/*********************************************************************************************************/ - -angular.module("Umbraco.canvasdesigner") - -.controller("Umbraco.canvasdesigner.color", function ($scope) { - - if (!$scope.item.values) { - $scope.item.values = { - color: '' - }; - } - + +.directive('onFinishRenderFilters', function ($timeout) { + return { + restrict: 'A', + link: function (scope, element, attr) { + if (scope.$last === true) { + $timeout(function () { + scope.$emit('ngRepeatFinished'); + }); + } + } + }; }) - -/*********************************************************************************************************/ -/* google font editor */ -/*********************************************************************************************************/ - -angular.module("Umbraco.canvasdesigner") - -.controller("Umbraco.canvasdesigner.googlefontpicker", function ($scope, dialogService) { - - if (!$scope.item.values) { - $scope.item.values = { - fontFamily: '', - fontType: '', - fontWeight: '', - fontStyle: '', - } - } - - $scope.setStyleVariant = function () { - if ($scope.item.values != undefined) { - return { - 'font-family': $scope.item.values.fontFamily, - 'font-weight': $scope.item.values.fontWeight, - 'font-style': $scope.item.values.fontStyle - } - } - }; - - $scope.open = function (field) { - - var config = { - template: "googlefontdialog.html", - change: function (data) { - $scope.item.values = data; - }, - callback: function (data) { - $scope.item.values = data; - }, - cancel: function (data) { - $scope.item.values = data; - }, - dialogData: $scope.googleFontFamilies, - dialogItem: $scope.item.values - }; - - dialogService.open(config); - - }; - -}) - -.controller("googlefontdialog.controller", function ($scope) { - - $scope.safeFonts = ["Arial, Helvetica", "Impact", "Lucida Sans Unicode", "Tahoma", "Trebuchet MS", "Verdana", "Georgia", "Times New Roman", "Courier New, Courier"]; - $scope.fonts = []; - $scope.selectedFont = {}; - - var googleGetWeight = function (googleVariant) { - return (googleVariant != undefined && googleVariant != "") ? googleVariant.replace("italic", "") : ""; - }; - - var googleGetStyle = function (googleVariant) { - var variantStyle = ""; - if (googleVariant != undefined && googleVariant != "" && googleVariant.indexOf("italic") >= 0) { - variantWeight = googleVariant.replace("italic", ""); - variantStyle = "italic"; - } - return variantStyle; - }; - - angular.forEach($scope.safeFonts, function (value, key) { - $scope.fonts.push({ - groupName: "Safe fonts", - fontType: "safe", - fontFamily: value, - fontWeight: "normal", - fontStyle: "normal", - }); - }); - - angular.forEach($scope.dialogData.items, function (value, key) { - var variants = value.variants; - var variant = value.variants.length > 0 ? value.variants[0] : ""; - var fontWeight = googleGetWeight(variant); - var fontStyle = googleGetStyle(variant); - $scope.fonts.push({ - groupName: "Google fonts", - fontType: "google", - fontFamily: value.family, - variants: value.variants, - variant: variant, - fontWeight: fontWeight, - fontStyle: fontStyle - }); - }); - - $scope.setStyleVariant = function () { - if ($scope.dialogItem != undefined) { - return { - 'font-family': $scope.selectedFont.fontFamily, - 'font-weight': $scope.selectedFont.fontWeight, - 'font-style': $scope.selectedFont.fontStyle - } - } - }; - - function loadFont(font, variant) { - WebFont.load({ - google: { - families: [font.fontFamily + ":" + variant] - }, - loading: function () { - console.log('loading'); - }, - active: function () { - $scope.selectedFont = font; - $scope.selectedFont.fontWeight = googleGetWeight(variant); - $scope.selectedFont.fontStyle = googleGetStyle(variant); - // If $apply isn't called, the new font family isn't applied until the next user click. - $scope.change({ - fontFamily: $scope.selectedFont.fontFamily, - fontType: $scope.selectedFont.fontType, - fontWeight: $scope.selectedFont.fontWeight, - fontStyle: $scope.selectedFont.fontStyle, - }); - } - }); - } - - var webFontScriptLoaded = false; - $scope.showFontPreview = function (font, variant) { - - if (!variant) - variant = font.variant; - - if (font != undefined && font.fontFamily != "" && font.fontType == "google") { - - // Font needs to be independently loaded in the iframe for live preview to work. - document.getElementById("resultFrame").contentWindow.getFont(font.fontFamily + ":" + variant); - - if (!webFontScriptLoaded) { - $.getScript('https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js') - .done(function() { - webFontScriptLoaded = true; - loadFont(font, variant); - }) - .fail(function() { - console.log('error loading webfont'); - }); - } - else { - loadFont(font, variant); - } - } - else { - - // Font is available, apply it immediately in modal preview. - $scope.selectedFont = font; - // If $apply isn't called, the new font family isn't applied until the next user click. - $scope.change({ - fontFamily: $scope.selectedFont.fontFamily, - fontType: $scope.selectedFont.fontType, - fontWeight: $scope.selectedFont.fontWeight, - fontStyle: $scope.selectedFont.fontStyle, - }); - } - - - - } - - $scope.cancelAndClose = function () { - $scope.cancel(); - } - - $scope.submitAndClose = function () { - $scope.submit({ - fontFamily: $scope.selectedFont.fontFamily, - fontType: $scope.selectedFont.fontType, - fontWeight: $scope.selectedFont.fontWeight, - fontStyle: $scope.selectedFont.fontStyle, - }); - }; - - if ($scope.dialogItem != undefined) { - angular.forEach($scope.fonts, function (value, key) { - if (value.fontFamily == $scope.dialogItem.fontFamily) { - $scope.selectedFont = value; - $scope.selectedFont.variant = $scope.dialogItem.fontWeight + $scope.dialogItem.fontStyle; - $scope.selectedFont.fontWeight = $scope.dialogItem.fontWeight; - $scope.selectedFont.fontStyle = $scope.dialogItem.fontStyle; - } - }); - } - + +.directive('iframeIsLoaded', function ($timeout) { + return { + restrict: 'A', + link: function (scope, element, attr) { + element.load(function () { + var iframe = (element.context.contentWindow || element.context.contentDocument); + if(iframe && iframe.document.getElementById("umbracoPreviewBadge")) + iframe.document.getElementById("umbracoPreviewBadge").style.display = "none"; + if (!document.getElementById("resultFrame").contentWindow.refreshLayout) { + scope.frameLoaded = true; + scope.$apply(); + } + }); + } + }; +}) + + +/*********************************************************************************************************/ +/* Background editor */ +/*********************************************************************************************************/ + +angular.module("Umbraco.canvasdesigner") + +.controller("Umbraco.canvasdesigner.background", function ($scope, dialogService) { + + if (!$scope.item.values) { + $scope.item.values = { + imageorpattern: '', + color: '' + }; + } + + $scope.open = function (field) { + + var config = { + template: "mediaPickerModal.html", + change: function (data) { + $scope.item.values.imageorpattern = data; + }, + callback: function (data) { + $scope.item.values.imageorpattern = data; + }, + cancel: function (data) { + $scope.item.values.imageorpattern = data; + }, + dialogData: $scope.googleFontFamilies, + dialogItem: $scope.item.values.imageorpattern + }; + + dialogService.open(config); + + }; + +}) + +.controller('canvasdesigner.mediaPickerModal', function ($scope, $http, mediaResource, umbRequestHelper, entityResource, mediaHelper) { + + if (mediaHelper && mediaHelper.registerFileResolver) { + mediaHelper.registerFileResolver("Umbraco.UploadField", function (property, entity, thumbnail) { + if (thumbnail) { + + if (mediaHelper.detectIfImageByExtension(property.value)) { + var thumbnailUrl = umbRequestHelper.getApiUrl( + "imagesApiBaseUrl", + "GetBigThumbnail", + [{ originalImagePath: property.value }]); + + return thumbnailUrl; + } + else { + return null; + } + + } + else { + return property.value; + } + }); + } + + var modalFieldvalue = $scope.dialogItem; + + $scope.currentFolder = {}; + $scope.currentFolder.children = []; + $scope.currentPath = []; + $scope.startNodeId = -1; + + $scope.options = { + url: umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostAddFile"), + formData: { + currentFolder: $scope.startNodeId + } + }; + + //preload selected item + $scope.selectedMedia = undefined; + + $scope.submitFolder = function (e) { + if (e.keyCode === 13) { + e.preventDefault(); + $scope.$parent.data.showFolderInput = false; + + if ($scope.$parent.data.newFolder && $scope.$parent.data.newFolder != "") { + mediaResource + .addFolder($scope.$parent.data.newFolder, $scope.currentFolder.id) + .then(function (data) { + $scope.$parent.data.newFolder = undefined; + $scope.gotoFolder(data); + }); + } + } + }; + + $scope.gotoFolder = function (folder) { + + if (!folder) { + folder = { id: $scope.startNodeId, name: "Media", icon: "icon-folder" }; + } + + if (folder.id > 0) { + var matches = _.filter($scope.currentPath, function (value, index) { + if (value.id == folder.id) { + value.indexInPath = index; + return value; + } + }); + + if (matches && matches.length > 0) { + $scope.currentPath = $scope.currentPath.slice(0, matches[0].indexInPath + 1); + } + else { + $scope.currentPath.push(folder); + } + } + else { + $scope.currentPath = []; + } + + //mediaResource.rootMedia() + mediaResource.getChildren(folder.id) + .then(function (data) { + folder.children = data.items ? data.items : []; + + angular.forEach(folder.children, function (child) { + child.isFolder = child.contentTypeAlias == "Folder" ? true : false; + if (!child.isFolder) { + angular.forEach(child.properties, function (property) { + if (property.alias == "umbracoFile" && property.value) + { + child.thumbnail = mediaHelper.resolveFile(child, true); + child.image = property.value; + } + }) + } + }); + + $scope.options.formData.currentFolder = folder.id; + $scope.currentFolder = folder; + }); + }; + + $scope.iconFolder = "glyphicons-icon folder-open" + + $scope.selectMedia = function (media) { + + if (!media.isFolder) { + //we have 3 options add to collection (if multi) show details, or submit it right back to the callback + $scope.selectedMedia = media; + modalFieldvalue = "url(" + $scope.selectedMedia.image + ")"; + $scope.change(modalFieldvalue); + } + else { + $scope.gotoFolder(media); + } + + }; + + //default root item + if (!$scope.selectedMedia) { + $scope.gotoFolder(); + } + + $scope.submitAndClose = function () { + if (modalFieldvalue != "") { + $scope.submit(modalFieldvalue); + } else { + $scope.cancel(); + } + + }; + + $scope.cancelAndClose = function () { + $scope.cancel(); + } + +}) + + +/*********************************************************************************************************/ +/* Background editor */ +/*********************************************************************************************************/ + +angular.module("Umbraco.canvasdesigner") + +.controller("Umbraco.canvasdesigner.border", function ($scope, dialogService) { + + $scope.defaultBorderList = ["all", "left", "right", "top", "bottom"]; + $scope.borderList = []; + + $scope.bordertypes = ["solid", "dashed", "dotted"]; + $scope.selectedBorder = { + name: "all", + size: 0, + color: '', + type: '' + }; + + $scope.setselectedBorder = function (bordertype) { + + if (bordertype == "all") { + $scope.selectedBorder.name="all"; + $scope.selectedBorder.size= $scope.item.values.bordersize; + $scope.selectedBorder.color= $scope.item.values.bordercolor; + $scope.selectedBorder.type= $scope.item.values.bordertype; + } + + if (bordertype == "left") { + $scope.selectedBorder.name= "left"; + $scope.selectedBorder.size= $scope.item.values.leftbordersize; + $scope.selectedBorder.color= $scope.item.values.leftbordercolor; + $scope.selectedBorder.type= $scope.item.values.leftbordertype; + } + + if (bordertype == "right") { + $scope.selectedBorder.name= "right"; + $scope.selectedBorder.size= $scope.item.values.rightbordersize; + $scope.selectedBorder.color= $scope.item.values.rightbordercolor; + $scope.selectedBorder.type= $scope.item.values.rightbordertype; + } + + if (bordertype == "top") { + $scope.selectedBorder.name= "top"; + $scope.selectedBorder.size= $scope.item.values.topbordersize; + $scope.selectedBorder.color= $scope.item.values.topbordercolor; + $scope.selectedBorder.type= $scope.item.values.topbordertype; + } + + if (bordertype == "bottom") { + $scope.selectedBorder.name= "bottom"; + $scope.selectedBorder.size= $scope.item.values.bottombordersize; + $scope.selectedBorder.color= $scope.item.values.bottombordercolor; + $scope.selectedBorder.type= $scope.item.values.bottombordertype; + } + + } + + if (!$scope.item.values) { + $scope.item.values = { + bordersize: '', + bordercolor: '', + bordertype: 'solid', + leftbordersize: '', + leftbordercolor: '', + leftbordertype: 'solid', + rightbordersize: '', + rightbordercolor: '', + rightbordertype: 'solid', + topbordersize: '', + topbordercolor: '', + topbordertype: 'solid', + bottombordersize: '', + bottombordercolor: '', + bottombordertype: 'solid', + }; + } + + if ($scope.item.enable) { + angular.forEach($scope.defaultBorderList, function (key, indexKey) { + if ($.inArray(key, $scope.item.enable) >= 0) { + $scope.borderList.splice($scope.borderList.length + 1, 0, key); + } + }) + } + else { + $scope.borderList = $scope.defaultBorderList; + } + + $scope.$watch("valueAreLoaded", function () { + $scope.setselectedBorder($scope.borderList[0]); + }, false); + + $scope.$watch("selectedBorder", function () { + + if ($scope.selectedBorder.name == "all") { + $scope.item.values.bordersize = $scope.selectedBorder.size; + $scope.item.values.bordertype = $scope.selectedBorder.type; + } + + if ($scope.selectedBorder.name == "left") { + $scope.item.values.leftbordersize = $scope.selectedBorder.size; + $scope.item.values.leftbordertype = $scope.selectedBorder.type; + } + + if ($scope.selectedBorder.name == "right") { + $scope.item.values.rightbordersize = $scope.selectedBorder.size; + $scope.item.values.rightbordertype = $scope.selectedBorder.type; + } + + if ($scope.selectedBorder.name == "top") { + $scope.item.values.topbordersize = $scope.selectedBorder.size; + $scope.item.values.topbordertype = $scope.selectedBorder.type; + } + + if ($scope.selectedBorder.name == "bottom") { + $scope.item.values.bottombordersize = $scope.selectedBorder.size; + $scope.item.values.bottombordertype = $scope.selectedBorder.type; + } + + }, true) + +}) + +/*********************************************************************************************************/ +/* color editor */ +/*********************************************************************************************************/ + +angular.module("Umbraco.canvasdesigner") + +.controller("Umbraco.canvasdesigner.color", function ($scope) { + + if (!$scope.item.values) { + $scope.item.values = { + color: '' + }; + } + +}) + +/*********************************************************************************************************/ +/* google font editor */ +/*********************************************************************************************************/ + +angular.module("Umbraco.canvasdesigner") + +.controller("Umbraco.canvasdesigner.googlefontpicker", function ($scope, dialogService) { + + if (!$scope.item.values) { + $scope.item.values = { + fontFamily: '', + fontType: '', + fontWeight: '', + fontStyle: '', + } + } + + $scope.setStyleVariant = function () { + if ($scope.item.values != undefined) { + return { + 'font-family': $scope.item.values.fontFamily, + 'font-weight': $scope.item.values.fontWeight, + 'font-style': $scope.item.values.fontStyle + } + } + }; + + $scope.open = function (field) { + + var config = { + template: "googlefontdialog.html", + change: function (data) { + $scope.item.values = data; + }, + callback: function (data) { + $scope.item.values = data; + }, + cancel: function (data) { + $scope.item.values = data; + }, + dialogData: $scope.googleFontFamilies, + dialogItem: $scope.item.values + }; + + dialogService.open(config); + + }; + +}) + +.controller("googlefontdialog.controller", function ($scope) { + + $scope.safeFonts = ["Arial, Helvetica", "Impact", "Lucida Sans Unicode", "Tahoma", "Trebuchet MS", "Verdana", "Georgia", "Times New Roman", "Courier New, Courier"]; + $scope.fonts = []; + $scope.selectedFont = {}; + + var googleGetWeight = function (googleVariant) { + return (googleVariant != undefined && googleVariant != "") ? googleVariant.replace("italic", "") : ""; + }; + + var googleGetStyle = function (googleVariant) { + var variantStyle = ""; + if (googleVariant != undefined && googleVariant != "" && googleVariant.indexOf("italic") >= 0) { + variantWeight = googleVariant.replace("italic", ""); + variantStyle = "italic"; + } + return variantStyle; + }; + + angular.forEach($scope.safeFonts, function (value, key) { + $scope.fonts.push({ + groupName: "Safe fonts", + fontType: "safe", + fontFamily: value, + fontWeight: "normal", + fontStyle: "normal", + }); + }); + + angular.forEach($scope.dialogData.items, function (value, key) { + var variants = value.variants; + var variant = value.variants.length > 0 ? value.variants[0] : ""; + var fontWeight = googleGetWeight(variant); + var fontStyle = googleGetStyle(variant); + $scope.fonts.push({ + groupName: "Google fonts", + fontType: "google", + fontFamily: value.family, + variants: value.variants, + variant: variant, + fontWeight: fontWeight, + fontStyle: fontStyle + }); + }); + + $scope.setStyleVariant = function () { + if ($scope.dialogItem != undefined) { + return { + 'font-family': $scope.selectedFont.fontFamily, + 'font-weight': $scope.selectedFont.fontWeight, + 'font-style': $scope.selectedFont.fontStyle + } + } + }; + + function loadFont(font, variant) { + WebFont.load({ + google: { + families: [font.fontFamily + ":" + variant] + }, + loading: function () { + console.log('loading'); + }, + active: function () { + $scope.selectedFont = font; + $scope.selectedFont.fontWeight = googleGetWeight(variant); + $scope.selectedFont.fontStyle = googleGetStyle(variant); + // If $apply isn't called, the new font family isn't applied until the next user click. + $scope.change({ + fontFamily: $scope.selectedFont.fontFamily, + fontType: $scope.selectedFont.fontType, + fontWeight: $scope.selectedFont.fontWeight, + fontStyle: $scope.selectedFont.fontStyle, + }); + } + }); + } + + var webFontScriptLoaded = false; + $scope.showFontPreview = function (font, variant) { + + if (!variant) + variant = font.variant; + + if (font != undefined && font.fontFamily != "" && font.fontType == "google") { + + // Font needs to be independently loaded in the iframe for live preview to work. + document.getElementById("resultFrame").contentWindow.getFont(font.fontFamily + ":" + variant); + + if (!webFontScriptLoaded) { + $.getScript('https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js') + .done(function() { + webFontScriptLoaded = true; + loadFont(font, variant); + }) + .fail(function() { + console.log('error loading webfont'); + }); + } + else { + loadFont(font, variant); + } + } + else { + + // Font is available, apply it immediately in modal preview. + $scope.selectedFont = font; + // If $apply isn't called, the new font family isn't applied until the next user click. + $scope.change({ + fontFamily: $scope.selectedFont.fontFamily, + fontType: $scope.selectedFont.fontType, + fontWeight: $scope.selectedFont.fontWeight, + fontStyle: $scope.selectedFont.fontStyle, + }); + } + + + + } + + $scope.cancelAndClose = function () { + $scope.cancel(); + } + + $scope.submitAndClose = function () { + $scope.submit({ + fontFamily: $scope.selectedFont.fontFamily, + fontType: $scope.selectedFont.fontType, + fontWeight: $scope.selectedFont.fontWeight, + fontStyle: $scope.selectedFont.fontStyle, + }); + }; + + if ($scope.dialogItem != undefined) { + angular.forEach($scope.fonts, function (value, key) { + if (value.fontFamily == $scope.dialogItem.fontFamily) { + $scope.selectedFont = value; + $scope.selectedFont.variant = $scope.dialogItem.fontWeight + $scope.dialogItem.fontStyle; + $scope.selectedFont.fontWeight = $scope.dialogItem.fontWeight; + $scope.selectedFont.fontStyle = $scope.dialogItem.fontStyle; + } + }); + } + }); - -/*********************************************************************************************************/ -/* grid row editor */ -/*********************************************************************************************************/ - -angular.module("Umbraco.canvasdesigner") - -.controller("Umbraco.canvasdesigner.gridRow", function ($scope) { - - if (!$scope.item.values) { - $scope.item.values = { - fullsize: false - }; - } - + +/*********************************************************************************************************/ +/* grid row editor */ +/*********************************************************************************************************/ + +angular.module("Umbraco.canvasdesigner") + +.controller("Umbraco.canvasdesigner.gridRow", function ($scope) { + + if (!$scope.item.values) { + $scope.item.values = { + fullsize: false + }; + } + }) - -/*********************************************************************************************************/ -/* Layout */ -/*********************************************************************************************************/ - -angular.module("Umbraco.canvasdesigner") - -.controller("Umbraco.canvasdesigner.layout", function ($scope) { - - if (!$scope.item.values) { - $scope.item.values = { - layout: "" - } - } - + +/*********************************************************************************************************/ +/* Layout */ +/*********************************************************************************************************/ + +angular.module("Umbraco.canvasdesigner") + +.controller("Umbraco.canvasdesigner.layout", function ($scope) { + + if (!$scope.item.values) { + $scope.item.values = { + layout: "" + } + } + }) - -/*********************************************************************************************************/ -/* margin editor */ -/*********************************************************************************************************/ - -angular.module("Umbraco.canvasdesigner") - -.controller("Umbraco.canvasdesigner.margin", function ($scope, dialogService) { - - $scope.defaultmarginList = ["all", "left", "right", "top", "bottom"]; - $scope.marginList = []; - - $scope.selectedmargin = { - name: "", - value: 0, - }; - - $scope.setSelectedmargin = function (margintype) { - - if (margintype == "all") { - $scope.selectedmargin.name = "all"; - $scope.selectedmargin.value = $scope.item.values.marginvalue; - } - - if (margintype == "left") { - $scope.selectedmargin.name = "left"; - $scope.selectedmargin.value = $scope.item.values.leftmarginvalue; - } - - if (margintype == "right") { - $scope.selectedmargin.name = "right"; - $scope.selectedmargin.value = $scope.item.values.rightmarginvalue; - } - - if (margintype == "top") { - $scope.selectedmargin.name = "top"; - $scope.selectedmargin.value = $scope.item.values.topmarginvalue; - } - - if (margintype == "bottom") { - $scope.selectedmargin.name = "bottom"; - $scope.selectedmargin.value = $scope.item.values.bottommarginvalue; - } - - } - - if (!$scope.item.values) { - $scope.item.values = { - marginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 0 ? $scope.item.defaultValue[0] : '', - leftmarginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 1 ? $scope.item.defaultValue[1] : '', - rightmarginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 2 ? $scope.item.defaultValue[2] : '', - topmarginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 3 ? $scope.item.defaultValue[3] : '', - bottommarginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 4 ? $scope.item.defaultValue[4] : '', - }; - } - - if ($scope.item.enable) { - angular.forEach($scope.defaultmarginList, function (key, indexKey) { - if ($.inArray(key, $scope.item.enable) >= 0) { - $scope.marginList.splice($scope.marginList.length + 1, 0, key); - } - }) - } - else { - $scope.marginList = $scope.defaultmarginList; - } - - $scope.$watch("valueAreLoaded", function () { - $scope.setSelectedmargin($scope.marginList[0]); - }, false); - - $scope.$watch("selectedmargin", function () { - - if ($scope.selectedmargin.name == "all") { - $scope.item.values.marginvalue = $scope.selectedmargin.value; - } - - if ($scope.selectedmargin.name == "left") { - $scope.item.values.leftmarginvalue = $scope.selectedmargin.value; - } - - if ($scope.selectedmargin.name == "right") { - $scope.item.values.rightmarginvalue = $scope.selectedmargin.value; - } - - if ($scope.selectedmargin.name == "top") { - $scope.item.values.topmarginvalue = $scope.selectedmargin.value; - } - - if ($scope.selectedmargin.name == "bottom") { - $scope.item.values.bottommarginvalue = $scope.selectedmargin.value; - } - - }, true) - - - + +/*********************************************************************************************************/ +/* margin editor */ +/*********************************************************************************************************/ + +angular.module("Umbraco.canvasdesigner") + +.controller("Umbraco.canvasdesigner.margin", function ($scope, dialogService) { + + $scope.defaultmarginList = ["all", "left", "right", "top", "bottom"]; + $scope.marginList = []; + + $scope.selectedmargin = { + name: "", + value: 0, + }; + + $scope.setSelectedmargin = function (margintype) { + + if (margintype == "all") { + $scope.selectedmargin.name = "all"; + $scope.selectedmargin.value = $scope.item.values.marginvalue; + } + + if (margintype == "left") { + $scope.selectedmargin.name = "left"; + $scope.selectedmargin.value = $scope.item.values.leftmarginvalue; + } + + if (margintype == "right") { + $scope.selectedmargin.name = "right"; + $scope.selectedmargin.value = $scope.item.values.rightmarginvalue; + } + + if (margintype == "top") { + $scope.selectedmargin.name = "top"; + $scope.selectedmargin.value = $scope.item.values.topmarginvalue; + } + + if (margintype == "bottom") { + $scope.selectedmargin.name = "bottom"; + $scope.selectedmargin.value = $scope.item.values.bottommarginvalue; + } + + } + + if (!$scope.item.values) { + $scope.item.values = { + marginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 0 ? $scope.item.defaultValue[0] : '', + leftmarginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 1 ? $scope.item.defaultValue[1] : '', + rightmarginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 2 ? $scope.item.defaultValue[2] : '', + topmarginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 3 ? $scope.item.defaultValue[3] : '', + bottommarginvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 4 ? $scope.item.defaultValue[4] : '', + }; + } + + if ($scope.item.enable) { + angular.forEach($scope.defaultmarginList, function (key, indexKey) { + if ($.inArray(key, $scope.item.enable) >= 0) { + $scope.marginList.splice($scope.marginList.length + 1, 0, key); + } + }) + } + else { + $scope.marginList = $scope.defaultmarginList; + } + + $scope.$watch("valueAreLoaded", function () { + $scope.setSelectedmargin($scope.marginList[0]); + }, false); + + $scope.$watch("selectedmargin", function () { + + if ($scope.selectedmargin.name == "all") { + $scope.item.values.marginvalue = $scope.selectedmargin.value; + } + + if ($scope.selectedmargin.name == "left") { + $scope.item.values.leftmarginvalue = $scope.selectedmargin.value; + } + + if ($scope.selectedmargin.name == "right") { + $scope.item.values.rightmarginvalue = $scope.selectedmargin.value; + } + + if ($scope.selectedmargin.name == "top") { + $scope.item.values.topmarginvalue = $scope.selectedmargin.value; + } + + if ($scope.selectedmargin.name == "bottom") { + $scope.item.values.bottommarginvalue = $scope.selectedmargin.value; + } + + }, true) + + + }) - -/*********************************************************************************************************/ -/* padding editor */ -/*********************************************************************************************************/ - -angular.module("Umbraco.canvasdesigner") - -.controller("Umbraco.canvasdesigner.padding", function ($scope, dialogService) { - - $scope.defaultPaddingList = ["all", "left", "right", "top", "bottom"]; - $scope.paddingList = []; - - $scope.selectedpadding = { - name: "", - value: 0, - }; - - $scope.setSelectedpadding = function (paddingtype) { - - if (paddingtype == "all") { - $scope.selectedpadding.name="all"; - $scope.selectedpadding.value= $scope.item.values.paddingvalue; - } - - if (paddingtype == "left") { - $scope.selectedpadding.name= "left"; - $scope.selectedpadding.value= $scope.item.values.leftpaddingvalue; - } - - if (paddingtype == "right") { - $scope.selectedpadding.name= "right"; - $scope.selectedpadding.value= $scope.item.values.rightpaddingvalue; - } - - if (paddingtype == "top") { - $scope.selectedpadding.name= "top"; - $scope.selectedpadding.value= $scope.item.values.toppaddingvalue; - } - - if (paddingtype == "bottom") { - $scope.selectedpadding.name= "bottom"; - $scope.selectedpadding.value= $scope.item.values.bottompaddingvalue; - } - - } - - if (!$scope.item.values) { - $scope.item.values = { - paddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 0 ? $scope.item.defaultValue[0] : '', - leftpaddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 1 ? $scope.item.defaultValue[1] : '', - rightpaddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 2 ? $scope.item.defaultValue[2] : '', - toppaddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 3 ? $scope.item.defaultValue[3] : '', - bottompaddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 4 ? $scope.item.defaultValue[4] : '', - }; - } - - if ($scope.item.enable) { - angular.forEach($scope.defaultPaddingList, function (key, indexKey) { - if ($.inArray(key, $scope.item.enable) >= 0) { - $scope.paddingList.splice($scope.paddingList.length + 1, 0, key); - } - }) - } - else { - $scope.paddingList = $scope.defaultPaddingList; - } - - $scope.$watch("valueAreLoaded", function () { - $scope.setSelectedpadding($scope.paddingList[0]); - }, false); - - $scope.$watch( "selectedpadding", function () { - - if ($scope.selectedpadding.name == "all") { - $scope.item.values.paddingvalue = $scope.selectedpadding.value; - } - - if ($scope.selectedpadding.name == "left") { - $scope.item.values.leftpaddingvalue = $scope.selectedpadding.value; - } - - if ($scope.selectedpadding.name == "right") { - $scope.item.values.rightpaddingvalue = $scope.selectedpadding.value; - } - - if ($scope.selectedpadding.name == "top") { - $scope.item.values.toppaddingvalue = $scope.selectedpadding.value; - } - - if ($scope.selectedpadding.name == "bottom") { - $scope.item.values.bottompaddingvalue = $scope.selectedpadding.value; - } - - }, true) - - - + +/*********************************************************************************************************/ +/* padding editor */ +/*********************************************************************************************************/ + +angular.module("Umbraco.canvasdesigner") + +.controller("Umbraco.canvasdesigner.padding", function ($scope, dialogService) { + + $scope.defaultPaddingList = ["all", "left", "right", "top", "bottom"]; + $scope.paddingList = []; + + $scope.selectedpadding = { + name: "", + value: 0, + }; + + $scope.setSelectedpadding = function (paddingtype) { + + if (paddingtype == "all") { + $scope.selectedpadding.name="all"; + $scope.selectedpadding.value= $scope.item.values.paddingvalue; + } + + if (paddingtype == "left") { + $scope.selectedpadding.name= "left"; + $scope.selectedpadding.value= $scope.item.values.leftpaddingvalue; + } + + if (paddingtype == "right") { + $scope.selectedpadding.name= "right"; + $scope.selectedpadding.value= $scope.item.values.rightpaddingvalue; + } + + if (paddingtype == "top") { + $scope.selectedpadding.name= "top"; + $scope.selectedpadding.value= $scope.item.values.toppaddingvalue; + } + + if (paddingtype == "bottom") { + $scope.selectedpadding.name= "bottom"; + $scope.selectedpadding.value= $scope.item.values.bottompaddingvalue; + } + + } + + if (!$scope.item.values) { + $scope.item.values = { + paddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 0 ? $scope.item.defaultValue[0] : '', + leftpaddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 1 ? $scope.item.defaultValue[1] : '', + rightpaddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 2 ? $scope.item.defaultValue[2] : '', + toppaddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 3 ? $scope.item.defaultValue[3] : '', + bottompaddingvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 4 ? $scope.item.defaultValue[4] : '', + }; + } + + if ($scope.item.enable) { + angular.forEach($scope.defaultPaddingList, function (key, indexKey) { + if ($.inArray(key, $scope.item.enable) >= 0) { + $scope.paddingList.splice($scope.paddingList.length + 1, 0, key); + } + }) + } + else { + $scope.paddingList = $scope.defaultPaddingList; + } + + $scope.$watch("valueAreLoaded", function () { + $scope.setSelectedpadding($scope.paddingList[0]); + }, false); + + $scope.$watch( "selectedpadding", function () { + + if ($scope.selectedpadding.name == "all") { + $scope.item.values.paddingvalue = $scope.selectedpadding.value; + } + + if ($scope.selectedpadding.name == "left") { + $scope.item.values.leftpaddingvalue = $scope.selectedpadding.value; + } + + if ($scope.selectedpadding.name == "right") { + $scope.item.values.rightpaddingvalue = $scope.selectedpadding.value; + } + + if ($scope.selectedpadding.name == "top") { + $scope.item.values.toppaddingvalue = $scope.selectedpadding.value; + } + + if ($scope.selectedpadding.name == "bottom") { + $scope.item.values.bottompaddingvalue = $scope.selectedpadding.value; + } + + }, true) + + + }) - -/*********************************************************************************************************/ -/* radius editor */ -/*********************************************************************************************************/ - -angular.module("Umbraco.canvasdesigner") - -.controller("Umbraco.canvasdesigner.radius", function ($scope, dialogService) { - - $scope.defaultRadiusList = ["all", "topleft", "topright", "bottomleft", "bottomright"]; - $scope.radiusList = []; - - $scope.selectedradius = { - name: "", - value: 0, - }; - - $scope.setSelectedradius = function (radiustype) { - - if (radiustype == "all") { - $scope.selectedradius.name="all"; - $scope.selectedradius.value= $scope.item.values.radiusvalue; - } - - if (radiustype == "topleft") { - $scope.selectedradius.name = "topleft"; - $scope.selectedradius.value = $scope.item.values.topleftradiusvalue; - } - - if (radiustype == "topright") { - $scope.selectedradius.name = "topright"; - $scope.selectedradius.value = $scope.item.values.toprightradiusvalue; - } - - if (radiustype == "bottomleft") { - $scope.selectedradius.name = "bottomleft"; - $scope.selectedradius.value = $scope.item.values.bottomleftradiusvalue; - } - - if (radiustype == "bottomright") { - $scope.selectedradius.name = "bottomright"; - $scope.selectedradius.value = $scope.item.values.bottomrightradiusvalue; - } - - } - - if (!$scope.item.values) { - $scope.item.values = { - radiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 0 ? $scope.item.defaultValue[0] : '', - topleftradiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 1 ? $scope.item.defaultValue[1] : '', - toprightradiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 2 ? $scope.item.defaultValue[2] : '', - bottomleftradiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 3 ? $scope.item.defaultValue[3] : '', - bottomrightradiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 4 ? $scope.item.defaultValue[4] : '', - }; - } - - if ($scope.item.enable) { - angular.forEach($scope.defaultRadiusList, function (key, indexKey) { - if ($.inArray(key, $scope.item.enable) >= 0) { - $scope.radiusList.splice($scope.radiusList.length + 1, 0, key); - } - }) - } - else { - $scope.radiusList = $scope.defaultRadiusList; - } - - $scope.$watch("valueAreLoaded", function () { - $scope.setSelectedradius($scope.radiusList[0]); - }, false); - - $scope.$watch( "selectedradius", function () { - - if ($scope.selectedradius.name == "all") { - $scope.item.values.radiusvalue = $scope.selectedradius.value; - } - - if ($scope.selectedradius.name == "topleft") { - $scope.item.values.topleftradiusvalue = $scope.selectedradius.value; - } - - if ($scope.selectedradius.name == "topright") { - $scope.item.values.toprightradiusvalue = $scope.selectedradius.value; - } - - if ($scope.selectedradius.name == "bottomleft") { - $scope.item.values.bottomleftradiusvalue = $scope.selectedradius.value; - } - - if ($scope.selectedradius.name == "bottomright") { - $scope.item.values.bottomrightradiusvalue = $scope.selectedradius.value; - } - - }, true) - + +/*********************************************************************************************************/ +/* radius editor */ +/*********************************************************************************************************/ + +angular.module("Umbraco.canvasdesigner") + +.controller("Umbraco.canvasdesigner.radius", function ($scope, dialogService) { + + $scope.defaultRadiusList = ["all", "topleft", "topright", "bottomleft", "bottomright"]; + $scope.radiusList = []; + + $scope.selectedradius = { + name: "", + value: 0, + }; + + $scope.setSelectedradius = function (radiustype) { + + if (radiustype == "all") { + $scope.selectedradius.name="all"; + $scope.selectedradius.value= $scope.item.values.radiusvalue; + } + + if (radiustype == "topleft") { + $scope.selectedradius.name = "topleft"; + $scope.selectedradius.value = $scope.item.values.topleftradiusvalue; + } + + if (radiustype == "topright") { + $scope.selectedradius.name = "topright"; + $scope.selectedradius.value = $scope.item.values.toprightradiusvalue; + } + + if (radiustype == "bottomleft") { + $scope.selectedradius.name = "bottomleft"; + $scope.selectedradius.value = $scope.item.values.bottomleftradiusvalue; + } + + if (radiustype == "bottomright") { + $scope.selectedradius.name = "bottomright"; + $scope.selectedradius.value = $scope.item.values.bottomrightradiusvalue; + } + + } + + if (!$scope.item.values) { + $scope.item.values = { + radiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 0 ? $scope.item.defaultValue[0] : '', + topleftradiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 1 ? $scope.item.defaultValue[1] : '', + toprightradiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 2 ? $scope.item.defaultValue[2] : '', + bottomleftradiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 3 ? $scope.item.defaultValue[3] : '', + bottomrightradiusvalue: $scope.item.defaultValue && $scope.item.defaultValue.length > 4 ? $scope.item.defaultValue[4] : '', + }; + } + + if ($scope.item.enable) { + angular.forEach($scope.defaultRadiusList, function (key, indexKey) { + if ($.inArray(key, $scope.item.enable) >= 0) { + $scope.radiusList.splice($scope.radiusList.length + 1, 0, key); + } + }) + } + else { + $scope.radiusList = $scope.defaultRadiusList; + } + + $scope.$watch("valueAreLoaded", function () { + $scope.setSelectedradius($scope.radiusList[0]); + }, false); + + $scope.$watch( "selectedradius", function () { + + if ($scope.selectedradius.name == "all") { + $scope.item.values.radiusvalue = $scope.selectedradius.value; + } + + if ($scope.selectedradius.name == "topleft") { + $scope.item.values.topleftradiusvalue = $scope.selectedradius.value; + } + + if ($scope.selectedradius.name == "topright") { + $scope.item.values.toprightradiusvalue = $scope.selectedradius.value; + } + + if ($scope.selectedradius.name == "bottomleft") { + $scope.item.values.bottomleftradiusvalue = $scope.selectedradius.value; + } + + if ($scope.selectedradius.name == "bottomright") { + $scope.item.values.bottomrightradiusvalue = $scope.selectedradius.value; + } + + }, true) + }) - -/*********************************************************************************************************/ -/* shadow editor */ -/*********************************************************************************************************/ - -angular.module("Umbraco.canvasdesigner") - -.controller("Umbraco.canvasdesigner.shadow", function ($scope) { - - if (!$scope.item.values) { - $scope.item.values = { - shadow: '' - } - } - + +/*********************************************************************************************************/ +/* shadow editor */ +/*********************************************************************************************************/ + +angular.module("Umbraco.canvasdesigner") + +.controller("Umbraco.canvasdesigner.shadow", function ($scope) { + + if (!$scope.item.values) { + $scope.item.values = { + shadow: '' + } + } + }) - -/*********************************************************************************************************/ -/* slider editor */ -/*********************************************************************************************************/ - -angular.module("Umbraco.canvasdesigner") - -.controller("Umbraco.canvasdesigner.slider", function ($scope) { - - if (!$scope.item.values) { - $scope.item.values = { - slider: '' - } - } - + +/*********************************************************************************************************/ +/* slider editor */ +/*********************************************************************************************************/ + +angular.module("Umbraco.canvasdesigner") + +.controller("Umbraco.canvasdesigner.slider", function ($scope) { + + if (!$scope.item.values) { + $scope.item.values = { + slider: '' + } + } + }) - -/*********************************************************************************************************/ -/* spectrum color picker directive */ -/*********************************************************************************************************/ - -angular.module('colorpicker', ['spectrumcolorpicker']) - .directive('colorpicker', ['dialogService', function (dialogService) { - return { - restrict: 'EA', - scope: { - ngModel: '=' - }, - link: function (scope, $element) { - - scope.openColorDialog = function () { - var config = { - template: "colorModal.html", - change: function (data) { - scope.ngModel = data; - }, - callback: function (data) { - scope.ngModel = data; - }, - cancel: function (data) { - scope.ngModel = data; - }, - dialogItem: scope.ngModel, - scope: scope - }; - - dialogService.open(config); - } - - scope.setColor = false; - - scope.submitAndClose = function () { - if (scope.ngModel != "") { - scope.setColor = true; - scope.submit(scope.ngModel); - } else { - scope.cancel(); - } - - }; - - scope.cancelAndClose = function () { - scope.cancel(); - } - - }, - template: - '
' + - '
' + - '' + - '
', - replace: true - }; + +/*********************************************************************************************************/ +/* spectrum color picker directive */ +/*********************************************************************************************************/ + +angular.module('colorpicker', ['spectrumcolorpicker']) + .directive('colorpicker', ['dialogService', function (dialogService) { + return { + restrict: 'EA', + scope: { + ngModel: '=' + }, + link: function (scope, $element) { + + scope.openColorDialog = function () { + var config = { + template: "colorModal.html", + change: function (data) { + scope.ngModel = data; + }, + callback: function (data) { + scope.ngModel = data; + }, + cancel: function (data) { + scope.ngModel = data; + }, + dialogItem: scope.ngModel, + scope: scope + }; + + dialogService.open(config); + } + + scope.setColor = false; + + scope.submitAndClose = function () { + if (scope.ngModel != "") { + scope.setColor = true; + scope.submit(scope.ngModel); + } else { + scope.cancel(); + } + + }; + + scope.cancelAndClose = function () { + scope.cancel(); + } + + }, + template: + '
' + + '
' + + '' + + '
', + replace: true + }; }]) - -/*********************************************************************************************************/ -/* jQuery UI Slider plugin wrapper */ -/*********************************************************************************************************/ - -angular.module("Umbraco.canvasdesigner").factory('dialogService', function ($rootScope, $q, $http, $timeout, $compile, $templateCache) { - - function closeDialog(dialog, destroyScope) { - if (dialog.element) { - dialog.element.removeClass("selected"); - dialog.element.html(""); - - if (destroyScope) { - dialog.scope.$destroy(); - } - } - } - - function open() { - } - - return { - - open: function (options) { - - var defaults = { - template: "", - callback: undefined, - change: undefined, - cancel: undefined, - element: undefined, - dialogItem: undefined, - dialogData: undefined - }; - - var dialog = angular.extend(defaults, options); - var destroyScope = true; - - if (options && options.scope) { - destroyScope = false; - } - var scope = (options && options.scope) || $rootScope.$new(); - - // Save original value for cancel action - var originalDialogItem = angular.copy(dialog.dialogItem); - - dialog.element = $(".float-panel"); - - - /************************************/ - // Close dialog if the user clicks outside the dialog. (Not working well with colorpickers and datepickers) - $(document).mousedown(function (e) { - var container = dialog.element; - if (!container.is(e.target) && container.has(e.target).length === 0) { - closeDialog(dialog, destroyScope); - } - }); - /************************************/ - - - $q.when($templateCache.get(dialog.template) || $http.get(dialog.template, { cache: true }).then(function (res) { return res.data; })) - .then(function onSuccess(template) { - - dialog.element.html(template); - - $timeout(function () { - $compile(dialog.element)(scope); - }); - - dialog.element.addClass("selected") - - scope.cancel = function () { - if (dialog.cancel) { - dialog.cancel(originalDialogItem); - } - closeDialog(dialog, destroyScope); - } - - scope.change = function (data) { - if (dialog.change) { - dialog.change(data); - } - } - - scope.submit = function (data) { - if (dialog.callback) { - dialog.callback(data); - } - closeDialog(dialog, destroyScope); - }; - - scope.close = function () { - closeDialog(dialog, destroyScope); - } - - scope.dialogData = dialog.dialogData; - scope.dialogItem = dialog.dialogItem; - - dialog.scope = scope; - - }); - - return dialog; - - }, - - close: function() { - var modal = $(".float-panel"); - modal.removeClass("selected") - } - - } - - -}); - -/*********************************************************************************************************/ -/* jQuery UI Slider plugin wrapper */ -/*********************************************************************************************************/ - -angular.module('ui.slider', []).value('uiSliderConfig', {}).directive('uiSlider', ['uiSliderConfig', '$timeout', function (uiSliderConfig, $timeout) { - uiSliderConfig = uiSliderConfig || {}; - return { - require: 'ngModel', - template: '
', - replace: true, - compile: function () { - return function (scope, elm, attrs, ngModel) { - - scope.value = ngModel.$viewValue; - - function parseNumber(n, decimals) { - return (decimals) ? parseFloat(n) : parseInt(n); - }; - - var options = angular.extend(scope.$eval(attrs.uiSlider) || {}, uiSliderConfig); - // Object holding range values - var prevRangeValues = { - min: null, - max: null - }; - - // convenience properties - var properties = ['min', 'max', 'step']; - var useDecimals = (!angular.isUndefined(attrs.useDecimals)) ? true : false; - - var init = function () { - // When ngModel is assigned an array of values then range is expected to be true. - // Warn user and change range to true else an error occurs when trying to drag handle - if (angular.isArray(ngModel.$viewValue) && options.range !== true) { - console.warn('Change your range option of ui-slider. When assigning ngModel an array of values then the range option should be set to true.'); - options.range = true; - } - - // Ensure the convenience properties are passed as options if they're defined - // This avoids init ordering issues where the slider's initial state (eg handle - // position) is calculated using widget defaults - // Note the properties take precedence over any duplicates in options - angular.forEach(properties, function (property) { - if (angular.isDefined(attrs[property])) { - options[property] = parseNumber(attrs[property], useDecimals); - } - }); - - elm.find(".slider").slider(options); - init = angular.noop; - }; - - // Find out if decimals are to be used for slider - angular.forEach(properties, function (property) { - // support {{}} and watch for updates - attrs.$observe(property, function (newVal) { - if (!!newVal) { - init(); - elm.find(".slider").slider('option', property, parseNumber(newVal, useDecimals)); - } - }); - }); - attrs.$observe('disabled', function (newVal) { - init(); - elm.find(".slider").slider('option', 'disabled', !!newVal); - }); - - // Watch ui-slider (byVal) for changes and update - scope.$watch(attrs.uiSlider, function (newVal) { - init(); - if (newVal != undefined) { - elm.find(".slider").slider('option', newVal); - elm.find(".ui-slider-handle").html("" + ui.value + "px") - } - }, true); - - // Late-bind to prevent compiler clobbering - $timeout(init, 0, true); - - // Update model value from slider - elm.find(".slider").bind('slidestop', function (event, ui) { - ngModel.$setViewValue(ui.values || ui.value); - scope.$apply(); - }); - - elm.bind('slide', function (event, ui) { - event.stopPropagation(); - elm.find(".slider-input").val(ui.value); - elm.find(".ui-slider-handle").html("" + ui.value + "px") - }); - - // Update slider from model value - ngModel.$render = function () { - init(); - var method = options.range === true ? 'values' : 'value'; - - if (isNaN(ngModel.$viewValue) && !(ngModel.$viewValue instanceof Array)) - ngModel.$viewValue = 0; - - if (ngModel.$viewValue == '') - ngModel.$viewValue = 0; - - scope.value = ngModel.$viewValue; - - // Do some sanity check of range values - if (options.range === true) { - - // Check outer bounds for min and max values - if (angular.isDefined(options.min) && options.min > ngModel.$viewValue[0]) { - ngModel.$viewValue[0] = options.min; - } - if (angular.isDefined(options.max) && options.max < ngModel.$viewValue[1]) { - ngModel.$viewValue[1] = options.max; - } - - // Check min and max range values - if (ngModel.$viewValue[0] >= ngModel.$viewValue[1]) { - // Min value should be less to equal to max value - if (prevRangeValues.min >= ngModel.$viewValue[1]) - ngModel.$viewValue[0] = prevRangeValues.min; - // Max value should be less to equal to min value - if (prevRangeValues.max <= ngModel.$viewValue[0]) - ngModel.$viewValue[1] = prevRangeValues.max; - } - - - - // Store values for later user - prevRangeValues.min = ngModel.$viewValue[0]; - prevRangeValues.max = ngModel.$viewValue[1]; - - } - elm.find(".slider").slider(method, ngModel.$viewValue); - elm.find(".ui-slider-handle").html("" + ngModel.$viewValue + "px") - }; - - scope.$watch("value", function () { - ngModel.$setViewValue(scope.value); - }, true); - - scope.$watch(attrs.ngModel, function () { - if (options.range === true) { - ngModel.$render(); - } - }, true); - - function destroy() { - elm.find(".slider").slider('destroy'); - } - elm.find(".slider").bind('$destroy', destroy); - }; - } - }; -}]); - - -/*********************************************************************************************************/ -/* spectrum color picker directive */ -/*********************************************************************************************************/ - -angular.module('spectrumcolorpicker', []) - .directive('spectrum', function () { - return { - restrict: 'E', - transclude: true, - scope: { - colorselected: '=', - setColor: '=', - flat: '=', - showPalette: '=' - }, - link: function (scope, $element) { - - var initColor; - - $element.find("input").spectrum({ - color: scope.colorselected, - allowEmpty: true, - preferredFormat: "hex", - showAlpha: true, - showInput: true, - flat: scope.flat, - localStorageKey: "spectrum.panel", - showPalette: scope.showPalette, - palette: [], - change: function (color) { - - if (color) { - scope.colorselected = color.toRgbString(); - } - else { - scope.colorselected = ''; - } - scope.$apply(); - }, - move: function (color) { - scope.colorselected = color.toRgbString(); - scope.$apply(); - }, - beforeShow: function (color) { - initColor = angular.copy(scope.colorselected); - $(this).spectrum("container").find(".sp-cancel").click(function (e) { - scope.colorselected = initColor; - scope.$apply(); - }); - }, - - }); - - scope.$watch('setcolor', function (setColor) { - if (scope.$eval(setColor) === true) { - $element.find("input").spectrum("set", scope.colorselected); - } - }, true); - - }, - template: - '
', - replace: true - }; +/*********************************************************************************************************/ +/* jQuery UI Slider plugin wrapper */ +/*********************************************************************************************************/ + +angular.module("Umbraco.canvasdesigner").factory('dialogService', function ($rootScope, $q, $http, $timeout, $compile, $templateCache) { + + function closeDialog(dialog, destroyScope) { + if (dialog.element) { + dialog.element.removeClass("selected"); + dialog.element.html(""); + + if (destroyScope) { + dialog.scope.$destroy(); + } + } + } + + function open() { + } + + return { + + open: function (options) { + + var defaults = { + template: "", + callback: undefined, + change: undefined, + cancel: undefined, + element: undefined, + dialogItem: undefined, + dialogData: undefined + }; + + var dialog = angular.extend(defaults, options); + var destroyScope = true; + + if (options && options.scope) { + destroyScope = false; + } + var scope = (options && options.scope) || $rootScope.$new(); + + // Save original value for cancel action + var originalDialogItem = angular.copy(dialog.dialogItem); + + dialog.element = $(".float-panel"); + + + /************************************/ + // Close dialog if the user clicks outside the dialog. (Not working well with colorpickers and datepickers) + $(document).mousedown(function (e) { + var container = dialog.element; + if (!container.is(e.target) && container.has(e.target).length === 0) { + closeDialog(dialog, destroyScope); + } + }); + /************************************/ + + + $q.when($templateCache.get(dialog.template) || $http.get(dialog.template, { cache: true }).then(function (res) { return res.data; })) + .then(function onSuccess(template) { + + dialog.element.html(template); + + $timeout(function () { + $compile(dialog.element)(scope); + }); + + dialog.element.addClass("selected") + + scope.cancel = function () { + if (dialog.cancel) { + dialog.cancel(originalDialogItem); + } + closeDialog(dialog, destroyScope); + } + + scope.change = function (data) { + if (dialog.change) { + dialog.change(data); + } + } + + scope.submit = function (data) { + if (dialog.callback) { + dialog.callback(data); + } + closeDialog(dialog, destroyScope); + }; + + scope.close = function () { + closeDialog(dialog, destroyScope); + } + + scope.dialogData = dialog.dialogData; + scope.dialogItem = dialog.dialogItem; + + dialog.scope = scope; + + }); + + return dialog; + + }, + + close: function() { + var modal = $(".float-panel"); + modal.removeClass("selected") + } + + } + + +}); + +/*********************************************************************************************************/ +/* jQuery UI Slider plugin wrapper */ +/*********************************************************************************************************/ + +angular.module('ui.slider', []).value('uiSliderConfig', {}).directive('uiSlider', ['uiSliderConfig', '$timeout', function (uiSliderConfig, $timeout) { + uiSliderConfig = uiSliderConfig || {}; + return { + require: 'ngModel', + template: '
', + replace: true, + compile: function () { + return function (scope, elm, attrs, ngModel) { + + scope.value = ngModel.$viewValue; + + function parseNumber(n, decimals) { + return (decimals) ? parseFloat(n) : parseInt(n); + }; + + var options = angular.extend(scope.$eval(attrs.uiSlider) || {}, uiSliderConfig); + // Object holding range values + var prevRangeValues = { + min: null, + max: null + }; + + // convenience properties + var properties = ['min', 'max', 'step']; + var useDecimals = (!angular.isUndefined(attrs.useDecimals)) ? true : false; + + var init = function () { + // When ngModel is assigned an array of values then range is expected to be true. + // Warn user and change range to true else an error occurs when trying to drag handle + if (angular.isArray(ngModel.$viewValue) && options.range !== true) { + console.warn('Change your range option of ui-slider. When assigning ngModel an array of values then the range option should be set to true.'); + options.range = true; + } + + // Ensure the convenience properties are passed as options if they're defined + // This avoids init ordering issues where the slider's initial state (eg handle + // position) is calculated using widget defaults + // Note the properties take precedence over any duplicates in options + angular.forEach(properties, function (property) { + if (angular.isDefined(attrs[property])) { + options[property] = parseNumber(attrs[property], useDecimals); + } + }); + + elm.find(".slider").slider(options); + init = angular.noop; + }; + + // Find out if decimals are to be used for slider + angular.forEach(properties, function (property) { + // support {{}} and watch for updates + attrs.$observe(property, function (newVal) { + if (!!newVal) { + init(); + elm.find(".slider").slider('option', property, parseNumber(newVal, useDecimals)); + } + }); + }); + attrs.$observe('disabled', function (newVal) { + init(); + elm.find(".slider").slider('option', 'disabled', !!newVal); + }); + + // Watch ui-slider (byVal) for changes and update + scope.$watch(attrs.uiSlider, function (newVal) { + init(); + if (newVal != undefined) { + elm.find(".slider").slider('option', newVal); + elm.find(".ui-slider-handle").html("" + ui.value + "px") + } + }, true); + + // Late-bind to prevent compiler clobbering + $timeout(init, 0, true); + + // Update model value from slider + elm.find(".slider").bind('slidestop', function (event, ui) { + ngModel.$setViewValue(ui.values || ui.value); + scope.$apply(); + }); + + elm.bind('slide', function (event, ui) { + event.stopPropagation(); + elm.find(".slider-input").val(ui.value); + elm.find(".ui-slider-handle").html("" + ui.value + "px") + }); + + // Update slider from model value + ngModel.$render = function () { + init(); + var method = options.range === true ? 'values' : 'value'; + + if (isNaN(ngModel.$viewValue) && !(ngModel.$viewValue instanceof Array)) + ngModel.$viewValue = 0; + + if (ngModel.$viewValue == '') + ngModel.$viewValue = 0; + + scope.value = ngModel.$viewValue; + + // Do some sanity check of range values + if (options.range === true) { + + // Check outer bounds for min and max values + if (angular.isDefined(options.min) && options.min > ngModel.$viewValue[0]) { + ngModel.$viewValue[0] = options.min; + } + if (angular.isDefined(options.max) && options.max < ngModel.$viewValue[1]) { + ngModel.$viewValue[1] = options.max; + } + + // Check min and max range values + if (ngModel.$viewValue[0] >= ngModel.$viewValue[1]) { + // Min value should be less to equal to max value + if (prevRangeValues.min >= ngModel.$viewValue[1]) + ngModel.$viewValue[0] = prevRangeValues.min; + // Max value should be less to equal to min value + if (prevRangeValues.max <= ngModel.$viewValue[0]) + ngModel.$viewValue[1] = prevRangeValues.max; + } + + + + // Store values for later user + prevRangeValues.min = ngModel.$viewValue[0]; + prevRangeValues.max = ngModel.$viewValue[1]; + + } + elm.find(".slider").slider(method, ngModel.$viewValue); + elm.find(".ui-slider-handle").html("" + ngModel.$viewValue + "px") + }; + + scope.$watch("value", function () { + ngModel.$setViewValue(scope.value); + }, true); + + scope.$watch(attrs.ngModel, function () { + if (options.range === true) { + ngModel.$render(); + } + }, true); + + function destroy() { + elm.find(".slider").slider('destroy'); + } + elm.find(".slider").bind('$destroy', destroy); + }; + } + }; +}]); + + + +/*********************************************************************************************************/ +/* spectrum color picker directive */ +/*********************************************************************************************************/ + +angular.module('spectrumcolorpicker', []) + .directive('spectrum', function () { + return { + restrict: 'E', + transclude: true, + scope: { + colorselected: '=', + setColor: '=', + flat: '=', + showPalette: '=' + }, + link: function (scope, $element) { + + var initColor; + + $element.find("input").spectrum({ + color: scope.colorselected, + allowEmpty: true, + preferredFormat: "hex", + showAlpha: true, + showInput: true, + flat: scope.flat, + localStorageKey: "spectrum.panel", + showPalette: scope.showPalette, + palette: [], + change: function (color) { + + if (color) { + scope.colorselected = color.toRgbString(); + } + else { + scope.colorselected = ''; + } + scope.$apply(); + }, + move: function (color) { + scope.colorselected = color.toRgbString(); + scope.$apply(); + }, + beforeShow: function (color) { + initColor = angular.copy(scope.colorselected); + $(this).spectrum("container").find(".sp-cancel").click(function (e) { + scope.colorselected = initColor; + scope.$apply(); + }); + }, + + }); + + scope.$watch('setcolor', function (setColor) { + if (scope.$eval(setColor) === true) { + $element.find("input").spectrum("set", scope.colorselected); + } + }, true); + + }, + template: + '
', + replace: true + }; }) \ No newline at end of file diff --git a/OurUmbraco.Site/umbraco/js/umbraco.controllers.js b/OurUmbraco.Site/umbraco/js/umbraco.controllers.js index ce2b5f78..97801174 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.controllers.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.controllers.js @@ -1,320 +1,320 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ (function() { - -/** - * @ngdoc controller - * @name Umbraco.MainController - * @function - * - * @description - * The main application controller - * - */ -function MainController($scope, $rootScope, $location, $routeParams, $timeout, $http, $log, appState, treeService, notificationsService, userService, navigationService, historyService, updateChecker, assetsService, eventsService, umbRequestHelper, tmhDynamicLocale) { - - //the null is important because we do an explicit bool check on this in the view - //the avatar is by default the umbraco logo - $scope.authenticated = null; - $scope.avatar = [ - { value: "assets/img/application/logo.png" }, - { value: "assets/img/application/logo@2x.png" }, - { value: "assets/img/application/logo@3x.png" } - ]; - $scope.touchDevice = appState.getGlobalState("touchDevice"); - - - $scope.removeNotification = function (index) { - notificationsService.remove(index); - }; - - $scope.closeDialogs = function (event) { - //only close dialogs if non-link and non-buttons are clicked - var el = event.target.nodeName; - var els = ["INPUT", "A", "BUTTON"]; - - if (els.indexOf(el) >= 0) { return; } - - var parents = $(event.target).parents("a,button"); - if (parents.length > 0) { - return; - } - - //SD: I've updated this so that we don't close the dialog when clicking inside of the dialog - var nav = $(event.target).parents("#dialog"); - if (nav.length === 1) { - return; - } - - eventsService.emit("app.closeDialogs", event); - }; - - var evts = []; - - //when a user logs out or timesout - evts.push(eventsService.on("app.notAuthenticated", function () { - $scope.authenticated = null; - $scope.user = null; - })); - - //when the app is read/user is logged in, setup the data - evts.push(eventsService.on("app.ready", function (evt, data) { - - $scope.authenticated = data.authenticated; - $scope.user = data.user; - - updateChecker.check().then(function(update) { - if (update && update !== "null") { - if (update.type !== "None") { - var notification = { - headline: "Update available", - message: "Click to download", - sticky: true, - type: "info", - url: update.url - }; - notificationsService.add(notification); - } - } - }); - - //if the user has changed we need to redirect to the root so they don't try to continue editing the - //last item in the URL (NOTE: the user id can equal zero, so we cannot just do !data.lastUserId since that will resolve to true) - if (data.lastUserId !== undefined && data.lastUserId !== null && data.lastUserId !== data.user.id) { - $location.path("/").search(""); - historyService.removeAll(); - treeService.clearCache(); - } - - //Load locale file - if ($scope.user.locale) { - tmhDynamicLocale.set($scope.user.locale); - } - - if ($scope.user.emailHash) { - - //let's attempt to load the avatar, it might not exist or we might not have - // internet access, well get an empty string back - $http.get(umbRequestHelper.getApiUrl("gravatarApiBaseUrl", "GetCurrentUserGravatarUrl")) - .then( - function successCallback(response) { - // if we can't download the gravatar for some reason, an null gets returned, we cannot do anything - if (response.data !== "null") { - if ($scope.user && $scope.user.emailHash) { - var avatarBaseUrl = "https://www.gravatar.com/avatar/"; - var hash = $scope.user.emailHash; - - $scope.avatar = [ - { value: avatarBaseUrl + hash + ".jpg?s=30&d=mm" }, - { value: avatarBaseUrl + hash + ".jpg?s=60&d=mm" }, - { value: avatarBaseUrl + hash + ".jpg?s=90&d=mm" } - ]; - } - } - - }, function errorCallback(response) { - //cannot load it from the server so we cannot do anything - }); - } - })); - - evts.push(eventsService.on("app.ysod", function (name, error) { - $scope.ysodOverlay = { - view: "ysod", - error: error, - show: true - }; - })); - - //ensure to unregister from all events! - $scope.$on('$destroy', function () { - for (var e in evts) { - eventsService.unsubscribe(evts[e]); - } - }); - -} - - -//register it -angular.module('umbraco').controller("Umbraco.MainController", MainController). - config(function (tmhDynamicLocaleProvider) { - //Set url for locale files - tmhDynamicLocaleProvider.localeLocationPattern('lib/angular/1.1.5/i18n/angular-locale_{{locale}}.js'); - }); - -/** - * @ngdoc controller - * @name Umbraco.NavigationController - * @function - * - * @description - * Handles the section area of the app - * - * @param {navigationService} navigationService A reference to the navigationService - */ -function NavigationController($scope, $rootScope, $location, $log, $routeParams, $timeout, appState, navigationService, keyboardService, dialogService, historyService, eventsService, sectionResource, angularHelper) { - - //TODO: Need to think about this and an nicer way to acheive what this is doing. - //the tree event handler i used to subscribe to the main tree click events - $scope.treeEventHandler = $({}); - navigationService.setupTreeEvents($scope.treeEventHandler); - - //Put the navigation service on this scope so we can use it's methods/properties in the view. - // IMPORTANT: all properties assigned to this scope are generally available on the scope object on dialogs since - // when we create a dialog we pass in this scope to be used for the dialog's scope instead of creating a new one. - $scope.nav = navigationService; - // TODO: Lets fix this, it is less than ideal to be passing in the navigationController scope to something else to be used as it's scope, - // this is going to lead to problems/confusion. I really don't think passing scope's around is very good practice. - $rootScope.nav = navigationService; - - //set up our scope vars - $scope.showContextMenuDialog = false; - $scope.showContextMenu = false; - $scope.showSearchResults = false; - $scope.menuDialogTitle = null; - $scope.menuActions = []; - $scope.menuNode = null; - - $scope.currentSection = appState.getSectionState("currentSection"); - $scope.showNavigation = appState.getGlobalState("showNavigation"); - - //trigger search with a hotkey: - keyboardService.bind("ctrl+shift+s", function () { - navigationService.showSearch(); - }); - - //trigger dialods with a hotkey: - keyboardService.bind("esc", function () { - eventsService.emit("app.closeDialogs"); - }); - - $scope.selectedId = navigationService.currentId; - - var evts = []; - - //Listen for global state changes - evts.push(eventsService.on("appState.globalState.changed", function(e, args) { - if (args.key === "showNavigation") { - $scope.showNavigation = args.value; - } - })); - - //Listen for menu state changes - evts.push(eventsService.on("appState.menuState.changed", function(e, args) { - if (args.key === "showMenuDialog") { - $scope.showContextMenuDialog = args.value; - } - if (args.key === "showMenu") { - $scope.showContextMenu = args.value; - } - if (args.key === "dialogTitle") { - $scope.menuDialogTitle = args.value; - } - if (args.key === "menuActions") { - $scope.menuActions = args.value; - } - if (args.key === "currentNode") { - $scope.menuNode = args.value; - } - })); - - //Listen for section state changes - evts.push(eventsService.on("appState.treeState.changed", function(e, args) { - var f = args; - if (args.value.root && args.value.root.metaData.containsTrees === false) { - $rootScope.emptySection = true; - } - else { - $rootScope.emptySection = false; - } - })); - - //Listen for section state changes - evts.push(eventsService.on("appState.sectionState.changed", function(e, args) { - //section changed - if (args.key === "currentSection") { - $scope.currentSection = args.value; - } - //show/hide search results - if (args.key === "showSearchResults") { - $scope.showSearchResults = args.value; - } - })); - - //This reacts to clicks passed to the body element which emits a global call to close all dialogs - evts.push(eventsService.on("app.closeDialogs", function(event) { - if (appState.getGlobalState("stickyNavigation")) { - navigationService.hideNavigation(); - //TODO: don't know why we need this? - we are inside of an angular event listener. - angularHelper.safeApply($scope); - } - })); - - //when a user logs out or timesout - evts.push(eventsService.on("app.notAuthenticated", function() { - $scope.authenticated = false; - })); - - //when the application is ready and the user is authorized setup the data - evts.push(eventsService.on("app.ready", function(evt, data) { - $scope.authenticated = true; - })); - - //this reacts to the options item in the tree - //todo, migrate to nav service - $scope.searchShowMenu = function (ev, args) { - //always skip default - args.skipDefault = true; - navigationService.showMenu(ev, args); - }; - - //todo, migrate to nav service - $scope.searchHide = function () { - navigationService.hideSearch(); - }; - - //the below assists with hiding/showing the tree - var treeActive = false; - - //Sets a service variable as soon as the user hovers the navigation with the mouse - //used by the leaveTree method to delay hiding - $scope.enterTree = function (event) { - treeActive = true; - }; - - // Hides navigation tree, with a short delay, is cancelled if the user moves the mouse over the tree again - $scope.leaveTree = function(event) { - //this is a hack to handle IE touch events which freaks out due to no mouse events so the tree instantly shuts down - if (!event) { - return; - } - if (!appState.getGlobalState("touchDevice")) { - treeActive = false; - $timeout(function() { - if (!treeActive) { - navigationService.hideTree(); - } - }, 300); - } - }; - - //ensure to unregister from all events! - $scope.$on('$destroy', function () { - for (var e in evts) { - eventsService.unsubscribe(evts[e]); - } - }); -} - -//register it -angular.module('umbraco').controller("Umbraco.NavigationController", NavigationController); +/** + * @ngdoc controller + * @name Umbraco.MainController + * @function + * + * @description + * The main application controller + * + */ +function MainController($scope, $rootScope, $location, $routeParams, $timeout, $http, $log, appState, treeService, notificationsService, userService, navigationService, historyService, updateChecker, assetsService, eventsService, umbRequestHelper, tmhDynamicLocale) { + + //the null is important because we do an explicit bool check on this in the view + //the avatar is by default the umbraco logo + $scope.authenticated = null; + $scope.avatar = [ + { value: "assets/img/application/logo.png" }, + { value: "assets/img/application/logo@2x.png" }, + { value: "assets/img/application/logo@3x.png" } + ]; + $scope.touchDevice = appState.getGlobalState("touchDevice"); + + + $scope.removeNotification = function (index) { + notificationsService.remove(index); + }; + + $scope.closeDialogs = function (event) { + //only close dialogs if non-link and non-buttons are clicked + var el = event.target.nodeName; + var els = ["INPUT", "A", "BUTTON"]; + + if (els.indexOf(el) >= 0) { return; } + + var parents = $(event.target).parents("a,button"); + if (parents.length > 0) { + return; + } + + //SD: I've updated this so that we don't close the dialog when clicking inside of the dialog + var nav = $(event.target).parents("#dialog"); + if (nav.length === 1) { + return; + } + + eventsService.emit("app.closeDialogs", event); + }; + + var evts = []; + + //when a user logs out or timesout + evts.push(eventsService.on("app.notAuthenticated", function () { + $scope.authenticated = null; + $scope.user = null; + })); + + //when the app is read/user is logged in, setup the data + evts.push(eventsService.on("app.ready", function (evt, data) { + + $scope.authenticated = data.authenticated; + $scope.user = data.user; + + updateChecker.check().then(function(update) { + if (update && update !== "null") { + if (update.type !== "None") { + var notification = { + headline: "Update available", + message: "Click to download", + sticky: true, + type: "info", + url: update.url + }; + notificationsService.add(notification); + } + } + }); + + //if the user has changed we need to redirect to the root so they don't try to continue editing the + //last item in the URL (NOTE: the user id can equal zero, so we cannot just do !data.lastUserId since that will resolve to true) + if (data.lastUserId !== undefined && data.lastUserId !== null && data.lastUserId !== data.user.id) { + $location.path("/").search(""); + historyService.removeAll(); + treeService.clearCache(); + } + + //Load locale file + if ($scope.user.locale) { + tmhDynamicLocale.set($scope.user.locale); + } + + if ($scope.user.emailHash) { + + //let's attempt to load the avatar, it might not exist or we might not have + // internet access, well get an empty string back + $http.get(umbRequestHelper.getApiUrl("gravatarApiBaseUrl", "GetCurrentUserGravatarUrl")) + .then( + function successCallback(response) { + // if we can't download the gravatar for some reason, an null gets returned, we cannot do anything + if (response.data !== "null") { + if ($scope.user && $scope.user.emailHash) { + var avatarBaseUrl = "https://www.gravatar.com/avatar/"; + var hash = $scope.user.emailHash; + + $scope.avatar = [ + { value: avatarBaseUrl + hash + ".jpg?s=30&d=mm" }, + { value: avatarBaseUrl + hash + ".jpg?s=60&d=mm" }, + { value: avatarBaseUrl + hash + ".jpg?s=90&d=mm" } + ]; + } + } + + }, function errorCallback(response) { + //cannot load it from the server so we cannot do anything + }); + } + })); + + evts.push(eventsService.on("app.ysod", function (name, error) { + $scope.ysodOverlay = { + view: "ysod", + error: error, + show: true + }; + })); + + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + +} + + +//register it +angular.module('umbraco').controller("Umbraco.MainController", MainController). + config(function (tmhDynamicLocaleProvider) { + //Set url for locale files + tmhDynamicLocaleProvider.localeLocationPattern('lib/angular/1.1.5/i18n/angular-locale_{{locale}}.js'); + }); + + +/** + * @ngdoc controller + * @name Umbraco.NavigationController + * @function + * + * @description + * Handles the section area of the app + * + * @param {navigationService} navigationService A reference to the navigationService + */ +function NavigationController($scope, $rootScope, $location, $log, $routeParams, $timeout, appState, navigationService, keyboardService, dialogService, historyService, eventsService, sectionResource, angularHelper) { + + //TODO: Need to think about this and an nicer way to acheive what this is doing. + //the tree event handler i used to subscribe to the main tree click events + $scope.treeEventHandler = $({}); + navigationService.setupTreeEvents($scope.treeEventHandler); + + //Put the navigation service on this scope so we can use it's methods/properties in the view. + // IMPORTANT: all properties assigned to this scope are generally available on the scope object on dialogs since + // when we create a dialog we pass in this scope to be used for the dialog's scope instead of creating a new one. + $scope.nav = navigationService; + // TODO: Lets fix this, it is less than ideal to be passing in the navigationController scope to something else to be used as it's scope, + // this is going to lead to problems/confusion. I really don't think passing scope's around is very good practice. + $rootScope.nav = navigationService; + + //set up our scope vars + $scope.showContextMenuDialog = false; + $scope.showContextMenu = false; + $scope.showSearchResults = false; + $scope.menuDialogTitle = null; + $scope.menuActions = []; + $scope.menuNode = null; + + $scope.currentSection = appState.getSectionState("currentSection"); + $scope.showNavigation = appState.getGlobalState("showNavigation"); + + //trigger search with a hotkey: + keyboardService.bind("ctrl+shift+s", function () { + navigationService.showSearch(); + }); + + //trigger dialods with a hotkey: + keyboardService.bind("esc", function () { + eventsService.emit("app.closeDialogs"); + }); + + $scope.selectedId = navigationService.currentId; + + var evts = []; + + //Listen for global state changes + evts.push(eventsService.on("appState.globalState.changed", function(e, args) { + if (args.key === "showNavigation") { + $scope.showNavigation = args.value; + } + })); + + //Listen for menu state changes + evts.push(eventsService.on("appState.menuState.changed", function(e, args) { + if (args.key === "showMenuDialog") { + $scope.showContextMenuDialog = args.value; + } + if (args.key === "showMenu") { + $scope.showContextMenu = args.value; + } + if (args.key === "dialogTitle") { + $scope.menuDialogTitle = args.value; + } + if (args.key === "menuActions") { + $scope.menuActions = args.value; + } + if (args.key === "currentNode") { + $scope.menuNode = args.value; + } + })); + + //Listen for section state changes + evts.push(eventsService.on("appState.treeState.changed", function(e, args) { + var f = args; + if (args.value.root && args.value.root.metaData.containsTrees === false) { + $rootScope.emptySection = true; + } + else { + $rootScope.emptySection = false; + } + })); + + //Listen for section state changes + evts.push(eventsService.on("appState.sectionState.changed", function(e, args) { + //section changed + if (args.key === "currentSection") { + $scope.currentSection = args.value; + } + //show/hide search results + if (args.key === "showSearchResults") { + $scope.showSearchResults = args.value; + } + })); + + //This reacts to clicks passed to the body element which emits a global call to close all dialogs + evts.push(eventsService.on("app.closeDialogs", function(event) { + if (appState.getGlobalState("stickyNavigation")) { + navigationService.hideNavigation(); + //TODO: don't know why we need this? - we are inside of an angular event listener. + angularHelper.safeApply($scope); + } + })); + + //when a user logs out or timesout + evts.push(eventsService.on("app.notAuthenticated", function() { + $scope.authenticated = false; + })); + + //when the application is ready and the user is authorized setup the data + evts.push(eventsService.on("app.ready", function(evt, data) { + $scope.authenticated = true; + })); + + //this reacts to the options item in the tree + //todo, migrate to nav service + $scope.searchShowMenu = function (ev, args) { + //always skip default + args.skipDefault = true; + navigationService.showMenu(ev, args); + }; + + //todo, migrate to nav service + $scope.searchHide = function () { + navigationService.hideSearch(); + }; + + //the below assists with hiding/showing the tree + var treeActive = false; + + //Sets a service variable as soon as the user hovers the navigation with the mouse + //used by the leaveTree method to delay hiding + $scope.enterTree = function (event) { + treeActive = true; + }; + + // Hides navigation tree, with a short delay, is cancelled if the user moves the mouse over the tree again + $scope.leaveTree = function(event) { + //this is a hack to handle IE touch events which freaks out due to no mouse events so the tree instantly shuts down + if (!event) { + return; + } + if (!appState.getGlobalState("touchDevice")) { + treeActive = false; + $timeout(function() { + if (!treeActive) { + navigationService.hideTree(); + } + }, 300); + } + }; + + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); +} + +//register it +angular.module('umbraco').controller("Umbraco.NavigationController", NavigationController); /** * @ngdoc controller @@ -443,37 +443,37 @@ function SearchController($scope, searchService, $log, $location, navigationServ } //register it angular.module('umbraco').controller("Umbraco.SearchController", SearchController); - -/** - * @ngdoc controller - * @name Umbraco.MainController - * @function - * - * @description - * The controller for the AuthorizeUpgrade login page - * - */ -function AuthorizeUpgradeController($scope, $window) { - - //Add this method to the scope - this method will be called by the login dialog controller when the login is successful - // then we'll handle the redirect. - $scope.submit = function (event) { - - var qry = $window.location.search.trimStart("?").split("&"); - var redir = _.find(qry, function(item) { - return item.startsWith("redir="); - }); - if (redir) { - $window.location = decodeURIComponent(redir.split("=")[1]); - } - else { - $window.location = "/"; - } - - }; - -} - + +/** + * @ngdoc controller + * @name Umbraco.MainController + * @function + * + * @description + * The controller for the AuthorizeUpgrade login page + * + */ +function AuthorizeUpgradeController($scope, $window) { + + //Add this method to the scope - this method will be called by the login dialog controller when the login is successful + // then we'll handle the redirect. + $scope.submit = function (event) { + + var qry = $window.location.search.trimStart("?").split("&"); + var redir = _.find(qry, function(item) { + return item.startsWith("redir="); + }); + if (redir) { + $window.location = decodeURIComponent(redir.split("=")[1]); + } + else { + $window.location = "/"; + } + + }; + +} + angular.module('umbraco').controller("Umbraco.AuthorizeUpgradeController", AuthorizeUpgradeController); /** * @ngdoc controller @@ -506,205 +506,205 @@ function DashboardController($scope, $routeParams, dashboardResource, localizati //register it angular.module('umbraco').controller("Umbraco.DashboardController", DashboardController); -angular.module("umbraco") - .controller("Umbraco.Dialogs.ApprovedColorPickerController", function ($scope, $http, umbPropEditorHelper, assetsService) { - assetsService.loadJs("lib/cssparser/cssparser.js") - .then(function () { - - var cssPath = $scope.dialogData.cssPath; - $scope.cssClass = $scope.dialogData.cssClass; - - $scope.classes = []; - - $scope.change = function (newClass) { - $scope.model.value = newClass; - } - - $http.get(cssPath) - .success(function (data) { - var parser = new CSSParser(); - $scope.classes = parser.parse(data, false, false).cssRules; - $scope.classes.splice(0, 0, "noclass"); - }) - - assetsService.loadCss("/App_Plugins/Lecoati.uSky.Grid/lib/uSky.Grid.ApprovedColorPicker.css"); - assetsService.loadCss(cssPath); - }); +angular.module("umbraco") + .controller("Umbraco.Dialogs.ApprovedColorPickerController", function ($scope, $http, umbPropEditorHelper, assetsService) { + assetsService.loadJs("lib/cssparser/cssparser.js") + .then(function () { + + var cssPath = $scope.dialogData.cssPath; + $scope.cssClass = $scope.dialogData.cssClass; + + $scope.classes = []; + + $scope.change = function (newClass) { + $scope.model.value = newClass; + } + + $http.get(cssPath) + .success(function (data) { + var parser = new CSSParser(); + $scope.classes = parser.parse(data, false, false).cssRules; + $scope.classes.splice(0, 0, "noclass"); + }) + + assetsService.loadCss("/App_Plugins/Lecoati.uSky.Grid/lib/uSky.Grid.ApprovedColorPicker.css"); + assetsService.loadCss(cssPath); + }); }); -function ContentEditDialogController($scope, editorState, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, umbModelMapper, $http) { - - $scope.defaultButton = null; - $scope.subButtons = []; - var dialogOptions = $scope.$parent.dialogOptions; - - // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish - function performSave(args) { - contentEditingHelper.contentEditorPerformSave({ - statusMessage: args.statusMessage, - saveMethod: args.saveMethod, - scope: $scope, - content: $scope.content - }).then(function (content) { - //success - if (dialogOptions.closeOnSave) { - $scope.submit(content); - } - - }, function(err) { - //error - }); - } - - function filterTabs(entity, blackList) { - if (blackList) { - _.each(entity.tabs, function (tab) { - tab.hide = _.contains(blackList, tab.alias); - }); - } - - return entity; - }; - - function init(content) { - var buttons = contentEditingHelper.configureContentEditorButtons({ - create: $routeParams.create, - content: content, - methods: { - saveAndPublish: $scope.saveAndPublish, - sendToPublish: $scope.sendToPublish, - save: $scope.save, - unPublish: angular.noop - } - }); - $scope.defaultButton = buttons.defaultButton; - $scope.subButtons = buttons.subButtons; - - //This is a total hack but we have really no other way of sharing data to the property editors of this - // content item, so we'll just set the property on the content item directly - $scope.content.isDialogEditor = true; - - editorState.set($scope.content); - } - - //check if the entity is being passed in, otherwise load it from the server - if (angular.isObject(dialogOptions.entity)) { - $scope.loaded = true; - $scope.content = filterTabs(dialogOptions.entity, dialogOptions.tabFilter); - init($scope.content); - } - else { - contentResource.getById(dialogOptions.id) - .then(function(data) { - $scope.loaded = true; - $scope.content = filterTabs(data, dialogOptions.tabFilter); - init($scope.content); - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.executeAndClearAllSubscriptions(); - }); - } - - $scope.sendToPublish = function () { - performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending..." }); - }; - - $scope.saveAndPublish = function () { - performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing..." }); - }; - - $scope.save = function () { - performSave({ saveMethod: contentResource.save, statusMessage: "Saving..." }); - }; - - // this method is called for all action buttons and then we proxy based on the btn definition - $scope.performAction = function (btn) { - - if (!btn || !angular.isFunction(btn.handler)) { - throw "btn.handler must be a function reference"; - } - - if (!$scope.busy) { - btn.handler.apply(this); - } - }; - -} - - -angular.module("umbraco") +function ContentEditDialogController($scope, editorState, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, umbModelMapper, $http) { + + $scope.defaultButton = null; + $scope.subButtons = []; + var dialogOptions = $scope.$parent.dialogOptions; + + // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish + function performSave(args) { + contentEditingHelper.contentEditorPerformSave({ + statusMessage: args.statusMessage, + saveMethod: args.saveMethod, + scope: $scope, + content: $scope.content + }).then(function (content) { + //success + if (dialogOptions.closeOnSave) { + $scope.submit(content); + } + + }, function(err) { + //error + }); + } + + function filterTabs(entity, blackList) { + if (blackList) { + _.each(entity.tabs, function (tab) { + tab.hide = _.contains(blackList, tab.alias); + }); + } + + return entity; + }; + + function init(content) { + var buttons = contentEditingHelper.configureContentEditorButtons({ + create: $routeParams.create, + content: content, + methods: { + saveAndPublish: $scope.saveAndPublish, + sendToPublish: $scope.sendToPublish, + save: $scope.save, + unPublish: angular.noop + } + }); + $scope.defaultButton = buttons.defaultButton; + $scope.subButtons = buttons.subButtons; + + //This is a total hack but we have really no other way of sharing data to the property editors of this + // content item, so we'll just set the property on the content item directly + $scope.content.isDialogEditor = true; + + editorState.set($scope.content); + } + + //check if the entity is being passed in, otherwise load it from the server + if (angular.isObject(dialogOptions.entity)) { + $scope.loaded = true; + $scope.content = filterTabs(dialogOptions.entity, dialogOptions.tabFilter); + init($scope.content); + } + else { + contentResource.getById(dialogOptions.id) + .then(function(data) { + $scope.loaded = true; + $scope.content = filterTabs(data, dialogOptions.tabFilter); + init($scope.content); + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); + }); + } + + $scope.sendToPublish = function () { + performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending..." }); + }; + + $scope.saveAndPublish = function () { + performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing..." }); + }; + + $scope.save = function () { + performSave({ saveMethod: contentResource.save, statusMessage: "Saving..." }); + }; + + // this method is called for all action buttons and then we proxy based on the btn definition + $scope.performAction = function (btn) { + + if (!btn || !angular.isFunction(btn.handler)) { + throw "btn.handler must be a function reference"; + } + + if (!$scope.busy) { + btn.handler.apply(this); + } + }; + +} + + +angular.module("umbraco") .controller("Umbraco.Dialogs.Content.EditController", ContentEditDialogController); -angular.module("umbraco") - .controller("Umbraco.Dialogs.HelpController", function ($scope, $location, $routeParams, helpService, userService, localizationService) { - $scope.section = $routeParams.section; - $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; - - if(!$scope.section){ - $scope.section = "content"; - } - - $scope.sectionName = $scope.section; - - var rq = {}; - rq.section = $scope.section; - - //translate section name - localizationService.localize("sections_" + rq.section).then(function (value) { - $scope.sectionName = value; - }); - - userService.getCurrentUser().then(function(user){ - - rq.usertype = user.userType; - rq.lang = user.locale; - - if($routeParams.url){ - rq.path = decodeURIComponent($routeParams.url); - - if(rq.path.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath) === 0){ - rq.path = rq.path.substring(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath.length); - } - - if(rq.path.indexOf(".aspx") > 0){ - rq.path = rq.path.substring(0, rq.path.indexOf(".aspx")); - } - - }else{ - rq.path = rq.section + "/" + $routeParams.tree + "/" + $routeParams.method; - } - - helpService.findHelp(rq).then(function(topics){ - $scope.topics = topics; - }); - - helpService.findVideos(rq).then(function(videos){ - $scope.videos = videos; - }); - - }); - - +angular.module("umbraco") + .controller("Umbraco.Dialogs.HelpController", function ($scope, $location, $routeParams, helpService, userService, localizationService) { + $scope.section = $routeParams.section; + $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; + + if(!$scope.section){ + $scope.section = "content"; + } + + $scope.sectionName = $scope.section; + + var rq = {}; + rq.section = $scope.section; + + //translate section name + localizationService.localize("sections_" + rq.section).then(function (value) { + $scope.sectionName = value; + }); + + userService.getCurrentUser().then(function(user){ + + rq.usertype = user.userType; + rq.lang = user.locale; + + if($routeParams.url){ + rq.path = decodeURIComponent($routeParams.url); + + if(rq.path.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath) === 0){ + rq.path = rq.path.substring(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath.length); + } + + if(rq.path.indexOf(".aspx") > 0){ + rq.path = rq.path.substring(0, rq.path.indexOf(".aspx")); + } + + }else{ + rq.path = rq.section + "/" + $routeParams.tree + "/" + $routeParams.method; + } + + helpService.findHelp(rq).then(function(topics){ + $scope.topics = topics; + }); + + helpService.findVideos(rq).then(function(videos){ + $scope.videos = videos; + }); + + }); + + }); -//used for the icon picker dialog -angular.module("umbraco") - .controller("Umbraco.Dialogs.IconPickerController", - function ($scope, iconHelper) { - - iconHelper.getIcons().then(function(icons){ - $scope.icons = icons; - }); - - $scope.submitClass = function (icon) { - if($scope.color) { - $scope.submit(icon + " " + $scope.color); - } - else { - $scope.submit(icon); - } - }; - - } +//used for the icon picker dialog +angular.module("umbraco") + .controller("Umbraco.Dialogs.IconPickerController", + function ($scope, iconHelper) { + + iconHelper.getIcons().then(function(icons){ + $scope.icons = icons; + }); + + $scope.submitClass = function (icon) { + if($scope.color) { + $scope.submit(icon + " " + $scope.color); + } + else { + $scope.submit(icon); + } + }; + + } ); /** * @ngdoc controller @@ -1533,115 +1533,115 @@ angular.module("umbraco").controller("Umbraco.Dialogs.RteEmbedController", funct $scope.submit($scope.form.preview); }; }); -angular.module("umbraco").controller('Umbraco.Dialogs.Template.QueryBuilderController', - function($scope, $http, dialogService){ - - - $http.get("backoffice/UmbracoApi/TemplateQuery/GetAllowedProperties").then(function(response) { - $scope.properties = response.data; - }); - - $http.get("backoffice/UmbracoApi/TemplateQuery/GetContentTypes").then(function (response) { - $scope.contentTypes = response.data; - }); - - $http.get("backoffice/UmbracoApi/TemplateQuery/GetFilterConditions").then(function (response) { - $scope.conditions = response.data; - }); - - - $scope.query = { - contentType: { - name: "Everything" - }, - source:{ - name: "My website" - }, - filters:[ - { - property:undefined, - operator: undefined - } - ], - sort:{ - property:{ - alias: "", - name: "", - }, - direction: "ascending" - } - }; - - - - $scope.chooseSource = function(query){ - dialogService.contentPicker({ - callback: function (data) { - - if (data.id > 0) { - query.source = { id: data.id, name: data.name }; - } else { - query.source.name = "My website"; - delete query.source.id; - } - } - }); - }; - - var throttledFunc = _.throttle(function() { - - $http.post("backoffice/UmbracoApi/TemplateQuery/PostTemplateQuery", $scope.query).then(function (response) { - $scope.result = response.data; - }); - - }, 200); - - $scope.$watch("query", function(value) { - throttledFunc(); - }, true); - - $scope.getPropertyOperators = function (property) { - - var conditions = _.filter($scope.conditions, function(condition) { - var index = condition.appliesTo.indexOf(property.type); - return index >= 0; - }); - return conditions; - }; - - - $scope.addFilter = function(query){ - query.filters.push({}); - }; - - $scope.trashFilter = function (query) { - query.filters.splice(query,1); - }; - - $scope.changeSortOrder = function(query){ - if(query.sort.direction === "ascending"){ - query.sort.direction = "descending"; - }else{ - query.sort.direction = "ascending"; - } - }; - - $scope.setSortProperty = function(query, property){ - query.sort.property = property; - if(property.type === "datetime"){ - query.sort.direction = "descending"; - }else{ - query.sort.direction = "ascending"; - } - }; +angular.module("umbraco").controller('Umbraco.Dialogs.Template.QueryBuilderController', + function($scope, $http, dialogService){ + + + $http.get("backoffice/UmbracoApi/TemplateQuery/GetAllowedProperties").then(function(response) { + $scope.properties = response.data; + }); + + $http.get("backoffice/UmbracoApi/TemplateQuery/GetContentTypes").then(function (response) { + $scope.contentTypes = response.data; + }); + + $http.get("backoffice/UmbracoApi/TemplateQuery/GetFilterConditions").then(function (response) { + $scope.conditions = response.data; + }); + + + $scope.query = { + contentType: { + name: "Everything" + }, + source:{ + name: "My website" + }, + filters:[ + { + property:undefined, + operator: undefined + } + ], + sort:{ + property:{ + alias: "", + name: "", + }, + direction: "ascending" + } + }; + + + + $scope.chooseSource = function(query){ + dialogService.contentPicker({ + callback: function (data) { + + if (data.id > 0) { + query.source = { id: data.id, name: data.name }; + } else { + query.source.name = "My website"; + delete query.source.id; + } + } + }); + }; + + var throttledFunc = _.throttle(function() { + + $http.post("backoffice/UmbracoApi/TemplateQuery/PostTemplateQuery", $scope.query).then(function (response) { + $scope.result = response.data; + }); + + }, 200); + + $scope.$watch("query", function(value) { + throttledFunc(); + }, true); + + $scope.getPropertyOperators = function (property) { + + var conditions = _.filter($scope.conditions, function(condition) { + var index = condition.appliesTo.indexOf(property.type); + return index >= 0; + }); + return conditions; + }; + + + $scope.addFilter = function(query){ + query.filters.push({}); + }; + + $scope.trashFilter = function (query) { + query.filters.splice(query,1); + }; + + $scope.changeSortOrder = function(query){ + if(query.sort.direction === "ascending"){ + query.sort.direction = "descending"; + }else{ + query.sort.direction = "ascending"; + } + }; + + $scope.setSortProperty = function(query, property){ + query.sort.property = property; + if(property.type === "datetime"){ + query.sort.direction = "descending"; + }else{ + query.sort.direction = "ascending"; + } + }; }); -angular.module("umbraco").controller('Umbraco.Dialogs.Template.SnippetController', - function($scope) { - $scope.type = $scope.dialogOptions.type; - $scope.section = { - name: "", - required: false - }; +angular.module("umbraco").controller('Umbraco.Dialogs.Template.SnippetController', + function($scope) { + $scope.type = $scope.dialogOptions.type; + $scope.section = { + name: "", + required: false + }; }); //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Dialogs.TreePickerController", @@ -2074,173 +2074,173 @@ angular.module("umbraco").controller("Umbraco.Dialogs.TreePickerController", $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); }); }); -angular.module("umbraco") - .controller("Umbraco.Dialogs.UserController", function ($scope, $location, $timeout, userService, historyService, eventsService, externalLoginInfo, authResource, currentUserResource, formHelper) { - - $scope.history = historyService.getCurrent(); - $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; - $scope.showPasswordFields = false; - $scope.changePasswordButtonState = "init"; - - $scope.externalLoginProviders = externalLoginInfo.providers; - $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; - var evts = []; - evts.push(eventsService.on("historyService.add", function (e, args) { - $scope.history = args.all; - })); - evts.push(eventsService.on("historyService.remove", function (e, args) { - $scope.history = args.all; - })); - evts.push(eventsService.on("historyService.removeAll", function (e, args) { - $scope.history = []; - })); - - $scope.logout = function () { - - //Add event listener for when there are pending changes on an editor which means our route was not successful - var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) { - //one time listener, remove the event - pendingChangeEvent(); - $scope.close(); - }); - - - //perform the path change, if it is successful then the promise will resolve otherwise it will fail - $scope.close(); - $location.path("/logout"); - }; - - $scope.gotoHistory = function (link) { - $location.path(link); - $scope.close(); - }; - - //Manually update the remaining timeout seconds - function updateTimeout() { - $timeout(function () { - if ($scope.remainingAuthSeconds > 0) { - $scope.remainingAuthSeconds--; - $scope.$digest(); - //recurse - updateTimeout(); - } - - }, 1000, false); // 1 second, do NOT execute a global digest - } - - function updateUserInfo() { - //get the user - userService.getCurrentUser().then(function (user) { - $scope.user = user; - if ($scope.user) { - $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; - $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; - //set the timer - updateTimeout(); - - authResource.getCurrentUserLinkedLogins().then(function(logins) { - //reset all to be un-linked - for (var provider in $scope.externalLoginProviders) { - $scope.externalLoginProviders[provider].linkedProviderKey = undefined; - } - - //set the linked logins - for (var login in logins) { - var found = _.find($scope.externalLoginProviders, function (i) { - return i.authType == login; - }); - if (found) { - found.linkedProviderKey = logins[login]; - } - } - }); - } - }); - } - - $scope.unlink = function (e, loginProvider, providerKey) { - var result = confirm("Are you sure you want to unlink this account?"); - if (!result) { - e.preventDefault(); - return; - } - - authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { - updateUserInfo(); - }); - } - - updateUserInfo(); - - //remove all event handlers - $scope.$on('$destroy', function () { - for (var e = 0; e < evts.length; e++) { - evts[e](); - } - - }); - - /* ---------- UPDATE PASSWORD ---------- */ - - //create the initial model for change password property editor - $scope.changePasswordModel = { - alias: "_umb_password", - view: "changepassword", - config: {}, - value: {} - }; - - //go get the config for the membership provider and add it to the model - currentUserResource.getMembershipProviderConfig().then(function(data) { - $scope.changePasswordModel.config = data; - //ensure the hasPassword config option is set to true (the user of course has a password already assigned) - //this will ensure the oldPassword is shown so they can change it - // disable reset password functionality beacuse it does not make sense inside the backoffice - $scope.changePasswordModel.config.hasPassword = true; - $scope.changePasswordModel.config.disableToggle = true; - $scope.changePasswordModel.config.enableReset = false; - }); - - $scope.changePassword = function() { - - if (formHelper.submitForm({ scope: $scope })) { - - $scope.changePasswordButtonState = "busy"; - - currentUserResource.changePassword($scope.changePasswordModel.value).then(function(data) { - - //if the password has been reset, then update our model - if (data.value) { - $scope.changePasswordModel.value.generatedPassword = data.value; - } - - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); - - $scope.changePasswordButtonState = "success"; - - }, function (err) { - - formHelper.handleError(err); - - $scope.changePasswordButtonState = "error"; - - }); - - } - - }; - - $scope.togglePasswordFields = function() { - clearPasswordFields(); - $scope.showPasswordFields = !$scope.showPasswordFields; - } - - function clearPasswordFields() { - $scope.changePasswordModel.value.newPassword = ""; - $scope.changePasswordModel.confirm = ""; - } - - }); +angular.module("umbraco") + .controller("Umbraco.Dialogs.UserController", function ($scope, $location, $timeout, userService, historyService, eventsService, externalLoginInfo, authResource, currentUserResource, formHelper) { + + $scope.history = historyService.getCurrent(); + $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; + $scope.showPasswordFields = false; + $scope.changePasswordButtonState = "init"; + + $scope.externalLoginProviders = externalLoginInfo.providers; + $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; + var evts = []; + evts.push(eventsService.on("historyService.add", function (e, args) { + $scope.history = args.all; + })); + evts.push(eventsService.on("historyService.remove", function (e, args) { + $scope.history = args.all; + })); + evts.push(eventsService.on("historyService.removeAll", function (e, args) { + $scope.history = []; + })); + + $scope.logout = function () { + + //Add event listener for when there are pending changes on an editor which means our route was not successful + var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) { + //one time listener, remove the event + pendingChangeEvent(); + $scope.close(); + }); + + + //perform the path change, if it is successful then the promise will resolve otherwise it will fail + $scope.close(); + $location.path("/logout"); + }; + + $scope.gotoHistory = function (link) { + $location.path(link); + $scope.close(); + }; + + //Manually update the remaining timeout seconds + function updateTimeout() { + $timeout(function () { + if ($scope.remainingAuthSeconds > 0) { + $scope.remainingAuthSeconds--; + $scope.$digest(); + //recurse + updateTimeout(); + } + + }, 1000, false); // 1 second, do NOT execute a global digest + } + + function updateUserInfo() { + //get the user + userService.getCurrentUser().then(function (user) { + $scope.user = user; + if ($scope.user) { + $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; + $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; + //set the timer + updateTimeout(); + + authResource.getCurrentUserLinkedLogins().then(function(logins) { + //reset all to be un-linked + for (var provider in $scope.externalLoginProviders) { + $scope.externalLoginProviders[provider].linkedProviderKey = undefined; + } + + //set the linked logins + for (var login in logins) { + var found = _.find($scope.externalLoginProviders, function (i) { + return i.authType == login; + }); + if (found) { + found.linkedProviderKey = logins[login]; + } + } + }); + } + }); + } + + $scope.unlink = function (e, loginProvider, providerKey) { + var result = confirm("Are you sure you want to unlink this account?"); + if (!result) { + e.preventDefault(); + return; + } + + authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { + updateUserInfo(); + }); + } + + updateUserInfo(); + + //remove all event handlers + $scope.$on('$destroy', function () { + for (var e = 0; e < evts.length; e++) { + evts[e](); + } + + }); + + /* ---------- UPDATE PASSWORD ---------- */ + + //create the initial model for change password property editor + $scope.changePasswordModel = { + alias: "_umb_password", + view: "changepassword", + config: {}, + value: {} + }; + + //go get the config for the membership provider and add it to the model + currentUserResource.getMembershipProviderConfig().then(function(data) { + $scope.changePasswordModel.config = data; + //ensure the hasPassword config option is set to true (the user of course has a password already assigned) + //this will ensure the oldPassword is shown so they can change it + // disable reset password functionality beacuse it does not make sense inside the backoffice + $scope.changePasswordModel.config.hasPassword = true; + $scope.changePasswordModel.config.disableToggle = true; + $scope.changePasswordModel.config.enableReset = false; + }); + + $scope.changePassword = function() { + + if (formHelper.submitForm({ scope: $scope })) { + + $scope.changePasswordButtonState = "busy"; + + currentUserResource.changePassword($scope.changePasswordModel.value).then(function(data) { + + //if the password has been reset, then update our model + if (data.value) { + $scope.changePasswordModel.value.generatedPassword = data.value; + } + + formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + + $scope.changePasswordButtonState = "success"; + + }, function (err) { + + formHelper.handleError(err); + + $scope.changePasswordButtonState = "error"; + + }); + + } + + }; + + $scope.togglePasswordFields = function() { + clearPasswordFields(); + $scope.showPasswordFields = !$scope.showPasswordFields; + } + + function clearPasswordFields() { + $scope.changePasswordModel.value.newPassword = ""; + $scope.changePasswordModel.confirm = ""; + } + + }); /** * @ngdoc controller @@ -2324,1073 +2324,1073 @@ angular.module('umbraco').controller("Umbraco.LoginController", function (events }); -//used for the media picker dialog -angular.module("umbraco").controller("Umbraco.Notifications.ConfirmRouteChangeController", - function ($scope, $location, $log, notificationsService) { - - $scope.discard = function(not){ - not.args.listener(); - - $location.search(""); - - //we need to break the path up into path and query - var parts = not.args.path.split("?"); - var query = {}; - if (parts.length > 1) { - _.each(parts[1].split("&"), function(q) { - var keyVal = q.split("="); - query[keyVal[0]] = keyVal[1]; - }); - } - - $location.path(parts[0]).search(query); - notificationsService.remove(not); - }; - - $scope.stay = function(not){ - notificationsService.remove(not); - }; - +//used for the media picker dialog +angular.module("umbraco").controller("Umbraco.Notifications.ConfirmRouteChangeController", + function ($scope, $location, $log, notificationsService) { + + $scope.discard = function(not){ + not.args.listener(); + + $location.search(""); + + //we need to break the path up into path and query + var parts = not.args.path.split("?"); + var query = {}; + if (parts.length > 1) { + _.each(parts[1].split("&"), function(q) { + var keyVal = q.split("="); + query[keyVal[0]] = keyVal[1]; + }); + } + + $location.path(parts[0]).search(query); + notificationsService.remove(not); + }; + + $scope.stay = function(not){ + notificationsService.remove(not); + }; + }); - (function() { - "use strict"; - - function CompositionsOverlay($scope) { - - var vm = this; - - vm.isSelected = isSelected; - - function isSelected(alias) { - if($scope.model.contentType.compositeContentTypes.indexOf(alias) !== -1) { - return true; - } - } - } - - angular.module("umbraco").controller("Umbraco.Overlays.CompositionsOverlay", CompositionsOverlay); - -})(); + (function() { + "use strict"; -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.PropertyController - * @function - * - * @description - * The controller for the content type editor property dialog - */ - -(function() { - "use strict"; - - function EditorPickerOverlay($scope, dataTypeResource, dataTypeHelper, contentTypeResource, localizationService) { - - var vm = this; - - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectEditor"); - } - - vm.searchTerm = ""; - vm.showTabs = false; - vm.tabsLoaded = 0; - vm.typesAndEditors = []; - vm.userConfigured = []; - vm.loading = false; - vm.tabs = [{ - active: true, - id: 1, - label: localizationService.localize("contentTypeEditor_availableEditors"), - alias: "Default", - typesAndEditors: [] - }, { - active: false, - id: 2, - label: localizationService.localize("contentTypeEditor_reuse"), - alias: "Reuse", - userConfigured: [] - }]; - - vm.filterItems = filterItems; - vm.showDetailsOverlay = showDetailsOverlay; - vm.hideDetailsOverlay = hideDetailsOverlay; - vm.pickEditor = pickEditor; - vm.pickDataType = pickDataType; - - function activate() { - - getGroupedDataTypes(); - getGroupedPropertyEditors(); - - } - - function getGroupedPropertyEditors() { - - vm.loading = true; - - dataTypeResource.getGroupedPropertyEditors().then(function(data) { - vm.tabs[0].typesAndEditors = data; - vm.typesAndEditors = data; - vm.tabsLoaded = vm.tabsLoaded + 1; - checkIfTabContentIsLoaded(); - }); - - } - - function getGroupedDataTypes() { - - vm.loading = true; - - dataTypeResource.getGroupedDataTypes().then(function(data) { - vm.tabs[1].userConfigured = data; - vm.userConfigured = data; - vm.tabsLoaded = vm.tabsLoaded + 1; - checkIfTabContentIsLoaded(); - }); - - } - - function checkIfTabContentIsLoaded() { - if (vm.tabsLoaded === 2) { - vm.loading = false; - vm.showTabs = true; - } - } - - function filterItems() { - // clear item details - $scope.model.itemDetails = null; - - if (vm.searchTerm) { - vm.showFilterResult = true; - vm.showTabs = false; - } else { - vm.showFilterResult = false; - vm.showTabs = true; - } - - } - - function showDetailsOverlay(property) { - - var propertyDetails = {}; - propertyDetails.icon = property.icon; - propertyDetails.title = property.name; - - $scope.model.itemDetails = propertyDetails; - - } - - function hideDetailsOverlay() { - $scope.model.itemDetails = null; - } - - function pickEditor(editor) { - - var parentId = -1; - - dataTypeResource.getScaffold(parentId).then(function(dataType) { - - // set alias - dataType.selectedEditor = editor.alias; - - // set name - var nameArray = []; - - if ($scope.model.contentTypeName) { - nameArray.push($scope.model.contentTypeName); - } - - if ($scope.model.property.label) { - nameArray.push($scope.model.property.label); - } - - if (editor.name) { - nameArray.push(editor.name); - } - - // make name - dataType.name = nameArray.join(" - "); - - // get pre values - dataTypeResource.getPreValues(dataType.selectedEditor).then(function(preValues) { - - dataType.preValues = preValues; - - openEditorSettingsOverlay(dataType, true); - - }); - - }); - - } - - function pickDataType(selectedDataType) { - - dataTypeResource.getById(selectedDataType.id).then(function(dataType) { - contentTypeResource.getPropertyTypeScaffold(dataType.id).then(function(propertyType) { - submitOverlay(dataType, propertyType, false); - }); - }); - - } - - function openEditorSettingsOverlay(dataType, isNew) { - vm.editorSettingsOverlay = { - title: localizationService.localize("contentTypeEditor_editorSettings"), - dataType: dataType, - view: "views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html", - show: true, - submit: function(model) { - var preValues = dataTypeHelper.createPreValueProps(model.dataType.preValues); - - dataTypeResource.save(model.dataType, preValues, isNew).then(function(newDataType) { - - contentTypeResource.getPropertyTypeScaffold(newDataType.id).then(function(propertyType) { - - submitOverlay(newDataType, propertyType, true); - - vm.editorSettingsOverlay.show = false; - vm.editorSettingsOverlay = null; - - }); - - }); - } - }; - - } - - function submitOverlay(dataType, propertyType, isNew) { - - // update property - $scope.model.property.config = propertyType.config; - $scope.model.property.editor = propertyType.editor; - $scope.model.property.view = propertyType.view; - $scope.model.property.dataTypeId = dataType.id; - $scope.model.property.dataTypeIcon = dataType.icon; - $scope.model.property.dataTypeName = dataType.name; - - $scope.model.updateSameDataTypes = isNew; - - $scope.model.submit($scope.model); - - } - - activate(); - - } - - angular.module("umbraco").controller("Umbraco.Overlays.EditorPickerOverlay", EditorPickerOverlay); - -})(); + function CompositionsOverlay($scope) { -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.PropertyController - * @function - * - * @description - * The controller for the content type editor property dialog - */ - - (function() { - "use strict"; - - function PropertySettingsOverlay($scope, contentTypeResource, dataTypeResource, dataTypeHelper, localizationService) { - - var vm = this; - - vm.showValidationPattern = false; - vm.focusOnPatternField = false; - vm.focusOnMandatoryField = false; - vm.selectedValidationType = {}; - vm.validationTypes = [ - { - "name": localizationService.localize("validation_validateAsEmail"), - "key": "email", - "pattern": "[a-zA-Z0-9_\.\+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-\.]+", - "enableEditing": true - }, - { - "name": localizationService.localize("validation_validateAsNumber"), - "key": "number", - "pattern": "^[0-9]*$", - "enableEditing": true - }, - { - "name": localizationService.localize("validation_validateAsUrl"), - "key": "url", - "pattern": "https?\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}", - "enableEditing": true - }, - { - "name": localizationService.localize("validation_enterCustomValidation"), - "key": "custom", - "pattern": "", - "enableEditing": true - } - ]; - - vm.changeValidationType = changeValidationType; - vm.changeValidationPattern = changeValidationPattern; - vm.openEditorPickerOverlay = openEditorPickerOverlay; - vm.openEditorSettingsOverlay = openEditorSettingsOverlay; - - function activate() { - - matchValidationType(); - - } - - function changeValidationPattern() { - matchValidationType(); - } - - function openEditorPickerOverlay(property) { - - vm.focusOnMandatoryField = false; - - vm.editorPickerOverlay = {}; - vm.editorPickerOverlay.property = $scope.model.property; - vm.editorPickerOverlay.contentTypeName = $scope.model.contentTypeName; - vm.editorPickerOverlay.view = "views/common/overlays/contenttypeeditor/editorpicker/editorpicker.html"; - vm.editorPickerOverlay.show = true; - - vm.editorPickerOverlay.submit = function(model) { - - $scope.model.updateSameDataTypes = model.updateSameDataTypes; - - vm.focusOnMandatoryField = true; - - // update property - property.config = model.property.config; - property.editor = model.property.editor; - property.view = model.property.view; - property.dataTypeId = model.property.dataTypeId; - property.dataTypeIcon = model.property.dataTypeIcon; - property.dataTypeName = model.property.dataTypeName; - - vm.editorPickerOverlay.show = false; - vm.editorPickerOverlay = null; - }; - - vm.editorPickerOverlay.close = function(model) { - vm.editorPickerOverlay.show = false; - vm.editorPickerOverlay = null; - }; - - } - - function openEditorSettingsOverlay(property) { - - vm.focusOnMandatoryField = false; - - // get data type - dataTypeResource.getById(property.dataTypeId).then(function(dataType) { - - vm.editorSettingsOverlay = {}; - vm.editorSettingsOverlay.title = "Editor settings"; - vm.editorSettingsOverlay.view = "views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html"; - vm.editorSettingsOverlay.dataType = dataType; - vm.editorSettingsOverlay.show = true; - - vm.editorSettingsOverlay.submit = function(model) { - - var preValues = dataTypeHelper.createPreValueProps(model.dataType.preValues); - - dataTypeResource.save(model.dataType, preValues, false).then(function(newDataType) { - - contentTypeResource.getPropertyTypeScaffold(newDataType.id).then(function(propertyType) { - - // update editor - property.config = propertyType.config; - property.editor = propertyType.editor; - property.view = propertyType.view; - property.dataTypeId = newDataType.id; - property.dataTypeIcon = newDataType.icon; - property.dataTypeName = newDataType.name; - - // set flag to update same data types - $scope.model.updateSameDataTypes = true; - - vm.focusOnMandatoryField = true; - - vm.editorSettingsOverlay.show = false; - vm.editorSettingsOverlay = null; - - }); - - }); - - }; - - vm.editorSettingsOverlay.close = function(oldModel) { - vm.editorSettingsOverlay.show = false; - vm.editorSettingsOverlay = null; - }; - - }); - - } - - function matchValidationType() { - - if($scope.model.property.validation.pattern !== null && $scope.model.property.validation.pattern !== "" && $scope.model.property.validation.pattern !== undefined) { - - var match = false; - - // find and show if a match from the list has been chosen - angular.forEach(vm.validationTypes, function(validationType, index){ - if($scope.model.property.validation.pattern === validationType.pattern) { - vm.selectedValidationType = vm.validationTypes[index]; - vm.showValidationPattern = true; - match = true; - } - }); - - // if there is no match - choose the custom validation option. - if(!match) { - angular.forEach(vm.validationTypes, function(validationType){ - if(validationType.key === "custom") { - vm.selectedValidationType = validationType; - vm.showValidationPattern = true; - } - }); - } - } - - } - - function changeValidationType(selectedValidationType) { - - if(selectedValidationType) { - $scope.model.property.validation.pattern = selectedValidationType.pattern; - vm.showValidationPattern = true; - - // set focus on textarea - if(selectedValidationType.key === "custom") { - vm.focusOnPatternField = true; - } - - } else { - $scope.model.property.validation.pattern = ""; - vm.showValidationPattern = false; - } - - } - - activate(); - - } - - angular.module("umbraco").controller("Umbraco.Overlay.PropertySettingsOverlay", PropertySettingsOverlay); - -})(); + var vm = this; - (function() { - "use strict"; - - function CopyOverlay($scope, localizationService, eventsService) { - - var vm = this; - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("general_copy"); - } - - vm.hideSearch = hideSearch; - vm.selectResult = selectResult; - vm.onSearchResults = onSearchResults; - - var dialogOptions = $scope.model; - var searchText = "Search..."; - var node = dialogOptions.currentNode; - - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - $scope.model.relateToOriginal = true; - $scope.dialogTreeEventHandler = $({}); - - vm.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - }; - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if (args.node.metaData.listViewNode) { - //check if list view 'search' node was selected - - vm.searchInfo.showSearch = true; - vm.searchInfo.searchFromId = args.node.metaData.listViewNode.id; - vm.searchInfo.searchFromName = args.node.metaData.listViewNode.name; - } - else { - //eventsService.emit("editors.content.copyController.select", args); - - if ($scope.model.target) { - //un-select if there's a current one selected - $scope.model.target.selected = false; - } - - $scope.model.target = args.node; - $scope.model.target.selected = true; - } - - } - - function nodeExpandedHandler(ev, args) { - if (angular.isArray(args.children)) { - - //iterate children - _.each(args.children, function (child) { - //check if any of the items are list views, if so we need to add a custom - // child: A node to activate the search - if (child.metaData.isContainer) { - child.hasChildren = true; - child.children = [ - { - level: child.level + 1, - hasChildren: false, - name: searchText, - metaData: { - listViewNode: child, - }, - cssClass: "icon umb-tree-icon sprTree icon-search", - cssClasses: ["not-published"] - } - ]; - } - }); - } - } - - function hideSearch() { - vm.searchInfo.showSearch = false; - vm.searchInfo.searchFromId = null; - vm.searchInfo.searchFromName = null; - vm.searchInfo.results = []; - } - - // method to select a search result - function selectResult(evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, { event: evt, node: result }); - } - - //callback when there are search results - function onSearchResults(results) { - vm.searchInfo.results = results; - vm.searchInfo.showSearch = true; - } - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - }); - - - } - - angular.module("umbraco").controller("Umbraco.Overlays.CopyOverlay", CopyOverlay); - -})(); + vm.isSelected = isSelected; -(function() { - "use strict"; - - function EmbedOverlay($scope, $http, umbRequestHelper, localizationService) { - - var vm = this; - var origWidth = 500; - var origHeight = 300; - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("general_embed"); - } - - $scope.model.embed = { - url: "", - width: 360, - height: 240, - constrain: true, - preview: "", - success: false, - info: "", - supportsDimensions: "" - }; - - vm.showPreview = showPreview; - vm.changeSize = changeSize; - - function showPreview() { - - if ($scope.model.embed.url) { - $scope.model.embed.show = true; - $scope.model.embed.preview = "
"; - $scope.model.embed.info = ""; - $scope.model.embed.success = false; - - $http({ - method: 'GET', - url: umbRequestHelper.getApiUrl("embedApiBaseUrl", "GetEmbed"), - params: { - url: $scope.model.embed.url, - width: $scope.model.embed.width, - height: $scope.model.embed.height - } - }) - .success(function(data) { - - $scope.model.embed.preview = ""; - - switch (data.Status) { - case 0: - //not supported - $scope.model.embed.info = "Not supported"; - break; - case 1: - //error - $scope.model.embed.info = "Could not embed media - please ensure the URL is valid"; - break; - case 2: - $scope.model.embed.preview = data.Markup; - $scope.model.embed.supportsDimensions = data.SupportsDimensions; - $scope.model.embed.success = true; - break; - } - }) - .error(function() { - $scope.model.embed.supportsDimensions = false; - $scope.model.embed.preview = ""; - $scope.model.embed.info = "Could not embed media - please ensure the URL is valid"; - }); - } else { - $scope.model.embed.supportsDimensions = false; - $scope.model.embed.preview = ""; - $scope.model.embed.info = "Please enter a URL"; - } - } - - function changeSize(type) { - - var width, height; - - if ($scope.model.embed.constrain) { - width = parseInt($scope.model.embed.width, 10); - height = parseInt($scope.model.embed.height, 10); - if (type == 'width') { - origHeight = Math.round((width / origWidth) * height); - $scope.model.embed.height = origHeight; - } else { - origWidth = Math.round((height / origHeight) * width); - $scope.model.embed.width = origWidth; - } - } - if ($scope.model.embed.url !== "") { - showPreview(); - } - - } - - } - - angular.module("umbraco").controller("Umbraco.Overlays.EmbedOverlay", EmbedOverlay); - -})(); + function isSelected(alias) { + if($scope.model.contentType.compositeContentTypes.indexOf(alias) !== -1) { + return true; + } + } + } -angular.module("umbraco") - .controller("Umbraco.Overlays.HelpController", function ($scope, $location, $routeParams, helpService, userService, localizationService) { - $scope.section = $routeParams.section; - $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; - $scope.model.subtitle = "Umbraco version" + " " + $scope.version; - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("general_help"); - } - - if(!$scope.section){ - $scope.section = "content"; - } - - $scope.sectionName = $scope.section; - - var rq = {}; - rq.section = $scope.section; - - //translate section name - localizationService.localize("sections_" + rq.section).then(function (value) { - $scope.sectionName = value; - }); - - userService.getCurrentUser().then(function(user){ - - rq.usertype = user.userType; - rq.lang = user.locale; - - if($routeParams.url){ - rq.path = decodeURIComponent($routeParams.url); - - if(rq.path.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath) === 0){ - rq.path = rq.path.substring(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath.length); - } - - if(rq.path.indexOf(".aspx") > 0){ - rq.path = rq.path.substring(0, rq.path.indexOf(".aspx")); - } - - }else{ - rq.path = rq.section + "/" + $routeParams.tree + "/" + $routeParams.method; - } - - helpService.findHelp(rq).then(function(topics){ - $scope.topics = topics; - }); - - helpService.findVideos(rq).then(function(videos){ - $scope.videos = videos; - }); - - }); - - - }); + angular.module("umbraco").controller("Umbraco.Overlays.CompositionsOverlay", CompositionsOverlay); -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.PropertyController - * @function - * - * @description - * The controller for the content type editor property dialog - */ -function IconPickerOverlay($scope, iconHelper, localizationService) { - - $scope.loading = true; - $scope.model.hideSubmitButton = true; - - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectIcon"); - } - - iconHelper.getIcons().then(function(icons) { - $scope.icons = icons; - $scope.loading = false; - }); - - $scope.selectIcon = function(icon, color) { - $scope.model.icon = icon; - $scope.model.color = color; - $scope.submitForm($scope.model); - }; - -} - -angular.module("umbraco").controller("Umbraco.Overlays.IconPickerOverlay", IconPickerOverlay); +})(); -function ItemPickerOverlay($scope, localizationService) { - - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectItem"); - } - - $scope.model.hideSubmitButton = true; - - $scope.selectItem = function(item) { - $scope.model.selectedItem = item; - $scope.submitForm($scope.model); - }; - -} - -angular.module("umbraco").controller("Umbraco.Overlays.ItemPickerOverlay", ItemPickerOverlay); +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.PropertyController + * @function + * + * @description + * The controller for the content type editor property dialog + */ -//used for the media picker dialog -angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", - function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService) { - var dialogOptions = $scope.model; - - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectLink"); - } - - $scope.dialogTreeEventHandler = $({}); - $scope.model.target = {}; - $scope.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - }; - - if (dialogOptions.currentTarget) { - $scope.model.target = dialogOptions.currentTarget; - - //if we have a node ID, we fetch the current node to build the form data - if ($scope.model.target.id) { - - if (!$scope.model.target.path) { - entityResource.getPath($scope.model.target.id, "Document").then(function (path) { - $scope.model.target.path = path; - //now sync the tree to this path - $scope.dialogTreeEventHandler.syncTree({ path: $scope.model.target.path, tree: "content" }); - }); - } - - contentResource.getNiceUrl($scope.model.target.id).then(function (url) { - $scope.model.target.url = url; - }); - } - } - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if (args.node.metaData.listViewNode) { - //check if list view 'search' node was selected - - $scope.searchInfo.showSearch = true; - $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; - $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; - } - else { - eventsService.emit("dialogs.linkPicker.select", args); - - if ($scope.currentNode) { - //un-select if there's a current one selected - $scope.currentNode.selected = false; - } - - $scope.currentNode = args.node; - $scope.currentNode.selected = true; - $scope.model.target.id = args.node.id; - $scope.model.target.name = args.node.name; - - if (args.node.id < 0) { - $scope.model.target.url = "/"; - } - else { - contentResource.getNiceUrl(args.node.id).then(function (url) { - $scope.model.target.url = url; - }); - } - - if (!angular.isUndefined($scope.model.target.isMedia)) { - delete $scope.model.target.isMedia; - } - } - } - - function nodeExpandedHandler(ev, args) { - if (angular.isArray(args.children)) { - - //iterate children - _.each(args.children, function (child) { - //check if any of the items are list views, if so we need to add a custom - // child: A node to activate the search - if (child.metaData.isContainer) { - child.hasChildren = true; - child.children = [ - { - level: child.level + 1, - hasChildren: false, - name: searchText, - metaData: { - listViewNode: child, - }, - cssClass: "icon umb-tree-icon sprTree icon-search", - cssClasses: ["not-published"] - } - ]; - } - }); - } - } - - $scope.switchToMediaPicker = function () { - userService.getCurrentUser().then(function (userData) { - $scope.mediaPickerOverlay = { - view: "mediapicker", - startNodeId: userData.startMediaId, - show: true, - submit: function(model) { - var media = model.selectedImages[0]; - - $scope.model.target.id = media.id; - $scope.model.target.isMedia = true; - $scope.model.target.name = media.name; - $scope.model.target.url = mediaHelper.resolveFile(media); - - $scope.mediaPickerOverlay.show = false; - $scope.mediaPickerOverlay = null; - } - }; - }); - }; - - $scope.hideSearch = function () { - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = null; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } - - // method to select a search result - $scope.selectResult = function (evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, {event: evt, node: result}); - }; - - //callback when there are search results - $scope.onSearchResults = function (results) { - $scope.searchInfo.results = results; - $scope.searchInfo.showSearch = true; - }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - }); - }); +(function() { + "use strict"; -function MacroPickerController($scope, entityResource, macroResource, umbPropEditorHelper, macroService, formHelper, localizationService) { - - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectMacro"); - } - - $scope.macros = []; - $scope.model.selectedMacro = null; - $scope.wizardStep = "macroSelect"; - $scope.model.macroParams = []; - $scope.noMacroParams = false; - - $scope.changeMacro = function() { - if ($scope.wizardStep === "macroSelect") { - editParams(); - } else { - submitForm(); - } - }; - - /** changes the view to edit the params of the selected macro */ - function editParams() { - //get the macro params if there are any - macroResource.getMacroParameters($scope.model.selectedMacro.id) - .then(function (data) { - - //go to next page if there are params otherwise we can just exit - if (!angular.isArray(data) || data.length === 0) { - - $scope.noMacroParams = true; - - } else { - $scope.wizardStep = "paramSelect"; - $scope.model.macroParams = data; - - //fill in the data if we are editing this macro - if ($scope.model.dialogData && $scope.model.dialogData.macroData && $scope.model.dialogData.macroData.macroParamsDictionary) { - _.each($scope.model.dialogData.macroData.macroParamsDictionary, function (val, key) { - var prop = _.find($scope.model.macroParams, function (item) { - return item.alias == key; - }); - if (prop) { - - if (_.isString(val)) { - //we need to unescape values as they have most likely been escaped while inserted - val = _.unescape(val); - - //detect if it is a json string - if (val.detectIsJson()) { - try { - //Parse it to json - prop.value = angular.fromJson(val); - } - catch (e) { - // not json - prop.value = val; - } - } - else { - prop.value = val; - } - } - else { - prop.value = val; - } - } - }); - - } - } - - }); - } - - //here we check to see if we've been passed a selected macro and if so we'll set the - //editor to start with parameter editing - if ($scope.model.dialogData && $scope.model.dialogData.macroData) { - $scope.wizardStep = "paramSelect"; - } - - //get the macro list - pass in a filter if it is only for rte - entityResource.getAll("Macro", ($scope.model.dialogData && $scope.model.dialogData.richTextEditor && $scope.model.dialogData.richTextEditor === true) ? "UseInEditor=true" : null) - .then(function (data) { - - //if 'allowedMacros' is specified, we need to filter - if (angular.isArray($scope.model.dialogData.allowedMacros) && $scope.model.dialogData.allowedMacros.length > 0) { - $scope.macros = _.filter(data, function(d) { - return _.contains($scope.model.dialogData.allowedMacros, d.alias); - }); - } - else { - $scope.macros = data; - } - - - //check if there's a pre-selected macro and if it exists - if ($scope.model.dialogData && $scope.model.dialogData.macroData && $scope.model.dialogData.macroData.macroAlias) { - var found = _.find(data, function (item) { - return item.alias === $scope.model.dialogData.macroData.macroAlias; - }); - if (found) { - //select the macro and go to next screen - $scope.model.selectedMacro = found; - editParams(); - return; - } - } - //we don't have a pre-selected macro so ensure the correct step is set - $scope.wizardStep = "macroSelect"; - }); - - -} - -angular.module("umbraco").controller("Umbraco.Overlays.MacroPickerController", MacroPickerController); + function EditorPickerOverlay($scope, dataTypeResource, dataTypeHelper, contentTypeResource, localizationService) { + + var vm = this; + + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectEditor"); + } + + vm.searchTerm = ""; + vm.showTabs = false; + vm.tabsLoaded = 0; + vm.typesAndEditors = []; + vm.userConfigured = []; + vm.loading = false; + vm.tabs = [{ + active: true, + id: 1, + label: localizationService.localize("contentTypeEditor_availableEditors"), + alias: "Default", + typesAndEditors: [] + }, { + active: false, + id: 2, + label: localizationService.localize("contentTypeEditor_reuse"), + alias: "Reuse", + userConfigured: [] + }]; + + vm.filterItems = filterItems; + vm.showDetailsOverlay = showDetailsOverlay; + vm.hideDetailsOverlay = hideDetailsOverlay; + vm.pickEditor = pickEditor; + vm.pickDataType = pickDataType; + + function activate() { + + getGroupedDataTypes(); + getGroupedPropertyEditors(); + + } + + function getGroupedPropertyEditors() { + + vm.loading = true; + + dataTypeResource.getGroupedPropertyEditors().then(function(data) { + vm.tabs[0].typesAndEditors = data; + vm.typesAndEditors = data; + vm.tabsLoaded = vm.tabsLoaded + 1; + checkIfTabContentIsLoaded(); + }); + + } + + function getGroupedDataTypes() { + + vm.loading = true; + + dataTypeResource.getGroupedDataTypes().then(function(data) { + vm.tabs[1].userConfigured = data; + vm.userConfigured = data; + vm.tabsLoaded = vm.tabsLoaded + 1; + checkIfTabContentIsLoaded(); + }); + + } + + function checkIfTabContentIsLoaded() { + if (vm.tabsLoaded === 2) { + vm.loading = false; + vm.showTabs = true; + } + } + + function filterItems() { + // clear item details + $scope.model.itemDetails = null; + + if (vm.searchTerm) { + vm.showFilterResult = true; + vm.showTabs = false; + } else { + vm.showFilterResult = false; + vm.showTabs = true; + } + + } + + function showDetailsOverlay(property) { + + var propertyDetails = {}; + propertyDetails.icon = property.icon; + propertyDetails.title = property.name; + + $scope.model.itemDetails = propertyDetails; + + } + + function hideDetailsOverlay() { + $scope.model.itemDetails = null; + } + + function pickEditor(editor) { + + var parentId = -1; + + dataTypeResource.getScaffold(parentId).then(function(dataType) { + + // set alias + dataType.selectedEditor = editor.alias; + + // set name + var nameArray = []; + + if ($scope.model.contentTypeName) { + nameArray.push($scope.model.contentTypeName); + } + + if ($scope.model.property.label) { + nameArray.push($scope.model.property.label); + } + + if (editor.name) { + nameArray.push(editor.name); + } + + // make name + dataType.name = nameArray.join(" - "); + + // get pre values + dataTypeResource.getPreValues(dataType.selectedEditor).then(function(preValues) { + + dataType.preValues = preValues; + + openEditorSettingsOverlay(dataType, true); + + }); + + }); + + } + + function pickDataType(selectedDataType) { + + dataTypeResource.getById(selectedDataType.id).then(function(dataType) { + contentTypeResource.getPropertyTypeScaffold(dataType.id).then(function(propertyType) { + submitOverlay(dataType, propertyType, false); + }); + }); + + } + + function openEditorSettingsOverlay(dataType, isNew) { + vm.editorSettingsOverlay = { + title: localizationService.localize("contentTypeEditor_editorSettings"), + dataType: dataType, + view: "views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html", + show: true, + submit: function(model) { + var preValues = dataTypeHelper.createPreValueProps(model.dataType.preValues); + + dataTypeResource.save(model.dataType, preValues, isNew).then(function(newDataType) { + + contentTypeResource.getPropertyTypeScaffold(newDataType.id).then(function(propertyType) { + + submitOverlay(newDataType, propertyType, true); + + vm.editorSettingsOverlay.show = false; + vm.editorSettingsOverlay = null; + + }); + + }); + } + }; + + } + + function submitOverlay(dataType, propertyType, isNew) { + + // update property + $scope.model.property.config = propertyType.config; + $scope.model.property.editor = propertyType.editor; + $scope.model.property.view = propertyType.view; + $scope.model.property.dataTypeId = dataType.id; + $scope.model.property.dataTypeIcon = dataType.icon; + $scope.model.property.dataTypeName = dataType.name; + + $scope.model.updateSameDataTypes = isNew; + + $scope.model.submit($scope.model); + + } + + activate(); + + } + + angular.module("umbraco").controller("Umbraco.Overlays.EditorPickerOverlay", EditorPickerOverlay); + +})(); + +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.PropertyController + * @function + * + * @description + * The controller for the content type editor property dialog + */ + + (function() { + "use strict"; + + function PropertySettingsOverlay($scope, contentTypeResource, dataTypeResource, dataTypeHelper, localizationService) { + + var vm = this; + + vm.showValidationPattern = false; + vm.focusOnPatternField = false; + vm.focusOnMandatoryField = false; + vm.selectedValidationType = {}; + vm.validationTypes = [ + { + "name": localizationService.localize("validation_validateAsEmail"), + "key": "email", + "pattern": "[a-zA-Z0-9_\.\+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-\.]+", + "enableEditing": true + }, + { + "name": localizationService.localize("validation_validateAsNumber"), + "key": "number", + "pattern": "^[0-9]*$", + "enableEditing": true + }, + { + "name": localizationService.localize("validation_validateAsUrl"), + "key": "url", + "pattern": "https?\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}", + "enableEditing": true + }, + { + "name": localizationService.localize("validation_enterCustomValidation"), + "key": "custom", + "pattern": "", + "enableEditing": true + } + ]; + + vm.changeValidationType = changeValidationType; + vm.changeValidationPattern = changeValidationPattern; + vm.openEditorPickerOverlay = openEditorPickerOverlay; + vm.openEditorSettingsOverlay = openEditorSettingsOverlay; + + function activate() { + + matchValidationType(); + + } + + function changeValidationPattern() { + matchValidationType(); + } + + function openEditorPickerOverlay(property) { + + vm.focusOnMandatoryField = false; + + vm.editorPickerOverlay = {}; + vm.editorPickerOverlay.property = $scope.model.property; + vm.editorPickerOverlay.contentTypeName = $scope.model.contentTypeName; + vm.editorPickerOverlay.view = "views/common/overlays/contenttypeeditor/editorpicker/editorpicker.html"; + vm.editorPickerOverlay.show = true; + + vm.editorPickerOverlay.submit = function(model) { + + $scope.model.updateSameDataTypes = model.updateSameDataTypes; + + vm.focusOnMandatoryField = true; + + // update property + property.config = model.property.config; + property.editor = model.property.editor; + property.view = model.property.view; + property.dataTypeId = model.property.dataTypeId; + property.dataTypeIcon = model.property.dataTypeIcon; + property.dataTypeName = model.property.dataTypeName; + + vm.editorPickerOverlay.show = false; + vm.editorPickerOverlay = null; + }; + + vm.editorPickerOverlay.close = function(model) { + vm.editorPickerOverlay.show = false; + vm.editorPickerOverlay = null; + }; + + } + + function openEditorSettingsOverlay(property) { + + vm.focusOnMandatoryField = false; + + // get data type + dataTypeResource.getById(property.dataTypeId).then(function(dataType) { + + vm.editorSettingsOverlay = {}; + vm.editorSettingsOverlay.title = "Editor settings"; + vm.editorSettingsOverlay.view = "views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html"; + vm.editorSettingsOverlay.dataType = dataType; + vm.editorSettingsOverlay.show = true; + + vm.editorSettingsOverlay.submit = function(model) { + + var preValues = dataTypeHelper.createPreValueProps(model.dataType.preValues); + + dataTypeResource.save(model.dataType, preValues, false).then(function(newDataType) { + + contentTypeResource.getPropertyTypeScaffold(newDataType.id).then(function(propertyType) { + + // update editor + property.config = propertyType.config; + property.editor = propertyType.editor; + property.view = propertyType.view; + property.dataTypeId = newDataType.id; + property.dataTypeIcon = newDataType.icon; + property.dataTypeName = newDataType.name; + + // set flag to update same data types + $scope.model.updateSameDataTypes = true; + + vm.focusOnMandatoryField = true; + + vm.editorSettingsOverlay.show = false; + vm.editorSettingsOverlay = null; + + }); + + }); + + }; + + vm.editorSettingsOverlay.close = function(oldModel) { + vm.editorSettingsOverlay.show = false; + vm.editorSettingsOverlay = null; + }; + + }); + + } + + function matchValidationType() { + + if($scope.model.property.validation.pattern !== null && $scope.model.property.validation.pattern !== "" && $scope.model.property.validation.pattern !== undefined) { + + var match = false; + + // find and show if a match from the list has been chosen + angular.forEach(vm.validationTypes, function(validationType, index){ + if($scope.model.property.validation.pattern === validationType.pattern) { + vm.selectedValidationType = vm.validationTypes[index]; + vm.showValidationPattern = true; + match = true; + } + }); + + // if there is no match - choose the custom validation option. + if(!match) { + angular.forEach(vm.validationTypes, function(validationType){ + if(validationType.key === "custom") { + vm.selectedValidationType = validationType; + vm.showValidationPattern = true; + } + }); + } + } + + } + + function changeValidationType(selectedValidationType) { + + if(selectedValidationType) { + $scope.model.property.validation.pattern = selectedValidationType.pattern; + vm.showValidationPattern = true; + + // set focus on textarea + if(selectedValidationType.key === "custom") { + vm.focusOnPatternField = true; + } + + } else { + $scope.model.property.validation.pattern = ""; + vm.showValidationPattern = false; + } + + } + + activate(); + + } + + angular.module("umbraco").controller("Umbraco.Overlay.PropertySettingsOverlay", PropertySettingsOverlay); + +})(); + + (function() { + "use strict"; + + function CopyOverlay($scope, localizationService, eventsService) { + + var vm = this; + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("general_copy"); + } + + vm.hideSearch = hideSearch; + vm.selectResult = selectResult; + vm.onSearchResults = onSearchResults; + + var dialogOptions = $scope.model; + var searchText = "Search..."; + var node = dialogOptions.currentNode; + + localizationService.localize("general_search").then(function (value) { + searchText = value + "..."; + }); + + $scope.model.relateToOriginal = true; + $scope.dialogTreeEventHandler = $({}); + + vm.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if (args.node.metaData.listViewNode) { + //check if list view 'search' node was selected + + vm.searchInfo.showSearch = true; + vm.searchInfo.searchFromId = args.node.metaData.listViewNode.id; + vm.searchInfo.searchFromName = args.node.metaData.listViewNode.name; + } + else { + //eventsService.emit("editors.content.copyController.select", args); + + if ($scope.model.target) { + //un-select if there's a current one selected + $scope.model.target.selected = false; + } + + $scope.model.target = args.node; + $scope.model.target.selected = true; + } + + } + + function nodeExpandedHandler(ev, args) { + if (angular.isArray(args.children)) { + + //iterate children + _.each(args.children, function (child) { + //check if any of the items are list views, if so we need to add a custom + // child: A node to activate the search + if (child.metaData.isContainer) { + child.hasChildren = true; + child.children = [ + { + level: child.level + 1, + hasChildren: false, + name: searchText, + metaData: { + listViewNode: child, + }, + cssClass: "icon umb-tree-icon sprTree icon-search", + cssClasses: ["not-published"] + } + ]; + } + }); + } + } + + function hideSearch() { + vm.searchInfo.showSearch = false; + vm.searchInfo.searchFromId = null; + vm.searchInfo.searchFromName = null; + vm.searchInfo.results = []; + } + + // method to select a search result + function selectResult(evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { event: evt, node: result }); + } + + //callback when there are search results + function onSearchResults(results) { + vm.searchInfo.results = results; + vm.searchInfo.showSearch = true; + } + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); + }); + + + } + + angular.module("umbraco").controller("Umbraco.Overlays.CopyOverlay", CopyOverlay); + +})(); + +(function() { + "use strict"; + + function EmbedOverlay($scope, $http, umbRequestHelper, localizationService) { + + var vm = this; + var origWidth = 500; + var origHeight = 300; + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("general_embed"); + } + + $scope.model.embed = { + url: "", + width: 360, + height: 240, + constrain: true, + preview: "", + success: false, + info: "", + supportsDimensions: "" + }; + + vm.showPreview = showPreview; + vm.changeSize = changeSize; + + function showPreview() { + + if ($scope.model.embed.url) { + $scope.model.embed.show = true; + $scope.model.embed.preview = "
"; + $scope.model.embed.info = ""; + $scope.model.embed.success = false; + + $http({ + method: 'GET', + url: umbRequestHelper.getApiUrl("embedApiBaseUrl", "GetEmbed"), + params: { + url: $scope.model.embed.url, + width: $scope.model.embed.width, + height: $scope.model.embed.height + } + }) + .success(function(data) { + + $scope.model.embed.preview = ""; + + switch (data.Status) { + case 0: + //not supported + $scope.model.embed.info = "Not supported"; + break; + case 1: + //error + $scope.model.embed.info = "Could not embed media - please ensure the URL is valid"; + break; + case 2: + $scope.model.embed.preview = data.Markup; + $scope.model.embed.supportsDimensions = data.SupportsDimensions; + $scope.model.embed.success = true; + break; + } + }) + .error(function() { + $scope.model.embed.supportsDimensions = false; + $scope.model.embed.preview = ""; + $scope.model.embed.info = "Could not embed media - please ensure the URL is valid"; + }); + } else { + $scope.model.embed.supportsDimensions = false; + $scope.model.embed.preview = ""; + $scope.model.embed.info = "Please enter a URL"; + } + } + + function changeSize(type) { + + var width, height; + + if ($scope.model.embed.constrain) { + width = parseInt($scope.model.embed.width, 10); + height = parseInt($scope.model.embed.height, 10); + if (type == 'width') { + origHeight = Math.round((width / origWidth) * height); + $scope.model.embed.height = origHeight; + } else { + origWidth = Math.round((height / origHeight) * width); + $scope.model.embed.width = origWidth; + } + } + if ($scope.model.embed.url !== "") { + showPreview(); + } + + } + + } + + angular.module("umbraco").controller("Umbraco.Overlays.EmbedOverlay", EmbedOverlay); + +})(); + +angular.module("umbraco") + .controller("Umbraco.Overlays.HelpController", function ($scope, $location, $routeParams, helpService, userService, localizationService) { + $scope.section = $routeParams.section; + $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; + $scope.model.subtitle = "Umbraco version" + " " + $scope.version; + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("general_help"); + } + + if(!$scope.section){ + $scope.section = "content"; + } + + $scope.sectionName = $scope.section; + + var rq = {}; + rq.section = $scope.section; + + //translate section name + localizationService.localize("sections_" + rq.section).then(function (value) { + $scope.sectionName = value; + }); + + userService.getCurrentUser().then(function(user){ + + rq.usertype = user.userType; + rq.lang = user.locale; + + if($routeParams.url){ + rq.path = decodeURIComponent($routeParams.url); + + if(rq.path.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath) === 0){ + rq.path = rq.path.substring(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath.length); + } + + if(rq.path.indexOf(".aspx") > 0){ + rq.path = rq.path.substring(0, rq.path.indexOf(".aspx")); + } + + }else{ + rq.path = rq.section + "/" + $routeParams.tree + "/" + $routeParams.method; + } + + helpService.findHelp(rq).then(function(topics){ + $scope.topics = topics; + }); + + helpService.findVideos(rq).then(function(videos){ + $scope.videos = videos; + }); + + }); + + + }); + +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.PropertyController + * @function + * + * @description + * The controller for the content type editor property dialog + */ +function IconPickerOverlay($scope, iconHelper, localizationService) { + + $scope.loading = true; + $scope.model.hideSubmitButton = true; + + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectIcon"); + } + + iconHelper.getIcons().then(function(icons) { + $scope.icons = icons; + $scope.loading = false; + }); + + $scope.selectIcon = function(icon, color) { + $scope.model.icon = icon; + $scope.model.color = color; + $scope.submitForm($scope.model); + }; + +} + +angular.module("umbraco").controller("Umbraco.Overlays.IconPickerOverlay", IconPickerOverlay); + +function ItemPickerOverlay($scope, localizationService) { + + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectItem"); + } + + $scope.model.hideSubmitButton = true; + + $scope.selectItem = function(item) { + $scope.model.selectedItem = item; + $scope.submitForm($scope.model); + }; + +} + +angular.module("umbraco").controller("Umbraco.Overlays.ItemPickerOverlay", ItemPickerOverlay); + +//used for the media picker dialog +angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", + function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService) { + var dialogOptions = $scope.model; + + var searchText = "Search..."; + localizationService.localize("general_search").then(function (value) { + searchText = value + "..."; + }); + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectLink"); + } + + $scope.dialogTreeEventHandler = $({}); + $scope.model.target = {}; + $scope.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + + if (dialogOptions.currentTarget) { + $scope.model.target = dialogOptions.currentTarget; + + //if we have a node ID, we fetch the current node to build the form data + if ($scope.model.target.id) { + + if (!$scope.model.target.path) { + entityResource.getPath($scope.model.target.id, "Document").then(function (path) { + $scope.model.target.path = path; + //now sync the tree to this path + $scope.dialogTreeEventHandler.syncTree({ path: $scope.model.target.path, tree: "content" }); + }); + } + + contentResource.getNiceUrl($scope.model.target.id).then(function (url) { + $scope.model.target.url = url; + }); + } + } + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if (args.node.metaData.listViewNode) { + //check if list view 'search' node was selected + + $scope.searchInfo.showSearch = true; + $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; + $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; + } + else { + eventsService.emit("dialogs.linkPicker.select", args); + + if ($scope.currentNode) { + //un-select if there's a current one selected + $scope.currentNode.selected = false; + } + + $scope.currentNode = args.node; + $scope.currentNode.selected = true; + $scope.model.target.id = args.node.id; + $scope.model.target.name = args.node.name; + + if (args.node.id < 0) { + $scope.model.target.url = "/"; + } + else { + contentResource.getNiceUrl(args.node.id).then(function (url) { + $scope.model.target.url = url; + }); + } + + if (!angular.isUndefined($scope.model.target.isMedia)) { + delete $scope.model.target.isMedia; + } + } + } + + function nodeExpandedHandler(ev, args) { + if (angular.isArray(args.children)) { + + //iterate children + _.each(args.children, function (child) { + //check if any of the items are list views, if so we need to add a custom + // child: A node to activate the search + if (child.metaData.isContainer) { + child.hasChildren = true; + child.children = [ + { + level: child.level + 1, + hasChildren: false, + name: searchText, + metaData: { + listViewNode: child, + }, + cssClass: "icon umb-tree-icon sprTree icon-search", + cssClasses: ["not-published"] + } + ]; + } + }); + } + } + + $scope.switchToMediaPicker = function () { + userService.getCurrentUser().then(function (userData) { + $scope.mediaPickerOverlay = { + view: "mediapicker", + startNodeId: userData.startMediaId, + show: true, + submit: function(model) { + var media = model.selectedImages[0]; + + $scope.model.target.id = media.id; + $scope.model.target.isMedia = true; + $scope.model.target.name = media.name; + $scope.model.target.url = mediaHelper.resolveFile(media); + + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + } + }; + }); + }; + + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = null; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + } + + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, {event: evt, node: result}); + }; + + //callback when there are search results + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); + }); + }); + +function MacroPickerController($scope, entityResource, macroResource, umbPropEditorHelper, macroService, formHelper, localizationService) { + + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectMacro"); + } + + $scope.macros = []; + $scope.model.selectedMacro = null; + $scope.wizardStep = "macroSelect"; + $scope.model.macroParams = []; + $scope.noMacroParams = false; + + $scope.changeMacro = function() { + if ($scope.wizardStep === "macroSelect") { + editParams(); + } else { + submitForm(); + } + }; + + /** changes the view to edit the params of the selected macro */ + function editParams() { + //get the macro params if there are any + macroResource.getMacroParameters($scope.model.selectedMacro.id) + .then(function (data) { + + //go to next page if there are params otherwise we can just exit + if (!angular.isArray(data) || data.length === 0) { + + $scope.noMacroParams = true; + + } else { + $scope.wizardStep = "paramSelect"; + $scope.model.macroParams = data; + + //fill in the data if we are editing this macro + if ($scope.model.dialogData && $scope.model.dialogData.macroData && $scope.model.dialogData.macroData.macroParamsDictionary) { + _.each($scope.model.dialogData.macroData.macroParamsDictionary, function (val, key) { + var prop = _.find($scope.model.macroParams, function (item) { + return item.alias == key; + }); + if (prop) { + + if (_.isString(val)) { + //we need to unescape values as they have most likely been escaped while inserted + val = _.unescape(val); + + //detect if it is a json string + if (val.detectIsJson()) { + try { + //Parse it to json + prop.value = angular.fromJson(val); + } + catch (e) { + // not json + prop.value = val; + } + } + else { + prop.value = val; + } + } + else { + prop.value = val; + } + } + }); + + } + } + + }); + } + + //here we check to see if we've been passed a selected macro and if so we'll set the + //editor to start with parameter editing + if ($scope.model.dialogData && $scope.model.dialogData.macroData) { + $scope.wizardStep = "paramSelect"; + } + + //get the macro list - pass in a filter if it is only for rte + entityResource.getAll("Macro", ($scope.model.dialogData && $scope.model.dialogData.richTextEditor && $scope.model.dialogData.richTextEditor === true) ? "UseInEditor=true" : null) + .then(function (data) { + + //if 'allowedMacros' is specified, we need to filter + if (angular.isArray($scope.model.dialogData.allowedMacros) && $scope.model.dialogData.allowedMacros.length > 0) { + $scope.macros = _.filter(data, function(d) { + return _.contains($scope.model.dialogData.allowedMacros, d.alias); + }); + } + else { + $scope.macros = data; + } + + + //check if there's a pre-selected macro and if it exists + if ($scope.model.dialogData && $scope.model.dialogData.macroData && $scope.model.dialogData.macroData.macroAlias) { + var found = _.find(data, function (item) { + return item.alias === $scope.model.dialogData.macroData.macroAlias; + }); + if (found) { + //select the macro and go to next screen + $scope.model.selectedMacro = found; + editParams(); + return; + } + } + //we don't have a pre-selected macro so ensure the correct step is set + $scope.wizardStep = "macroSelect"; + }); + + +} + +angular.module("umbraco").controller("Umbraco.Overlays.MacroPickerController", MacroPickerController); //used for the media picker dialog angular.module("umbraco") @@ -3635,16 +3635,16 @@ angular.module("umbraco") }; }); -angular.module("umbraco").controller("Umbraco.Overlays.MediaTypePickerController", - function ($scope) { - - $scope.select = function(mediatype){ - $scope.model.selectedType = mediatype; - $scope.model.submit($scope.model); - $scope.model.show = false; - } - - }); +angular.module("umbraco").controller("Umbraco.Overlays.MediaTypePickerController", + function ($scope) { + + $scope.select = function(mediatype){ + $scope.model.selectedType = mediatype; + $scope.model.submit($scope.model); + $scope.model.show = false; + } + + }); //used for the member picker dialog angular.module("umbraco").controller("Umbraco.Overlays.MemberGroupPickerController", @@ -3711,124 +3711,124 @@ angular.module("umbraco").controller("Umbraco.Overlays.MemberGroupPickerControll }); - (function() { - "use strict"; - - function MoveOverlay($scope, localizationService, eventsService) { - - var vm = this; - - vm.hideSearch = hideSearch; - vm.selectResult = selectResult; - vm.onSearchResults = onSearchResults; - - var dialogOptions = $scope.model; - var searchText = "Search..."; - var node = dialogOptions.currentNode; - - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("actions_move"); - } - - $scope.model.relateToOriginal = true; - $scope.dialogTreeEventHandler = $({}); - - vm.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - }; - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if (args.node.metaData.listViewNode) { - //check if list view 'search' node was selected - - vm.searchInfo.showSearch = true; - vm.searchInfo.searchFromId = args.node.metaData.listViewNode.id; - vm.searchInfo.searchFromName = args.node.metaData.listViewNode.name; - } - else { - //eventsService.emit("editors.content.copyController.select", args); - - if ($scope.model.target) { - //un-select if there's a current one selected - $scope.model.target.selected = false; - } - - $scope.model.target = args.node; - $scope.model.target.selected = true; - } - - } - - function nodeExpandedHandler(ev, args) { - if (angular.isArray(args.children)) { - - //iterate children - _.each(args.children, function (child) { - //check if any of the items are list views, if so we need to add a custom - // child: A node to activate the search - if (child.metaData.isContainer) { - child.hasChildren = true; - child.children = [ - { - level: child.level + 1, - hasChildren: false, - name: searchText, - metaData: { - listViewNode: child, - }, - cssClass: "icon umb-tree-icon sprTree icon-search", - cssClasses: ["not-published"] - } - ]; - } - }); - } - } - - function hideSearch() { - vm.searchInfo.showSearch = false; - vm.searchInfo.searchFromId = null; - vm.searchInfo.searchFromName = null; - vm.searchInfo.results = []; - } - - // method to select a search result - function selectResult(evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, { event: evt, node: result }); - } - - //callback when there are search results - function onSearchResults(results) { - vm.searchInfo.results = results; - vm.searchInfo.showSearch = true; - } - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - }); - - - } - - angular.module("umbraco").controller("Umbraco.Overlays.MoveOverlay", MoveOverlay); - -})(); + (function() { + "use strict"; + + function MoveOverlay($scope, localizationService, eventsService) { + + var vm = this; + + vm.hideSearch = hideSearch; + vm.selectResult = selectResult; + vm.onSearchResults = onSearchResults; + + var dialogOptions = $scope.model; + var searchText = "Search..."; + var node = dialogOptions.currentNode; + + localizationService.localize("general_search").then(function (value) { + searchText = value + "..."; + }); + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("actions_move"); + } + + $scope.model.relateToOriginal = true; + $scope.dialogTreeEventHandler = $({}); + + vm.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if (args.node.metaData.listViewNode) { + //check if list view 'search' node was selected + + vm.searchInfo.showSearch = true; + vm.searchInfo.searchFromId = args.node.metaData.listViewNode.id; + vm.searchInfo.searchFromName = args.node.metaData.listViewNode.name; + } + else { + //eventsService.emit("editors.content.copyController.select", args); + + if ($scope.model.target) { + //un-select if there's a current one selected + $scope.model.target.selected = false; + } + + $scope.model.target = args.node; + $scope.model.target.selected = true; + } + + } + + function nodeExpandedHandler(ev, args) { + if (angular.isArray(args.children)) { + + //iterate children + _.each(args.children, function (child) { + //check if any of the items are list views, if so we need to add a custom + // child: A node to activate the search + if (child.metaData.isContainer) { + child.hasChildren = true; + child.children = [ + { + level: child.level + 1, + hasChildren: false, + name: searchText, + metaData: { + listViewNode: child, + }, + cssClass: "icon umb-tree-icon sprTree icon-search", + cssClasses: ["not-published"] + } + ]; + } + }); + } + } + + function hideSearch() { + vm.searchInfo.showSearch = false; + vm.searchInfo.searchFromId = null; + vm.searchInfo.searchFromName = null; + vm.searchInfo.results = []; + } + + // method to select a search result + function selectResult(evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { event: evt, node: result }); + } + + //callback when there are search results + function onSearchResults(results) { + vm.searchInfo.results = results; + vm.searchInfo.showSearch = true; + } + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); + }); + + + } + + angular.module("umbraco").controller("Umbraco.Overlays.MoveOverlay", MoveOverlay); + +})(); //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", @@ -4329,182 +4329,182 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", }); }); -angular.module("umbraco") - .controller("Umbraco.Overlays.UserController", function ($scope, $location, $timeout, userService, historyService, eventsService, externalLoginInfo, authResource, currentUserResource, formHelper, localizationService) { - - $scope.history = historyService.getCurrent(); - $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; - $scope.showPasswordFields = false; - $scope.changePasswordButtonState = "init"; - $scope.model.subtitle = "Umbraco version" + " " + $scope.version; - - if(!$scope.model.title) { - $scope.model.title = localizationService.localize("general_user"); - } - - $scope.externalLoginProviders = externalLoginInfo.providers; - $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; - var evts = []; - evts.push(eventsService.on("historyService.add", function (e, args) { - $scope.history = args.all; - })); - evts.push(eventsService.on("historyService.remove", function (e, args) { - $scope.history = args.all; - })); - evts.push(eventsService.on("historyService.removeAll", function (e, args) { - $scope.history = []; - })); - - $scope.logout = function () { - - //Add event listener for when there are pending changes on an editor which means our route was not successful - var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) { - //one time listener, remove the event - pendingChangeEvent(); - $scope.model.close(); - }); - - - //perform the path change, if it is successful then the promise will resolve otherwise it will fail - $scope.model.close(); - $location.path("/logout"); - }; - - $scope.gotoHistory = function (link) { - $location.path(link); - $scope.model.close(); - }; - - //Manually update the remaining timeout seconds - function updateTimeout() { - $timeout(function () { - if ($scope.remainingAuthSeconds > 0) { - $scope.remainingAuthSeconds--; - $scope.$digest(); - //recurse - updateTimeout(); - } - - }, 1000, false); // 1 second, do NOT execute a global digest - } - - function updateUserInfo() { - //get the user - userService.getCurrentUser().then(function (user) { - $scope.user = user; - if ($scope.user) { - $scope.model.title = user.name; - $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; - $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; - //set the timer - updateTimeout(); - - authResource.getCurrentUserLinkedLogins().then(function(logins) { - //reset all to be un-linked - for (var provider in $scope.externalLoginProviders) { - $scope.externalLoginProviders[provider].linkedProviderKey = undefined; - } - - //set the linked logins - for (var login in logins) { - var found = _.find($scope.externalLoginProviders, function (i) { - return i.authType == login; - }); - if (found) { - found.linkedProviderKey = logins[login]; - } - } - }); - } - }); - } - - $scope.unlink = function (e, loginProvider, providerKey) { - var result = confirm("Are you sure you want to unlink this account?"); - if (!result) { - e.preventDefault(); - return; - } - - authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { - updateUserInfo(); - }); - } - - updateUserInfo(); - - //remove all event handlers - $scope.$on('$destroy', function () { - for (var e = 0; e < evts.length; e++) { - evts[e](); - } - - }); - - /* ---------- UPDATE PASSWORD ---------- */ - - //create the initial model for change password property editor - $scope.changePasswordModel = { - alias: "_umb_password", - view: "changepassword", - config: {}, - value: {} - }; - - //go get the config for the membership provider and add it to the model - currentUserResource.getMembershipProviderConfig().then(function(data) { - $scope.changePasswordModel.config = data; - //ensure the hasPassword config option is set to true (the user of course has a password already assigned) - //this will ensure the oldPassword is shown so they can change it - // disable reset password functionality beacuse it does not make sense inside the backoffice - $scope.changePasswordModel.config.hasPassword = true; - $scope.changePasswordModel.config.disableToggle = true; - $scope.changePasswordModel.config.enableReset = false; - }); - - $scope.changePassword = function() { - - if (formHelper.submitForm({ scope: $scope })) { - - $scope.changePasswordButtonState = "busy"; - - currentUserResource.changePassword($scope.changePasswordModel.value).then(function(data) { - - //if the password has been reset, then update our model - if (data.value) { - $scope.changePasswordModel.value.generatedPassword = data.value; - } - - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); - - $scope.changePasswordButtonState = "success"; - $timeout(function() { - $scope.togglePasswordFields(); - }, 2000); - - }, function (err) { - - formHelper.handleError(err); - - $scope.changePasswordButtonState = "error"; - - }); - - } - - }; - - $scope.togglePasswordFields = function() { - clearPasswordFields(); - $scope.showPasswordFields = !$scope.showPasswordFields; - } - - function clearPasswordFields() { - $scope.changePasswordModel.value.newPassword = ""; - $scope.changePasswordModel.confirm = ""; - } - - }); +angular.module("umbraco") + .controller("Umbraco.Overlays.UserController", function ($scope, $location, $timeout, userService, historyService, eventsService, externalLoginInfo, authResource, currentUserResource, formHelper, localizationService) { + + $scope.history = historyService.getCurrent(); + $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; + $scope.showPasswordFields = false; + $scope.changePasswordButtonState = "init"; + $scope.model.subtitle = "Umbraco version" + " " + $scope.version; + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("general_user"); + } + + $scope.externalLoginProviders = externalLoginInfo.providers; + $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; + var evts = []; + evts.push(eventsService.on("historyService.add", function (e, args) { + $scope.history = args.all; + })); + evts.push(eventsService.on("historyService.remove", function (e, args) { + $scope.history = args.all; + })); + evts.push(eventsService.on("historyService.removeAll", function (e, args) { + $scope.history = []; + })); + + $scope.logout = function () { + + //Add event listener for when there are pending changes on an editor which means our route was not successful + var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) { + //one time listener, remove the event + pendingChangeEvent(); + $scope.model.close(); + }); + + + //perform the path change, if it is successful then the promise will resolve otherwise it will fail + $scope.model.close(); + $location.path("/logout"); + }; + + $scope.gotoHistory = function (link) { + $location.path(link); + $scope.model.close(); + }; + + //Manually update the remaining timeout seconds + function updateTimeout() { + $timeout(function () { + if ($scope.remainingAuthSeconds > 0) { + $scope.remainingAuthSeconds--; + $scope.$digest(); + //recurse + updateTimeout(); + } + + }, 1000, false); // 1 second, do NOT execute a global digest + } + + function updateUserInfo() { + //get the user + userService.getCurrentUser().then(function (user) { + $scope.user = user; + if ($scope.user) { + $scope.model.title = user.name; + $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; + $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; + //set the timer + updateTimeout(); + + authResource.getCurrentUserLinkedLogins().then(function(logins) { + //reset all to be un-linked + for (var provider in $scope.externalLoginProviders) { + $scope.externalLoginProviders[provider].linkedProviderKey = undefined; + } + + //set the linked logins + for (var login in logins) { + var found = _.find($scope.externalLoginProviders, function (i) { + return i.authType == login; + }); + if (found) { + found.linkedProviderKey = logins[login]; + } + } + }); + } + }); + } + + $scope.unlink = function (e, loginProvider, providerKey) { + var result = confirm("Are you sure you want to unlink this account?"); + if (!result) { + e.preventDefault(); + return; + } + + authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { + updateUserInfo(); + }); + } + + updateUserInfo(); + + //remove all event handlers + $scope.$on('$destroy', function () { + for (var e = 0; e < evts.length; e++) { + evts[e](); + } + + }); + + /* ---------- UPDATE PASSWORD ---------- */ + + //create the initial model for change password property editor + $scope.changePasswordModel = { + alias: "_umb_password", + view: "changepassword", + config: {}, + value: {} + }; + + //go get the config for the membership provider and add it to the model + currentUserResource.getMembershipProviderConfig().then(function(data) { + $scope.changePasswordModel.config = data; + //ensure the hasPassword config option is set to true (the user of course has a password already assigned) + //this will ensure the oldPassword is shown so they can change it + // disable reset password functionality beacuse it does not make sense inside the backoffice + $scope.changePasswordModel.config.hasPassword = true; + $scope.changePasswordModel.config.disableToggle = true; + $scope.changePasswordModel.config.enableReset = false; + }); + + $scope.changePassword = function() { + + if (formHelper.submitForm({ scope: $scope })) { + + $scope.changePasswordButtonState = "busy"; + + currentUserResource.changePassword($scope.changePasswordModel.value).then(function(data) { + + //if the password has been reset, then update our model + if (data.value) { + $scope.changePasswordModel.value.generatedPassword = data.value; + } + + formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + + $scope.changePasswordButtonState = "success"; + $timeout(function() { + $scope.togglePasswordFields(); + }, 2000); + + }, function (err) { + + formHelper.handleError(err); + + $scope.changePasswordButtonState = "error"; + + }); + + } + + }; + + $scope.togglePasswordFields = function() { + clearPasswordFields(); + $scope.showPasswordFields = !$scope.showPasswordFields; + } + + function clearPasswordFields() { + $scope.changePasswordModel.value.newPassword = ""; + $scope.changePasswordModel.confirm = ""; + } + + }); angular.module("umbraco") .controller("Umbraco.Overlays.YsodController", function ($scope, legacyResource, treeService, navigationService, localizationService) { @@ -4532,145 +4532,145 @@ angular.module("umbraco") }); -angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", - function ($scope, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { - - var dialogOptions = $scope.dialogOptions; - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - $scope.relateToOriginal = true; - $scope.recursive = true; - $scope.dialogTreeEventHandler = $({}); - $scope.busy = false; - $scope.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - } - - var node = dialogOptions.currentNode; - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if (args.node.metaData.listViewNode) { - //check if list view 'search' node was selected - - $scope.searchInfo.showSearch = true; - $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; - $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; - } - else { - eventsService.emit("editors.content.copyController.select", args); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; - } - - $scope.target = args.node; - $scope.target.selected = true; - } - - } - - function nodeExpandedHandler(ev, args) { - if (angular.isArray(args.children)) { - - //iterate children - _.each(args.children, function (child) { - //check if any of the items are list views, if so we need to add a custom - // child: A node to activate the search - if (child.metaData.isContainer) { - child.hasChildren = true; - child.children = [ - { - level: child.level + 1, - hasChildren: false, - name: searchText, - metaData: { - listViewNode: child, - }, - cssClass: "icon umb-tree-icon sprTree icon-search", - cssClasses: ["not-published"] - } - ]; - } - }); - } - } - - $scope.hideSearch = function () { - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = null; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } - - // method to select a search result - $scope.selectResult = function (evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, { event: evt, node: result }); - }; - - //callback when there are search results - $scope.onSearchResults = function (results) { - $scope.searchInfo.results = results; - $scope.searchInfo.showSearch = true; - }; - - $scope.copy = function () { - - $scope.busy = true; - $scope.error = false; - - contentResource.copy({ parentId: $scope.target.id, id: node.id, relateToOriginal: $scope.relateToOriginal, recursive: $scope.recursive }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - $scope.busy = false; - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the copied content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was copied!!) - - navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "content", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - }, function (err) { - $scope.success = false; - $scope.error = err; - $scope.busy = false; - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - }); - }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - }); - }); +angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", + function ($scope, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { + + var dialogOptions = $scope.dialogOptions; + var searchText = "Search..."; + localizationService.localize("general_search").then(function (value) { + searchText = value + "..."; + }); + + $scope.relateToOriginal = true; + $scope.recursive = true; + $scope.dialogTreeEventHandler = $({}); + $scope.busy = false; + $scope.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + } + + var node = dialogOptions.currentNode; + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if (args.node.metaData.listViewNode) { + //check if list view 'search' node was selected + + $scope.searchInfo.showSearch = true; + $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; + $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; + } + else { + eventsService.emit("editors.content.copyController.select", args); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + } + + function nodeExpandedHandler(ev, args) { + if (angular.isArray(args.children)) { + + //iterate children + _.each(args.children, function (child) { + //check if any of the items are list views, if so we need to add a custom + // child: A node to activate the search + if (child.metaData.isContainer) { + child.hasChildren = true; + child.children = [ + { + level: child.level + 1, + hasChildren: false, + name: searchText, + metaData: { + listViewNode: child, + }, + cssClass: "icon umb-tree-icon sprTree icon-search", + cssClasses: ["not-published"] + } + ]; + } + }); + } + } + + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = null; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + } + + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { event: evt, node: result }); + }; + + //callback when there are search results + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; + }; + + $scope.copy = function () { + + $scope.busy = true; + $scope.error = false; + + contentResource.copy({ parentId: $scope.target.id, id: node.id, relateToOriginal: $scope.relateToOriginal, recursive: $scope.recursive }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the copied content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was copied!!) + + navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "content", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); + }); + }); /** * @ngdoc controller @@ -4761,238 +4761,238 @@ function ContentDeleteController($scope, contentResource, treeService, navigatio angular.module("umbraco").controller("Umbraco.Editors.Content.DeleteController", ContentDeleteController); -/** - * @ngdoc controller - * @name Umbraco.Editors.Content.EditController - * @function - * - * @description - * The controller for the content editor - */ -function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http) { - - //setup scope vars - $scope.defaultButton = null; - $scope.subButtons = []; - - $scope.page = {}; - $scope.page.loading = false; - $scope.page.menu = {}; - $scope.page.menu.currentNode = null; - $scope.page.menu.currentSection = appState.getSectionState("currentSection"); - $scope.page.listViewPath = null; - $scope.page.isNew = $routeParams.create; - $scope.page.buttonGroupState = "init"; - - function init(content) { - - var buttons = contentEditingHelper.configureContentEditorButtons({ - create: $routeParams.create, - content: content, - methods: { - saveAndPublish: $scope.saveAndPublish, - sendToPublish: $scope.sendToPublish, - save: $scope.save, - unPublish: $scope.unPublish - } - }); - $scope.defaultButton = buttons.defaultButton; - $scope.subButtons = buttons.subButtons; - - editorState.set($scope.content); - - //We fetch all ancestors of the node to generate the footer breadcrumb navigation - if (!$routeParams.create) { - if (content.parentId && content.parentId != -1) { - entityResource.getAncestors(content.id, "document") - .then(function (anc) { - $scope.ancestors = anc; - }); - } - } - } - - /** Syncs the content item to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(content, path, initialLoad) { - - if (!$scope.content.isChildOfListView) { - navigationService.syncTree({ tree: "content", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - $scope.page.menu.currentNode = syncArgs.node; - }); - } - else if (initialLoad === true) { - - //it's a child item, just sync the ui node to the parent - navigationService.syncTree({ tree: "content", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); - - //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node - // from the server so that we can load in the actions menu. - umbRequestHelper.resourcePromise( - $http.get(content.treeNodeUrl), - 'Failed to retrieve data for child node ' + content.id).then(function (node) { - $scope.page.menu.currentNode = node; - }); - } - } - - // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish - function performSave(args) { - var deferred = $q.defer(); - - $scope.page.buttonGroupState = "busy"; - - contentEditingHelper.contentEditorPerformSave({ - statusMessage: args.statusMessage, - saveMethod: args.saveMethod, - scope: $scope, - content: $scope.content, - action: args.action - }).then(function (data) { - //success - init($scope.content); - syncTreeNode($scope.content, data.path); - - $scope.page.buttonGroupState = "success"; - - deferred.resolve(data); - }, function (err) { - //error - if (err) { - editorState.set($scope.content); - } - - $scope.page.buttonGroupState = "error"; - - deferred.reject(err); - }); - - return deferred.promise; - } - - function resetLastListPageNumber(content) { - // We're using rootScope to store the page number for list views, so if returning to the list - // we can restore the page. If we've moved on to edit a piece of content that's not the list or it's children - // we should remove this so as not to confuse if navigating to a different list - if (!content.isChildOfListView && !content.isContainer) { - $rootScope.lastListViewPageViewed = null; - } - } - - if ($routeParams.create) { - - $scope.page.loading = true; - - //we are creating so get an empty content item - contentResource.getScaffold($routeParams.id, $routeParams.doctype) - .then(function (data) { - - $scope.content = data; - - init($scope.content); - - resetLastListPageNumber($scope.content); - - $scope.page.loading = false; - - }); - } - else { - - $scope.page.loading = true; - - //we are editing so get the content item from the server - contentResource.getById($routeParams.id) - .then(function (data) { - - $scope.content = data; - - if (data.isChildOfListView && data.trashed === false) { - $scope.page.listViewPath = ($routeParams.page) - ? "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page - : "/content/content/edit/" + data.parentId; - } - - init($scope.content); - - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.executeAndClearAllSubscriptions(); - - syncTreeNode($scope.content, data.path, true); - - resetLastListPageNumber($scope.content); - - $scope.page.loading = false; - - }); - } - - - $scope.unPublish = function () { - - if (formHelper.submitForm({ scope: $scope, statusMessage: "Unpublishing...", skipValidation: true })) { - - $scope.page.buttonGroupState = "busy"; - - contentResource.unPublish($scope.content.id) - .then(function (data) { - - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); - - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) - }); - - init($scope.content); - - syncTreeNode($scope.content, data.path); - - $scope.page.buttonGroupState = "success"; - - }); - } - - }; - - $scope.sendToPublish = function () { - return performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending...", action: "sendToPublish" }); - }; - - $scope.saveAndPublish = function () { - return performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing...", action: "publish" }); - }; - - $scope.save = function () { - return performSave({ saveMethod: contentResource.save, statusMessage: "Saving...", action: "save" }); - }; - - $scope.preview = function (content) { - - - if (!$scope.busy) { - - // Chromes popup blocker will kick in if a window is opened - // outwith the initial scoped request. This trick will fix that. - // - var previewWindow = $window.open('preview/?id=' + content.id, 'umbpreview'); - $scope.save().then(function (data) { - // Build the correct path so both /#/ and #/ work. - var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?id=' + data.id; - previewWindow.location.href = redirect; - }); - - - } - - }; - -} - -angular.module("umbraco").controller("Umbraco.Editors.Content.EditController", ContentEditController); +/** + * @ngdoc controller + * @name Umbraco.Editors.Content.EditController + * @function + * + * @description + * The controller for the content editor + */ +function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http) { + + //setup scope vars + $scope.defaultButton = null; + $scope.subButtons = []; + + $scope.page = {}; + $scope.page.loading = false; + $scope.page.menu = {}; + $scope.page.menu.currentNode = null; + $scope.page.menu.currentSection = appState.getSectionState("currentSection"); + $scope.page.listViewPath = null; + $scope.page.isNew = $routeParams.create; + $scope.page.buttonGroupState = "init"; + + function init(content) { + + var buttons = contentEditingHelper.configureContentEditorButtons({ + create: $routeParams.create, + content: content, + methods: { + saveAndPublish: $scope.saveAndPublish, + sendToPublish: $scope.sendToPublish, + save: $scope.save, + unPublish: $scope.unPublish + } + }); + $scope.defaultButton = buttons.defaultButton; + $scope.subButtons = buttons.subButtons; + + editorState.set($scope.content); + + //We fetch all ancestors of the node to generate the footer breadcrumb navigation + if (!$routeParams.create) { + if (content.parentId && content.parentId != -1) { + entityResource.getAncestors(content.id, "document") + .then(function (anc) { + $scope.ancestors = anc; + }); + } + } + } + + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(content, path, initialLoad) { + + if (!$scope.content.isChildOfListView) { + navigationService.syncTree({ tree: "content", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { + $scope.page.menu.currentNode = syncArgs.node; + }); + } + else if (initialLoad === true) { + + //it's a child item, just sync the ui node to the parent + navigationService.syncTree({ tree: "content", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); + + //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node + // from the server so that we can load in the actions menu. + umbRequestHelper.resourcePromise( + $http.get(content.treeNodeUrl), + 'Failed to retrieve data for child node ' + content.id).then(function (node) { + $scope.page.menu.currentNode = node; + }); + } + } + + // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish + function performSave(args) { + var deferred = $q.defer(); + + $scope.page.buttonGroupState = "busy"; + + contentEditingHelper.contentEditorPerformSave({ + statusMessage: args.statusMessage, + saveMethod: args.saveMethod, + scope: $scope, + content: $scope.content, + action: args.action + }).then(function (data) { + //success + init($scope.content); + syncTreeNode($scope.content, data.path); + + $scope.page.buttonGroupState = "success"; + + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } + + $scope.page.buttonGroupState = "error"; + + deferred.reject(err); + }); + + return deferred.promise; + } + + function resetLastListPageNumber(content) { + // We're using rootScope to store the page number for list views, so if returning to the list + // we can restore the page. If we've moved on to edit a piece of content that's not the list or it's children + // we should remove this so as not to confuse if navigating to a different list + if (!content.isChildOfListView && !content.isContainer) { + $rootScope.lastListViewPageViewed = null; + } + } + + if ($routeParams.create) { + + $scope.page.loading = true; + + //we are creating so get an empty content item + contentResource.getScaffold($routeParams.id, $routeParams.doctype) + .then(function (data) { + + $scope.content = data; + + init($scope.content); + + resetLastListPageNumber($scope.content); + + $scope.page.loading = false; + + }); + } + else { + + $scope.page.loading = true; + + //we are editing so get the content item from the server + contentResource.getById($routeParams.id) + .then(function (data) { + + $scope.content = data; + + if (data.isChildOfListView && data.trashed === false) { + $scope.page.listViewPath = ($routeParams.page) + ? "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page + : "/content/content/edit/" + data.parentId; + } + + init($scope.content); + + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); + + syncTreeNode($scope.content, data.path, true); + + resetLastListPageNumber($scope.content); + + $scope.page.loading = false; + + }); + } + + + $scope.unPublish = function () { + + if (formHelper.submitForm({ scope: $scope, statusMessage: "Unpublishing...", skipValidation: true })) { + + $scope.page.buttonGroupState = "busy"; + + contentResource.unPublish($scope.content.id) + .then(function (data) { + + formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data, + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) + }); + + init($scope.content); + + syncTreeNode($scope.content, data.path); + + $scope.page.buttonGroupState = "success"; + + }); + } + + }; + + $scope.sendToPublish = function () { + return performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending...", action: "sendToPublish" }); + }; + + $scope.saveAndPublish = function () { + return performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing...", action: "publish" }); + }; + + $scope.save = function () { + return performSave({ saveMethod: contentResource.save, statusMessage: "Saving...", action: "save" }); + }; + + $scope.preview = function (content) { + + + if (!$scope.busy) { + + // Chromes popup blocker will kick in if a window is opened + // outwith the initial scoped request. This trick will fix that. + // + var previewWindow = $window.open('preview/?id=' + content.id, 'umbpreview'); + $scope.save().then(function (data) { + // Build the correct path so both /#/ and #/ work. + var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?id=' + data.id; + previewWindow.location.href = redirect; + }); + + + } + + }; + +} + +angular.module("umbraco").controller("Umbraco.Editors.Content.EditController", ContentEditController); /** * @ngdoc controller @@ -5040,270 +5040,270 @@ function ContentEmptyRecycleBinController($scope, contentResource, treeService, angular.module("umbraco").controller("Umbraco.Editors.Content.EmptyRecycleBinController", ContentEmptyRecycleBinController); -angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", - function ($scope, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { - - var dialogOptions = $scope.dialogOptions; - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - $scope.dialogTreeEventHandler = $({}); - $scope.busy = false; - $scope.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - } - - var node = dialogOptions.currentNode; - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if (args.node.metaData.listViewNode) { - //check if list view 'search' node was selected - - $scope.searchInfo.showSearch = true; - $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; - $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; - } - else { - eventsService.emit("editors.content.moveController.select", args); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; - } - - $scope.target = args.node; - $scope.target.selected = true; - } - } - - function nodeExpandedHandler(ev, args) { - if (angular.isArray(args.children)) { - - //iterate children - _.each(args.children, function (child) { - //check if any of the items are list views, if so we need to add a custom - // child: A node to activate the search - if (child.metaData.isContainer) { - child.hasChildren = true; - child.children = [ - { - level: child.level + 1, - hasChildren: false, - name: searchText, - metaData: { - listViewNode: child, - }, - cssClass: "icon umb-tree-icon sprTree icon-search", - cssClasses: ["not-published"] - } - ]; - } - }); - } - } - - $scope.hideSearch = function () { - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = null; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } - - // method to select a search result - $scope.selectResult = function (evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, { event: evt, node: result }); - }; - - //callback when there are search results - $scope.onSearchResults = function (results) { - $scope.searchInfo.results = results; - $scope.searchInfo.showSearch = true; - }; - - $scope.move = function () { - - $scope.busy = true; - $scope.error = false; - - contentResource.move({ parentId: $scope.target.id, id: node.id }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - $scope.busy = false; - - //first we need to remove the node that launched the dialog - treeService.removeNode($scope.currentNode); - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the moved content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was moved!!) - - navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "content", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - }, function (err) { - $scope.success = false; - $scope.error = err; - $scope.busy = false; - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - }); - }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - }); - }); -/** - * @ngdoc controller - * @name Umbraco.Editors.Content.RecycleBinController - * @function - * - * @description - * Controls the recycle bin for content - * - */ - -function ContentRecycleBinController($scope, $routeParams, contentResource, navigationService, localizationService) { - - //ensures the list view doesn't actually load until we query for the list view config - // for the section - $scope.page = {}; - $scope.page.name = "Recycle Bin"; - $scope.page.nameLocked = true; - - //ensures the list view doesn't actually load until we query for the list view config - // for the section - $scope.listViewPath = null; - - $routeParams.id = "-20"; - contentResource.getRecycleBin().then(function (result) { - //we'll get the 'content item' for the recycle bin, we know that it will contain a single tab and a - // single property, so we'll extract that property (list view) and use it's data. - var listproperty = result.tabs[0].properties[0]; - - _.each(listproperty.config, function (val, key) { - $scope.model.config[key] = val; - }); - $scope.listViewPath = 'views/propertyeditors/listview/listview.html'; - }); - - $scope.model = { config: { entityType: $routeParams.section, layouts: [] } }; - - // sync tree node - navigationService.syncTree({ tree: "content", path: ["-1", $routeParams.id], forceReload: false }); - - localizePageName(); - - function localizePageName() { - - var pageName = "general_recycleBin"; - - localizationService.localize(pageName).then(function (value) { - $scope.page.name = value; - }); - - } -} - -angular.module('umbraco').controller("Umbraco.Editors.Content.RecycleBinController", ContentRecycleBinController); +angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", + function ($scope, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { -angular.module("umbraco").controller("Umbraco.Editors.Content.RestoreController", - function ($scope, relationResource, contentResource, navigationService, appState, treeService) { - var dialogOptions = $scope.dialogOptions; - - var node = dialogOptions.currentNode; - - $scope.error = null; - $scope.success = false; - - relationResource.getByChildId(node.id, "relateParentDocumentOnDelete").then(function (data) { - - if (data.length == 0) { - $scope.success = false; - $scope.error = { - errorMsg: "Cannot automatically restore this item", - data: { - Message: "There is no 'restore' relation found for this node. Use the Move menu item to move it manually." - } - } - return; - } - - $scope.relation = data[0]; - - if ($scope.relation.parentId == -1) { - $scope.target = { id: -1, name: "Root" }; - - } else { - contentResource.getById($scope.relation.parentId).then(function (data) { - $scope.target = data; - - }, function (err) { - $scope.success = false; - $scope.error = err; - }); - } - - }, function (err) { - $scope.success = false; - $scope.error = err; - }); - - $scope.restore = function () { - // this code was copied from `content.move.controller.js` - contentResource.move({ parentId: $scope.target.id, id: node.id }) - .then(function (path) { - - $scope.success = true; - - //first we need to remove the node that launched the dialog - treeService.removeNode($scope.currentNode); - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the moved content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was moved!!) - - navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "content", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - }, function (err) { - $scope.success = false; - $scope.error = err; - }); - }; + var dialogOptions = $scope.dialogOptions; + var searchText = "Search..."; + localizationService.localize("general_search").then(function (value) { + searchText = value + "..."; + }); + + $scope.dialogTreeEventHandler = $({}); + $scope.busy = false; + $scope.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + } + + var node = dialogOptions.currentNode; + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if (args.node.metaData.listViewNode) { + //check if list view 'search' node was selected + + $scope.searchInfo.showSearch = true; + $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; + $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; + } + else { + eventsService.emit("editors.content.moveController.select", args); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + } + + function nodeExpandedHandler(ev, args) { + if (angular.isArray(args.children)) { + + //iterate children + _.each(args.children, function (child) { + //check if any of the items are list views, if so we need to add a custom + // child: A node to activate the search + if (child.metaData.isContainer) { + child.hasChildren = true; + child.children = [ + { + level: child.level + 1, + hasChildren: false, + name: searchText, + metaData: { + listViewNode: child, + }, + cssClass: "icon umb-tree-icon sprTree icon-search", + cssClasses: ["not-published"] + } + ]; + } + }); + } + } + + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = null; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + } + + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { event: evt, node: result }); + }; + + //callback when there are search results + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; + }; + + $scope.move = function () { + + $scope.busy = true; + $scope.error = false; + + contentResource.move({ parentId: $scope.target.id, id: node.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + + navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "content", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); + }); + }); +/** + * @ngdoc controller + * @name Umbraco.Editors.Content.RecycleBinController + * @function + * + * @description + * Controls the recycle bin for content + * + */ + +function ContentRecycleBinController($scope, $routeParams, contentResource, navigationService, localizationService) { + + //ensures the list view doesn't actually load until we query for the list view config + // for the section + $scope.page = {}; + $scope.page.name = "Recycle Bin"; + $scope.page.nameLocked = true; + + //ensures the list view doesn't actually load until we query for the list view config + // for the section + $scope.listViewPath = null; + + $routeParams.id = "-20"; + contentResource.getRecycleBin().then(function (result) { + //we'll get the 'content item' for the recycle bin, we know that it will contain a single tab and a + // single property, so we'll extract that property (list view) and use it's data. + var listproperty = result.tabs[0].properties[0]; + + _.each(listproperty.config, function (val, key) { + $scope.model.config[key] = val; + }); + $scope.listViewPath = 'views/propertyeditors/listview/listview.html'; + }); + + $scope.model = { config: { entityType: $routeParams.section, layouts: [] } }; + + // sync tree node + navigationService.syncTree({ tree: "content", path: ["-1", $routeParams.id], forceReload: false }); + + localizePageName(); + + function localizePageName() { + + var pageName = "general_recycleBin"; + + localizationService.localize(pageName).then(function (value) { + $scope.page.name = value; + }); + + } +} + +angular.module('umbraco').controller("Umbraco.Editors.Content.RecycleBinController", ContentRecycleBinController); + +angular.module("umbraco").controller("Umbraco.Editors.Content.RestoreController", + function ($scope, relationResource, contentResource, navigationService, appState, treeService) { + var dialogOptions = $scope.dialogOptions; + + var node = dialogOptions.currentNode; + + $scope.error = null; + $scope.success = false; + + relationResource.getByChildId(node.id, "relateParentDocumentOnDelete").then(function (data) { + + if (data.length == 0) { + $scope.success = false; + $scope.error = { + errorMsg: "Cannot automatically restore this item", + data: { + Message: "There is no 'restore' relation found for this node. Use the Move menu item to move it manually." + } + } + return; + } + + $scope.relation = data[0]; + + if ($scope.relation.parentId == -1) { + $scope.target = { id: -1, name: "Root" }; + + } else { + contentResource.getById($scope.relation.parentId).then(function (data) { + $scope.target = data; + + }, function (err) { + $scope.success = false; + $scope.error = err; + }); + } + + }, function (err) { + $scope.success = false; + $scope.error = err; + }); + + $scope.restore = function () { + // this code was copied from `content.move.controller.js` + contentResource.move({ parentId: $scope.target.id, id: node.id }) + .then(function (path) { + + $scope.success = true; + + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + + navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "content", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + }, function (err) { + $scope.success = false; + $scope.error = err; + }); + }; }); function startUpVideosDashboardController($scope, xmlhelper, $log, $http) { $scope.videos = []; @@ -5575,619 +5575,619 @@ function MediaFolderBrowserDashboardController($rootScope, $scope, $location, co } angular.module("umbraco").controller("Umbraco.Dashboard.MediaFolderBrowserDashboardController", MediaFolderBrowserDashboardController); -function ExamineMgmtController($scope, umbRequestHelper, $log, $http, $q, $timeout) { - - $scope.indexerDetails = []; - $scope.searcherDetails = []; - $scope.loading = true; - - function checkProcessing(indexer, checkActionName) { - umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", - checkActionName, - { indexerName: indexer.name })), - 'Failed to check index processing') - .then(function(data) { - - if (data !== null && data !== "null") { - - //copy all resulting properties - for (var k in data) { - indexer[k] = data[k]; - } - indexer.isProcessing = false; - } else { - $timeout(function() { - //don't continue if we've tried 100 times - if (indexer.processingAttempts < 100) { - checkProcessing(indexer, checkActionName); - //add an attempt - indexer.processingAttempts++; - } else { - //we've exceeded 100 attempts, stop processing - indexer.isProcessing = false; - } - }, - 1000); - } - }); - } - - $scope.search = function(searcher, e) { - if (e && e.keyCode !== 13) { - return; - } - - umbRequestHelper.resourcePromise( - $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", - "GetSearchResults", - { - searcherName: searcher.name, - query: encodeURIComponent(searcher.searchText), - queryType: searcher.searchType - })), - 'Failed to search') - .then(function(searchResults) { - searcher.isSearching = true; - searcher.searchResults = searchResults; - }); - } - - $scope.toggle = function(provider, propName) { - if (provider[propName] !== undefined) { - provider[propName] = !provider[propName]; - } else { - provider[propName] = true; - } - } - - $scope.rebuildIndex = function(indexer) { - if (confirm("This will cause the index to be rebuilt. " + - "Depending on how much content there is in your site this could take a while. " + - "It is not recommended to rebuild an index during times of high website traffic " + - "or when editors are editing content.")) { - - indexer.isProcessing = true; - indexer.processingAttempts = 0; - - umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", - "PostRebuildIndex", - { indexerName: indexer.name })), - 'Failed to rebuild index') - .then(function() { - - //rebuilding has started, nothing is returned accept a 200 status code. - //lets poll to see if it is done. - $timeout(function() { - checkProcessing(indexer, "PostCheckRebuildIndex"); - }, - 1000); - - }); - } - } - - $scope.optimizeIndex = function(indexer) { - if (confirm("This will cause the index to be optimized which will improve its performance. " + - "It is not recommended to optimize an index during times of high website traffic " + - "or when editors are editing content.")) { - indexer.isProcessing = true; - - umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", - "PostOptimizeIndex", - { indexerName: indexer.name })), - 'Failed to optimize index') - .then(function() { - - //optimizing has started, nothing is returned accept a 200 status code. - //lets poll to see if it is done. - $timeout(function() { - checkProcessing(indexer, "PostCheckOptimizeIndex"); - }, - 1000); - - }); - } - } - - $scope.closeSearch = function(searcher) { - searcher.isSearching = true; - } - - //go get the data - - //combine two promises and execute when they are both done - $q.all([ - - //get the indexer details - umbRequestHelper.resourcePromise( - $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetIndexerDetails")), - 'Failed to retrieve indexer details') - .then(function(data) { - $scope.indexerDetails = data; - }), - - //get the searcher details - umbRequestHelper.resourcePromise( - $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetSearcherDetails")), - 'Failed to retrieve searcher details') - .then(function(data) { - $scope.searcherDetails = data; - for (var s in $scope.searcherDetails) { - $scope.searcherDetails[s].searchType = "text"; - } - }) - ]) - .then(function() { - //all init loading is complete - $scope.loading = false; - }); -} - +function ExamineMgmtController($scope, umbRequestHelper, $log, $http, $q, $timeout) { + + $scope.indexerDetails = []; + $scope.searcherDetails = []; + $scope.loading = true; + + function checkProcessing(indexer, checkActionName) { + umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", + checkActionName, + { indexerName: indexer.name })), + 'Failed to check index processing') + .then(function(data) { + + if (data !== null && data !== "null") { + + //copy all resulting properties + for (var k in data) { + indexer[k] = data[k]; + } + indexer.isProcessing = false; + } else { + $timeout(function() { + //don't continue if we've tried 100 times + if (indexer.processingAttempts < 100) { + checkProcessing(indexer, checkActionName); + //add an attempt + indexer.processingAttempts++; + } else { + //we've exceeded 100 attempts, stop processing + indexer.isProcessing = false; + } + }, + 1000); + } + }); + } + + $scope.search = function(searcher, e) { + if (e && e.keyCode !== 13) { + return; + } + + umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", + "GetSearchResults", + { + searcherName: searcher.name, + query: encodeURIComponent(searcher.searchText), + queryType: searcher.searchType + })), + 'Failed to search') + .then(function(searchResults) { + searcher.isSearching = true; + searcher.searchResults = searchResults; + }); + } + + $scope.toggle = function(provider, propName) { + if (provider[propName] !== undefined) { + provider[propName] = !provider[propName]; + } else { + provider[propName] = true; + } + } + + $scope.rebuildIndex = function(indexer) { + if (confirm("This will cause the index to be rebuilt. " + + "Depending on how much content there is in your site this could take a while. " + + "It is not recommended to rebuild an index during times of high website traffic " + + "or when editors are editing content.")) { + + indexer.isProcessing = true; + indexer.processingAttempts = 0; + + umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", + "PostRebuildIndex", + { indexerName: indexer.name })), + 'Failed to rebuild index') + .then(function() { + + //rebuilding has started, nothing is returned accept a 200 status code. + //lets poll to see if it is done. + $timeout(function() { + checkProcessing(indexer, "PostCheckRebuildIndex"); + }, + 1000); + + }); + } + } + + $scope.optimizeIndex = function(indexer) { + if (confirm("This will cause the index to be optimized which will improve its performance. " + + "It is not recommended to optimize an index during times of high website traffic " + + "or when editors are editing content.")) { + indexer.isProcessing = true; + + umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", + "PostOptimizeIndex", + { indexerName: indexer.name })), + 'Failed to optimize index') + .then(function() { + + //optimizing has started, nothing is returned accept a 200 status code. + //lets poll to see if it is done. + $timeout(function() { + checkProcessing(indexer, "PostCheckOptimizeIndex"); + }, + 1000); + + }); + } + } + + $scope.closeSearch = function(searcher) { + searcher.isSearching = true; + } + + //go get the data + + //combine two promises and execute when they are both done + $q.all([ + + //get the indexer details + umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetIndexerDetails")), + 'Failed to retrieve indexer details') + .then(function(data) { + $scope.indexerDetails = data; + }), + + //get the searcher details + umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetSearcherDetails")), + 'Failed to retrieve searcher details') + .then(function(data) { + $scope.searcherDetails = data; + for (var s in $scope.searcherDetails) { + $scope.searcherDetails[s].searchType = "text"; + } + }) + ]) + .then(function() { + //all init loading is complete + $scope.loading = false; + }); +} + angular.module("umbraco").controller("Umbraco.Dashboard.ExamineMgmtController", ExamineMgmtController); -(function() { - "use strict"; - - function HealthCheckController($scope, healthCheckResource) { - var SUCCESS = 0; - var WARNING = 1; - var ERROR = 2; - var INFO = 3; - - var vm = this; - - vm.viewState = "list"; - vm.groups = []; - vm.selectedGroup = {}; - - vm.getStatus = getStatus; - vm.executeAction = executeAction; - vm.checkAllGroups = checkAllGroups; - vm.checkAllInGroup = checkAllInGroup; - vm.openGroup = openGroup; - vm.setViewState = setViewState; - - // Get a (grouped) list of all health checks - healthCheckResource.getAllChecks() - .then(function(response) { - vm.groups = response; - }); - - function setGroupGlobalResultType(group) { - var totalSuccess = 0; - var totalError = 0; - var totalWarning = 0; - var totalInfo = 0; - - // count total number of statusses - angular.forEach(group.checks, - function(check) { - angular.forEach(check.status, - function(status) { - switch (status.resultType) { - case SUCCESS: - totalSuccess = totalSuccess + 1; - break; - case WARNING: - totalWarning = totalWarning + 1; - break; - case ERROR: - totalError = totalError + 1; - break; - case INFO: - totalInfo = totalInfo + 1; - break; - } - }); - }); - - group.totalSuccess = totalSuccess; - group.totalError = totalError; - group.totalWarning = totalWarning; - group.totalInfo = totalInfo; - - } - - // Get the status of an individual check - function getStatus(check) { - check.loading = true; - check.status = null; - healthCheckResource.getStatus(check.id) - .then(function(response) { - check.loading = false; - check.status = response; - }); - } - - function executeAction(check, index, action) { - check.loading = true; - healthCheckResource.executeAction(action) - .then(function(response) { - check.status[index] = response; - check.loading = false; - }); - } - - function checkAllGroups(groups) { - // set number of checks which has been executed - for (var i = 0; i < groups.length; i++) { - var group = groups[i]; - checkAllInGroup(group, group.checks); - } - vm.groups = groups; - } - - function checkAllInGroup(group, checks) { - group.checkCounter = 0; - group.loading = true; - - angular.forEach(checks, - function(check) { - - check.loading = true; - - healthCheckResource.getStatus(check.id) - .then(function(response) { - check.status = response; - group.checkCounter = group.checkCounter + 1; - check.loading = false; - - // when all checks are done, set global group result - if (group.checkCounter === checks.length) { - setGroupGlobalResultType(group); - group.loading = false; - } - }); - }); - } - - function openGroup(group) { - vm.selectedGroup = group; - vm.viewState = "details"; - } - - function setViewState(state) { - vm.viewState = state; - - if (state === 'list') { - - for (var i = 0; i < vm.groups.length; i++) { - var group = vm.groups[i]; - setGroupGlobalResultType(group); - } - } - } - } - - angular.module("umbraco").controller("Umbraco.Dashboard.HealthCheckController", HealthCheckController); -})(); +(function() { + "use strict"; -(function() { - "use strict"; - - function RedirectUrlsController($scope, redirectUrlsResource, notificationsService, localizationService, $q) { - //...todo - //search by url or url part - //search by domain - //display domain in dashboard results? - - //used to cancel any request in progress if another one needs to take it's place - var vm = this; - var canceler = null; - - vm.dashboard = { - searchTerm: "", - loading: false, - urlTrackerDisabled: false, - userIsAdmin: false - }; - - vm.pagination = { - pageIndex: 0, - pageNumber: 1, - totalPages: 1, - pageSize: 20 - }; - - vm.goToPage = goToPage; - vm.search = search; - vm.removeRedirect = removeRedirect; - vm.disableUrlTracker = disableUrlTracker; - vm.enableUrlTracker = enableUrlTracker; - vm.filter = filter; - vm.checkEnabled = checkEnabled; - - function activate() { - vm.checkEnabled().then(function() { - vm.search(); - }); - } - - function checkEnabled() { - vm.dashboard.loading = true; - return redirectUrlsResource.getEnableState().then(function (response) { - vm.dashboard.urlTrackerDisabled = response.enabled !== true; - vm.dashboard.userIsAdmin = response.userIsAdmin; - vm.dashboard.loading = false; - }); - } - - function goToPage(pageNumber) { - vm.pagination.pageIndex = pageNumber - 1; - vm.pagination.pageNumber = pageNumber; - vm.search(); - } - - function search() { - - vm.dashboard.loading = true; - - var searchTerm = vm.dashboard.searchTerm; - if (searchTerm === undefined) { - searchTerm = ""; - } - - redirectUrlsResource.searchRedirectUrls(searchTerm, vm.pagination.pageIndex, vm.pagination.pageSize).then(function(response) { - - vm.redirectUrls = response.searchResults; - - // update pagination - vm.pagination.pageIndex = response.currentPage; - vm.pagination.pageNumber = response.currentPage + 1; - vm.pagination.totalPages = response.pageCount; - - vm.dashboard.loading = false; - - }); - } - - function removeRedirect(redirectToDelete) { - localizationService.localize("redirectUrls_confirmRemove", [redirectToDelete.originalUrl, redirectToDelete.destinationUrl]).then(function (value) { - var toggleConfirm = confirm(value); - - if (toggleConfirm) { - redirectUrlsResource.deleteRedirectUrl(redirectToDelete.redirectId).then(function () { - - var index = vm.redirectUrls.indexOf(redirectToDelete); - vm.redirectUrls.splice(index, 1); - notificationsService.success(localizationService.localize("redirectUrls_redirectRemoved")); - - // check if new redirects needs to be loaded - if (vm.redirectUrls.length === 0 && vm.pagination.totalPages > 1) { - - // if we are not on the first page - get records from the previous - if (vm.pagination.pageIndex > 0) { - vm.pagination.pageIndex = vm.pagination.pageIndex - 1; - vm.pagination.pageNumber = vm.pagination.pageNumber - 1; - } - - search(); - } - }, function (error) { - notificationsService.error(localizationService.localize("redirectUrls_redirectRemoveError")); - }); - } - }); - } - - function disableUrlTracker() { - localizationService.localize("redirectUrls_confirmDisable").then(function(value) { - var toggleConfirm = confirm(value); - if (toggleConfirm) { - - redirectUrlsResource.toggleUrlTracker(true).then(function () { - activate(); - notificationsService.success(localizationService.localize("redirectUrls_disabledConfirm")); - }, function (error) { - notificationsService.warning(localizationService.localize("redirectUrls_disableError")); - }); - - } - }); - } - - function enableUrlTracker() { - redirectUrlsResource.toggleUrlTracker(false).then(function() { - activate(); - notificationsService.success(localizationService.localize("redirectUrls_enabledConfirm")); - }, function(error) { - notificationsService.warning(localizationService.localize("redirectUrls_enableError")); - }); - } - - var filterDebounced = _.debounce(function(e) { - - $scope.$apply(function() { - - //a canceler exists, so perform the cancelation operation and reset - if (canceler) { - canceler.resolve(); - canceler = $q.defer(); - } else { - canceler = $q.defer(); - } - - vm.search(); - - }); - - }, 200); - - function filter() { - vm.dashboard.loading = true; - filterDebounced(); - } - - activate(); - - } - - angular.module("umbraco").controller("Umbraco.Dashboard.RedirectUrlsController", RedirectUrlsController); -})(); + function HealthCheckController($scope, healthCheckResource) { + var SUCCESS = 0; + var WARNING = 1; + var ERROR = 2; + var INFO = 3; + + var vm = this; + + vm.viewState = "list"; + vm.groups = []; + vm.selectedGroup = {}; + + vm.getStatus = getStatus; + vm.executeAction = executeAction; + vm.checkAllGroups = checkAllGroups; + vm.checkAllInGroup = checkAllInGroup; + vm.openGroup = openGroup; + vm.setViewState = setViewState; + + // Get a (grouped) list of all health checks + healthCheckResource.getAllChecks() + .then(function(response) { + vm.groups = response; + }); + + function setGroupGlobalResultType(group) { + var totalSuccess = 0; + var totalError = 0; + var totalWarning = 0; + var totalInfo = 0; + + // count total number of statusses + angular.forEach(group.checks, + function(check) { + angular.forEach(check.status, + function(status) { + switch (status.resultType) { + case SUCCESS: + totalSuccess = totalSuccess + 1; + break; + case WARNING: + totalWarning = totalWarning + 1; + break; + case ERROR: + totalError = totalError + 1; + break; + case INFO: + totalInfo = totalInfo + 1; + break; + } + }); + }); + + group.totalSuccess = totalSuccess; + group.totalError = totalError; + group.totalWarning = totalWarning; + group.totalInfo = totalInfo; + + } + + // Get the status of an individual check + function getStatus(check) { + check.loading = true; + check.status = null; + healthCheckResource.getStatus(check.id) + .then(function(response) { + check.loading = false; + check.status = response; + }); + } + + function executeAction(check, index, action) { + check.loading = true; + healthCheckResource.executeAction(action) + .then(function(response) { + check.status[index] = response; + check.loading = false; + }); + } + + function checkAllGroups(groups) { + // set number of checks which has been executed + for (var i = 0; i < groups.length; i++) { + var group = groups[i]; + checkAllInGroup(group, group.checks); + } + vm.groups = groups; + } + + function checkAllInGroup(group, checks) { + group.checkCounter = 0; + group.loading = true; + + angular.forEach(checks, + function(check) { + + check.loading = true; + + healthCheckResource.getStatus(check.id) + .then(function(response) { + check.status = response; + group.checkCounter = group.checkCounter + 1; + check.loading = false; + + // when all checks are done, set global group result + if (group.checkCounter === checks.length) { + setGroupGlobalResultType(group); + group.loading = false; + } + }); + }); + } + + function openGroup(group) { + vm.selectedGroup = group; + vm.viewState = "details"; + } + + function setViewState(state) { + vm.viewState = state; + + if (state === 'list') { + + for (var i = 0; i < vm.groups.length; i++) { + var group = vm.groups[i]; + setGroupGlobalResultType(group); + } + } + } + } + + angular.module("umbraco").controller("Umbraco.Dashboard.HealthCheckController", HealthCheckController); +})(); + +(function() { + "use strict"; + + function RedirectUrlsController($scope, redirectUrlsResource, notificationsService, localizationService, $q) { + //...todo + //search by url or url part + //search by domain + //display domain in dashboard results? + + //used to cancel any request in progress if another one needs to take it's place + var vm = this; + var canceler = null; + + vm.dashboard = { + searchTerm: "", + loading: false, + urlTrackerDisabled: false, + userIsAdmin: false + }; + + vm.pagination = { + pageIndex: 0, + pageNumber: 1, + totalPages: 1, + pageSize: 20 + }; + + vm.goToPage = goToPage; + vm.search = search; + vm.removeRedirect = removeRedirect; + vm.disableUrlTracker = disableUrlTracker; + vm.enableUrlTracker = enableUrlTracker; + vm.filter = filter; + vm.checkEnabled = checkEnabled; + + function activate() { + vm.checkEnabled().then(function() { + vm.search(); + }); + } + + function checkEnabled() { + vm.dashboard.loading = true; + return redirectUrlsResource.getEnableState().then(function (response) { + vm.dashboard.urlTrackerDisabled = response.enabled !== true; + vm.dashboard.userIsAdmin = response.userIsAdmin; + vm.dashboard.loading = false; + }); + } + + function goToPage(pageNumber) { + vm.pagination.pageIndex = pageNumber - 1; + vm.pagination.pageNumber = pageNumber; + vm.search(); + } + + function search() { + + vm.dashboard.loading = true; + + var searchTerm = vm.dashboard.searchTerm; + if (searchTerm === undefined) { + searchTerm = ""; + } + + redirectUrlsResource.searchRedirectUrls(searchTerm, vm.pagination.pageIndex, vm.pagination.pageSize).then(function(response) { + + vm.redirectUrls = response.searchResults; + + // update pagination + vm.pagination.pageIndex = response.currentPage; + vm.pagination.pageNumber = response.currentPage + 1; + vm.pagination.totalPages = response.pageCount; + + vm.dashboard.loading = false; + + }); + } + + function removeRedirect(redirectToDelete) { + localizationService.localize("redirectUrls_confirmRemove", [redirectToDelete.originalUrl, redirectToDelete.destinationUrl]).then(function (value) { + var toggleConfirm = confirm(value); + + if (toggleConfirm) { + redirectUrlsResource.deleteRedirectUrl(redirectToDelete.redirectId).then(function () { + + var index = vm.redirectUrls.indexOf(redirectToDelete); + vm.redirectUrls.splice(index, 1); + notificationsService.success(localizationService.localize("redirectUrls_redirectRemoved")); + + // check if new redirects needs to be loaded + if (vm.redirectUrls.length === 0 && vm.pagination.totalPages > 1) { + + // if we are not on the first page - get records from the previous + if (vm.pagination.pageIndex > 0) { + vm.pagination.pageIndex = vm.pagination.pageIndex - 1; + vm.pagination.pageNumber = vm.pagination.pageNumber - 1; + } + + search(); + } + }, function (error) { + notificationsService.error(localizationService.localize("redirectUrls_redirectRemoveError")); + }); + } + }); + } + + function disableUrlTracker() { + localizationService.localize("redirectUrls_confirmDisable").then(function(value) { + var toggleConfirm = confirm(value); + if (toggleConfirm) { + + redirectUrlsResource.toggleUrlTracker(true).then(function () { + activate(); + notificationsService.success(localizationService.localize("redirectUrls_disabledConfirm")); + }, function (error) { + notificationsService.warning(localizationService.localize("redirectUrls_disableError")); + }); + + } + }); + } + + function enableUrlTracker() { + redirectUrlsResource.toggleUrlTracker(false).then(function() { + activate(); + notificationsService.success(localizationService.localize("redirectUrls_enabledConfirm")); + }, function(error) { + notificationsService.warning(localizationService.localize("redirectUrls_enableError")); + }); + } + + var filterDebounced = _.debounce(function(e) { + + $scope.$apply(function() { + + //a canceler exists, so perform the cancelation operation and reset + if (canceler) { + canceler.resolve(); + canceler = $q.defer(); + } else { + canceler = $q.defer(); + } + + vm.search(); + + }); + + }, 200); + + function filter() { + vm.dashboard.loading = true; + filterDebounced(); + } + + activate(); + + } + + angular.module("umbraco").controller("Umbraco.Dashboard.RedirectUrlsController", RedirectUrlsController); +})(); + +function XmlDataIntegrityReportController($scope, umbRequestHelper, $log, $http) { + + function check(item) { + var action = item.check; + umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("xmlDataIntegrityBaseUrl", action)), + 'Failed to retrieve data integrity status') + .then(function(result) { + item.checking = false; + item.invalid = result === "false"; + }); + } + + $scope.fix = function(item) { + var action = item.fix; + if (item.fix) { + if (confirm("This will cause all xml structures for this type to be rebuilt. " + + "Depending on how much content there is in your site this could take a while. " + + "It is not recommended to rebuild xml structures if they are not out of sync, during times of high website traffic " + + "or when editors are editing content.")) { + item.fixing = true; + umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("xmlDataIntegrityBaseUrl", action)), + 'Failed to retrieve data integrity status') + .then(function(result) { + item.fixing = false; + item.invalid = result === "false"; + }); + } + } + } + + $scope.items = { + "contentXml": { + label: "Content in the cmsContentXml table", + checking: true, + fixing: false, + fix: "FixContentXmlTable", + check: "CheckContentXmlTable" + }, + "mediaXml": { + label: "Media in the cmsContentXml table", + checking: true, + fixing: false, + fix: "FixMediaXmlTable", + check: "CheckMediaXmlTable" + }, + "memberXml": { + label: "Members in the cmsContentXml table", + checking: true, + fixing: false, + fix: "FixMembersXmlTable", + check: "CheckMembersXmlTable" + } + }; + + for (var i in $scope.items) { + check($scope.items[i]); + } +} -function XmlDataIntegrityReportController($scope, umbRequestHelper, $log, $http) { - - function check(item) { - var action = item.check; - umbRequestHelper.resourcePromise( - $http.get(umbRequestHelper.getApiUrl("xmlDataIntegrityBaseUrl", action)), - 'Failed to retrieve data integrity status') - .then(function(result) { - item.checking = false; - item.invalid = result === "false"; - }); - } - - $scope.fix = function(item) { - var action = item.fix; - if (item.fix) { - if (confirm("This will cause all xml structures for this type to be rebuilt. " + - "Depending on how much content there is in your site this could take a while. " + - "It is not recommended to rebuild xml structures if they are not out of sync, during times of high website traffic " + - "or when editors are editing content.")) { - item.fixing = true; - umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("xmlDataIntegrityBaseUrl", action)), - 'Failed to retrieve data integrity status') - .then(function(result) { - item.fixing = false; - item.invalid = result === "false"; - }); - } - } - } - - $scope.items = { - "contentXml": { - label: "Content in the cmsContentXml table", - checking: true, - fixing: false, - fix: "FixContentXmlTable", - check: "CheckContentXmlTable" - }, - "mediaXml": { - label: "Media in the cmsContentXml table", - checking: true, - fixing: false, - fix: "FixMediaXmlTable", - check: "CheckMediaXmlTable" - }, - "memberXml": { - label: "Members in the cmsContentXml table", - checking: true, - fixing: false, - fix: "FixMembersXmlTable", - check: "CheckMembersXmlTable" - } - }; - - for (var i in $scope.items) { - check($scope.items[i]); - } -} - angular.module("umbraco").controller("Umbraco.Dashboard.XmlDataIntegrityReportController", XmlDataIntegrityReportController); -/** - * @ngdoc controller - * @name Umbraco.Editors.DataType.CreateController - * @function - * - * @description - * The controller for the data type creation dialog - */ -function DataTypeCreateController($scope, $location, navigationService, dataTypeResource, formHelper, appState) { - - $scope.model = { - folderName: "", - creatingFolder: false - }; - - var node = $scope.dialogOptions.currentNode; - - $scope.showCreateFolder = function() { - $scope.model.creatingFolder = true; - } - - $scope.createContainer = function () { - if (formHelper.submitForm({ scope: $scope, formCtrl: this.createFolderForm, statusMessage: "Creating folder..." })) { - dataTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { - - navigationService.hideMenu(); - var currPath = node.path ? node.path : "-1"; - navigationService.syncTree({ tree: "datatypes", path: currPath + "," + folderId, forceReload: true, activate: true }); - - formHelper.resetForm({ scope: $scope }); - - var section = appState.getSectionState("currentSection"); - - }, function(err) { - - //TODO: Handle errors - }); - }; - } - - $scope.createDataType = function() { - $location.search('create', null); - $location.path("/developer/datatypes/edit/" + node.id).search("create", "true"); - navigationService.hideMenu(); - } -} - -angular.module('umbraco').controller("Umbraco.Editors.DataType.CreateController", DataTypeCreateController); +/** + * @ngdoc controller + * @name Umbraco.Editors.DataType.CreateController + * @function + * + * @description + * The controller for the data type creation dialog + */ +function DataTypeCreateController($scope, $location, navigationService, dataTypeResource, formHelper, appState) { -/** - * @ngdoc controller - * @name Umbraco.Editors.ContentDeleteController - * @function - * - * @description - * The controller for deleting content - */ -function DataTypeDeleteController($scope, dataTypeResource, treeService, navigationService) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - dataTypeResource.deleteById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.performContainerDelete = function () { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - dataTypeResource.deleteContainerById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.DataType.DeleteController", DataTypeDeleteController); + $scope.model = { + folderName: "", + creatingFolder: false + }; + + var node = $scope.dialogOptions.currentNode; + + $scope.showCreateFolder = function() { + $scope.model.creatingFolder = true; + } + + $scope.createContainer = function () { + if (formHelper.submitForm({ scope: $scope, formCtrl: this.createFolderForm, statusMessage: "Creating folder..." })) { + dataTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { + + navigationService.hideMenu(); + var currPath = node.path ? node.path : "-1"; + navigationService.syncTree({ tree: "datatypes", path: currPath + "," + folderId, forceReload: true, activate: true }); + + formHelper.resetForm({ scope: $scope }); + + var section = appState.getSectionState("currentSection"); + + }, function(err) { + + //TODO: Handle errors + }); + }; + } + + $scope.createDataType = function() { + $location.search('create', null); + $location.path("/developer/datatypes/edit/" + node.id).search("create", "true"); + navigationService.hideMenu(); + } +} + +angular.module('umbraco').controller("Umbraco.Editors.DataType.CreateController", DataTypeCreateController); + +/** + * @ngdoc controller + * @name Umbraco.Editors.ContentDeleteController + * @function + * + * @description + * The controller for deleting content + */ +function DataTypeDeleteController($scope, dataTypeResource, treeService, navigationService) { + + $scope.performDelete = function() { + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + dataTypeResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + + }; + + $scope.performContainerDelete = function () { + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + dataTypeResource.deleteContainerById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + + }; + + $scope.cancel = function() { + navigationService.hideDialog(); + }; +} + +angular.module("umbraco").controller("Umbraco.Editors.DataType.DeleteController", DataTypeDeleteController); /** * @ngdoc controller @@ -6391,266 +6391,266 @@ function DataTypeEditController($scope, $routeParams, $location, appState, navig angular.module("umbraco").controller("Umbraco.Editors.DataType.EditController", DataTypeEditController); -angular.module("umbraco") -.controller("Umbraco.Editors.DataType.MoveController", - function ($scope, dataTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { - var dialogOptions = $scope.dialogOptions; - $scope.dialogTreeEventHandler = $({}); - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; - } - - $scope.target = args.node; - $scope.target.selected = true; - } - - $scope.move = function () { - - $scope.busy = true; - $scope.error = false; - - dataTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - $scope.busy = false; - - //first we need to remove the node that launched the dialog - treeService.removeNode($scope.currentNode); - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the moved content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was moved!!) - - navigationService.syncTree({ tree: "dataTypes", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "dataTypes", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - eventsService.emit('app.refreshEditor'); - - }, function (err) { - $scope.success = false; - $scope.error = err; - $scope.busy = false; - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - }); - }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - }); +angular.module("umbraco") +.controller("Umbraco.Editors.DataType.MoveController", + function ($scope, dataTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); -angular.module("umbraco") -.controller("Umbraco.Editors.DocumentTypes.CopyController", - function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { - var dialogOptions = $scope.dialogOptions; - $scope.dialogTreeEventHandler = $({}); - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; - } - - $scope.target = args.node; - $scope.target.selected = true; - } - - $scope.copy = function () { - - $scope.busy = true; - $scope.error = false; - - contentTypeResource.copy({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - $scope.busy = false; - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the copied content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was copied!!) - - navigationService.syncTree({ tree: "documentTypes", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "documentTypes", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - }, function (err) { - $scope.success = false; - $scope.error = err; - $scope.busy = false; - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - }); - }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - }); + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.CreateController - * @function - * - * @description - * The controller for the doc type creation dialog - */ -function DocumentTypesCreateController($scope, $location, navigationService, contentTypeResource, formHelper, appState, notificationsService, localizationService) { - - $scope.model = { - allowCreateFolder: $scope.dialogOptions.currentNode.parentId === null || $scope.dialogOptions.currentNode.nodeType === "container", - folderName: "", - creatingFolder: false, - }; - - var node = $scope.dialogOptions.currentNode, - localizeCreateFolder = localizationService.localize("defaultdialog_createFolder"); - - $scope.showCreateFolder = function() { - $scope.model.creatingFolder = true; - }; - - $scope.createContainer = function() { - - if (formHelper.submitForm({scope: $scope, formCtrl: this.createFolderForm, statusMessage: localizeCreateFolder})) { - - contentTypeResource.createContainer(node.id, $scope.model.folderName).then(function(folderId) { - - navigationService.hideMenu(); - - var currPath = node.path ? node.path : "-1"; - - navigationService.syncTree({ - tree: "documenttypes", - path: currPath + "," + folderId, - forceReload: true, - activate: true - }); - - formHelper.resetForm({ - scope: $scope - }); - - var section = appState.getSectionState("currentSection"); - - }, function(err) { - - $scope.error = err; - - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - }); - } - }; - - $scope.createDocType = function() { - $location.search('create', null); - $location.search('notemplate', null); - $location.path("/settings/documenttypes/edit/" + node.id).search("create", "true"); - navigationService.hideMenu(); - }; - - $scope.createComponent = function() { - $location.search('create', null); - $location.search('notemplate', null); - $location.path("/settings/documenttypes/edit/" + node.id).search("create", "true").search("notemplate", "true"); - navigationService.hideMenu(); - }; -} - -angular.module('umbraco').controller("Umbraco.Editors.DocumentTypes.CreateController", DocumentTypesCreateController); + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.DeleteController - * @function - * - * @description - * The controller for deleting content - */ -function DocumentTypesDeleteController($scope, dataTypeResource, contentTypeResource, treeService, navigationService) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - contentTypeResource.deleteById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.performContainerDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - contentTypeResource.deleteContainerById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.DocumentTypes.DeleteController", DocumentTypesDeleteController); + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.move = function () { + + $scope.busy = true; + $scope.error = false; + + dataTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + + navigationService.syncTree({ tree: "dataTypes", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "dataTypes", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + eventsService.emit('app.refreshEditor'); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); + }); + +angular.module("umbraco") +.controller("Umbraco.Editors.DocumentTypes.CopyController", + function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.copy = function () { + + $scope.busy = true; + $scope.error = false; + + contentTypeResource.copy({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the copied content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was copied!!) + + navigationService.syncTree({ tree: "documentTypes", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "documentTypes", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); + }); + +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.CreateController + * @function + * + * @description + * The controller for the doc type creation dialog + */ +function DocumentTypesCreateController($scope, $location, navigationService, contentTypeResource, formHelper, appState, notificationsService, localizationService) { + + $scope.model = { + allowCreateFolder: $scope.dialogOptions.currentNode.parentId === null || $scope.dialogOptions.currentNode.nodeType === "container", + folderName: "", + creatingFolder: false, + }; + + var node = $scope.dialogOptions.currentNode, + localizeCreateFolder = localizationService.localize("defaultdialog_createFolder"); + + $scope.showCreateFolder = function() { + $scope.model.creatingFolder = true; + }; + + $scope.createContainer = function() { + + if (formHelper.submitForm({scope: $scope, formCtrl: this.createFolderForm, statusMessage: localizeCreateFolder})) { + + contentTypeResource.createContainer(node.id, $scope.model.folderName).then(function(folderId) { + + navigationService.hideMenu(); + + var currPath = node.path ? node.path : "-1"; + + navigationService.syncTree({ + tree: "documenttypes", + path: currPath + "," + folderId, + forceReload: true, + activate: true + }); + + formHelper.resetForm({ + scope: $scope + }); + + var section = appState.getSectionState("currentSection"); + + }, function(err) { + + $scope.error = err; + + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + } + }; + + $scope.createDocType = function() { + $location.search('create', null); + $location.search('notemplate', null); + $location.path("/settings/documenttypes/edit/" + node.id).search("create", "true"); + navigationService.hideMenu(); + }; + + $scope.createComponent = function() { + $location.search('create', null); + $location.search('notemplate', null); + $location.path("/settings/documenttypes/edit/" + node.id).search("create", "true").search("notemplate", "true"); + navigationService.hideMenu(); + }; +} + +angular.module('umbraco').controller("Umbraco.Editors.DocumentTypes.CreateController", DocumentTypesCreateController); + +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.DeleteController + * @function + * + * @description + * The controller for deleting content + */ +function DocumentTypesDeleteController($scope, dataTypeResource, contentTypeResource, treeService, navigationService) { + + $scope.performDelete = function() { + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + contentTypeResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + + }; + + $scope.performContainerDelete = function() { + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + contentTypeResource.deleteContainerById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + + }; + + $scope.cancel = function() { + navigationService.hideDialog(); + }; +} + +angular.module("umbraco").controller("Umbraco.Editors.DocumentTypes.DeleteController", DocumentTypesDeleteController); /** * @ngdoc controller @@ -7029,202 +7029,202 @@ angular.module("umbraco").controller("Umbraco.Editors.DocumentTypes.DeleteContro angular.module("umbraco").controller("Umbraco.Editors.DocumentTypes.EditController", DocumentTypesEditController); })(); -angular.module("umbraco") -.controller("Umbraco.Editors.DocumentTypes.MoveController", - function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { - var dialogOptions = $scope.dialogOptions; - $scope.dialogTreeEventHandler = $({}); - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; - } - - $scope.target = args.node; - $scope.target.selected = true; - } - - $scope.move = function () { - - $scope.busy = true; - $scope.error = false; - - contentTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - $scope.busy = false; - - //first we need to remove the node that launched the dialog - treeService.removeNode($scope.currentNode); - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the moved content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was moved!!) - - navigationService.syncTree({ tree: "documentTypes", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "documentTypes", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - eventsService.emit('app.refreshEditor'); - - }, function (err) { - $scope.success = false; - $scope.error = err; - $scope.busy = false; - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - }); - }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - }); +angular.module("umbraco") +.controller("Umbraco.Editors.DocumentTypes.MoveController", + function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.PropertyController - * @function - * - * @description - * The controller for the content type editor property dialog - */ -(function() { - 'use strict'; - - function PermissionsController($scope, contentTypeResource, iconHelper, contentTypeHelper, localizationService) { - - /* ----------- SCOPE VARIABLES ----------- */ - - var vm = this; - var childNodeSelectorOverlayTitle = ""; - - vm.contentTypes = []; - vm.selectedChildren = []; - - vm.overlayTitle = ""; - - vm.addChild = addChild; - vm.removeChild = removeChild; - - /* ---------- INIT ---------- */ - - init(); - - function init() { - - childNodeSelectorOverlayTitle = localizationService.localize("contentTypeEditor_chooseChildNode"); - - contentTypeResource.getAll().then(function(contentTypes){ - - vm.contentTypes = contentTypes; - - // convert legacy icons - iconHelper.formatContentTypeIcons(vm.contentTypes); - - vm.selectedChildren = contentTypeHelper.makeObjectArrayFromId($scope.model.allowedContentTypes, vm.contentTypes); - - if($scope.model.id === 0) { - contentTypeHelper.insertChildNodePlaceholder(vm.contentTypes, $scope.model.name, $scope.model.icon, $scope.model.id); - } - - }); - - } - - function addChild($event) { - vm.childNodeSelectorOverlay = { - view: "itempicker", - title: childNodeSelectorOverlayTitle, - availableItems: vm.contentTypes, - selectedItems: vm.selectedChildren, - event: $event, - show: true, - submit: function(model) { - vm.selectedChildren.push(model.selectedItem); - $scope.model.allowedContentTypes.push(model.selectedItem.id); - vm.childNodeSelectorOverlay.show = false; - vm.childNodeSelectorOverlay = null; - } - }; - } - - function removeChild(selectedChild, index) { - // remove from vm - vm.selectedChildren.splice(index, 1); - - // remove from content type model - var selectedChildIndex = $scope.model.allowedContentTypes.indexOf(selectedChild.id); - $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); - } - - } - - angular.module("umbraco").controller("Umbraco.Editors.DocumentType.PermissionsController", PermissionsController); -})(); + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.TemplatesController - * @function - * - * @description - * The controller for the content type editor templates sub view - */ -(function() { - 'use strict'; - - function TemplatesController($scope, entityResource, contentTypeHelper, $routeParams) { - - /* ----------- SCOPE VARIABLES ----------- */ - - var vm = this; - - vm.availableTemplates = []; - vm.updateTemplatePlaceholder = false; - - - /* ---------- INIT ---------- */ - - init(); - - function init() { - - entityResource.getAll("Template").then(function(templates){ - - vm.availableTemplates = templates; - - // update placeholder template information on new doc types - if (!$routeParams.notemplate && $scope.model.id === 0) { - vm.updateTemplatePlaceholder = true; - vm.availableTemplates = contentTypeHelper.insertTemplatePlaceholder(vm.availableTemplates); - } - - }); - - } - - } - - angular.module("umbraco").controller("Umbraco.Editors.DocumentType.TemplatesController", TemplatesController); -})(); + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.move = function () { + + $scope.busy = true; + $scope.error = false; + + contentTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + + navigationService.syncTree({ tree: "documentTypes", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "documentTypes", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + eventsService.emit('app.refreshEditor'); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); + }); + +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.PropertyController + * @function + * + * @description + * The controller for the content type editor property dialog + */ +(function() { + 'use strict'; + + function PermissionsController($scope, contentTypeResource, iconHelper, contentTypeHelper, localizationService) { + + /* ----------- SCOPE VARIABLES ----------- */ + + var vm = this; + var childNodeSelectorOverlayTitle = ""; + + vm.contentTypes = []; + vm.selectedChildren = []; + + vm.overlayTitle = ""; + + vm.addChild = addChild; + vm.removeChild = removeChild; + + /* ---------- INIT ---------- */ + + init(); + + function init() { + + childNodeSelectorOverlayTitle = localizationService.localize("contentTypeEditor_chooseChildNode"); + + contentTypeResource.getAll().then(function(contentTypes){ + + vm.contentTypes = contentTypes; + + // convert legacy icons + iconHelper.formatContentTypeIcons(vm.contentTypes); + + vm.selectedChildren = contentTypeHelper.makeObjectArrayFromId($scope.model.allowedContentTypes, vm.contentTypes); + + if($scope.model.id === 0) { + contentTypeHelper.insertChildNodePlaceholder(vm.contentTypes, $scope.model.name, $scope.model.icon, $scope.model.id); + } + + }); + + } + + function addChild($event) { + vm.childNodeSelectorOverlay = { + view: "itempicker", + title: childNodeSelectorOverlayTitle, + availableItems: vm.contentTypes, + selectedItems: vm.selectedChildren, + event: $event, + show: true, + submit: function(model) { + vm.selectedChildren.push(model.selectedItem); + $scope.model.allowedContentTypes.push(model.selectedItem.id); + vm.childNodeSelectorOverlay.show = false; + vm.childNodeSelectorOverlay = null; + } + }; + } + + function removeChild(selectedChild, index) { + // remove from vm + vm.selectedChildren.splice(index, 1); + + // remove from content type model + var selectedChildIndex = $scope.model.allowedContentTypes.indexOf(selectedChild.id); + $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); + } + + } + + angular.module("umbraco").controller("Umbraco.Editors.DocumentType.PermissionsController", PermissionsController); +})(); + +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.TemplatesController + * @function + * + * @description + * The controller for the content type editor templates sub view + */ +(function() { + 'use strict'; + + function TemplatesController($scope, entityResource, contentTypeHelper, $routeParams) { + + /* ----------- SCOPE VARIABLES ----------- */ + + var vm = this; + + vm.availableTemplates = []; + vm.updateTemplatePlaceholder = false; + + + /* ---------- INIT ---------- */ + + init(); + + function init() { + + entityResource.getAll("Template").then(function(templates){ + + vm.availableTemplates = templates; + + // update placeholder template information on new doc types + if (!$routeParams.notemplate && $scope.model.id === 0) { + vm.updateTemplatePlaceholder = true; + vm.availableTemplates = contentTypeHelper.insertTemplatePlaceholder(vm.availableTemplates); + } + + }); + + } + + } + + angular.module("umbraco").controller("Umbraco.Editors.DocumentType.TemplatesController", TemplatesController); +})(); /** * @ngdoc controller @@ -7243,78 +7243,78 @@ function mediaCreateController($scope, $routeParams, mediaTypeResource, iconHelp } angular.module('umbraco').controller("Umbraco.Editors.Media.CreateController", mediaCreateController); -/** - * @ngdoc controller - * @name Umbraco.Editors.ContentDeleteController - * @function - * - * @description - * The controller for deleting content - */ -function MediaDeleteController($scope, mediaResource, treeService, navigationService, editorState, $location, dialogService, notificationsService) { - - $scope.performDelete = function() { - - // stop from firing again on double-click - if ($scope.busy) { return false; } - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - $scope.busy = true; - - mediaResource.deleteById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - treeService.removeNode($scope.currentNode); - - if (rootNode) { - //ensure the recycle bin has child nodes now - var recycleBin = treeService.getDescendantNode(rootNode, -21); - if (recycleBin) { - recycleBin.hasChildren = true; - } - } - - //if the current edited item is the same one as we're deleting, we need to navigate elsewhere - if (editorState.current && editorState.current.id == $scope.currentNode.id) { - - //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent - var location = "/media"; - if ($scope.currentNode.parentId.toString() !== "-1") - location = "/media/media/edit/" + $scope.currentNode.parentId; - - $location.path(location); - } - - navigationService.hideMenu(); - - }, function (err) { - - $scope.currentNode.loading = false; - $scope.busy = false; - - //check if response is ysod - if (err.status && err.status >= 500) { - dialogService.ysodDialog(err); - } - - if (err.data && angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - }); - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.Media.DeleteController", MediaDeleteController); +/** + * @ngdoc controller + * @name Umbraco.Editors.ContentDeleteController + * @function + * + * @description + * The controller for deleting content + */ +function MediaDeleteController($scope, mediaResource, treeService, navigationService, editorState, $location, dialogService, notificationsService) { + + $scope.performDelete = function() { + + // stop from firing again on double-click + if ($scope.busy) { return false; } + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + $scope.busy = true; + + mediaResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + + treeService.removeNode($scope.currentNode); + + if (rootNode) { + //ensure the recycle bin has child nodes now + var recycleBin = treeService.getDescendantNode(rootNode, -21); + if (recycleBin) { + recycleBin.hasChildren = true; + } + } + + //if the current edited item is the same one as we're deleting, we need to navigate elsewhere + if (editorState.current && editorState.current.id == $scope.currentNode.id) { + + //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent + var location = "/media"; + if ($scope.currentNode.parentId.toString() !== "-1") + location = "/media/media/edit/" + $scope.currentNode.parentId; + + $location.path(location); + } + + navigationService.hideMenu(); + + }, function (err) { + + $scope.currentNode.loading = false; + $scope.busy = false; + + //check if response is ysod + if (err.status && err.status >= 500) { + dialogService.ysodDialog(err); + } + + if (err.data && angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + + $scope.cancel = function() { + navigationService.hideDialog(); + }; +} + +angular.module("umbraco").controller("Umbraco.Editors.Media.DeleteController", MediaDeleteController); /** * @ngdoc controller @@ -7468,333 +7468,333 @@ function mediaEditController($scope, $routeParams, appState, mediaResource, enti angular.module("umbraco") .controller("Umbraco.Editors.Media.EditController", mediaEditController); -/** - * @ngdoc controller - * @name Umbraco.Editors.Media.EmptyRecycleBinController - * @function - * - * @description - * The controller for deleting media - */ -function MediaEmptyRecycleBinController($scope, mediaResource, treeService, navigationService, notificationsService, $route) { - - $scope.busy = false; - - $scope.performDelete = function() { - - //(used in the UI) - $scope.busy = true; - $scope.currentNode.loading = true; - - mediaResource.emptyRecycleBin($scope.currentNode.id).then(function (result) { - - $scope.busy = false; - $scope.currentNode.loading = false; - - //show any notifications - if (angular.isArray(result.notifications)) { - for (var i = 0; i < result.notifications.length; i++) { - notificationsService.showNotification(result.notifications[i]); - } - } - - treeService.removeChildNodes($scope.currentNode); - navigationService.hideMenu(); - - //reload the current view - $route.reload(); - }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.Media.EmptyRecycleBinController", MediaEmptyRecycleBinController); +/** + * @ngdoc controller + * @name Umbraco.Editors.Media.EmptyRecycleBinController + * @function + * + * @description + * The controller for deleting media + */ +function MediaEmptyRecycleBinController($scope, mediaResource, treeService, navigationService, notificationsService, $route) { -//used for the media picker dialog -angular.module("umbraco").controller("Umbraco.Editors.Media.MoveController", - function ($scope, eventsService, mediaResource, appState, treeService, navigationService) { - var dialogOptions = $scope.dialogOptions; - - $scope.dialogTreeEventHandler = $({}); - var node = dialogOptions.currentNode; - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - eventsService.emit("editors.media.moveController.select", args); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; - } - - $scope.target = args.node; - $scope.target.selected = true; - } - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - - $scope.move = function () { - mediaResource.move({ parentId: $scope.target.id, id: node.id }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - - //first we need to remove the node that launched the dialog - treeService.removeNode($scope.currentNode); - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the moved content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was moved!!) - - navigationService.syncTree({ tree: "media", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "media", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - }, function (err) { - $scope.success = false; - $scope.error = err; - }); - }; - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); + $scope.busy = false; + + $scope.performDelete = function() { + + //(used in the UI) + $scope.busy = true; + $scope.currentNode.loading = true; + + mediaResource.emptyRecycleBin($scope.currentNode.id).then(function (result) { + + $scope.busy = false; + $scope.currentNode.loading = false; + + //show any notifications + if (angular.isArray(result.notifications)) { + for (var i = 0; i < result.notifications.length; i++) { + notificationsService.showNotification(result.notifications[i]); + } + } + + treeService.removeChildNodes($scope.currentNode); + navigationService.hideMenu(); + + //reload the current view + $route.reload(); + }); + + }; + + $scope.cancel = function() { + navigationService.hideDialog(); + }; +} + +angular.module("umbraco").controller("Umbraco.Editors.Media.EmptyRecycleBinController", MediaEmptyRecycleBinController); + +//used for the media picker dialog +angular.module("umbraco").controller("Umbraco.Editors.Media.MoveController", + function ($scope, eventsService, mediaResource, appState, treeService, navigationService) { + var dialogOptions = $scope.dialogOptions; + + $scope.dialogTreeEventHandler = $({}); + var node = dialogOptions.currentNode; + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + eventsService.emit("editors.media.moveController.select", args); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + + $scope.move = function () { + mediaResource.move({ parentId: $scope.target.id, id: node.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + + navigationService.syncTree({ tree: "media", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "media", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + }, function (err) { + $scope.success = false; + $scope.error = err; + }); + }; + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); }); -/** - * @ngdoc controller - * @name Umbraco.Editors.Content.MediaRecycleBinController - * @function - * - * @description - * Controls the recycle bin for media - * - */ - -function MediaRecycleBinController($scope, $routeParams, mediaResource, navigationService, localizationService) { - - //ensures the list view doesn't actually load until we query for the list view config - // for the section - $scope.page = {}; - $scope.page.name = "Recycle Bin"; - $scope.page.nameLocked = true; - - //ensures the list view doesn't actually load until we query for the list view config - // for the section - $scope.listViewPath = null; - - $routeParams.id = "-21"; - mediaResource.getRecycleBin().then(function (result) { - //we'll get the 'content item' for the recycle bin, we know that it will contain a single tab and a - // single property, so we'll extract that property (list view) and use it's data. - var listproperty = result.tabs[0].properties[0]; - - _.each(listproperty.config, function (val, key) { - $scope.model.config[key] = val; - }); - $scope.listViewPath = 'views/propertyeditors/listview/listview.html'; - }); - - $scope.model = { config: { entityType: $routeParams.section, layouts: [] } }; - - // sync tree node - navigationService.syncTree({ tree: "media", path: ["-1", $routeParams.id], forceReload: false }); - - localizePageName(); - - function localizePageName() { - - var pageName = "general_recycleBin"; - - localizationService.localize(pageName).then(function (value) { - $scope.page.name = value; - }); - - } -} - -angular.module('umbraco').controller("Umbraco.Editors.Media.RecycleBinController", MediaRecycleBinController); +/** + * @ngdoc controller + * @name Umbraco.Editors.Content.MediaRecycleBinController + * @function + * + * @description + * Controls the recycle bin for media + * + */ -angular.module("umbraco") -.controller("Umbraco.Editors.MediaTypes.CopyController", - function ($scope, mediaTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { - var dialogOptions = $scope.dialogOptions; - $scope.dialogTreeEventHandler = $({}); - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; - } - - $scope.target = args.node; - $scope.target.selected = true; - } - - $scope.copy = function () { - - $scope.busy = true; - $scope.error = false; - - mediaTypeResource.copy({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - $scope.busy = false; - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the copied content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was copied!!) - - navigationService.syncTree({ tree: "mediaTypes", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "mediaTypes", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - }, function (err) { - $scope.success = false; - $scope.error = err; - $scope.busy = false; - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - }); - }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - }); +function MediaRecycleBinController($scope, $routeParams, mediaResource, navigationService, localizationService) { -/** - * @ngdoc controller - * @name Umbraco.Editors.MediaType.CreateController - * @function - * - * @description - * The controller for the media type creation dialog - */ -function MediaTypesCreateController($scope, $location, navigationService, mediaTypeResource, formHelper, appState, localizationService) { - - $scope.model = { - folderName: "", - creatingFolder: false - }; - - var node = $scope.dialogOptions.currentNode, - localizeCreateFolder = localizationService.localize("defaultdialog_createFolder"); - - $scope.showCreateFolder = function() { - $scope.model.creatingFolder = true; - } - - $scope.createContainer = function () { - if (formHelper.submitForm({ - scope: $scope, - formCtrl: this.createFolderForm, - statusMessage: localizeCreateFolder - })) { - mediaTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { - - navigationService.hideMenu(); - var currPath = node.path ? node.path : "-1"; - navigationService.syncTree({ tree: "mediatypes", path: currPath + "," + folderId, forceReload: true, activate: true }); - - formHelper.resetForm({ scope: $scope }); - - var section = appState.getSectionState("currentSection"); - - }, function(err) { - - //TODO: Handle errors - }); - }; - } - - $scope.createMediaType = function() { - $location.search('create', null); - $location.path("/settings/mediatypes/edit/" + node.id).search("create", "true"); - navigationService.hideMenu(); - } -} - -angular.module('umbraco').controller("Umbraco.Editors.MediaTypes.CreateController", MediaTypesCreateController); + //ensures the list view doesn't actually load until we query for the list view config + // for the section + $scope.page = {}; + $scope.page.name = "Recycle Bin"; + $scope.page.nameLocked = true; -/** - * @ngdoc controller - * @name Umbraco.Editors.MediaType.DeleteController - * @function - * - * @description - * The controller for the media type delete dialog - */ -function MediaTypesDeleteController($scope, dataTypeResource, mediaTypeResource, treeService, navigationService) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - mediaTypeResource.deleteById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.performContainerDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - mediaTypeResource.deleteContainerById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.MediaTypes.DeleteController", MediaTypesDeleteController); + //ensures the list view doesn't actually load until we query for the list view config + // for the section + $scope.listViewPath = null; + + $routeParams.id = "-21"; + mediaResource.getRecycleBin().then(function (result) { + //we'll get the 'content item' for the recycle bin, we know that it will contain a single tab and a + // single property, so we'll extract that property (list view) and use it's data. + var listproperty = result.tabs[0].properties[0]; + + _.each(listproperty.config, function (val, key) { + $scope.model.config[key] = val; + }); + $scope.listViewPath = 'views/propertyeditors/listview/listview.html'; + }); + + $scope.model = { config: { entityType: $routeParams.section, layouts: [] } }; + + // sync tree node + navigationService.syncTree({ tree: "media", path: ["-1", $routeParams.id], forceReload: false }); + + localizePageName(); + + function localizePageName() { + + var pageName = "general_recycleBin"; + + localizationService.localize(pageName).then(function (value) { + $scope.page.name = value; + }); + + } +} + +angular.module('umbraco').controller("Umbraco.Editors.Media.RecycleBinController", MediaRecycleBinController); + +angular.module("umbraco") +.controller("Umbraco.Editors.MediaTypes.CopyController", + function ($scope, mediaTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.copy = function () { + + $scope.busy = true; + $scope.error = false; + + mediaTypeResource.copy({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the copied content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was copied!!) + + navigationService.syncTree({ tree: "mediaTypes", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "mediaTypes", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); + }); + +/** + * @ngdoc controller + * @name Umbraco.Editors.MediaType.CreateController + * @function + * + * @description + * The controller for the media type creation dialog + */ +function MediaTypesCreateController($scope, $location, navigationService, mediaTypeResource, formHelper, appState, localizationService) { + + $scope.model = { + folderName: "", + creatingFolder: false + }; + + var node = $scope.dialogOptions.currentNode, + localizeCreateFolder = localizationService.localize("defaultdialog_createFolder"); + + $scope.showCreateFolder = function() { + $scope.model.creatingFolder = true; + } + + $scope.createContainer = function () { + if (formHelper.submitForm({ + scope: $scope, + formCtrl: this.createFolderForm, + statusMessage: localizeCreateFolder + })) { + mediaTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { + + navigationService.hideMenu(); + var currPath = node.path ? node.path : "-1"; + navigationService.syncTree({ tree: "mediatypes", path: currPath + "," + folderId, forceReload: true, activate: true }); + + formHelper.resetForm({ scope: $scope }); + + var section = appState.getSectionState("currentSection"); + + }, function(err) { + + //TODO: Handle errors + }); + }; + } + + $scope.createMediaType = function() { + $location.search('create', null); + $location.path("/settings/mediatypes/edit/" + node.id).search("create", "true"); + navigationService.hideMenu(); + } +} + +angular.module('umbraco').controller("Umbraco.Editors.MediaTypes.CreateController", MediaTypesCreateController); + +/** + * @ngdoc controller + * @name Umbraco.Editors.MediaType.DeleteController + * @function + * + * @description + * The controller for the media type delete dialog + */ +function MediaTypesDeleteController($scope, dataTypeResource, mediaTypeResource, treeService, navigationService) { + + $scope.performDelete = function() { + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + mediaTypeResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + + }; + + $scope.performContainerDelete = function() { + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + mediaTypeResource.deleteContainerById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + + }; + + $scope.cancel = function() { + navigationService.hideDialog(); + }; +} + +angular.module("umbraco").controller("Umbraco.Editors.MediaTypes.DeleteController", MediaTypesDeleteController); /** * @ngdoc controller @@ -8145,147 +8145,147 @@ angular.module("umbraco").controller("Umbraco.Editors.MediaTypes.DeleteControlle angular.module("umbraco").controller("Umbraco.Editors.MediaTypes.EditController", MediaTypesEditController); })(); -angular.module("umbraco") -.controller("Umbraco.Editors.MediaTypes.MoveController", - function ($scope, mediaTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { - - var dialogOptions = $scope.dialogOptions; - $scope.dialogTreeEventHandler = $({}); - - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if ($scope.target) { - //un-select if there's a current one selected - $scope.target.selected = false; - } - - $scope.target = args.node; - $scope.target.selected = true; - } - - $scope.move = function () { - - $scope.busy = true; - $scope.error = false; - - mediaTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) - .then(function (path) { - $scope.error = false; - $scope.success = true; - $scope.busy = false; - - //first we need to remove the node that launched the dialog - treeService.removeNode($scope.currentNode); - - //get the currently edited node (if any) - var activeNode = appState.getTreeState("selectedNode"); - - //we need to do a double sync here: first sync to the moved content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was moved!!) - - navigationService.syncTree({ tree: "mediaTypes", path: path, forceReload: true, activate: false }).then(function (args) { - if (activeNode) { - var activeNodePath = treeService.getPath(activeNode).join(); - //sync to this node now - depending on what was copied this might already be synced but might not be - navigationService.syncTree({ tree: "mediaTypes", path: activeNodePath, forceReload: false, activate: true }); - } - }); - - eventsService.emit('app.refreshEditor'); - - }, function (err) { - $scope.success = false; - $scope.error = err; - $scope.busy = false; - //show any notifications - if (angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - }); - }; - - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - }); +angular.module("umbraco") +.controller("Umbraco.Editors.MediaTypes.MoveController", + function ($scope, mediaTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { -(function() { - 'use strict'; - - function PermissionsController($scope, mediaTypeResource, iconHelper, contentTypeHelper, localizationService) { - - /* ----------- SCOPE VARIABLES ----------- */ - - var vm = this; - var childNodeSelectorOverlayTitle = ""; - - vm.mediaTypes = []; - vm.selectedChildren = []; - - vm.addChild = addChild; - vm.removeChild = removeChild; - - /* ---------- INIT ---------- */ - - init(); - - function init() { - - childNodeSelectorOverlayTitle = localizationService.localize("contentTypeEditor_chooseChildNode"); - - mediaTypeResource.getAll().then(function(mediaTypes){ - - vm.mediaTypes = mediaTypes; - - // convert legacy icons - iconHelper.formatContentTypeIcons(vm.mediaTypes); - - vm.selectedChildren = contentTypeHelper.makeObjectArrayFromId($scope.model.allowedContentTypes, vm.mediaTypes); - - if($scope.model.id === 0) { - contentTypeHelper.insertChildNodePlaceholder(vm.mediaTypes, $scope.model.name, $scope.model.icon, $scope.model.id); - } - - }); - - } - - function addChild($event) { - vm.childNodeSelectorOverlay = { - view: "itempicker", - title: childNodeSelectorOverlayTitle, - availableItems: vm.mediaTypes, - selectedItems: vm.selectedChildren, - event: $event, - show: true, - submit: function(model) { - vm.selectedChildren.push(model.selectedItem); - $scope.model.allowedContentTypes.push(model.selectedItem.id); - vm.childNodeSelectorOverlay.show = false; - vm.childNodeSelectorOverlay = null; - } - }; - } - - function removeChild(selectedChild, index) { - // remove from vm - vm.selectedChildren.splice(index, 1); - - // remove from content type model - var selectedChildIndex = $scope.model.allowedContentTypes.indexOf(selectedChild.id); - $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); - } - - } - - angular.module("umbraco").controller("Umbraco.Editors.MediaType.PermissionsController", PermissionsController); -})(); + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.move = function () { + + $scope.busy = true; + $scope.error = false; + + mediaTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + + navigationService.syncTree({ tree: "mediaTypes", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "mediaTypes", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + eventsService.emit('app.refreshEditor'); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); + }); + +(function() { + 'use strict'; + + function PermissionsController($scope, mediaTypeResource, iconHelper, contentTypeHelper, localizationService) { + + /* ----------- SCOPE VARIABLES ----------- */ + + var vm = this; + var childNodeSelectorOverlayTitle = ""; + + vm.mediaTypes = []; + vm.selectedChildren = []; + + vm.addChild = addChild; + vm.removeChild = removeChild; + + /* ---------- INIT ---------- */ + + init(); + + function init() { + + childNodeSelectorOverlayTitle = localizationService.localize("contentTypeEditor_chooseChildNode"); + + mediaTypeResource.getAll().then(function(mediaTypes){ + + vm.mediaTypes = mediaTypes; + + // convert legacy icons + iconHelper.formatContentTypeIcons(vm.mediaTypes); + + vm.selectedChildren = contentTypeHelper.makeObjectArrayFromId($scope.model.allowedContentTypes, vm.mediaTypes); + + if($scope.model.id === 0) { + contentTypeHelper.insertChildNodePlaceholder(vm.mediaTypes, $scope.model.name, $scope.model.icon, $scope.model.id); + } + + }); + + } + + function addChild($event) { + vm.childNodeSelectorOverlay = { + view: "itempicker", + title: childNodeSelectorOverlayTitle, + availableItems: vm.mediaTypes, + selectedItems: vm.selectedChildren, + event: $event, + show: true, + submit: function(model) { + vm.selectedChildren.push(model.selectedItem); + $scope.model.allowedContentTypes.push(model.selectedItem.id); + vm.childNodeSelectorOverlay.show = false; + vm.childNodeSelectorOverlay = null; + } + }; + } + + function removeChild(selectedChild, index) { + // remove from vm + vm.selectedChildren.splice(index, 1); + + // remove from content type model + var selectedChildIndex = $scope.model.allowedContentTypes.indexOf(selectedChild.id); + $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); + } + + } + + angular.module("umbraco").controller("Umbraco.Editors.MediaType.PermissionsController", PermissionsController); +})(); /** * @ngdoc controller @@ -8518,1170 +8518,1170 @@ function MemberEditController($scope, $routeParams, $location, $q, $window, appS angular.module("umbraco").controller("Umbraco.Editors.Member.EditController", MemberEditController); -/** - * @ngdoc controller - * @name Umbraco.Editors.Member.ListController - * @function - * - * @description - * The controller for the member list view - */ -function MemberListController($scope, $routeParams, $location, $q, $window, appState, memberResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, formHelper, umbModelMapper, editorState, localizationService) { - - //setup scope vars - $scope.currentSection = appState.getSectionState("currentSection"); - $scope.currentNode = null; //the editors affiliated node - - $scope.page = {}; - $scope.page.lockedName = true; - $scope.page.loading = true; - - //we are editing so get the content item from the server - memberResource.getListNode($routeParams.id) - .then(function (data) { - - $scope.content = data; - - //translate "All Members" - if ($scope.content != null && $scope.content.name != null && $scope.content.name.replace(" ", "").toLowerCase() == "allmembers") { - localizationService.localize("member_allMembers").then(function (value) { - $scope.content.name = value; - }); - } - - editorState.set($scope.content); - - navigationService.syncTree({ tree: "member", path: data.path.split(",") }).then(function (syncArgs) { - $scope.currentNode = syncArgs.node; - }); - - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.executeAndClearAllSubscriptions(); - - $scope.page.loading = false; - - }); -} - -angular.module("umbraco").controller("Umbraco.Editors.Member.ListController", MemberListController); +/** + * @ngdoc controller + * @name Umbraco.Editors.Member.ListController + * @function + * + * @description + * The controller for the member list view + */ +function MemberListController($scope, $routeParams, $location, $q, $window, appState, memberResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, formHelper, umbModelMapper, editorState, localizationService) { -/** - * @ngdoc controller - * @name Umbraco.Editors.MemberType.CreateController - * @function - * - * @description - * The controller for the member type creation dialog - */ -function MemberTypesCreateController($scope, $location, navigationService, memberTypeResource, formHelper, appState, localizationService) { - - $scope.model = { - folderName: "", - creatingFolder: false - }; - - var node = $scope.dialogOptions.currentNode, - localizeCreateFolder = localizationService.localize("defaultdialog_createFolder"); - - - $scope.showCreateFolder = function() { - $scope.model.creatingFolder = true; - } - - $scope.createContainer = function () { - if (formHelper.submitForm({ - scope: $scope, - formCtrl: this.createFolderForm, - statusMessage: localizeCreateFolder - })) { - memberTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { - - navigationService.hideMenu(); - var currPath = node.path ? node.path : "-1"; - navigationService.syncTree({ tree: "membertypes", path: currPath + "," + folderId, forceReload: true, activate: true }); - - formHelper.resetForm({ scope: $scope }); - - var section = appState.getSectionState("currentSection"); - - }, function(err) { - - //TODO: Handle errors - }); - }; - } - - $scope.createMemberType = function() { - $location.search('create', null); - $location.path("/settings/membertypes/edit/" + node.id).search("create", "true"); - navigationService.hideMenu(); - } -} - -angular.module('umbraco').controller("Umbraco.Editors.MemberTypes.CreateController", MemberTypesCreateController); + //setup scope vars + $scope.currentSection = appState.getSectionState("currentSection"); + $scope.currentNode = null; //the editors affiliated node -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.DeleteController - * @function - * - * @description - * The controller for deleting content - */ -function MemberTypesDeleteController($scope, memberTypeResource, treeService, navigationService) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - memberTypeResource.deleteById($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - //TODO: Need to sync tree, etc... - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.MemberTypes.DeleteController", MemberTypesDeleteController); + $scope.page = {}; + $scope.page.lockedName = true; + $scope.page.loading = true; -/** - * @ngdoc controller - * @name Umbraco.Editors.MemberType.EditController - * @function - * - * @description - * The controller for the member type editor - */ -(function () { - "use strict"; - - function MemberTypesEditController($scope, $rootScope, $routeParams, $log, $filter, memberTypeResource, dataTypeResource, editorState, iconHelper, formHelper, navigationService, contentEditingHelper, notificationsService, $q, localizationService, overlayHelper, contentTypeHelper) { - - var vm = this; - var localizeSaving = localizationService.localize("general_saving"); - - vm.save = save; - - vm.currentNode = null; - vm.contentType = {}; - vm.page = {}; - vm.page.loading = false; - vm.page.saveButtonState = "init"; - vm.page.navigation = [ - { - "name": localizationService.localize("general_design"), - "icon": "icon-document-dashed-line", - "view": "views/membertypes/views/design/design.html", - "active": true - } - ]; - - vm.page.keyboardShortcutsOverview = [ - { - "name": localizationService.localize("shortcuts_shortcut"), - "shortcuts": [ - { - "description": localizationService.localize("shortcuts_addTab"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] - }, - { - "description": localizationService.localize("shortcuts_addProperty"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] - }, - { - "description": localizationService.localize("shortcuts_addEditor"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "e" }] - }, - { - "description": localizationService.localize("shortcuts_editDataType"), - "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] - } - ] - } - ]; - - contentTypeHelper.checkModelsBuilderStatus().then(function (result) { - vm.page.modelsBuilder = result; - if (result) { - //Models builder mode: - vm.page.defaultButton = { - hotKey: "ctrl+s", - hotKeyWhenHidden: true, - labelKey: "buttons_save", - letter: "S", - type: "submit", - handler: function () { vm.save(); } - }; - vm.page.subButtons = [{ - hotKey: "ctrl+g", - hotKeyWhenHidden: true, - labelKey: "buttons_saveAndGenerateModels", - letter: "G", - handler: function () { - - vm.page.saveButtonState = "busy"; - - vm.save().then(function (result) { - - vm.page.saveButtonState = "busy"; - - localizationService.localize("modelsBuilder_buildingModels").then(function (headerValue) { - localizationService.localize("modelsBuilder_waitingMessage").then(function(msgValue) { - notificationsService.info(headerValue, msgValue); - }); - }); - - contentTypeHelper.generateModels().then(function (result) { - - if (result.success) { - - //re-check model status - contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { - vm.page.modelsBuilder = statusResult; - }); - - //clear and add success - vm.page.saveButtonState = "init"; - localizationService.localize("modelsBuilder_modelsGenerated").then(function(value) { - notificationsService.success(value); - }); - - } else { - vm.page.saveButtonState = "error"; - localizationService.localize("modelsBuilder_modelsExceptionInUlog").then(function(value) { - notificationsService.error(value); - }); - } - - }, function () { - vm.page.saveButtonState = "error"; - localizationService.localize("modelsBuilder_modelsGeneratedError").then(function(value) { - notificationsService.error(value); - }); - }); - - - }); - - } - }]; - } - }); - - if ($routeParams.create) { - - vm.page.loading = true; - - //we are creating so get an empty data type item - memberTypeResource.getScaffold($routeParams.id) - .then(function (dt) { - init(dt); - - vm.page.loading = false; - }); - } - else { - - vm.page.loading = true; - - memberTypeResource.getById($routeParams.id).then(function (dt) { - init(dt); - - syncTreeNode(vm.contentType, dt.path, true); - - vm.page.loading = false; - }); - } - - function save() { - // only save if there is no overlays open - if(overlayHelper.getNumberOfOverlays() === 0) { - - var deferred = $q.defer(); - - vm.page.saveButtonState = "busy"; - - contentEditingHelper.contentEditorPerformSave({ - statusMessage: localizeSaving, - saveMethod: memberTypeResource.save, - scope: $scope, - content: vm.contentType, - //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc - // type when server side validation fails - as opposed to content where we are capable of saving the content - // item if server side validation fails - redirectOnFailure: false, - // we need to rebind... the IDs that have been created! - rebindCallback: function (origContentType, savedContentType) { - vm.contentType.id = savedContentType.id; - vm.contentType.groups.forEach(function (group) { - if (!group.name) return; - - var k = 0; - while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) - k++; - if (k == savedContentType.groups.length) { - group.id = 0; - return; - } - - var savedGroup = savedContentType.groups[k]; - if (!group.id) group.id = savedGroup.id; - - group.properties.forEach(function (property) { - if (property.id || !property.alias) return; - - k = 0; - while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) - k++; - if (k == savedGroup.properties.length) { - property.id = 0; - return; - } - - var savedProperty = savedGroup.properties[k]; - property.id = savedProperty.id; - }); - }); - } - }).then(function (data) { - //success - syncTreeNode(vm.contentType, data.path); - - vm.page.saveButtonState = "success"; - - deferred.resolve(data); - }, function (err) { - //error - if (err) { - editorState.set($scope.content); - } - else { - localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { - notificationsService.error(headerValue, msgValue); - }); - }); - } - - vm.page.saveButtonState = "error"; - - deferred.reject(err); - }); - - return deferred.promise; - } - - } - - function init(contentType) { - - // set all tab to inactive - if (contentType.groups.length !== 0) { - angular.forEach(contentType.groups, function (group) { - - angular.forEach(group.properties, function (property) { - // get data type details for each property - getDataTypeDetails(property); - }); - - }); - } - - // convert legacy icons - convertLegacyIcons(contentType); - - //set a shared state - editorState.set(contentType); - - vm.contentType = contentType; - - } - - function convertLegacyIcons(contentType) { - - // make array to store contentType icon - var contentTypeArray = []; - - // push icon to array - contentTypeArray.push({ "icon": contentType.icon }); - - // run through icon method - iconHelper.formatContentTypeIcons(contentTypeArray); - - // set icon back on contentType - contentType.icon = contentTypeArray[0].icon; - - } - - function getDataTypeDetails(property) { - - if (property.propertyState !== "init") { - - dataTypeResource.getById(property.dataTypeId) - .then(function (dataType) { - property.dataTypeIcon = dataType.icon; - property.dataTypeName = dataType.name; - }); - } - } - - /** Syncs the content type to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(dt, path, initialLoad) { - - navigationService.syncTree({ tree: "membertypes", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - vm.currentNode = syncArgs.node; - }); - - } - - - } - - angular.module("umbraco").controller("Umbraco.Editors.MemberTypes.EditController", MemberTypesEditController); - -})(); + //we are editing so get the content item from the server + memberResource.getListNode($routeParams.id) + .then(function (data) { -angular.module("umbraco") -.controller("Umbraco.Editors.MemberTypes.MoveController", - function($scope){ - - }); + $scope.content = data; -/** - * @ngdoc controller - * @name Umbraco.Editors.Packages.DeleteController - * @function - * - * @description - * The controller for deleting content - */ -function PackageDeleteController($scope, packageResource, treeService, navigationService) { - - $scope.performDelete = function() { - - //mark it for deletion (used in the UI) - $scope.currentNode.loading = true; - packageResource.deleteCreatedPackage($scope.currentNode.id).then(function () { - $scope.currentNode.loading = false; - - //get the root node before we remove it - var rootNode = treeService.getTreeRoot($scope.currentNode); - - treeService.removeNode($scope.currentNode); - navigationService.hideMenu(); - }); - - }; - - $scope.cancel = function() { - navigationService.hideDialog(); - }; -} - -angular.module("umbraco").controller("Umbraco.Editors.Packages.DeleteController", PackageDeleteController); + //translate "All Members" + if ($scope.content != null && $scope.content.name != null && $scope.content.name.replace(" ", "").toLowerCase() == "allmembers") { + localizationService.localize("member_allMembers").then(function (value) { + $scope.content.name = value; + }); + } -(function () { - "use strict"; - - function PackagesOverviewController($scope, $route, $location, navigationService, $timeout, localStorageService) { - - //Hack! - // if there is a cookie value for packageInstallUri then we need to redirect there, - // the issue is that we still have webforms and we cannot go to a hash location and then window.reload - // because it will double load it. - // we will refresh and then navigate there. - - var installPackageUri = localStorageService.get("packageInstallUri"); - if (installPackageUri) { - localStorageService.remove("packageInstallUri"); - } - if (installPackageUri && installPackageUri !== "installed") { - //navigate to the custom installer screen, if it is just "installed", then we'll - //show the installed view - $location.path(installPackageUri).search(""); - } - else { - var vm = this; - - vm.page = {}; - vm.page.name = "Packages"; - vm.page.navigation = [ - { - "name": "Packages", - "icon": "icon-cloud", - "view": "views/packager/views/repo.html", - "active": !installPackageUri || installPackageUri === "navigation" - }, - { - "name": "Installed", - "icon": "icon-box", - "view": "views/packager/views/installed.html", - "active": installPackageUri === "installed" - }, - { - "name": "Install local", - "icon": "icon-add", - "view": "views/packager/views/install-local.html", - "active": installPackageUri === "local" - } - ]; - - $timeout(function () { - navigationService.syncTree({ tree: "packager", path: "-1" }); - }); - } - - } - - angular.module("umbraco").controller("Umbraco.Editors.Packages.OverviewController", PackagesOverviewController); - -})(); + editorState.set($scope.content); -(function () { - "use strict"; - - function PackagesInstallLocalController($scope, $route, $location, Upload, umbRequestHelper, packageResource, localStorageService, $timeout, $window, localizationService) { - - var vm = this; - vm.state = "upload"; - - vm.localPackage = {}; - vm.installPackage = installPackage; - vm.installState = { - status: "", - progress:0 - }; - vm.installCompleted = false; - vm.zipFile = { - uploadStatus: "idle", - uploadProgress: 0, - serverErrorMessage: null - }; - - $scope.handleFiles = function (files, event) { - if (files) { - for (var i = 0; i < files.length; i++) { - upload(files[i]); - } - } - }; - - function upload(file) { - - Upload.upload({ - url: umbRequestHelper.getApiUrl("packageInstallApiBaseUrl", "UploadLocalPackage"), - fields: {}, - file: file - }).progress(function (evt) { - - // set view state to uploading - vm.state = 'uploading'; - - // calculate progress in percentage - var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); - - // set percentage property on file - vm.zipFile.uploadProgress = progressPercentage; - - // set uploading status on file - vm.zipFile.uploadStatus = "uploading"; - - }).success(function (data, status, headers, config) { - - if (data.notifications && data.notifications.length > 0) { - - // set error status on file - vm.zipFile.uploadStatus = "error"; - - // Throw message back to user with the cause of the error - vm.zipFile.serverErrorMessage = data.notifications[0].message; - - } else { - - // set done status on file - vm.zipFile.uploadStatus = "done"; - loadPackage(); - vm.localPackage = data; - } - - }).error(function (evt, status, headers, config) { - - // set status done - vm.zipFile.uploadStatus = "error"; - - // If file not found, server will return a 404 and display this message - if (status === 404) { - vm.zipFile.serverErrorMessage = "File not found"; - } - else if (status == 400) { - //it's a validation error - vm.zipFile.serverErrorMessage = evt.message; - } - else { - //it's an unhandled error - //if the service returns a detailed error - if (evt.InnerException) { - vm.zipFile.serverErrorMessage = evt.InnerException.ExceptionMessage; - - //Check if its the common "too large file" exception - if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { - vm.zipFile.serverErrorMessage = "File too large to upload"; - } - - } else if (evt.Message) { - file.serverErrorMessage = evt.Message; - } - } - }); - } - - function loadPackage() { - if (vm.zipFile.uploadStatus === "done") { - vm.state = "packageDetails"; - } - } - - function installPackage() { - vm.installState.status = localizationService.localize("packager_installStateImporting"); - vm.installState.progress = "0"; - - packageResource - .import(vm.localPackage) - .then(function(pack) { - vm.installState.progress = "25"; - vm.installState.status = localizationService.localize("packager_installStateInstalling"); - vm.installState.progress = "50"; - return packageResource.installFiles(pack); - }, - installError) - .then(function(pack) { - vm.installState.status = localizationService.localize("packager_installStateRestarting"); - vm.installState.progress = "75"; - return packageResource.installData(pack); - }, - installError) - .then(function(pack) { - vm.installState.status = localizationService.localize("packager_installStateComplete"); - vm.installState.progress = "100"; - return packageResource.cleanUp(pack); - }, - installError) - .then(function(result) { - - if (result.postInstallationPath) { - //Put the redirect Uri in a cookie so we can use after reloading - localStorageService.set("packageInstallUri", result.postInstallationPath); - } - else { - //set to a constant value so it knows to just go to the installed view - localStorageService.set("packageInstallUri", "installed"); - } - - vm.installState.status = localizationService.localize("packager_installStateCompleted"); - vm.installCompleted = true; - - - - }, - installError); - } - - function installError() { - //This will return a rejection meaning that the promise change above will stop - return $q.reject(); - } - - vm.reloadPage = function() { - //reload on next digest (after cookie) - $timeout(function () { - $window.location.reload(true); - }); - } - } - - angular.module("umbraco").controller("Umbraco.Editors.Packages.InstallLocalController", PackagesInstallLocalController); - -})(); + navigationService.syncTree({ tree: "member", path: data.path.split(",") }).then(function (syncArgs) { + $scope.currentNode = syncArgs.node; + }); -(function () { - "use strict"; - - function PackagesInstalledController($scope, $route, $location, packageResource, $timeout, $window, localStorageService, localizationService) { - - var vm = this; - - vm.confirmUninstall = confirmUninstall; - vm.uninstallPackage = uninstallPackage; - vm.state = "list"; - vm.installState = { - status: "" - }; - vm.package = {}; - - function init() { - packageResource.getInstalled() - .then(function (packs) { - vm.installedPackages = packs; - }); - vm.installState.status = ""; - vm.state = "list"; - } - - function confirmUninstall(pck) { - vm.state = "packageDetails"; - vm.package = pck; - } - - function uninstallPackage(installedPackage) { - vm.installState.status = localizationService.localize("packager_installStateUninstalling"); - vm.installState.progress = "0"; - - packageResource.uninstall(installedPackage.id) - .then(function () { - - if (installedPackage.files.length > 0) { - vm.installState.status = localizationService.localize("packager_installStateComplete"); - vm.installState.progress = "100"; - - //set this flag so that on refresh it shows the installed packages list - localStorageService.set("packageInstallUri", "installed"); - - //reload on next digest (after cookie) - $timeout(function () { - $window.location.reload(true); - }); - - } - else { - init(); - } - }); - } - - init(); - - } - - angular.module("umbraco").controller("Umbraco.Editors.Packages.InstalledController", PackagesInstalledController); - -})(); + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); -(function () { - "use strict"; - - function PackagesRepoController($scope, $route, $location, $timeout, ourPackageRepositoryResource, $q, packageResource, localStorageService, localizationService) { - - var vm = this; - - vm.packageViewState = "packageList"; - vm.categories = []; - vm.loading = true; - vm.pagination = { - pageNumber: 1, - totalPages: 10, - pageSize: 24 - }; - vm.searchQuery = ""; - vm.installState = { - status: "", - progress: 0, - type: "ok" - }; - vm.selectCategory = selectCategory; - vm.showPackageDetails = showPackageDetails; - vm.setPackageViewState = setPackageViewState; - vm.nextPage = nextPage; - vm.prevPage = prevPage; - vm.goToPage = goToPage; - vm.installPackage = installPackage; - vm.downloadPackage = downloadPackage; - vm.openLightbox = openLightbox; - vm.closeLightbox = closeLightbox; - vm.search = search; - vm.installCompleted = false; - - var currSort = "Latest"; - //used to cancel any request in progress if another one needs to take it's place - var canceler = null; - - function getActiveCategory() { - if (vm.searchQuery !== "") { - return ""; - } - for (var i = 0; i < vm.categories.length; i++) { - if (vm.categories[i].active === true) { - return vm.categories[i].name; - } - } - return ""; - } - - function init() { - - vm.loading = true; - - $q.all([ - ourPackageRepositoryResource.getCategories() - .then(function(cats) { - vm.categories = cats; - }), - ourPackageRepositoryResource.getPopular(8) - .then(function(pack) { - vm.popular = pack.packages; - }), - ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort) - .then(function(pack) { - vm.packages = pack.packages; - vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); - }) - ]) - .then(function() { - vm.loading = false; - }); - - } - - function selectCategory(selectedCategory, categories) { - var reset = false; - for (var i = 0; i < categories.length; i++) { - var category = categories[i]; - if (category.name === selectedCategory.name && category.active === true) { - //it's already selected, let's unselect to show all again - reset = true; - } - category.active = false; - } - - vm.loading = true; - vm.searchQuery = ""; - var searchCategory = selectedCategory.name; - if (reset === true) { - searchCategory = ""; - } - - currSort = "Latest"; - - $q.all([ - ourPackageRepositoryResource.getPopular(8, searchCategory) - .then(function(pack) { - vm.popular = pack.packages; - }), - ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort, searchCategory, vm.searchQuery) - .then(function(pack) { - vm.packages = pack.packages; - vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); - vm.pagination.pageNumber = 1; - }) - ]) - .then(function() { - vm.loading = false; - selectedCategory.active = reset === false; - }); - } - - function showPackageDetails(selectedPackage) { - ourPackageRepositoryResource.getDetails(selectedPackage.id) - .then(function (pack) { - packageResource.validateInstalled(pack.name, pack.latestVersion) - .then(function() { - //ok, can install - vm.package = pack; - vm.package.isValid = true; - vm.packageViewState = "packageDetails"; - }, function() { - //nope, cannot install - vm.package = pack; - vm.package.isValid = false; - vm.packageViewState = "packageDetails"; - }) - }); - } - - function setPackageViewState(state) { - if(state) { - vm.packageViewState = state; - } - } - - function nextPage(pageNumber) { - ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery) - .then(function (pack) { - vm.packages = pack.packages; - vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); - }); - } - - function prevPage(pageNumber) { - ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery) - .then(function (pack) { - vm.packages = pack.packages; - vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); - }); - } - - function goToPage(pageNumber) { - ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery) - .then(function (pack) { - vm.packages = pack.packages; - vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); - }); - } - - function downloadPackage(selectedPackage) { - vm.loading = true; - - packageResource - .fetch(selectedPackage.id) - .then(function(pack) { - vm.packageViewState = "packageInstall"; - vm.loading = false; - vm.localPackage = pack; - vm.localPackage.allowed = true; - }, function (evt, status, headers, config) { - - if (status == 400) { - //it's a validation error - vm.installState.type = "error"; - vm.zipFile.serverErrorMessage = evt.message; - } - }); - } - - function error(e, args) { - //This will return a rejection meaning that the promise change above will stop - return $q.reject(); - } - - function installPackage(selectedPackage) { - - vm.installState.status = localizationService.localize("packager_installStateImporting"); - vm.installState.progress = "0"; - - packageResource - .import(selectedPackage) - .then(function(pack) { - vm.installState.status = localizationService.localize("packager_installStateInstalling"); - vm.installState.progress = "33"; - return packageResource.installFiles(pack); - }, - error) - .then(function(pack) { - vm.installState.status = localizationService.localize("packager_installStateRestarting"); - vm.installState.progress = "66"; - return packageResource.installData(pack); - }, - error) - .then(function(pack) { - vm.installState.status = localizationService.localize("packager_installStateComplete"); - vm.installState.progress = "100"; - return packageResource.cleanUp(pack); - }, - error) - .then(function(result) { - - if (result.postInstallationPath) { - //Put the redirect Uri in a cookie so we can use after reloading - localStorageService.set("packageInstallUri", result.postInstallationPath); - } - - vm.installState.status = localizationService.localize("packager_installStateCompleted"); - vm.installCompleted = true; - - }, - error); - } - - function openLightbox(itemIndex, items) { - vm.lightbox = { - show: true, - items: items, - activeIndex: itemIndex - }; - } - - function closeLightbox() { - vm.lightbox.show = false; - vm.lightbox = null; - } - - - var searchDebounced = _.debounce(function(e) { - - $scope.$apply(function () { - - //a canceler exists, so perform the cancelation operation and reset - if (canceler) { - canceler.resolve(); - canceler = $q.defer(); - } - else { - canceler = $q.defer(); - } - - currSort = vm.searchQuery ? "Default" : "Latest"; - - ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, - vm.pagination.pageSize, - currSort, - "", - vm.searchQuery, - canceler) - .then(function(pack) { - vm.packages = pack.packages; - vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); - vm.pagination.pageNumber = 1; - vm.loading = false; - //set back to null so it can be re-created - canceler = null; - }); - - }); - - }, 200); - - function search(searchQuery) { - vm.loading = true; - searchDebounced(); - } - - vm.reloadPage = function () { - //reload on next digest (after cookie) - $timeout(function () { - window.location.reload(true); - }); - } - - init(); - - } - - angular.module("umbraco").controller("Umbraco.Editors.Packages.RepoController", PackagesRepoController); - -})(); + $scope.page.loading = false; -function imageFilePickerController($scope) { - - $scope.pick = function() { - $scope.mediaPickerDialog = {}; - $scope.mediaPickerDialog.view = "mediapicker"; - $scope.mediaPickerDialog.show = true; - - $scope.mediaPickerDialog.submit = function(model) { - $scope.model.value = model.selectedImages[0].image; - $scope.mediaPickerDialog.show = false; - $scope.mediaPickerDialog = null; - }; - - $scope.mediaPickerDialog.close = function(oldModel) { - $scope.mediaPickerDialog.show = false; - $scope.mediaPickerDialog = null; - }; - }; - -} - -angular.module('umbraco').controller("Umbraco.PrevalueEditors.ImageFilePickerController", imageFilePickerController); + }); +} + +angular.module("umbraco").controller("Umbraco.Editors.Member.ListController", MemberListController); + +/** + * @ngdoc controller + * @name Umbraco.Editors.MemberType.CreateController + * @function + * + * @description + * The controller for the member type creation dialog + */ +function MemberTypesCreateController($scope, $location, navigationService, memberTypeResource, formHelper, appState, localizationService) { + + $scope.model = { + folderName: "", + creatingFolder: false + }; + + var node = $scope.dialogOptions.currentNode, + localizeCreateFolder = localizationService.localize("defaultdialog_createFolder"); + + + $scope.showCreateFolder = function() { + $scope.model.creatingFolder = true; + } + + $scope.createContainer = function () { + if (formHelper.submitForm({ + scope: $scope, + formCtrl: this.createFolderForm, + statusMessage: localizeCreateFolder + })) { + memberTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { + + navigationService.hideMenu(); + var currPath = node.path ? node.path : "-1"; + navigationService.syncTree({ tree: "membertypes", path: currPath + "," + folderId, forceReload: true, activate: true }); + + formHelper.resetForm({ scope: $scope }); + + var section = appState.getSectionState("currentSection"); + + }, function(err) { + + //TODO: Handle errors + }); + }; + } + + $scope.createMemberType = function() { + $location.search('create', null); + $location.path("/settings/membertypes/edit/" + node.id).search("create", "true"); + navigationService.hideMenu(); + } +} + +angular.module('umbraco').controller("Umbraco.Editors.MemberTypes.CreateController", MemberTypesCreateController); + +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.DeleteController + * @function + * + * @description + * The controller for deleting content + */ +function MemberTypesDeleteController($scope, memberTypeResource, treeService, navigationService) { + + $scope.performDelete = function() { + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + memberTypeResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + + }; + + $scope.cancel = function() { + navigationService.hideDialog(); + }; +} + +angular.module("umbraco").controller("Umbraco.Editors.MemberTypes.DeleteController", MemberTypesDeleteController); + +/** + * @ngdoc controller + * @name Umbraco.Editors.MemberType.EditController + * @function + * + * @description + * The controller for the member type editor + */ +(function () { + "use strict"; + + function MemberTypesEditController($scope, $rootScope, $routeParams, $log, $filter, memberTypeResource, dataTypeResource, editorState, iconHelper, formHelper, navigationService, contentEditingHelper, notificationsService, $q, localizationService, overlayHelper, contentTypeHelper) { + + var vm = this; + var localizeSaving = localizationService.localize("general_saving"); + + vm.save = save; + + vm.currentNode = null; + vm.contentType = {}; + vm.page = {}; + vm.page.loading = false; + vm.page.saveButtonState = "init"; + vm.page.navigation = [ + { + "name": localizationService.localize("general_design"), + "icon": "icon-document-dashed-line", + "view": "views/membertypes/views/design/design.html", + "active": true + } + ]; + + vm.page.keyboardShortcutsOverview = [ + { + "name": localizationService.localize("shortcuts_shortcut"), + "shortcuts": [ + { + "description": localizationService.localize("shortcuts_addTab"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] + }, + { + "description": localizationService.localize("shortcuts_addProperty"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] + }, + { + "description": localizationService.localize("shortcuts_addEditor"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "e" }] + }, + { + "description": localizationService.localize("shortcuts_editDataType"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] + } + ] + } + ]; + + contentTypeHelper.checkModelsBuilderStatus().then(function (result) { + vm.page.modelsBuilder = result; + if (result) { + //Models builder mode: + vm.page.defaultButton = { + hotKey: "ctrl+s", + hotKeyWhenHidden: true, + labelKey: "buttons_save", + letter: "S", + type: "submit", + handler: function () { vm.save(); } + }; + vm.page.subButtons = [{ + hotKey: "ctrl+g", + hotKeyWhenHidden: true, + labelKey: "buttons_saveAndGenerateModels", + letter: "G", + handler: function () { + + vm.page.saveButtonState = "busy"; + + vm.save().then(function (result) { + + vm.page.saveButtonState = "busy"; + + localizationService.localize("modelsBuilder_buildingModels").then(function (headerValue) { + localizationService.localize("modelsBuilder_waitingMessage").then(function(msgValue) { + notificationsService.info(headerValue, msgValue); + }); + }); + + contentTypeHelper.generateModels().then(function (result) { + + if (result.success) { + + //re-check model status + contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { + vm.page.modelsBuilder = statusResult; + }); + + //clear and add success + vm.page.saveButtonState = "init"; + localizationService.localize("modelsBuilder_modelsGenerated").then(function(value) { + notificationsService.success(value); + }); + + } else { + vm.page.saveButtonState = "error"; + localizationService.localize("modelsBuilder_modelsExceptionInUlog").then(function(value) { + notificationsService.error(value); + }); + } + + }, function () { + vm.page.saveButtonState = "error"; + localizationService.localize("modelsBuilder_modelsGeneratedError").then(function(value) { + notificationsService.error(value); + }); + }); + + + }); + + } + }]; + } + }); + + if ($routeParams.create) { + + vm.page.loading = true; + + //we are creating so get an empty data type item + memberTypeResource.getScaffold($routeParams.id) + .then(function (dt) { + init(dt); + + vm.page.loading = false; + }); + } + else { + + vm.page.loading = true; + + memberTypeResource.getById($routeParams.id).then(function (dt) { + init(dt); + + syncTreeNode(vm.contentType, dt.path, true); + + vm.page.loading = false; + }); + } + + function save() { + // only save if there is no overlays open + if(overlayHelper.getNumberOfOverlays() === 0) { + + var deferred = $q.defer(); + + vm.page.saveButtonState = "busy"; + + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: memberTypeResource.save, + scope: $scope, + content: vm.contentType, + //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc + // type when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + // we need to rebind... the IDs that have been created! + rebindCallback: function (origContentType, savedContentType) { + vm.contentType.id = savedContentType.id; + vm.contentType.groups.forEach(function (group) { + if (!group.name) return; + + var k = 0; + while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) + k++; + if (k == savedContentType.groups.length) { + group.id = 0; + return; + } + + var savedGroup = savedContentType.groups[k]; + if (!group.id) group.id = savedGroup.id; + + group.properties.forEach(function (property) { + if (property.id || !property.alias) return; + + k = 0; + while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) + k++; + if (k == savedGroup.properties.length) { + property.id = 0; + return; + } + + var savedProperty = savedGroup.properties[k]; + property.id = savedProperty.id; + }); + }); + } + }).then(function (data) { + //success + syncTreeNode(vm.contentType, data.path); + + vm.page.saveButtonState = "success"; + + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } + else { + localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); + } + + vm.page.saveButtonState = "error"; + + deferred.reject(err); + }); + + return deferred.promise; + } + + } + + function init(contentType) { + + // set all tab to inactive + if (contentType.groups.length !== 0) { + angular.forEach(contentType.groups, function (group) { + + angular.forEach(group.properties, function (property) { + // get data type details for each property + getDataTypeDetails(property); + }); + + }); + } + + // convert legacy icons + convertLegacyIcons(contentType); + + //set a shared state + editorState.set(contentType); + + vm.contentType = contentType; + + } + + function convertLegacyIcons(contentType) { + + // make array to store contentType icon + var contentTypeArray = []; + + // push icon to array + contentTypeArray.push({ "icon": contentType.icon }); + + // run through icon method + iconHelper.formatContentTypeIcons(contentTypeArray); + + // set icon back on contentType + contentType.icon = contentTypeArray[0].icon; + + } + + function getDataTypeDetails(property) { + + if (property.propertyState !== "init") { + + dataTypeResource.getById(property.dataTypeId) + .then(function (dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; + }); + } + } + + /** Syncs the content type to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(dt, path, initialLoad) { + + navigationService.syncTree({ tree: "membertypes", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { + vm.currentNode = syncArgs.node; + }); + + } + + + } + + angular.module("umbraco").controller("Umbraco.Editors.MemberTypes.EditController", MemberTypesEditController); + +})(); + +angular.module("umbraco") +.controller("Umbraco.Editors.MemberTypes.MoveController", + function($scope){ + + }); + +/** + * @ngdoc controller + * @name Umbraco.Editors.Packages.DeleteController + * @function + * + * @description + * The controller for deleting content + */ +function PackageDeleteController($scope, packageResource, treeService, navigationService) { + + $scope.performDelete = function() { + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + packageResource.deleteCreatedPackage($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + + }; + + $scope.cancel = function() { + navigationService.hideDialog(); + }; +} + +angular.module("umbraco").controller("Umbraco.Editors.Packages.DeleteController", PackageDeleteController); + +(function () { + "use strict"; + + function PackagesOverviewController($scope, $route, $location, navigationService, $timeout, localStorageService) { + + //Hack! + // if there is a cookie value for packageInstallUri then we need to redirect there, + // the issue is that we still have webforms and we cannot go to a hash location and then window.reload + // because it will double load it. + // we will refresh and then navigate there. + + var installPackageUri = localStorageService.get("packageInstallUri"); + if (installPackageUri) { + localStorageService.remove("packageInstallUri"); + } + if (installPackageUri && installPackageUri !== "installed") { + //navigate to the custom installer screen, if it is just "installed", then we'll + //show the installed view + $location.path(installPackageUri).search(""); + } + else { + var vm = this; + + vm.page = {}; + vm.page.name = "Packages"; + vm.page.navigation = [ + { + "name": "Packages", + "icon": "icon-cloud", + "view": "views/packager/views/repo.html", + "active": !installPackageUri || installPackageUri === "navigation" + }, + { + "name": "Installed", + "icon": "icon-box", + "view": "views/packager/views/installed.html", + "active": installPackageUri === "installed" + }, + { + "name": "Install local", + "icon": "icon-add", + "view": "views/packager/views/install-local.html", + "active": installPackageUri === "local" + } + ]; + + $timeout(function () { + navigationService.syncTree({ tree: "packager", path: "-1" }); + }); + } + + } + + angular.module("umbraco").controller("Umbraco.Editors.Packages.OverviewController", PackagesOverviewController); + +})(); + +(function () { + "use strict"; + + function PackagesInstallLocalController($scope, $route, $location, Upload, umbRequestHelper, packageResource, localStorageService, $timeout, $window, localizationService) { + + var vm = this; + vm.state = "upload"; + + vm.localPackage = {}; + vm.installPackage = installPackage; + vm.installState = { + status: "", + progress:0 + }; + vm.installCompleted = false; + vm.zipFile = { + uploadStatus: "idle", + uploadProgress: 0, + serverErrorMessage: null + }; + + $scope.handleFiles = function (files, event) { + if (files) { + for (var i = 0; i < files.length; i++) { + upload(files[i]); + } + } + }; + + function upload(file) { + + Upload.upload({ + url: umbRequestHelper.getApiUrl("packageInstallApiBaseUrl", "UploadLocalPackage"), + fields: {}, + file: file + }).progress(function (evt) { + + // set view state to uploading + vm.state = 'uploading'; + + // calculate progress in percentage + var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); + + // set percentage property on file + vm.zipFile.uploadProgress = progressPercentage; + + // set uploading status on file + vm.zipFile.uploadStatus = "uploading"; + + }).success(function (data, status, headers, config) { + + if (data.notifications && data.notifications.length > 0) { + + // set error status on file + vm.zipFile.uploadStatus = "error"; + + // Throw message back to user with the cause of the error + vm.zipFile.serverErrorMessage = data.notifications[0].message; + + } else { + + // set done status on file + vm.zipFile.uploadStatus = "done"; + loadPackage(); + vm.localPackage = data; + } + + }).error(function (evt, status, headers, config) { + + // set status done + vm.zipFile.uploadStatus = "error"; + + // If file not found, server will return a 404 and display this message + if (status === 404) { + vm.zipFile.serverErrorMessage = "File not found"; + } + else if (status == 400) { + //it's a validation error + vm.zipFile.serverErrorMessage = evt.message; + } + else { + //it's an unhandled error + //if the service returns a detailed error + if (evt.InnerException) { + vm.zipFile.serverErrorMessage = evt.InnerException.ExceptionMessage; + + //Check if its the common "too large file" exception + if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { + vm.zipFile.serverErrorMessage = "File too large to upload"; + } + + } else if (evt.Message) { + file.serverErrorMessage = evt.Message; + } + } + }); + } + + function loadPackage() { + if (vm.zipFile.uploadStatus === "done") { + vm.state = "packageDetails"; + } + } + + function installPackage() { + vm.installState.status = localizationService.localize("packager_installStateImporting"); + vm.installState.progress = "0"; + + packageResource + .import(vm.localPackage) + .then(function(pack) { + vm.installState.progress = "25"; + vm.installState.status = localizationService.localize("packager_installStateInstalling"); + vm.installState.progress = "50"; + return packageResource.installFiles(pack); + }, + installError) + .then(function(pack) { + vm.installState.status = localizationService.localize("packager_installStateRestarting"); + vm.installState.progress = "75"; + return packageResource.installData(pack); + }, + installError) + .then(function(pack) { + vm.installState.status = localizationService.localize("packager_installStateComplete"); + vm.installState.progress = "100"; + return packageResource.cleanUp(pack); + }, + installError) + .then(function(result) { + + if (result.postInstallationPath) { + //Put the redirect Uri in a cookie so we can use after reloading + localStorageService.set("packageInstallUri", result.postInstallationPath); + } + else { + //set to a constant value so it knows to just go to the installed view + localStorageService.set("packageInstallUri", "installed"); + } + + vm.installState.status = localizationService.localize("packager_installStateCompleted"); + vm.installCompleted = true; + + + + }, + installError); + } + + function installError() { + //This will return a rejection meaning that the promise change above will stop + return $q.reject(); + } + + vm.reloadPage = function() { + //reload on next digest (after cookie) + $timeout(function () { + $window.location.reload(true); + }); + } + } + + angular.module("umbraco").controller("Umbraco.Editors.Packages.InstallLocalController", PackagesInstallLocalController); + +})(); + +(function () { + "use strict"; + + function PackagesInstalledController($scope, $route, $location, packageResource, $timeout, $window, localStorageService, localizationService) { + + var vm = this; + + vm.confirmUninstall = confirmUninstall; + vm.uninstallPackage = uninstallPackage; + vm.state = "list"; + vm.installState = { + status: "" + }; + vm.package = {}; + + function init() { + packageResource.getInstalled() + .then(function (packs) { + vm.installedPackages = packs; + }); + vm.installState.status = ""; + vm.state = "list"; + } + + function confirmUninstall(pck) { + vm.state = "packageDetails"; + vm.package = pck; + } + + function uninstallPackage(installedPackage) { + vm.installState.status = localizationService.localize("packager_installStateUninstalling"); + vm.installState.progress = "0"; + + packageResource.uninstall(installedPackage.id) + .then(function () { + + if (installedPackage.files.length > 0) { + vm.installState.status = localizationService.localize("packager_installStateComplete"); + vm.installState.progress = "100"; + + //set this flag so that on refresh it shows the installed packages list + localStorageService.set("packageInstallUri", "installed"); + + //reload on next digest (after cookie) + $timeout(function () { + $window.location.reload(true); + }); + + } + else { + init(); + } + }); + } + + init(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.Packages.InstalledController", PackagesInstalledController); + +})(); + +(function () { + "use strict"; + + function PackagesRepoController($scope, $route, $location, $timeout, ourPackageRepositoryResource, $q, packageResource, localStorageService, localizationService) { + + var vm = this; + + vm.packageViewState = "packageList"; + vm.categories = []; + vm.loading = true; + vm.pagination = { + pageNumber: 1, + totalPages: 10, + pageSize: 24 + }; + vm.searchQuery = ""; + vm.installState = { + status: "", + progress: 0, + type: "ok" + }; + vm.selectCategory = selectCategory; + vm.showPackageDetails = showPackageDetails; + vm.setPackageViewState = setPackageViewState; + vm.nextPage = nextPage; + vm.prevPage = prevPage; + vm.goToPage = goToPage; + vm.installPackage = installPackage; + vm.downloadPackage = downloadPackage; + vm.openLightbox = openLightbox; + vm.closeLightbox = closeLightbox; + vm.search = search; + vm.installCompleted = false; + + var currSort = "Latest"; + //used to cancel any request in progress if another one needs to take it's place + var canceler = null; + + function getActiveCategory() { + if (vm.searchQuery !== "") { + return ""; + } + for (var i = 0; i < vm.categories.length; i++) { + if (vm.categories[i].active === true) { + return vm.categories[i].name; + } + } + return ""; + } + + function init() { + + vm.loading = true; + + $q.all([ + ourPackageRepositoryResource.getCategories() + .then(function(cats) { + vm.categories = cats; + }), + ourPackageRepositoryResource.getPopular(8) + .then(function(pack) { + vm.popular = pack.packages; + }), + ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort) + .then(function(pack) { + vm.packages = pack.packages; + vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + }) + ]) + .then(function() { + vm.loading = false; + }); + + } + + function selectCategory(selectedCategory, categories) { + var reset = false; + for (var i = 0; i < categories.length; i++) { + var category = categories[i]; + if (category.name === selectedCategory.name && category.active === true) { + //it's already selected, let's unselect to show all again + reset = true; + } + category.active = false; + } + + vm.loading = true; + vm.searchQuery = ""; + var searchCategory = selectedCategory.name; + if (reset === true) { + searchCategory = ""; + } + + currSort = "Latest"; + + $q.all([ + ourPackageRepositoryResource.getPopular(8, searchCategory) + .then(function(pack) { + vm.popular = pack.packages; + }), + ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort, searchCategory, vm.searchQuery) + .then(function(pack) { + vm.packages = pack.packages; + vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + vm.pagination.pageNumber = 1; + }) + ]) + .then(function() { + vm.loading = false; + selectedCategory.active = reset === false; + }); + } + + function showPackageDetails(selectedPackage) { + ourPackageRepositoryResource.getDetails(selectedPackage.id) + .then(function (pack) { + packageResource.validateInstalled(pack.name, pack.latestVersion) + .then(function() { + //ok, can install + vm.package = pack; + vm.package.isValid = true; + vm.packageViewState = "packageDetails"; + }, function() { + //nope, cannot install + vm.package = pack; + vm.package.isValid = false; + vm.packageViewState = "packageDetails"; + }) + }); + } + + function setPackageViewState(state) { + if(state) { + vm.packageViewState = state; + } + } + + function nextPage(pageNumber) { + ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery) + .then(function (pack) { + vm.packages = pack.packages; + vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + }); + } + + function prevPage(pageNumber) { + ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery) + .then(function (pack) { + vm.packages = pack.packages; + vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + }); + } + + function goToPage(pageNumber) { + ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery) + .then(function (pack) { + vm.packages = pack.packages; + vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + }); + } + + function downloadPackage(selectedPackage) { + vm.loading = true; + + packageResource + .fetch(selectedPackage.id) + .then(function(pack) { + vm.packageViewState = "packageInstall"; + vm.loading = false; + vm.localPackage = pack; + vm.localPackage.allowed = true; + }, function (evt, status, headers, config) { + + if (status == 400) { + //it's a validation error + vm.installState.type = "error"; + vm.zipFile.serverErrorMessage = evt.message; + } + }); + } + + function error(e, args) { + //This will return a rejection meaning that the promise change above will stop + return $q.reject(); + } + + function installPackage(selectedPackage) { + + vm.installState.status = localizationService.localize("packager_installStateImporting"); + vm.installState.progress = "0"; + + packageResource + .import(selectedPackage) + .then(function(pack) { + vm.installState.status = localizationService.localize("packager_installStateInstalling"); + vm.installState.progress = "33"; + return packageResource.installFiles(pack); + }, + error) + .then(function(pack) { + vm.installState.status = localizationService.localize("packager_installStateRestarting"); + vm.installState.progress = "66"; + return packageResource.installData(pack); + }, + error) + .then(function(pack) { + vm.installState.status = localizationService.localize("packager_installStateComplete"); + vm.installState.progress = "100"; + return packageResource.cleanUp(pack); + }, + error) + .then(function(result) { + + if (result.postInstallationPath) { + //Put the redirect Uri in a cookie so we can use after reloading + localStorageService.set("packageInstallUri", result.postInstallationPath); + } + + vm.installState.status = localizationService.localize("packager_installStateCompleted"); + vm.installCompleted = true; + + }, + error); + } + + function openLightbox(itemIndex, items) { + vm.lightbox = { + show: true, + items: items, + activeIndex: itemIndex + }; + } + + function closeLightbox() { + vm.lightbox.show = false; + vm.lightbox = null; + } + + + var searchDebounced = _.debounce(function(e) { + + $scope.$apply(function () { + + //a canceler exists, so perform the cancelation operation and reset + if (canceler) { + canceler.resolve(); + canceler = $q.defer(); + } + else { + canceler = $q.defer(); + } + + currSort = vm.searchQuery ? "Default" : "Latest"; + + ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, + vm.pagination.pageSize, + currSort, + "", + vm.searchQuery, + canceler) + .then(function(pack) { + vm.packages = pack.packages; + vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + vm.pagination.pageNumber = 1; + vm.loading = false; + //set back to null so it can be re-created + canceler = null; + }); + + }); + + }, 200); + + function search(searchQuery) { + vm.loading = true; + searchDebounced(); + } + + vm.reloadPage = function () { + //reload on next digest (after cookie) + $timeout(function () { + window.location.reload(true); + }); + } + + init(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.Packages.RepoController", PackagesRepoController); + +})(); + +function imageFilePickerController($scope) { + + $scope.pick = function() { + $scope.mediaPickerDialog = {}; + $scope.mediaPickerDialog.view = "mediapicker"; + $scope.mediaPickerDialog.show = true; + + $scope.mediaPickerDialog.submit = function(model) { + $scope.model.value = model.selectedImages[0].image; + $scope.mediaPickerDialog.show = false; + $scope.mediaPickerDialog = null; + }; + + $scope.mediaPickerDialog.close = function(oldModel) { + $scope.mediaPickerDialog.show = false; + $scope.mediaPickerDialog = null; + }; + }; + +} + +angular.module('umbraco').controller("Umbraco.PrevalueEditors.ImageFilePickerController", imageFilePickerController); + +//this controller simply tells the dialogs service to open a mediaPicker window +//with a specified callback, this callback will receive an object with a selection on it +function mediaPickerController($scope, dialogService, entityResource, $log, iconHelper) { + + function trim(str, chr) { + var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); + return str.replace(rgxtrim, ''); + } + + $scope.renderModel = []; + + var dialogOptions = { + multiPicker: false, + entityType: "Media", + section: "media", + treeAlias: "media" + }; + + $scope.openContentPicker = function() { + $scope.contentPickerOverlay = dialogOptions; + $scope.contentPickerOverlay.view = "treePicker"; + $scope.contentPickerOverlay.show = true; + + $scope.contentPickerOverlay.submit = function(model) { + + if ($scope.contentPickerOverlay.multiPicker) { + _.each(model.selection, function (item, i) { + $scope.add(item); + }); + } + else { + $scope.clear(); + $scope.add(model.selection[0]); + } + + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + }; + + $scope.contentPickerOverlay.close = function(oldModel) { + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + }; + } + + $scope.remove =function(index, event){ + event.preventDefault(); + $scope.renderModel.splice(index, 1); + }; + + $scope.clear = function() { + $scope.renderModel = []; + }; + + $scope.add = function (item) { + var currIds = _.map($scope.renderModel, function (i) { + return i.id; + }); + if (currIds.indexOf(item.id) < 0) { + item.icon = iconHelper.convertFromLegacyIcon(item.icon); + $scope.renderModel.push({name: item.name, id: item.id, icon: item.icon}); + } + }; + + var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { + var currIds = _.map($scope.renderModel, function (i) { + return i.id; + }); + $scope.model.value = trim(currIds.join(), ","); + }); + + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); + + //load media data + var modelIds = $scope.model.value ? $scope.model.value.split(',') : []; + entityResource.getByIds(modelIds, dialogOptions.entityType).then(function (data) { + _.each(data, function (item, i) { + item.icon = iconHelper.convertFromLegacyIcon(item.icon); + $scope.renderModel.push({ name: item.name, id: item.id, icon: item.icon }); + }); + }); + +} -//this controller simply tells the dialogs service to open a mediaPicker window -//with a specified callback, this callback will receive an object with a selection on it -function mediaPickerController($scope, dialogService, entityResource, $log, iconHelper) { - - function trim(str, chr) { - var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); - return str.replace(rgxtrim, ''); - } - - $scope.renderModel = []; - - var dialogOptions = { - multiPicker: false, - entityType: "Media", - section: "media", - treeAlias: "media" - }; - - $scope.openContentPicker = function() { - $scope.contentPickerOverlay = dialogOptions; - $scope.contentPickerOverlay.view = "treePicker"; - $scope.contentPickerOverlay.show = true; - - $scope.contentPickerOverlay.submit = function(model) { - - if ($scope.contentPickerOverlay.multiPicker) { - _.each(model.selection, function (item, i) { - $scope.add(item); - }); - } - else { - $scope.clear(); - $scope.add(model.selection[0]); - } - - $scope.contentPickerOverlay.show = false; - $scope.contentPickerOverlay = null; - }; - - $scope.contentPickerOverlay.close = function(oldModel) { - $scope.contentPickerOverlay.show = false; - $scope.contentPickerOverlay = null; - }; - } - - $scope.remove =function(index, event){ - event.preventDefault(); - $scope.renderModel.splice(index, 1); - }; - - $scope.clear = function() { - $scope.renderModel = []; - }; - - $scope.add = function (item) { - var currIds = _.map($scope.renderModel, function (i) { - return i.id; - }); - if (currIds.indexOf(item.id) < 0) { - item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.renderModel.push({name: item.name, id: item.id, icon: item.icon}); - } - }; - - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - var currIds = _.map($scope.renderModel, function (i) { - return i.id; - }); - $scope.model.value = trim(currIds.join(), ","); - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); - - //load media data - var modelIds = $scope.model.value ? $scope.model.value.split(',') : []; - entityResource.getByIds(modelIds, dialogOptions.entityType).then(function (data) { - _.each(data, function (item, i) { - item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.renderModel.push({ name: item.name, id: item.id, icon: item.icon }); - }); - }); - -} - angular.module('umbraco').controller("Umbraco.PrevalueEditors.MediaPickerController",mediaPickerController); angular.module("umbraco").controller("Umbraco.PrevalueEditors.MultiValuesController", function ($scope, $timeout) { @@ -9873,77 +9873,77 @@ angular.module('umbraco') } }); -//this controller simply tells the dialogs service to open a mediaPicker window -//with a specified callback, this callback will receive an object with a selection on it -angular.module('umbraco') -.controller("Umbraco.PrevalueEditors.TreeSourceController", - - function($scope, dialogService, entityResource, $log, iconHelper){ - - if (!$scope.model) { - $scope.model = {}; - } - if (!$scope.model.value) { - $scope.model.value = { - type: "content" - }; - } - - if($scope.model.value.id && $scope.model.value.type !== "member"){ - var ent = "Document"; - if($scope.model.value.type === "media"){ - ent = "Media"; - } - - entityResource.getById($scope.model.value.id, ent).then(function(item){ - item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.node = item; - }); - } - - - $scope.openContentPicker =function(){ - $scope.treePickerOverlay = { - view: "treepicker", - section: $scope.model.value.type, - treeAlias: $scope.model.value.type, - multiPicker: false, - show: true, - submit: function(model) { - var item = model.selection[0]; - populate(item); - $scope.treePickerOverlay.show = false; - $scope.treePickerOverlay = null; - } - }; - }; - - $scope.clear = function() { - $scope.model.value.id = undefined; - $scope.node = undefined; - $scope.model.value.query = undefined; - }; - - - //we always need to ensure we dont submit anything broken - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - if($scope.model.value.type === "member"){ - $scope.model.value.id = -1; - $scope.model.value.query = ""; - } - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); - - function populate(item){ - $scope.clear(); - item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.node = item; - $scope.model.value.id = item.id; - } +//this controller simply tells the dialogs service to open a mediaPicker window +//with a specified callback, this callback will receive an object with a selection on it +angular.module('umbraco') +.controller("Umbraco.PrevalueEditors.TreeSourceController", + + function($scope, dialogService, entityResource, $log, iconHelper){ + + if (!$scope.model) { + $scope.model = {}; + } + if (!$scope.model.value) { + $scope.model.value = { + type: "content" + }; + } + + if($scope.model.value.id && $scope.model.value.type !== "member"){ + var ent = "Document"; + if($scope.model.value.type === "media"){ + ent = "Media"; + } + + entityResource.getById($scope.model.value.id, ent).then(function(item){ + item.icon = iconHelper.convertFromLegacyIcon(item.icon); + $scope.node = item; + }); + } + + + $scope.openContentPicker =function(){ + $scope.treePickerOverlay = { + view: "treepicker", + section: $scope.model.value.type, + treeAlias: $scope.model.value.type, + multiPicker: false, + show: true, + submit: function(model) { + var item = model.selection[0]; + populate(item); + $scope.treePickerOverlay.show = false; + $scope.treePickerOverlay = null; + } + }; + }; + + $scope.clear = function() { + $scope.model.value.id = undefined; + $scope.node = undefined; + $scope.model.value.query = undefined; + }; + + + //we always need to ensure we dont submit anything broken + var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { + if($scope.model.value.type === "member"){ + $scope.model.value.id = -1; + $scope.model.value.query = ""; + } + }); + + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); + + function populate(item){ + $scope.clear(); + item.icon = iconHelper.convertFromLegacyIcon(item.icon); + $scope.node = item; + $scope.model.value.id = item.id; + } }); function booleanEditorController($scope, $rootScope, assetsService) { @@ -10213,93 +10213,93 @@ function ColorPickerController($scope) { angular.module("umbraco").controller("Umbraco.PropertyEditors.ColorPickerController", ColorPickerController); -angular.module("umbraco").controller("Umbraco.PrevalueEditors.MultiColorPickerController", - function ($scope, $timeout, assetsService, angularHelper, $element) { - //NOTE: We need to make each color an object, not just a string because you cannot 2-way bind to a primitive. - var defaultColor = "000000"; - - $scope.newColor = defaultColor; - $scope.hasError = false; - - assetsService.load([ - //"lib/spectrum/tinycolor.js", - "lib/spectrum/spectrum.js" - ], $scope).then(function () { - var elem = $element.find("input"); - elem.spectrum({ - color: null, - showInitial: false, - chooseText: "choose", // TODO: These can be localised - cancelText: "cancel", // TODO: These can be localised - preferredFormat: "hex", - showInput: true, - clickoutFiresChange: true, - hide: function (color) { - //show the add butotn - $element.find(".btn.add").show(); - }, - change: function (color) { - angularHelper.safeApply($scope, function () { - $scope.newColor = color.toHexString().trimStart("#"); // #ff0000 - }); - }, - show: function() { - //hide the add butotn - $element.find(".btn.add").hide(); - } - }); - }); - - if (!angular.isArray($scope.model.value)) { - //make an array from the dictionary - var items = []; - for (var i in $scope.model.value) { - items.push({ - value: $scope.model.value[i], - id: i - }); - } - //now make the editor model the array - $scope.model.value = items; - } - - $scope.remove = function (item, evt) { - - evt.preventDefault(); - - $scope.model.value = _.reject($scope.model.value, function (x) { - return x.value === item.value; - }); - - }; - - $scope.add = function (evt) { - - evt.preventDefault(); - - if ($scope.newColor) { - var exists = _.find($scope.model.value, function(item) { - return item.value.toUpperCase() == $scope.newColor.toUpperCase(); - }); - if (!exists) { - $scope.model.value.push({ value: $scope.newColor }); - //$scope.newColor = defaultColor; - // set colorpicker to default color - //var elem = $element.find("input"); - //elem.spectrum("set", $scope.newColor); - $scope.hasError = false; - return; - } - - //there was an error, do the highlight (will be set back by the directive) - $scope.hasError = true; - } - - }; - - //load the separate css for the editor to avoid it blocking our js loading - assetsService.loadCss("lib/spectrum/spectrum.css"); - }); +angular.module("umbraco").controller("Umbraco.PrevalueEditors.MultiColorPickerController", + function ($scope, $timeout, assetsService, angularHelper, $element) { + //NOTE: We need to make each color an object, not just a string because you cannot 2-way bind to a primitive. + var defaultColor = "000000"; + + $scope.newColor = defaultColor; + $scope.hasError = false; + + assetsService.load([ + //"lib/spectrum/tinycolor.js", + "lib/spectrum/spectrum.js" + ], $scope).then(function () { + var elem = $element.find("input"); + elem.spectrum({ + color: null, + showInitial: false, + chooseText: "choose", // TODO: These can be localised + cancelText: "cancel", // TODO: These can be localised + preferredFormat: "hex", + showInput: true, + clickoutFiresChange: true, + hide: function (color) { + //show the add butotn + $element.find(".btn.add").show(); + }, + change: function (color) { + angularHelper.safeApply($scope, function () { + $scope.newColor = color.toHexString().trimStart("#"); // #ff0000 + }); + }, + show: function() { + //hide the add butotn + $element.find(".btn.add").hide(); + } + }); + }); + + if (!angular.isArray($scope.model.value)) { + //make an array from the dictionary + var items = []; + for (var i in $scope.model.value) { + items.push({ + value: $scope.model.value[i], + id: i + }); + } + //now make the editor model the array + $scope.model.value = items; + } + + $scope.remove = function (item, evt) { + + evt.preventDefault(); + + $scope.model.value = _.reject($scope.model.value, function (x) { + return x.value === item.value; + }); + + }; + + $scope.add = function (evt) { + + evt.preventDefault(); + + if ($scope.newColor) { + var exists = _.find($scope.model.value, function(item) { + return item.value.toUpperCase() == $scope.newColor.toUpperCase(); + }); + if (!exists) { + $scope.model.value.push({ value: $scope.newColor }); + //$scope.newColor = defaultColor; + // set colorpicker to default color + //var elem = $element.find("input"); + //elem.spectrum("set", $scope.newColor); + $scope.hasError = false; + return; + } + + //there was an error, do the highlight (will be set back by the directive) + $scope.hasError = true; + } + + }; + + //load the separate css for the editor to avoid it blocking our js loading + assetsService.loadCss("lib/spectrum/spectrum.css"); + }); //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it @@ -11032,35 +11032,35 @@ angular.module("umbraco") } }); -angular.module("umbraco") - -//this controller is obsolete and should not be used anymore -//it proxies everything to the system media list view which has overtaken -//all the work this property editor used to perform -.controller("Umbraco.PropertyEditors.FolderBrowserController", - function ($rootScope, $scope, contentTypeResource) { - //get the system media listview - contentTypeResource.getPropertyTypeScaffold(-96) - .then(function(dt) { - - $scope.fakeProperty = { - alias: "contents", - config: dt.config, - description: "", - editor: dt.editor, - hideLabel: true, - id: 1, - label: "Contents:", - validation: { - mandatory: false, - pattern: null - }, - value: "", - view: dt.view - }; - - }); -}); +angular.module("umbraco") + +//this controller is obsolete and should not be used anymore +//it proxies everything to the system media list view which has overtaken +//all the work this property editor used to perform +.controller("Umbraco.PropertyEditors.FolderBrowserController", + function ($rootScope, $scope, contentTypeResource) { + //get the system media listview + contentTypeResource.getPropertyTypeScaffold(-96) + .then(function(dt) { + + $scope.fakeProperty = { + alias: "contents", + config: dt.config, + description: "", + editor: dt.editor, + hideLabel: true, + id: 1, + label: "Contents:", + validation: { + mandatory: false, + pattern: null + }, + value: "", + view: dt.view + }; + + }); +}); angular.module("umbraco") .controller("Umbraco.PropertyEditors.GoogleMapsController", @@ -11159,2296 +11159,2296 @@ angular.module("umbraco") initMap(); }; }); -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.GridPrevalueEditor.LayoutConfigController", - function ($scope) { - - $scope.currentLayout = $scope.model.currentLayout; - $scope.columns = $scope.model.columns; - $scope.rows = $scope.model.rows; - - $scope.scaleUp = function(section, max, overflow){ - var add = 1; - if(overflow !== true){ - add = (max > 1) ? 1 : max; - } - //var add = (max > 1) ? 1 : max; - section.grid = section.grid+add; - }; - - $scope.scaleDown = function(section){ - var remove = (section.grid > 1) ? 1 : 0; - section.grid = section.grid-remove; - }; - - $scope.percentage = function(spans){ - return ((spans / $scope.columns) * 100).toFixed(8); - }; - - $scope.toggleCollection = function(collection, toggle){ - if(toggle){ - collection = []; - }else{ - delete collection; - } - }; - - - - /**************** - Section - *****************/ - $scope.configureSection = function(section, template){ - if(section === undefined){ - var space = ($scope.availableLayoutSpace > 4) ? 4 : $scope.availableLayoutSpace; - section = { - grid: space - }; - template.sections.push(section); - } - - $scope.currentSection = section; - }; - - $scope.deleteSection = function(section, template) { - if ($scope.currentSection === section) { - $scope.currentSection = undefined; - } - var index = template.sections.indexOf(section) - template.sections.splice(index, 1); - }; - - $scope.closeSection = function(){ - $scope.currentSection = undefined; - }; - - $scope.$watch("currentLayout", function(layout){ - if(layout){ - var total = 0; - _.forEach(layout.sections, function(section){ - total = (total + section.grid); - }); - - $scope.availableLayoutSpace = $scope.columns - total; - } - }, true); - }); +angular.module("umbraco") + .controller("Umbraco.PropertyEditors.GridPrevalueEditor.LayoutConfigController", + function ($scope) { -function RowConfigController($scope) { - - $scope.currentRow = $scope.model.currentRow; - $scope.editors = $scope.model.editors; - $scope.columns = $scope.model.columns; - - $scope.scaleUp = function(section, max, overflow) { - var add = 1; - if (overflow !== true) { - add = (max > 1) ? 1 : max; - } - //var add = (max > 1) ? 1 : max; - section.grid = section.grid + add; - }; - - $scope.scaleDown = function(section) { - var remove = (section.grid > 1) ? 1 : 0; - section.grid = section.grid - remove; - }; - - $scope.percentage = function(spans) { - return ((spans / $scope.columns) * 100).toFixed(8); - }; - - $scope.toggleCollection = function(collection, toggle) { - if (toggle) { - collection = []; - } - else { - delete collection; - } - }; - - - /**************** - area - *****************/ - $scope.configureCell = function(cell, row) { - if ($scope.currentCell && $scope.currentCell === cell) { - delete $scope.currentCell; - } - else { - if (cell === undefined) { - var available = $scope.availableRowSpace; - var space = 4; - - if (available < 4 && available > 0) { - space = available; - } - - cell = { - grid: space - }; - - row.areas.push(cell); - } - $scope.currentCell = cell; - } - }; - - $scope.deleteArea = function (cell, row) { - if ($scope.currentCell === cell) { - $scope.currentCell = undefined; - } - var index = row.areas.indexOf(cell) - row.areas.splice(index, 1); - }; - - $scope.closeArea = function() { - $scope.currentCell = undefined; - }; - - $scope.nameChanged = false; - var originalName = $scope.currentRow.name; - $scope.$watch("currentRow", function(row) { - if (row) { - - var total = 0; - _.forEach(row.areas, function(area) { - total = (total + area.grid); - }); - - $scope.availableRowSpace = $scope.columns - total; - - if (originalName) { - if (originalName != row.name) { - $scope.nameChanged = true; - } - else { - $scope.nameChanged = false; - } - } - } - }, true); - -} - -angular.module("umbraco").controller("Umbraco.PropertyEditors.GridPrevalueEditor.RowConfigController", RowConfigController); + $scope.currentLayout = $scope.model.currentLayout; + $scope.columns = $scope.model.columns; + $scope.rows = $scope.model.rows; -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.Grid.EmbedController", - function ($scope, $rootScope, $timeout) { - - $scope.setEmbed = function(){ - $scope.embedDialog = {}; - $scope.embedDialog.view = "embed"; - $scope.embedDialog.show = true; - - $scope.embedDialog.submit = function(model) { - $scope.control.value = model.embed.preview; - $scope.embedDialog.show = false; - $scope.embedDialog = null; - }; - - $scope.embedDialog.close = function(oldModel) { - $scope.embedDialog.show = false; - $scope.embedDialog = null; - }; - - }; - - $timeout(function(){ - if($scope.control.$initializing){ - $scope.setEmbed(); - } - }, 200); -}); + $scope.scaleUp = function(section, max, overflow){ + var add = 1; + if(overflow !== true){ + add = (max > 1) ? 1 : max; + } + //var add = (max > 1) ? 1 : max; + section.grid = section.grid+add; + }; -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.Grid.MacroController", - function ($scope, $rootScope, $timeout, dialogService, macroResource, macroService, $routeParams) { - - $scope.title = "Click to insert macro"; - - $scope.setMacro = function(){ - - var dialogData = { - richTextEditor: true, - macroData: $scope.control.value || { - macroAlias: $scope.control.editor.config && $scope.control.editor.config.macroAlias - ? $scope.control.editor.config.macroAlias : "" - } - }; - - $scope.macroPickerOverlay = {}; - $scope.macroPickerOverlay.view = "macropicker"; - $scope.macroPickerOverlay.dialogData = dialogData; - $scope.macroPickerOverlay.show = true; - - $scope.macroPickerOverlay.submit = function(model) { - - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); - - $scope.control.value = { - macroAlias: macroObject.macroAlias, - macroParamsDictionary: macroObject.macroParamsDictionary - }; - - $scope.setPreview($scope.control.value ); - - $scope.macroPickerOverlay.show = false; - $scope.macroPickerOverlay = null; - }; - - $scope.macroPickerOverlay.close = function(oldModel) { - $scope.macroPickerOverlay.show = false; - $scope.macroPickerOverlay = null; - }; - - }; - - $scope.setPreview = function(macro){ - var contentId = $routeParams.id; - - macroResource.getMacroResultAsHtmlForEditor(macro.macroAlias, contentId, macro.macroParamsDictionary) - .then(function (htmlResult) { - $scope.title = macro.macroAlias; - if(htmlResult.trim().length > 0 && htmlResult.indexOf("Macro:") < 0){ - $scope.preview = htmlResult; - } - }); - - }; - - $timeout(function(){ - if($scope.control.$initializing){ - $scope.setMacro(); - }else if($scope.control.value){ - $scope.setPreview($scope.control.value); - } - }, 200); -}); + $scope.scaleDown = function(section){ + var remove = (section.grid > 1) ? 1 : 0; + section.grid = section.grid-remove; + }; -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.Grid.MediaController", - function ($scope, $rootScope, $timeout) { - - $scope.setImage = function(){ - $scope.mediaPickerOverlay = {}; - $scope.mediaPickerOverlay.view = "mediapicker"; - $scope.mediaPickerOverlay.cropSize = $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined; - $scope.mediaPickerOverlay.showDetails = true; - $scope.mediaPickerOverlay.disableFolderSelect = true; - $scope.mediaPickerOverlay.onlyImages = true; - $scope.mediaPickerOverlay.show = true; - - $scope.mediaPickerOverlay.submit = function(model) { - var selectedImage = model.selectedImages[0]; - - $scope.control.value = { - focalPoint: selectedImage.focalPoint, - id: selectedImage.id, - image: selectedImage.image, - altText: selectedImage.altText - }; - - $scope.setUrl(); - - $scope.mediaPickerOverlay.show = false; - $scope.mediaPickerOverlay = null; - }; - - $scope.mediaPickerOverlay.close = function(oldModel) { - $scope.mediaPickerOverlay.show = false; - $scope.mediaPickerOverlay = null; - }; - }; - - $scope.setUrl = function(){ - - if($scope.control.value.image){ - var url = $scope.control.value.image; - - if($scope.control.editor.config && $scope.control.editor.config.size){ - url += "?width=" + $scope.control.editor.config.size.width; - url += "&height=" + $scope.control.editor.config.size.height; - url += "&animationprocessmode=first"; - - if($scope.control.value.focalPoint){ - url += "¢er=" + $scope.control.value.focalPoint.top +"," + $scope.control.value.focalPoint.left; - url += "&mode=crop"; - } - } - - // set default size if no crop present (moved from the view) - if (url.indexOf('?') == -1) - { - url += "?width=800&upscale=false&animationprocessmode=false" - } - $scope.url = url; - } - }; - - $timeout(function(){ - if($scope.control.$initializing){ - $scope.setImage(); - }else if($scope.control.value){ - $scope.setUrl(); - } - }, 200); -}); + $scope.percentage = function(spans){ + return ((spans / $scope.columns) * 100).toFixed(8); + }; -(function() { - "use strict"; - - function GridRichTextEditorController($scope, tinyMceService, macroService) { - - var vm = this; - - vm.openLinkPicker = openLinkPicker; - vm.openMediaPicker = openMediaPicker; - vm.openMacroPicker = openMacroPicker; - vm.openEmbed = openEmbed; - - function openLinkPicker(editor, currentTarget, anchorElement) { - vm.linkPickerOverlay = { - view: "linkpicker", - currentTarget: currentTarget, - show: true, - submit: function(model) { - tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); - vm.linkPickerOverlay.show = false; - vm.linkPickerOverlay = null; - } - }; - } - - function openMediaPicker(editor, currentTarget, userData) { - vm.mediaPickerOverlay = { - currentTarget: currentTarget, - onlyImages: true, - showDetails: true, - startNodeId: userData.startMediaId, - view: "mediapicker", - show: true, - submit: function(model) { - tinyMceService.insertMediaInEditor(editor, model.selectedImages[0]); - vm.mediaPickerOverlay.show = false; - vm.mediaPickerOverlay = null; - } - }; - } - - function openEmbed(editor) { - vm.embedOverlay = { - view: "embed", - show: true, - submit: function(model) { - tinyMceService.insertEmbeddedMediaInEditor(editor, model.embed.preview); - vm.embedOverlay.show = false; - vm.embedOverlay = null; - } - }; - } - - function openMacroPicker(editor, dialogData) { - vm.macroPickerOverlay = { - view: "macropicker", - dialogData: dialogData, - show: true, - submit: function(model) { - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); - tinyMceService.insertMacroInEditor(editor, macroObject, $scope); - vm.macroPickerOverlay.show = false; - vm.macroPickerOverlay = null; - } - }; - } - - - - } - - angular.module("umbraco").controller("Umbraco.PropertyEditors.Grid.RichTextEditorController", GridRichTextEditorController); - -})(); + $scope.toggleCollection = function(collection, toggle){ + if(toggle){ + collection = []; + }else{ + delete collection; + } + }; -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.Grid.TextStringController", - function ($scope, $rootScope, $timeout, dialogService) { - - - - }); - -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.GridController", - function ($scope, $http, assetsService, localizationService, $rootScope, dialogService, gridService, mediaResource, imageHelper, $timeout, umbRequestHelper, angularHelper) { - - // Grid status variables - var placeHolder = ""; - var currentForm = angularHelper.getCurrentForm($scope); - - $scope.currentRow = null; - $scope.currentCell = null; - $scope.currentToolsControl = null; - $scope.currentControl = null; - $scope.openRTEToolbarId = null; - $scope.hasSettings = false; - $scope.showRowConfigurations = true; - $scope.sortMode = false; - $scope.reorderKey = "general_reorder"; - - // ********************************************* - // Sortable options - // ********************************************* - - var draggedRteSettings; - - $scope.sortableOptionsRow = { - distance: 10, - cursor: "move", - placeholder: "ui-sortable-placeholder", - handle: ".umb-row-title-bar", - helper: "clone", - forcePlaceholderSize: true, - tolerance: "pointer", - zIndex: 999999999999999999, - scrollSensitivity: 100, - cursorAt: { - top: 40, - left: 60 - }, - - sort: function (event, ui) { - /* prevent vertical scroll out of the screen */ - var max = $(".umb-grid").width() - 150; - if (parseInt(ui.helper.css("left")) > max) { - ui.helper.css({ "left": max + "px" }); - } - if (parseInt(ui.helper.css("left")) < 20) { - ui.helper.css({ "left": 20 }); - } - }, - - start: function (e, ui) { - - // Fade out row when sorting - ui.item.context.style.display = "block"; - ui.item.context.style.opacity = "0.5"; - - draggedRteSettings = {}; - ui.item.find(".mceNoEditor").each(function () { - // remove all RTEs in the dragged row and save their settings - var id = $(this).attr("id"); - draggedRteSettings[id] = _.findWhere(tinyMCE.editors, { id: id }).settings; - // tinyMCE.execCommand("mceRemoveEditor", false, id); - }); - }, - - stop: function (e, ui) { - - // Fade in row when sorting stops - ui.item.context.style.opacity = "1"; - - // reset all RTEs affected by the dragging - ui.item.parents(".umb-column").find(".mceNoEditor").each(function () { - var id = $(this).attr("id"); - draggedRteSettings[id] = draggedRteSettings[id] || _.findWhere(tinyMCE.editors, { id: id }).settings; - tinyMCE.execCommand("mceRemoveEditor", false, id); - tinyMCE.init(draggedRteSettings[id]); - }); - currentForm.$setDirty(); - } - }; - - var notIncludedRte = []; - var cancelMove = false; - - $scope.sortableOptionsCell = { - distance: 10, - cursor: "move", - placeholder: "ui-sortable-placeholder", - handle: ".umb-control-handle", - helper: "clone", - connectWith: ".umb-cell-inner", - forcePlaceholderSize: true, - tolerance: "pointer", - zIndex: 999999999999999999, - scrollSensitivity: 100, - cursorAt: { - top: 45, - left: 90 - }, - - sort: function (event, ui) { - - /* prevent vertical scroll out of the screen */ - var position = parseInt(ui.item.parent().offset().left) + parseInt(ui.helper.css("left")) - parseInt($(".umb-grid").offset().left); - var max = $(".umb-grid").width() - 220; - if (position > max) { - ui.helper.css({ "left": max - parseInt(ui.item.parent().offset().left) + parseInt($(".umb-grid").offset().left) + "px" }); - } - if (position < 0) { - ui.helper.css({ "left": 0 - parseInt(ui.item.parent().offset().left) + parseInt($(".umb-grid").offset().left) + "px" }); - } - }, - - over: function (event, ui) { - var allowedEditors = $(event.target).scope().area.allowed; - - if ($.inArray(ui.item.scope().control.editor.alias, allowedEditors) < 0 && allowedEditors) { - - $scope.$apply(function () { - $(event.target).scope().area.dropNotAllowed = true; - }); - - ui.placeholder.hide(); - cancelMove = true; - } - else { - if ($(event.target).scope().area.controls.length == 0){ - - $scope.$apply(function () { - $(event.target).scope().area.dropOnEmpty = true; - }); - ui.placeholder.hide(); - } else { - ui.placeholder.show(); - } - cancelMove = false; - } - }, - - out: function(event, ui) { - $scope.$apply(function () { - $(event.target).scope().area.dropNotAllowed = false; - $(event.target).scope().area.dropOnEmpty = false; - }); - }, - - update: function (event, ui) { - /* add all RTEs which are affected by the dragging */ - if (!ui.sender) { - if (cancelMove) { - ui.item.sortable.cancel(); - } - ui.item.parents(".umb-cell.content").find(".mceNoEditor").each(function () { - if ($.inArray($(this).attr("id"), notIncludedRte) < 0) { - notIncludedRte.splice(0, 0, $(this).attr("id")); - } - }); - } - else { - $(event.target).find(".mceNoEditor").each(function () { - if ($.inArray($(this).attr("id"), notIncludedRte) < 0) { - notIncludedRte.splice(0, 0, $(this).attr("id")); - } - }); - } - currentForm.$setDirty(); - }, - - start: function (e, ui) { - - // fade out control when sorting - ui.item.context.style.display = "block"; - ui.item.context.style.opacity = "0.5"; - - // reset dragged RTE settings in case a RTE isn't dragged - draggedRteSettings = undefined; - ui.item.context.style.display = "block"; - ui.item.find(".mceNoEditor").each(function () { - notIncludedRte = []; - var editors = _.findWhere(tinyMCE.editors, { id: $(this).attr("id") }); - - // save the dragged RTE settings - if(editors) { - draggedRteSettings = editors.settings; - - // remove the dragged RTE - tinyMCE.execCommand("mceRemoveEditor", false, $(this).attr("id")); - - } - - }); - }, - - stop: function (e, ui) { - - // Fade in control when sorting stops - ui.item.context.style.opacity = "1"; - - ui.item.parents(".umb-cell-content").find(".mceNoEditor").each(function () { - if ($.inArray($(this).attr("id"), notIncludedRte) < 0) { - // add all dragged's neighbouring RTEs in the new cell - notIncludedRte.splice(0, 0, $(this).attr("id")); - } - }); - $timeout(function () { - // reconstruct the dragged RTE (could be undefined when dragging something else than RTE) - if (draggedRteSettings !== undefined) { - tinyMCE.init(draggedRteSettings); - } - - _.forEach(notIncludedRte, function (id) { - // reset all the other RTEs - if (draggedRteSettings === undefined || id !== draggedRteSettings.id) { - var rteSettings = _.findWhere(tinyMCE.editors, { id: id }).settings; - tinyMCE.execCommand("mceRemoveEditor", false, id); - tinyMCE.init(rteSettings); - } - }); - }, 500, false); - - $scope.$apply(function () { - - var cell = $(e.target).scope().area; - cell.hasActiveChild = hasActiveChild(cell, cell.controls); - cell.active = false; - }); - } - - }; - - $scope.toggleSortMode = function() { - $scope.sortMode = !$scope.sortMode; - if($scope.sortMode) { - $scope.reorderKey = "general_reorderDone"; - } else { - $scope.reorderKey = "general_reorder"; - } - }; - - $scope.showReorderButton = function() { - if($scope.model.value && $scope.model.value.sections) { - for(var i = 0; $scope.model.value.sections.length > i; i++) { - var section = $scope.model.value.sections[i]; - if(section.rows && section.rows.length > 0) { - return true; - } - } - } - }; - - // ********************************************* - // Add items overlay menu - // ********************************************* - $scope.openEditorOverlay = function(event, area, index, key) { - $scope.editorOverlay = { - view: "itempicker", - filter: false, - title: localizationService.localize("grid_insertControl"), - availableItems: area.$allowedEditors, - event: event, - show: true, - submit: function(model) { - $scope.addControl(model.selectedItem, area, index); - $scope.editorOverlay.show = false; - $scope.editorOverlay = null; - } - }; - }; - - // ********************************************* - // Template management functions - // ********************************************* - - $scope.addTemplate = function (template) { - $scope.model.value = angular.copy(template); - - //default row data - _.forEach($scope.model.value.sections, function (section) { - $scope.initSection(section); - }); - }; - - - // ********************************************* - // Row management function - // ********************************************* - - $scope.clickRow = function(index, rows) { - rows[index].active = true; - }; - - $scope.clickOutsideRow = function(index, rows) { - rows[index].active = false; - }; - - function getAllowedLayouts(section) { - - var layouts = $scope.model.config.items.layouts; - - //This will occur if it is a new section which has been - // created from a 'template' - if (section.allowed && section.allowed.length > 0) { - return _.filter(layouts, function (layout) { - return _.indexOf(section.allowed, layout.name) >= 0; - }); - } - else { - - - return layouts; - } - } - - $scope.addRow = function (section, layout) { - - //copy the selected layout into the rows collection - var row = angular.copy(layout); - - // Init row value - row = $scope.initRow(row); - - // Push the new row - if (row) { - section.rows.push(row); - } - - currentForm.$setDirty(); - - $scope.showRowConfigurations = false; - - }; - - $scope.removeRow = function (section, $index) { - if (section.rows.length > 0) { - section.rows.splice($index, 1); - $scope.currentRow = null; - $scope.openRTEToolbarId = null; - currentForm.$setDirty(); - } - - if(section.rows.length === 0) { - $scope.showRowConfigurations = true; - } - }; - - var shouldApply = function(item, itemType, gridItem) { - if (item.applyTo === undefined || item.applyTo === null || item.applyTo === "") { - return true; - } - - if (typeof (item.applyTo) === "string") { - return item.applyTo === itemType; - } - - if (itemType === "row") { - if (item.applyTo.row === undefined) { - return false; - } - if (item.applyTo.row === null || item.applyTo.row === "") { - return true; - } - var rows = item.applyTo.row.split(','); - return _.indexOf(rows, gridItem.name) !== -1; - } else if (itemType === "cell") { - if (item.applyTo.cell === undefined) { - return false; - } - if (item.applyTo.cell === null || item.applyTo.cell === "") { - return true; - } - var cells = item.applyTo.cell.split(','); - var cellSize = gridItem.grid.toString(); - return _.indexOf(cells, cellSize) !== -1; - } - } - - $scope.editGridItemSettings = function (gridItem, itemType) { - - placeHolder = "{0}"; - - var styles, config; - if (itemType === 'control') { - styles = null; - config = angular.copy(gridItem.editor.config.settings); - } else { - styles = _.filter(angular.copy($scope.model.config.items.styles), function (item) { return shouldApply(item, itemType, gridItem); }); - config = _.filter(angular.copy($scope.model.config.items.config), function (item) { return shouldApply(item, itemType, gridItem); }); - } - - if(angular.isObject(gridItem.config)){ - _.each(config, function(cfg){ - var val = gridItem.config[cfg.key]; - if(val){ - cfg.value = stripModifier(val, cfg.modifier); - } - }); - } - - if(angular.isObject(gridItem.styles)){ - _.each(styles, function(style){ - var val = gridItem.styles[style.key]; - if(val){ - style.value = stripModifier(val, style.modifier); - } - }); - } - - $scope.gridItemSettingsDialog = {}; - $scope.gridItemSettingsDialog.view = "views/propertyeditors/grid/dialogs/config.html"; - $scope.gridItemSettingsDialog.title = "Settings"; - $scope.gridItemSettingsDialog.styles = styles; - $scope.gridItemSettingsDialog.config = config; - - $scope.gridItemSettingsDialog.show = true; - - $scope.gridItemSettingsDialog.submit = function(model) { - - var styleObject = {}; - var configObject = {}; - - _.each(model.styles, function(style){ - if(style.value){ - styleObject[style.key] = addModifier(style.value, style.modifier); - } - }); - _.each(model.config, function (cfg) { - if (cfg.value) { - configObject[cfg.key] = addModifier(cfg.value, cfg.modifier); - } - }); - - gridItem.styles = styleObject; - gridItem.config = configObject; - gridItem.hasConfig = gridItemHasConfig(styleObject, configObject); - - currentForm.$setDirty(); - - $scope.gridItemSettingsDialog.show = false; - $scope.gridItemSettingsDialog = null; - }; - - $scope.gridItemSettingsDialog.close = function(oldModel) { - $scope.gridItemSettingsDialog.show = false; - $scope.gridItemSettingsDialog = null; - }; - - }; - - function stripModifier(val, modifier) { - if (!val || !modifier || modifier.indexOf(placeHolder) < 0) { - return val; - } else { - var paddArray = modifier.split(placeHolder); - if(paddArray.length == 1){ - if (modifier.indexOf(placeHolder) === 0) { - return val.slice(0, -paddArray[0].length); - } else { - return val.slice(paddArray[0].length, 0); - } - } else { - if (paddArray[1].length === 0) { - return val.slice(paddArray[0].length); - } - return val.slice(paddArray[0].length, -paddArray[1].length); - } - } - } - - var addModifier = function(val, modifier){ - if (!modifier || modifier.indexOf(placeHolder) < 0) { - return val; - } else { - return modifier.replace(placeHolder, val); - } - }; - - function gridItemHasConfig(styles, config) { - - if(_.isEmpty(styles) && _.isEmpty(config)) { - return false; - } else { - return true; - } - - } - - // ********************************************* - // Area management functions - // ********************************************* - - $scope.clickCell = function(index, cells, row) { - cells[index].active = true; - row.hasActiveChild = true; - }; - - $scope.clickOutsideCell = function(index, cells, row) { - cells[index].active = false; - row.hasActiveChild = hasActiveChild(row, cells); - }; - - $scope.cellPreview = function (cell) { - if (cell && cell.$allowedEditors) { - var editor = cell.$allowedEditors[0]; - return editor.icon; - } else { - return "icon-layout"; - } - }; - - - // ********************************************* - // Control management functions - // ********************************************* - $scope.clickControl = function (index, controls, cell) { - controls[index].active = true; - cell.hasActiveChild = true; - }; - - $scope.clickOutsideControl = function (index, controls, cell) { - controls[index].active = false; - cell.hasActiveChild = hasActiveChild(cell, controls); - }; - - function hasActiveChild(item, children) { - - var activeChild = false; - - for(var i = 0; children.length > i; i++) { - var child = children[i]; - - if(child.active) { - activeChild = true; - } - } - - if(activeChild) { - return true; - } - - } - - - var guid = (function () { - function s4() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - } - return function () { - return s4() + s4() + "-" + s4() + "-" + s4() + "-" + - s4() + "-" + s4() + s4() + s4(); - }; - })(); - - $scope.setUniqueId = function (cell, index) { - return guid(); - }; - - $scope.addControl = function (editor, cell, index, initialize) { - - initialize = (initialize !== false); - - var newControl = { - value: null, - editor: editor, - $initializing: initialize - }; - - if (index === undefined) { - index = cell.controls.length; - } - - newControl.active = true; - - //populate control - $scope.initControl(newControl, index + 1); - - cell.controls.push(newControl); - - }; - - $scope.addTinyMce = function (cell) { - var rte = $scope.getEditor("rte"); - $scope.addControl(rte, cell); - }; - - $scope.getEditor = function (alias) { - return _.find($scope.availableEditors, function (editor) { return editor.alias === alias; }); - }; - - $scope.removeControl = function (cell, $index) { - $scope.currentControl = null; - cell.controls.splice($index, 1); - }; - - $scope.percentage = function (spans) { - return ((spans / $scope.model.config.items.columns) * 100).toFixed(8); - }; - - - $scope.clearPrompt = function (scopedObject, e) { - scopedObject.deletePrompt = false; - e.preventDefault(); - e.stopPropagation(); - }; - - $scope.togglePrompt = function (scopedObject) { - scopedObject.deletePrompt = !scopedObject.deletePrompt; - }; - - $scope.hidePrompt = function (scopedObject) { - scopedObject.deletePrompt = false; - }; - - $scope.toggleAddRow = function() { - $scope.showRowConfigurations = !$scope.showRowConfigurations; - }; - - - // ********************************************* - // Initialization - // these methods are called from ng-init on the template - // so we can controll their first load data - // - // intialization sets non-saved data like percentage sizing, allowed editors and - // other data that should all be pre-fixed with $ to strip it out on save - // ********************************************* - - // ********************************************* - // Init template + sections - // ********************************************* - $scope.initContent = function () { - var clear = true; - - //settings indicator shortcut - if ( ($scope.model.config.items.config && $scope.model.config.items.config.length > 0) || ($scope.model.config.items.styles && $scope.model.config.items.styles.length > 0)) { - $scope.hasSettings = true; - } - - //ensure the grid has a column value set, - //if nothing is found, set it to 12 - if ($scope.model.config.items.columns && angular.isString($scope.model.config.items.columns)) { - $scope.model.config.items.columns = parseInt($scope.model.config.items.columns); - } else { - $scope.model.config.items.columns = 12; - } - - if ($scope.model.value && $scope.model.value.sections && $scope.model.value.sections.length > 0 && $scope.model.value.sections[0].rows && $scope.model.value.sections[0].rows.length > 0) { - - if ($scope.model.value.name && angular.isArray($scope.model.config.items.templates)) { - - //This will occur if it is an existing value, in which case - // we need to determine which layout was applied by looking up - // the name - // TODO: We need to change this to an immutable ID!! - - var found = _.find($scope.model.config.items.templates, function (t) { - return t.name === $scope.model.value.name; - }); - - if (found && angular.isArray(found.sections) && found.sections.length === $scope.model.value.sections.length) { - - //Cool, we've found the template associated with our current value with matching sections counts, now we need to - // merge this template data on to our current value (as if it was new) so that we can preserve what is and isn't - // allowed for this template based on the current config. - - _.each(found.sections, function (templateSection, index) { - angular.extend($scope.model.value.sections[index], angular.copy(templateSection)); - }); - - } - } - - _.forEach($scope.model.value.sections, function (section, index) { - - if (section.grid > 0) { - $scope.initSection(section); - - //we do this to ensure that the grid can be reset by deleting the last row - if (section.rows.length > 0) { - clear = false; - } - } else { - $scope.model.value.sections.splice(index, 1); - } - }); - } else if ($scope.model.config.items.templates && $scope.model.config.items.templates.length === 1) { - $scope.addTemplate($scope.model.config.items.templates[0]); - clear = false; - } - - if (clear) { - $scope.model.value = undefined; - } - }; - - $scope.initSection = function (section) { - section.$percentage = $scope.percentage(section.grid); - - section.$allowedLayouts = getAllowedLayouts(section); - - if (!section.rows || section.rows.length === 0) { - section.rows = []; - if(section.$allowedLayouts.length === 1){ - $scope.addRow(section, section.$allowedLayouts[0]); - } - } else { - _.forEach(section.rows, function (row, index) { - if (!row.$initialized) { - var initd = $scope.initRow(row); - - //if init fails, remove - if (!initd) { - section.rows.splice(index, 1); - } else { - section.rows[index] = initd; - } - } - }); - - // if there is more than one row added - hide row add tools - $scope.showRowConfigurations = false; - } - }; - - - // ********************************************* - // Init layout / row - // ********************************************* - $scope.initRow = function (row) { - - //merge the layout data with the original config data - //if there are no config info on this, splice it out - var original = _.find($scope.model.config.items.layouts, function (o) { return o.name === row.name; }); - - if (!original) { - return null; - } else { - //make a copy to not touch the original config - original = angular.copy(original); - original.styles = row.styles; - original.config = row.config; - original.hasConfig = gridItemHasConfig(row.styles, row.config); - - - //sync area configuration - _.each(original.areas, function (area, areaIndex) { - - - if (area.grid > 0) { - var currentArea = row.areas[areaIndex]; - - if (currentArea) { - area.config = currentArea.config; - area.styles = currentArea.styles; - area.hasConfig = gridItemHasConfig(currentArea.styles, currentArea.config); - } - - //set editor permissions - if (!area.allowed || area.allowAll === true) { - area.$allowedEditors = $scope.availableEditors; - area.$allowsRTE = true; - } else { - area.$allowedEditors = _.filter($scope.availableEditors, function (editor) { - return _.indexOf(area.allowed, editor.alias) >= 0; - }); - - if (_.indexOf(area.allowed, "rte") >= 0) { - area.$allowsRTE = true; - } - } - - //copy over existing controls into the new areas - if (row.areas.length > areaIndex && row.areas[areaIndex].controls) { - area.controls = currentArea.controls; - - _.forEach(area.controls, function (control, controlIndex) { - $scope.initControl(control, controlIndex); - }); - - } else { - //if empty - area.controls = []; - - //if only one allowed editor - if(area.$allowedEditors.length === 1){ - $scope.addControl(area.$allowedEditors[0], area, 0, false); - } - } - - //set width - area.$percentage = $scope.percentage(area.grid); - area.$uniqueId = $scope.setUniqueId(); - - } else { - original.areas.splice(areaIndex, 1); - } - }); - - //replace the old row - original.$initialized = true; - - //set a disposable unique ID - original.$uniqueId = $scope.setUniqueId(); - - //set a no disposable unique ID (util for row styling) - original.id = !row.id ? $scope.setUniqueId() : row.id; - - return original; - } - - }; - - - // ********************************************* - // Init control - // ********************************************* - - $scope.initControl = function (control, index) { - control.$index = index; - control.$uniqueId = $scope.setUniqueId(); - - //error handling in case of missing editor.. - //should only happen if stripped earlier - if (!control.editor) { - control.$editorPath = "views/propertyeditors/grid/editors/error.html"; - } - - if (!control.$editorPath) { - var editorConfig = $scope.getEditor(control.editor.alias); - - if (editorConfig) { - control.editor = editorConfig; - - //if its an absolute path - if (control.editor.view.startsWith("/") || control.editor.view.startsWith("~/")) { - control.$editorPath = umbRequestHelper.convertVirtualToAbsolutePath(control.editor.view); - } - else { - //use convention - control.$editorPath = "views/propertyeditors/grid/editors/" + control.editor.view + ".html"; - } - } - else { - control.$editorPath = "views/propertyeditors/grid/editors/error.html"; - } - } - - - }; - - - gridService.getGridEditors().then(function (response) { - $scope.availableEditors = response.data; - - $scope.contentReady = true; - - // ********************************************* - // Init grid - // ********************************************* - $scope.initContent(); - - }); - - //Clean the grid value before submitting to the server, we don't need - // all of that grid configuration in the value to be stored!! All of that - // needs to be merged in at runtime to ensure that the real config values are used - // if they are ever updated. - - var unsubscribe = $scope.$on("formSubmitting", function () { - - if ($scope.model.value && $scope.model.value.sections) { - _.each($scope.model.value.sections, function(section) { - if (section.rows) { - _.each(section.rows, function (row) { - if (row.areas) { - _.each(row.areas, function (area) { - - //Remove the 'editors' - these are the allowed editors, these will - // be injected at runtime to this editor, it should not be persisted - - if (area.editors) { - delete area.editors; - } - - if (area.controls) { - _.each(area.controls, function (control) { - if (control.editor) { - //replace - var alias = control.editor.alias; - control.editor = { - alias: alias - }; - } - }); - } - }); - } - }); - } - }); - } - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on("$destroy", function () { - unsubscribe(); - }); - - }); -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.GridPrevalueEditorController", - function ($scope, $http, assetsService, $rootScope, dialogService, mediaResource, gridService, imageHelper, $timeout) { - - var emptyModel = { - styles:[ - { - label: "Set a background image", - description: "Set a row background", - key: "background-image", - view: "imagepicker", - modifier: "url({0})" - } - ], - - config:[ - { - label: "Class", - description: "Set a css class", - key: "class", - view: "textstring" - } - ], - - columns: 12, - templates:[ - { - name: "1 column layout", - sections: [ - { - grid: 12, - } - ] - }, - { - name: "2 column layout", - sections: [ - { - grid: 4, - }, - { - grid: 8 - } - ] - } - ], - - - layouts:[ - { - label: "Headline", - name: "Headline", - areas: [ - { - grid: 12, - editors: ["headline"] - } - ] - }, - { - label: "Article", - name: "Article", - areas: [ - { - grid: 4 - }, - { - grid: 8 - } - ] - } - ] - }; - - /**************** - template - *****************/ - - $scope.configureTemplate = function(template) { - - var templatesCopy = angular.copy($scope.model.value.templates); - - if (template === undefined) { - template = { - name: "", - sections: [ - - ] - }; - $scope.model.value.templates.push(template); - } - - $scope.layoutConfigOverlay = {}; - $scope.layoutConfigOverlay.view = "views/propertyEditors/grid/dialogs/layoutconfig.html"; - $scope.layoutConfigOverlay.currentLayout = template; - $scope.layoutConfigOverlay.rows = $scope.model.value.layouts; - $scope.layoutConfigOverlay.columns = $scope.model.value.columns; - $scope.layoutConfigOverlay.show = true; - - $scope.layoutConfigOverlay.submit = function(model) { - $scope.layoutConfigOverlay.show = false; - $scope.layoutConfigOverlay = null; - }; - - $scope.layoutConfigOverlay.close = function(oldModel) { - - //reset templates - $scope.model.value.templates = templatesCopy; - - $scope.layoutConfigOverlay.show = false; - $scope.layoutConfigOverlay = null; - } - - }; - - $scope.deleteTemplate = function(index){ - $scope.model.value.templates.splice(index, 1); - }; - - - /**************** - Row - *****************/ - - $scope.configureLayout = function(layout) { - - var layoutsCopy = angular.copy($scope.model.value.layouts); - - if(layout === undefined){ - layout = { - name: "", - areas:[ - - ] - }; - $scope.model.value.layouts.push(layout); - } - - $scope.rowConfigOverlay = {}; - $scope.rowConfigOverlay.view = "views/propertyEditors/grid/dialogs/rowconfig.html"; - $scope.rowConfigOverlay.currentRow = layout; - $scope.rowConfigOverlay.editors = $scope.editors; - $scope.rowConfigOverlay.columns = $scope.model.value.columns; - $scope.rowConfigOverlay.show = true; - - $scope.rowConfigOverlay.submit = function(model) { - $scope.rowConfigOverlay.show = false; - $scope.rowConfigOverlay = null; - }; - - $scope.rowConfigOverlay.close = function(oldModel) { - $scope.model.value.layouts = layoutsCopy; - $scope.rowConfigOverlay.show = false; - $scope.rowConfigOverlay = null; - }; - - }; - - //var rowDeletesPending = false; - $scope.deleteLayout = function(index) { - - $scope.rowDeleteOverlay = {}; - $scope.rowDeleteOverlay.view = "views/propertyEditors/grid/dialogs/rowdeleteconfirm.html"; - $scope.rowDeleteOverlay.dialogData = { - rowName: $scope.model.value.layouts[index].name - }; - $scope.rowDeleteOverlay.show = true; - - $scope.rowDeleteOverlay.submit = function(model) { - - $scope.model.value.layouts.splice(index, 1); - - $scope.rowDeleteOverlay.show = false; - $scope.rowDeleteOverlay = null; - }; - - $scope.rowDeleteOverlay.close = function(oldModel) { - $scope.rowDeleteOverlay.show = false; - $scope.rowDeleteOverlay = null; - }; - - }; - - - /**************** - utillities - *****************/ - $scope.toggleCollection = function(collection, toggle){ - if(toggle){ - collection = []; - }else{ - delete collection; - } - }; - - $scope.percentage = function(spans){ - return ((spans / $scope.model.value.columns) * 100).toFixed(8); - }; - - $scope.zeroWidthFilter = function (cell) { - return cell.grid > 0; - }; - - /**************** - Config - *****************/ - - $scope.removeConfigValue = function(collection, index){ - collection.splice(index, 1); - }; - - var editConfigCollection = function(configValues, title, callback) { - - $scope.editConfigCollectionOverlay = {}; - $scope.editConfigCollectionOverlay.view = "views/propertyeditors/grid/dialogs/editconfig.html"; - $scope.editConfigCollectionOverlay.config = configValues; - $scope.editConfigCollectionOverlay.title = title; - $scope.editConfigCollectionOverlay.show = true; - - $scope.editConfigCollectionOverlay.submit = function(model) { - - callback(model.config) - - $scope.editConfigCollectionOverlay.show = false; - $scope.editConfigCollectionOverlay = null; - }; - - $scope.editConfigCollectionOverlay.close = function(oldModel) { - $scope.editConfigCollectionOverlay.show = false; - $scope.editConfigCollectionOverlay = null; - }; - - }; - - $scope.editConfig = function() { - editConfigCollection($scope.model.value.config, "Settings", function(data) { - $scope.model.value.config = data; - }); - }; - - $scope.editStyles = function() { - editConfigCollection($scope.model.value.styles, "Styling", function(data){ - $scope.model.value.styles = data; - }); - }; - - /**************** - editors - *****************/ - gridService.getGridEditors().then(function(response){ - $scope.editors = response.data; - }); - - - /* init grid data */ - if (!$scope.model.value || $scope.model.value === "" || !$scope.model.value.templates) { - $scope.model.value = emptyModel; - } else { - - if (!$scope.model.value.columns) { - $scope.model.value.columns = emptyModel.columns; - } - - - if (!$scope.model.value.config) { - $scope.model.value.config = []; - } - - if (!$scope.model.value.styles) { - $scope.model.value.styles = []; - } - } - - /**************** - Clean up - *****************/ - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - var ts = $scope.model.value.templates; - var ls = $scope.model.value.layouts; - - _.each(ts, function(t){ - _.each(t.sections, function(section, index){ - if(section.grid === 0){ - t.sections.splice(index, 1); - } - }); - }); - - _.each(ls, function(l){ - _.each(l.areas, function(area, index){ - if(area.grid === 0){ - l.areas.splice(index, 1); - } - }); - }); - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); - - }); + /**************** + Section + *****************/ + $scope.configureSection = function(section, template){ + if(section === undefined){ + var space = ($scope.availableLayoutSpace > 4) ? 4 : $scope.availableLayoutSpace; + section = { + grid: space + }; + template.sections.push(section); + } + + $scope.currentSection = section; + }; -//this controller simply tells the dialogs service to open a mediaPicker window -//with a specified callback, this callback will receive an object with a selection on it -angular.module('umbraco') - .controller("Umbraco.PropertyEditors.ImageCropperController", - function ($rootScope, $routeParams, $scope, $log, mediaHelper, cropperHelper, $timeout, editorState, umbRequestHelper, fileManager, angularHelper) { - - var config = angular.copy($scope.model.config); - $scope.imageIsLoaded = false; - - //move previously saved value to the editor - if ($scope.model.value) { - //backwards compat with the old file upload (incase some-one swaps them..) - if (angular.isString($scope.model.value)) { - config.src = $scope.model.value; - $scope.model.value = config; - } else if ($scope.model.value.crops) { - //sync any config changes with the editor and drop outdated crops - _.each($scope.model.value.crops, function (saved) { - var configured = _.find(config.crops, function (item) { return item.alias === saved.alias }); - - if (configured && configured.height === saved.height && configured.width === saved.width) { - configured.coordinates = saved.coordinates; - } - }); - $scope.model.value.crops = config.crops; - - //restore focalpoint if missing - if (!$scope.model.value.focalPoint) { - $scope.model.value.focalPoint = { left: 0.5, top: 0.5 }; - } - } - - $scope.imageSrc = $scope.model.value.src; - } - - - //crop a specific crop - $scope.crop = function (crop) { - $scope.currentCrop = crop; - $scope.currentPoint = undefined; - }; - - //done cropping - $scope.done = function () { - $scope.currentCrop = undefined; - $scope.currentPoint = undefined; - }; - - //crop a specific crop - $scope.clear = function (crop) { - //clear current uploaded files - fileManager.setFiles($scope.model.alias, []); - - //clear the ui - $scope.imageSrc = undefined; - if ($scope.model.value) { - delete $scope.model.value; - } - - // set form to dirty to tricker discard changes dialog - var currForm = angularHelper.getCurrentForm($scope); - currForm.$setDirty(); - }; - - //show previews - $scope.togglePreviews = function () { - if ($scope.showPreviews) { - $scope.showPreviews = false; - $scope.tempShowPreviews = false; - } else { - $scope.showPreviews = true; - } - }; - - $scope.imageLoaded = function() { - $scope.imageIsLoaded = true; - }; - - //on image selected, update the cropper - $scope.$on("filesSelected", function (ev, args) { - $scope.model.value = config; - - if (args.files && args.files[0]) { - - fileManager.setFiles($scope.model.alias, args.files); - - var reader = new FileReader(); - reader.onload = function (e) { - - $scope.$apply(function () { - $scope.imageSrc = e.target.result; - }); - - }; - - reader.readAsDataURL(args.files[0]); - } - }); - - - //here we declare a special method which will be called whenever the value has changed from the server - $scope.model.onValueChanged = function (newVal, oldVal) { - //clear current uploaded files - fileManager.setFiles($scope.model.alias, []); - }; - - var unsubscribe = $scope.$on("formSubmitting", function () { - $scope.done(); - }); - - $scope.$on('$destroy', function () { - unsubscribe(); - }); - }) - .run(function (mediaHelper, umbRequestHelper) { - if (mediaHelper && mediaHelper.registerFileResolver) { - - //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource - // they contain different data structures so if we need to query against it we need to be aware of this. - mediaHelper.registerFileResolver("Umbraco.ImageCropper", function (property, entity, thumbnail) { - if (property.value && property.value.src) { - - if (thumbnail === true) { - return property.value.src + "?width=500&mode=max&animationprocessmode=first"; - } - else { - return property.value.src; - } - - //this is a fallback in case the cropper has been asssigned a upload field - } - else if (angular.isString(property.value)) { - if (thumbnail) { - - if (mediaHelper.detectIfImageByExtension(property.value)) { - - var thumbnailUrl = umbRequestHelper.getApiUrl( - "imagesApiBaseUrl", - "GetBigThumbnail", - [{ originalImagePath: property.value }]); - - return thumbnailUrl; - } - else { - return null; - } - - } - else { - return property.value; - } - } - - return null; - }); - } - }); + $scope.deleteSection = function(section, template) { + if ($scope.currentSection === section) { + $scope.currentSection = undefined; + } + var index = template.sections.indexOf(section) + template.sections.splice(index, 1); + }; + + $scope.closeSection = function(){ + $scope.currentSection = undefined; + }; -angular.module("umbraco").controller("Umbraco.PrevalueEditors.CropSizesController", - function ($scope, $timeout) { - - if (!$scope.model.value) { - $scope.model.value = []; - } - - $scope.remove = function (item, evt) { - evt.preventDefault(); - $scope.model.value = _.reject($scope.model.value, function (x) { - return x.alias === item.alias; - }); - }; - - $scope.edit = function (item, evt) { - evt.preventDefault(); - $scope.newItem = item; - }; - - $scope.cancel = function (evt) { - evt.preventDefault(); - $scope.newItem = null; - }; - - $scope.add = function (evt) { - evt.preventDefault(); - - if ($scope.newItem && $scope.newItem.alias && - angular.isNumber($scope.newItem.width) && angular.isNumber($scope.newItem.height) && - $scope.newItem.width > 0 && $scope.newItem.height > 0) { - - var exists = _.find($scope.model.value, function (item) { return $scope.newItem.alias === item.alias; }); - if (!exists) { - $scope.model.value.push($scope.newItem); - $scope.newItem = {}; - $scope.hasError = false; - return; - } - } - - //there was an error, do the highlight (will be set back by the directive) - $scope.hasError = true; - }; + $scope.$watch("currentLayout", function(layout){ + if(layout){ + var total = 0; + _.forEach(layout.sections, function(section){ + total = (total + section.grid); + }); + + $scope.availableLayoutSpace = $scope.columns - total; + } + }, true); + }); + +function RowConfigController($scope) { + + $scope.currentRow = $scope.model.currentRow; + $scope.editors = $scope.model.editors; + $scope.columns = $scope.model.columns; + + $scope.scaleUp = function(section, max, overflow) { + var add = 1; + if (overflow !== true) { + add = (max > 1) ? 1 : max; + } + //var add = (max > 1) ? 1 : max; + section.grid = section.grid + add; + }; + + $scope.scaleDown = function(section) { + var remove = (section.grid > 1) ? 1 : 0; + section.grid = section.grid - remove; + }; + + $scope.percentage = function(spans) { + return ((spans / $scope.columns) * 100).toFixed(8); + }; + + $scope.toggleCollection = function(collection, toggle) { + if (toggle) { + collection = []; + } + else { + delete collection; + } + }; + + + /**************** + area + *****************/ + $scope.configureCell = function(cell, row) { + if ($scope.currentCell && $scope.currentCell === cell) { + delete $scope.currentCell; + } + else { + if (cell === undefined) { + var available = $scope.availableRowSpace; + var space = 4; + + if (available < 4 && available > 0) { + space = available; + } + + cell = { + grid: space + }; + + row.areas.push(cell); + } + $scope.currentCell = cell; + } + }; + + $scope.deleteArea = function (cell, row) { + if ($scope.currentCell === cell) { + $scope.currentCell = undefined; + } + var index = row.areas.indexOf(cell) + row.areas.splice(index, 1); + }; + + $scope.closeArea = function() { + $scope.currentCell = undefined; + }; + + $scope.nameChanged = false; + var originalName = $scope.currentRow.name; + $scope.$watch("currentRow", function(row) { + if (row) { + + var total = 0; + _.forEach(row.areas, function(area) { + total = (total + area.grid); + }); + + $scope.availableRowSpace = $scope.columns - total; + + if (originalName) { + if (originalName != row.name) { + $scope.nameChanged = true; + } + else { + $scope.nameChanged = false; + } + } + } + }, true); + +} + +angular.module("umbraco").controller("Umbraco.PropertyEditors.GridPrevalueEditor.RowConfigController", RowConfigController); + +angular.module("umbraco") + .controller("Umbraco.PropertyEditors.Grid.EmbedController", + function ($scope, $rootScope, $timeout) { + + $scope.setEmbed = function(){ + $scope.embedDialog = {}; + $scope.embedDialog.view = "embed"; + $scope.embedDialog.show = true; + + $scope.embedDialog.submit = function(model) { + $scope.control.value = model.embed.preview; + $scope.embedDialog.show = false; + $scope.embedDialog = null; + }; + + $scope.embedDialog.close = function(oldModel) { + $scope.embedDialog.show = false; + $scope.embedDialog = null; + }; + + }; + + $timeout(function(){ + if($scope.control.$initializing){ + $scope.setEmbed(); + } + }, 200); +}); + +angular.module("umbraco") + .controller("Umbraco.PropertyEditors.Grid.MacroController", + function ($scope, $rootScope, $timeout, dialogService, macroResource, macroService, $routeParams) { + + $scope.title = "Click to insert macro"; + + $scope.setMacro = function(){ + + var dialogData = { + richTextEditor: true, + macroData: $scope.control.value || { + macroAlias: $scope.control.editor.config && $scope.control.editor.config.macroAlias + ? $scope.control.editor.config.macroAlias : "" + } + }; + + $scope.macroPickerOverlay = {}; + $scope.macroPickerOverlay.view = "macropicker"; + $scope.macroPickerOverlay.dialogData = dialogData; + $scope.macroPickerOverlay.show = true; + + $scope.macroPickerOverlay.submit = function(model) { + + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); + + $scope.control.value = { + macroAlias: macroObject.macroAlias, + macroParamsDictionary: macroObject.macroParamsDictionary + }; + + $scope.setPreview($scope.control.value ); + + $scope.macroPickerOverlay.show = false; + $scope.macroPickerOverlay = null; + }; + + $scope.macroPickerOverlay.close = function(oldModel) { + $scope.macroPickerOverlay.show = false; + $scope.macroPickerOverlay = null; + }; + + }; + + $scope.setPreview = function(macro){ + var contentId = $routeParams.id; + + macroResource.getMacroResultAsHtmlForEditor(macro.macroAlias, contentId, macro.macroParamsDictionary) + .then(function (htmlResult) { + $scope.title = macro.macroAlias; + if(htmlResult.trim().length > 0 && htmlResult.indexOf("Macro:") < 0){ + $scope.preview = htmlResult; + } + }); + + }; + + $timeout(function(){ + if($scope.control.$initializing){ + $scope.setMacro(); + }else if($scope.control.value){ + $scope.setPreview($scope.control.value); + } + }, 200); +}); + +angular.module("umbraco") + .controller("Umbraco.PropertyEditors.Grid.MediaController", + function ($scope, $rootScope, $timeout) { + + $scope.setImage = function(){ + $scope.mediaPickerOverlay = {}; + $scope.mediaPickerOverlay.view = "mediapicker"; + $scope.mediaPickerOverlay.cropSize = $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined; + $scope.mediaPickerOverlay.showDetails = true; + $scope.mediaPickerOverlay.disableFolderSelect = true; + $scope.mediaPickerOverlay.onlyImages = true; + $scope.mediaPickerOverlay.show = true; + + $scope.mediaPickerOverlay.submit = function(model) { + var selectedImage = model.selectedImages[0]; + + $scope.control.value = { + focalPoint: selectedImage.focalPoint, + id: selectedImage.id, + image: selectedImage.image, + altText: selectedImage.altText + }; + + $scope.setUrl(); + + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + }; + + $scope.mediaPickerOverlay.close = function(oldModel) { + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + }; + }; + + $scope.setUrl = function(){ + + if($scope.control.value.image){ + var url = $scope.control.value.image; + + if($scope.control.editor.config && $scope.control.editor.config.size){ + url += "?width=" + $scope.control.editor.config.size.width; + url += "&height=" + $scope.control.editor.config.size.height; + url += "&animationprocessmode=first"; + + if($scope.control.value.focalPoint){ + url += "¢er=" + $scope.control.value.focalPoint.top +"," + $scope.control.value.focalPoint.left; + url += "&mode=crop"; + } + } + + // set default size if no crop present (moved from the view) + if (url.indexOf('?') == -1) + { + url += "?width=800&upscale=false&animationprocessmode=false" + } + $scope.url = url; + } + }; + + $timeout(function(){ + if($scope.control.$initializing){ + $scope.setImage(); + }else if($scope.control.value){ + $scope.setUrl(); + } + }, 200); +}); + +(function() { + "use strict"; + + function GridRichTextEditorController($scope, tinyMceService, macroService) { + + var vm = this; + + vm.openLinkPicker = openLinkPicker; + vm.openMediaPicker = openMediaPicker; + vm.openMacroPicker = openMacroPicker; + vm.openEmbed = openEmbed; + + function openLinkPicker(editor, currentTarget, anchorElement) { + vm.linkPickerOverlay = { + view: "linkpicker", + currentTarget: currentTarget, + show: true, + submit: function(model) { + tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); + vm.linkPickerOverlay.show = false; + vm.linkPickerOverlay = null; + } + }; + } + + function openMediaPicker(editor, currentTarget, userData) { + vm.mediaPickerOverlay = { + currentTarget: currentTarget, + onlyImages: true, + showDetails: true, + startNodeId: userData.startMediaId, + view: "mediapicker", + show: true, + submit: function(model) { + tinyMceService.insertMediaInEditor(editor, model.selectedImages[0]); + vm.mediaPickerOverlay.show = false; + vm.mediaPickerOverlay = null; + } + }; + } + + function openEmbed(editor) { + vm.embedOverlay = { + view: "embed", + show: true, + submit: function(model) { + tinyMceService.insertEmbeddedMediaInEditor(editor, model.embed.preview); + vm.embedOverlay.show = false; + vm.embedOverlay = null; + } + }; + } + + function openMacroPicker(editor, dialogData) { + vm.macroPickerOverlay = { + view: "macropicker", + dialogData: dialogData, + show: true, + submit: function(model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); + tinyMceService.insertMacroInEditor(editor, macroObject, $scope); + vm.macroPickerOverlay.show = false; + vm.macroPickerOverlay = null; + } + }; + } + + + + } + + angular.module("umbraco").controller("Umbraco.PropertyEditors.Grid.RichTextEditorController", GridRichTextEditorController); + +})(); + +angular.module("umbraco") + .controller("Umbraco.PropertyEditors.Grid.TextStringController", + function ($scope, $rootScope, $timeout, dialogService) { + + + + }); + + +angular.module("umbraco") + .controller("Umbraco.PropertyEditors.GridController", + function ($scope, $http, assetsService, localizationService, $rootScope, dialogService, gridService, mediaResource, imageHelper, $timeout, umbRequestHelper, angularHelper) { + + // Grid status variables + var placeHolder = ""; + var currentForm = angularHelper.getCurrentForm($scope); + + $scope.currentRow = null; + $scope.currentCell = null; + $scope.currentToolsControl = null; + $scope.currentControl = null; + $scope.openRTEToolbarId = null; + $scope.hasSettings = false; + $scope.showRowConfigurations = true; + $scope.sortMode = false; + $scope.reorderKey = "general_reorder"; + + // ********************************************* + // Sortable options + // ********************************************* + + var draggedRteSettings; + + $scope.sortableOptionsRow = { + distance: 10, + cursor: "move", + placeholder: "ui-sortable-placeholder", + handle: ".umb-row-title-bar", + helper: "clone", + forcePlaceholderSize: true, + tolerance: "pointer", + zIndex: 999999999999999999, + scrollSensitivity: 100, + cursorAt: { + top: 40, + left: 60 + }, + + sort: function (event, ui) { + /* prevent vertical scroll out of the screen */ + var max = $(".umb-grid").width() - 150; + if (parseInt(ui.helper.css("left")) > max) { + ui.helper.css({ "left": max + "px" }); + } + if (parseInt(ui.helper.css("left")) < 20) { + ui.helper.css({ "left": 20 }); + } + }, + + start: function (e, ui) { + + // Fade out row when sorting + ui.item.context.style.display = "block"; + ui.item.context.style.opacity = "0.5"; + + draggedRteSettings = {}; + ui.item.find(".mceNoEditor").each(function () { + // remove all RTEs in the dragged row and save their settings + var id = $(this).attr("id"); + draggedRteSettings[id] = _.findWhere(tinyMCE.editors, { id: id }).settings; + // tinyMCE.execCommand("mceRemoveEditor", false, id); + }); + }, + + stop: function (e, ui) { + + // Fade in row when sorting stops + ui.item.context.style.opacity = "1"; + + // reset all RTEs affected by the dragging + ui.item.parents(".umb-column").find(".mceNoEditor").each(function () { + var id = $(this).attr("id"); + draggedRteSettings[id] = draggedRteSettings[id] || _.findWhere(tinyMCE.editors, { id: id }).settings; + tinyMCE.execCommand("mceRemoveEditor", false, id); + tinyMCE.init(draggedRteSettings[id]); + }); + currentForm.$setDirty(); + } + }; + + var notIncludedRte = []; + var cancelMove = false; + + $scope.sortableOptionsCell = { + distance: 10, + cursor: "move", + placeholder: "ui-sortable-placeholder", + handle: ".umb-control-handle", + helper: "clone", + connectWith: ".umb-cell-inner", + forcePlaceholderSize: true, + tolerance: "pointer", + zIndex: 999999999999999999, + scrollSensitivity: 100, + cursorAt: { + top: 45, + left: 90 + }, + + sort: function (event, ui) { + + /* prevent vertical scroll out of the screen */ + var position = parseInt(ui.item.parent().offset().left) + parseInt(ui.helper.css("left")) - parseInt($(".umb-grid").offset().left); + var max = $(".umb-grid").width() - 220; + if (position > max) { + ui.helper.css({ "left": max - parseInt(ui.item.parent().offset().left) + parseInt($(".umb-grid").offset().left) + "px" }); + } + if (position < 0) { + ui.helper.css({ "left": 0 - parseInt(ui.item.parent().offset().left) + parseInt($(".umb-grid").offset().left) + "px" }); + } + }, + + over: function (event, ui) { + var allowedEditors = $(event.target).scope().area.allowed; + + if ($.inArray(ui.item.scope().control.editor.alias, allowedEditors) < 0 && allowedEditors) { + + $scope.$apply(function () { + $(event.target).scope().area.dropNotAllowed = true; + }); + + ui.placeholder.hide(); + cancelMove = true; + } + else { + if ($(event.target).scope().area.controls.length == 0){ + + $scope.$apply(function () { + $(event.target).scope().area.dropOnEmpty = true; + }); + ui.placeholder.hide(); + } else { + ui.placeholder.show(); + } + cancelMove = false; + } + }, + + out: function(event, ui) { + $scope.$apply(function () { + $(event.target).scope().area.dropNotAllowed = false; + $(event.target).scope().area.dropOnEmpty = false; + }); + }, + + update: function (event, ui) { + /* add all RTEs which are affected by the dragging */ + if (!ui.sender) { + if (cancelMove) { + ui.item.sortable.cancel(); + } + ui.item.parents(".umb-cell.content").find(".mceNoEditor").each(function () { + if ($.inArray($(this).attr("id"), notIncludedRte) < 0) { + notIncludedRte.splice(0, 0, $(this).attr("id")); + } + }); + } + else { + $(event.target).find(".mceNoEditor").each(function () { + if ($.inArray($(this).attr("id"), notIncludedRte) < 0) { + notIncludedRte.splice(0, 0, $(this).attr("id")); + } + }); + } + currentForm.$setDirty(); + }, + + start: function (e, ui) { + + // fade out control when sorting + ui.item.context.style.display = "block"; + ui.item.context.style.opacity = "0.5"; + + // reset dragged RTE settings in case a RTE isn't dragged + draggedRteSettings = undefined; + ui.item.context.style.display = "block"; + ui.item.find(".mceNoEditor").each(function () { + notIncludedRte = []; + var editors = _.findWhere(tinyMCE.editors, { id: $(this).attr("id") }); + + // save the dragged RTE settings + if(editors) { + draggedRteSettings = editors.settings; + + // remove the dragged RTE + tinyMCE.execCommand("mceRemoveEditor", false, $(this).attr("id")); + + } + + }); + }, + + stop: function (e, ui) { + + // Fade in control when sorting stops + ui.item.context.style.opacity = "1"; + + ui.item.parents(".umb-cell-content").find(".mceNoEditor").each(function () { + if ($.inArray($(this).attr("id"), notIncludedRte) < 0) { + // add all dragged's neighbouring RTEs in the new cell + notIncludedRte.splice(0, 0, $(this).attr("id")); + } + }); + $timeout(function () { + // reconstruct the dragged RTE (could be undefined when dragging something else than RTE) + if (draggedRteSettings !== undefined) { + tinyMCE.init(draggedRteSettings); + } + + _.forEach(notIncludedRte, function (id) { + // reset all the other RTEs + if (draggedRteSettings === undefined || id !== draggedRteSettings.id) { + var rteSettings = _.findWhere(tinyMCE.editors, { id: id }).settings; + tinyMCE.execCommand("mceRemoveEditor", false, id); + tinyMCE.init(rteSettings); + } + }); + }, 500, false); + + $scope.$apply(function () { + + var cell = $(e.target).scope().area; + cell.hasActiveChild = hasActiveChild(cell, cell.controls); + cell.active = false; + }); + } + + }; + + $scope.toggleSortMode = function() { + $scope.sortMode = !$scope.sortMode; + if($scope.sortMode) { + $scope.reorderKey = "general_reorderDone"; + } else { + $scope.reorderKey = "general_reorder"; + } + }; + + $scope.showReorderButton = function() { + if($scope.model.value && $scope.model.value.sections) { + for(var i = 0; $scope.model.value.sections.length > i; i++) { + var section = $scope.model.value.sections[i]; + if(section.rows && section.rows.length > 0) { + return true; + } + } + } + }; + + // ********************************************* + // Add items overlay menu + // ********************************************* + $scope.openEditorOverlay = function(event, area, index, key) { + $scope.editorOverlay = { + view: "itempicker", + filter: false, + title: localizationService.localize("grid_insertControl"), + availableItems: area.$allowedEditors, + event: event, + show: true, + submit: function(model) { + $scope.addControl(model.selectedItem, area, index); + $scope.editorOverlay.show = false; + $scope.editorOverlay = null; + } + }; + }; + + // ********************************************* + // Template management functions + // ********************************************* + + $scope.addTemplate = function (template) { + $scope.model.value = angular.copy(template); + + //default row data + _.forEach($scope.model.value.sections, function (section) { + $scope.initSection(section); + }); + }; + + + // ********************************************* + // Row management function + // ********************************************* + + $scope.clickRow = function(index, rows) { + rows[index].active = true; + }; + + $scope.clickOutsideRow = function(index, rows) { + rows[index].active = false; + }; + + function getAllowedLayouts(section) { + + var layouts = $scope.model.config.items.layouts; + + //This will occur if it is a new section which has been + // created from a 'template' + if (section.allowed && section.allowed.length > 0) { + return _.filter(layouts, function (layout) { + return _.indexOf(section.allowed, layout.name) >= 0; + }); + } + else { + + + return layouts; + } + } + + $scope.addRow = function (section, layout) { + + //copy the selected layout into the rows collection + var row = angular.copy(layout); + + // Init row value + row = $scope.initRow(row); + + // Push the new row + if (row) { + section.rows.push(row); + } + + currentForm.$setDirty(); + + $scope.showRowConfigurations = false; + + }; + + $scope.removeRow = function (section, $index) { + if (section.rows.length > 0) { + section.rows.splice($index, 1); + $scope.currentRow = null; + $scope.openRTEToolbarId = null; + currentForm.$setDirty(); + } + + if(section.rows.length === 0) { + $scope.showRowConfigurations = true; + } + }; + + var shouldApply = function(item, itemType, gridItem) { + if (item.applyTo === undefined || item.applyTo === null || item.applyTo === "") { + return true; + } + + if (typeof (item.applyTo) === "string") { + return item.applyTo === itemType; + } + + if (itemType === "row") { + if (item.applyTo.row === undefined) { + return false; + } + if (item.applyTo.row === null || item.applyTo.row === "") { + return true; + } + var rows = item.applyTo.row.split(','); + return _.indexOf(rows, gridItem.name) !== -1; + } else if (itemType === "cell") { + if (item.applyTo.cell === undefined) { + return false; + } + if (item.applyTo.cell === null || item.applyTo.cell === "") { + return true; + } + var cells = item.applyTo.cell.split(','); + var cellSize = gridItem.grid.toString(); + return _.indexOf(cells, cellSize) !== -1; + } + } + + $scope.editGridItemSettings = function (gridItem, itemType) { + + placeHolder = "{0}"; + + var styles, config; + if (itemType === 'control') { + styles = null; + config = angular.copy(gridItem.editor.config.settings); + } else { + styles = _.filter(angular.copy($scope.model.config.items.styles), function (item) { return shouldApply(item, itemType, gridItem); }); + config = _.filter(angular.copy($scope.model.config.items.config), function (item) { return shouldApply(item, itemType, gridItem); }); + } + + if(angular.isObject(gridItem.config)){ + _.each(config, function(cfg){ + var val = gridItem.config[cfg.key]; + if(val){ + cfg.value = stripModifier(val, cfg.modifier); + } + }); + } + + if(angular.isObject(gridItem.styles)){ + _.each(styles, function(style){ + var val = gridItem.styles[style.key]; + if(val){ + style.value = stripModifier(val, style.modifier); + } + }); + } + + $scope.gridItemSettingsDialog = {}; + $scope.gridItemSettingsDialog.view = "views/propertyeditors/grid/dialogs/config.html"; + $scope.gridItemSettingsDialog.title = "Settings"; + $scope.gridItemSettingsDialog.styles = styles; + $scope.gridItemSettingsDialog.config = config; + + $scope.gridItemSettingsDialog.show = true; + + $scope.gridItemSettingsDialog.submit = function(model) { + + var styleObject = {}; + var configObject = {}; + + _.each(model.styles, function(style){ + if(style.value){ + styleObject[style.key] = addModifier(style.value, style.modifier); + } + }); + _.each(model.config, function (cfg) { + if (cfg.value) { + configObject[cfg.key] = addModifier(cfg.value, cfg.modifier); + } + }); + + gridItem.styles = styleObject; + gridItem.config = configObject; + gridItem.hasConfig = gridItemHasConfig(styleObject, configObject); + + currentForm.$setDirty(); + + $scope.gridItemSettingsDialog.show = false; + $scope.gridItemSettingsDialog = null; + }; + + $scope.gridItemSettingsDialog.close = function(oldModel) { + $scope.gridItemSettingsDialog.show = false; + $scope.gridItemSettingsDialog = null; + }; + + }; + + function stripModifier(val, modifier) { + if (!val || !modifier || modifier.indexOf(placeHolder) < 0) { + return val; + } else { + var paddArray = modifier.split(placeHolder); + if(paddArray.length == 1){ + if (modifier.indexOf(placeHolder) === 0) { + return val.slice(0, -paddArray[0].length); + } else { + return val.slice(paddArray[0].length, 0); + } + } else { + if (paddArray[1].length === 0) { + return val.slice(paddArray[0].length); + } + return val.slice(paddArray[0].length, -paddArray[1].length); + } + } + } + + var addModifier = function(val, modifier){ + if (!modifier || modifier.indexOf(placeHolder) < 0) { + return val; + } else { + return modifier.replace(placeHolder, val); + } + }; + + function gridItemHasConfig(styles, config) { + + if(_.isEmpty(styles) && _.isEmpty(config)) { + return false; + } else { + return true; + } + + } + + // ********************************************* + // Area management functions + // ********************************************* + + $scope.clickCell = function(index, cells, row) { + cells[index].active = true; + row.hasActiveChild = true; + }; + + $scope.clickOutsideCell = function(index, cells, row) { + cells[index].active = false; + row.hasActiveChild = hasActiveChild(row, cells); + }; + + $scope.cellPreview = function (cell) { + if (cell && cell.$allowedEditors) { + var editor = cell.$allowedEditors[0]; + return editor.icon; + } else { + return "icon-layout"; + } + }; + + + // ********************************************* + // Control management functions + // ********************************************* + $scope.clickControl = function (index, controls, cell) { + controls[index].active = true; + cell.hasActiveChild = true; + }; + + $scope.clickOutsideControl = function (index, controls, cell) { + controls[index].active = false; + cell.hasActiveChild = hasActiveChild(cell, controls); + }; + + function hasActiveChild(item, children) { + + var activeChild = false; + + for(var i = 0; children.length > i; i++) { + var child = children[i]; + + if(child.active) { + activeChild = true; + } + } + + if(activeChild) { + return true; + } + + } + + + var guid = (function () { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return function () { + return s4() + s4() + "-" + s4() + "-" + s4() + "-" + + s4() + "-" + s4() + s4() + s4(); + }; + })(); + + $scope.setUniqueId = function (cell, index) { + return guid(); + }; + + $scope.addControl = function (editor, cell, index, initialize) { + + initialize = (initialize !== false); + + var newControl = { + value: null, + editor: editor, + $initializing: initialize + }; + + if (index === undefined) { + index = cell.controls.length; + } + + newControl.active = true; + + //populate control + $scope.initControl(newControl, index + 1); + + cell.controls.push(newControl); + + }; + + $scope.addTinyMce = function (cell) { + var rte = $scope.getEditor("rte"); + $scope.addControl(rte, cell); + }; + + $scope.getEditor = function (alias) { + return _.find($scope.availableEditors, function (editor) { return editor.alias === alias; }); + }; + + $scope.removeControl = function (cell, $index) { + $scope.currentControl = null; + cell.controls.splice($index, 1); + }; + + $scope.percentage = function (spans) { + return ((spans / $scope.model.config.items.columns) * 100).toFixed(8); + }; + + + $scope.clearPrompt = function (scopedObject, e) { + scopedObject.deletePrompt = false; + e.preventDefault(); + e.stopPropagation(); + }; + + $scope.togglePrompt = function (scopedObject) { + scopedObject.deletePrompt = !scopedObject.deletePrompt; + }; + + $scope.hidePrompt = function (scopedObject) { + scopedObject.deletePrompt = false; + }; + + $scope.toggleAddRow = function() { + $scope.showRowConfigurations = !$scope.showRowConfigurations; + }; + + + // ********************************************* + // Initialization + // these methods are called from ng-init on the template + // so we can controll their first load data + // + // intialization sets non-saved data like percentage sizing, allowed editors and + // other data that should all be pre-fixed with $ to strip it out on save + // ********************************************* + + // ********************************************* + // Init template + sections + // ********************************************* + $scope.initContent = function () { + var clear = true; + + //settings indicator shortcut + if ( ($scope.model.config.items.config && $scope.model.config.items.config.length > 0) || ($scope.model.config.items.styles && $scope.model.config.items.styles.length > 0)) { + $scope.hasSettings = true; + } + + //ensure the grid has a column value set, + //if nothing is found, set it to 12 + if ($scope.model.config.items.columns && angular.isString($scope.model.config.items.columns)) { + $scope.model.config.items.columns = parseInt($scope.model.config.items.columns); + } else { + $scope.model.config.items.columns = 12; + } + + if ($scope.model.value && $scope.model.value.sections && $scope.model.value.sections.length > 0 && $scope.model.value.sections[0].rows && $scope.model.value.sections[0].rows.length > 0) { + + if ($scope.model.value.name && angular.isArray($scope.model.config.items.templates)) { + + //This will occur if it is an existing value, in which case + // we need to determine which layout was applied by looking up + // the name + // TODO: We need to change this to an immutable ID!! + + var found = _.find($scope.model.config.items.templates, function (t) { + return t.name === $scope.model.value.name; + }); + + if (found && angular.isArray(found.sections) && found.sections.length === $scope.model.value.sections.length) { + + //Cool, we've found the template associated with our current value with matching sections counts, now we need to + // merge this template data on to our current value (as if it was new) so that we can preserve what is and isn't + // allowed for this template based on the current config. + + _.each(found.sections, function (templateSection, index) { + angular.extend($scope.model.value.sections[index], angular.copy(templateSection)); + }); + + } + } + + _.forEach($scope.model.value.sections, function (section, index) { + + if (section.grid > 0) { + $scope.initSection(section); + + //we do this to ensure that the grid can be reset by deleting the last row + if (section.rows.length > 0) { + clear = false; + } + } else { + $scope.model.value.sections.splice(index, 1); + } + }); + } else if ($scope.model.config.items.templates && $scope.model.config.items.templates.length === 1) { + $scope.addTemplate($scope.model.config.items.templates[0]); + clear = false; + } + + if (clear) { + $scope.model.value = undefined; + } + }; + + $scope.initSection = function (section) { + section.$percentage = $scope.percentage(section.grid); + + section.$allowedLayouts = getAllowedLayouts(section); + + if (!section.rows || section.rows.length === 0) { + section.rows = []; + if(section.$allowedLayouts.length === 1){ + $scope.addRow(section, section.$allowedLayouts[0]); + } + } else { + _.forEach(section.rows, function (row, index) { + if (!row.$initialized) { + var initd = $scope.initRow(row); + + //if init fails, remove + if (!initd) { + section.rows.splice(index, 1); + } else { + section.rows[index] = initd; + } + } + }); + + // if there is more than one row added - hide row add tools + $scope.showRowConfigurations = false; + } + }; + + + // ********************************************* + // Init layout / row + // ********************************************* + $scope.initRow = function (row) { + + //merge the layout data with the original config data + //if there are no config info on this, splice it out + var original = _.find($scope.model.config.items.layouts, function (o) { return o.name === row.name; }); + + if (!original) { + return null; + } else { + //make a copy to not touch the original config + original = angular.copy(original); + original.styles = row.styles; + original.config = row.config; + original.hasConfig = gridItemHasConfig(row.styles, row.config); + + + //sync area configuration + _.each(original.areas, function (area, areaIndex) { + + + if (area.grid > 0) { + var currentArea = row.areas[areaIndex]; + + if (currentArea) { + area.config = currentArea.config; + area.styles = currentArea.styles; + area.hasConfig = gridItemHasConfig(currentArea.styles, currentArea.config); + } + + //set editor permissions + if (!area.allowed || area.allowAll === true) { + area.$allowedEditors = $scope.availableEditors; + area.$allowsRTE = true; + } else { + area.$allowedEditors = _.filter($scope.availableEditors, function (editor) { + return _.indexOf(area.allowed, editor.alias) >= 0; + }); + + if (_.indexOf(area.allowed, "rte") >= 0) { + area.$allowsRTE = true; + } + } + + //copy over existing controls into the new areas + if (row.areas.length > areaIndex && row.areas[areaIndex].controls) { + area.controls = currentArea.controls; + + _.forEach(area.controls, function (control, controlIndex) { + $scope.initControl(control, controlIndex); + }); + + } else { + //if empty + area.controls = []; + + //if only one allowed editor + if(area.$allowedEditors.length === 1){ + $scope.addControl(area.$allowedEditors[0], area, 0, false); + } + } + + //set width + area.$percentage = $scope.percentage(area.grid); + area.$uniqueId = $scope.setUniqueId(); + + } else { + original.areas.splice(areaIndex, 1); + } + }); + + //replace the old row + original.$initialized = true; + + //set a disposable unique ID + original.$uniqueId = $scope.setUniqueId(); + + //set a no disposable unique ID (util for row styling) + original.id = !row.id ? $scope.setUniqueId() : row.id; + + return original; + } + + }; + + + // ********************************************* + // Init control + // ********************************************* + + $scope.initControl = function (control, index) { + control.$index = index; + control.$uniqueId = $scope.setUniqueId(); + + //error handling in case of missing editor.. + //should only happen if stripped earlier + if (!control.editor) { + control.$editorPath = "views/propertyeditors/grid/editors/error.html"; + } + + if (!control.$editorPath) { + var editorConfig = $scope.getEditor(control.editor.alias); + + if (editorConfig) { + control.editor = editorConfig; + + //if its an absolute path + if (control.editor.view.startsWith("/") || control.editor.view.startsWith("~/")) { + control.$editorPath = umbRequestHelper.convertVirtualToAbsolutePath(control.editor.view); + } + else { + //use convention + control.$editorPath = "views/propertyeditors/grid/editors/" + control.editor.view + ".html"; + } + } + else { + control.$editorPath = "views/propertyeditors/grid/editors/error.html"; + } + } + + + }; + + + gridService.getGridEditors().then(function (response) { + $scope.availableEditors = response.data; + + $scope.contentReady = true; + + // ********************************************* + // Init grid + // ********************************************* + $scope.initContent(); + + }); + + //Clean the grid value before submitting to the server, we don't need + // all of that grid configuration in the value to be stored!! All of that + // needs to be merged in at runtime to ensure that the real config values are used + // if they are ever updated. + + var unsubscribe = $scope.$on("formSubmitting", function () { + + if ($scope.model.value && $scope.model.value.sections) { + _.each($scope.model.value.sections, function(section) { + if (section.rows) { + _.each(section.rows, function (row) { + if (row.areas) { + _.each(row.areas, function (area) { + + //Remove the 'editors' - these are the allowed editors, these will + // be injected at runtime to this editor, it should not be persisted + + if (area.editors) { + delete area.editors; + } + + if (area.controls) { + _.each(area.controls, function (control) { + if (control.editor) { + //replace + var alias = control.editor.alias; + control.editor = { + alias: alias + }; + } + }); + } + }); + } + }); + } + }); + } + }); + + //when the scope is destroyed we need to unsubscribe + $scope.$on("$destroy", function () { + unsubscribe(); + }); + + }); + +angular.module("umbraco") + .controller("Umbraco.PropertyEditors.GridPrevalueEditorController", + function ($scope, $http, assetsService, $rootScope, dialogService, mediaResource, gridService, imageHelper, $timeout) { + + var emptyModel = { + styles:[ + { + label: "Set a background image", + description: "Set a row background", + key: "background-image", + view: "imagepicker", + modifier: "url({0})" + } + ], + + config:[ + { + label: "Class", + description: "Set a css class", + key: "class", + view: "textstring" + } + ], + + columns: 12, + templates:[ + { + name: "1 column layout", + sections: [ + { + grid: 12, + } + ] + }, + { + name: "2 column layout", + sections: [ + { + grid: 4, + }, + { + grid: 8 + } + ] + } + ], + + + layouts:[ + { + label: "Headline", + name: "Headline", + areas: [ + { + grid: 12, + editors: ["headline"] + } + ] + }, + { + label: "Article", + name: "Article", + areas: [ + { + grid: 4 + }, + { + grid: 8 + } + ] + } + ] + }; + + /**************** + template + *****************/ + + $scope.configureTemplate = function(template) { + + var templatesCopy = angular.copy($scope.model.value.templates); + + if (template === undefined) { + template = { + name: "", + sections: [ + + ] + }; + $scope.model.value.templates.push(template); + } + + $scope.layoutConfigOverlay = {}; + $scope.layoutConfigOverlay.view = "views/propertyEditors/grid/dialogs/layoutconfig.html"; + $scope.layoutConfigOverlay.currentLayout = template; + $scope.layoutConfigOverlay.rows = $scope.model.value.layouts; + $scope.layoutConfigOverlay.columns = $scope.model.value.columns; + $scope.layoutConfigOverlay.show = true; + + $scope.layoutConfigOverlay.submit = function(model) { + $scope.layoutConfigOverlay.show = false; + $scope.layoutConfigOverlay = null; + }; + + $scope.layoutConfigOverlay.close = function(oldModel) { + + //reset templates + $scope.model.value.templates = templatesCopy; + + $scope.layoutConfigOverlay.show = false; + $scope.layoutConfigOverlay = null; + } + + }; + + $scope.deleteTemplate = function(index){ + $scope.model.value.templates.splice(index, 1); + }; + + + /**************** + Row + *****************/ + + $scope.configureLayout = function(layout) { + + var layoutsCopy = angular.copy($scope.model.value.layouts); + + if(layout === undefined){ + layout = { + name: "", + areas:[ + + ] + }; + $scope.model.value.layouts.push(layout); + } + + $scope.rowConfigOverlay = {}; + $scope.rowConfigOverlay.view = "views/propertyEditors/grid/dialogs/rowconfig.html"; + $scope.rowConfigOverlay.currentRow = layout; + $scope.rowConfigOverlay.editors = $scope.editors; + $scope.rowConfigOverlay.columns = $scope.model.value.columns; + $scope.rowConfigOverlay.show = true; + + $scope.rowConfigOverlay.submit = function(model) { + $scope.rowConfigOverlay.show = false; + $scope.rowConfigOverlay = null; + }; + + $scope.rowConfigOverlay.close = function(oldModel) { + $scope.model.value.layouts = layoutsCopy; + $scope.rowConfigOverlay.show = false; + $scope.rowConfigOverlay = null; + }; + + }; + + //var rowDeletesPending = false; + $scope.deleteLayout = function(index) { + + $scope.rowDeleteOverlay = {}; + $scope.rowDeleteOverlay.view = "views/propertyEditors/grid/dialogs/rowdeleteconfirm.html"; + $scope.rowDeleteOverlay.dialogData = { + rowName: $scope.model.value.layouts[index].name + }; + $scope.rowDeleteOverlay.show = true; + + $scope.rowDeleteOverlay.submit = function(model) { + + $scope.model.value.layouts.splice(index, 1); + + $scope.rowDeleteOverlay.show = false; + $scope.rowDeleteOverlay = null; + }; + + $scope.rowDeleteOverlay.close = function(oldModel) { + $scope.rowDeleteOverlay.show = false; + $scope.rowDeleteOverlay = null; + }; + + }; + + + /**************** + utillities + *****************/ + $scope.toggleCollection = function(collection, toggle){ + if(toggle){ + collection = []; + }else{ + delete collection; + } + }; + + $scope.percentage = function(spans){ + return ((spans / $scope.model.value.columns) * 100).toFixed(8); + }; + + $scope.zeroWidthFilter = function (cell) { + return cell.grid > 0; + }; + + /**************** + Config + *****************/ + + $scope.removeConfigValue = function(collection, index){ + collection.splice(index, 1); + }; + + var editConfigCollection = function(configValues, title, callback) { + + $scope.editConfigCollectionOverlay = {}; + $scope.editConfigCollectionOverlay.view = "views/propertyeditors/grid/dialogs/editconfig.html"; + $scope.editConfigCollectionOverlay.config = configValues; + $scope.editConfigCollectionOverlay.title = title; + $scope.editConfigCollectionOverlay.show = true; + + $scope.editConfigCollectionOverlay.submit = function(model) { + + callback(model.config) + + $scope.editConfigCollectionOverlay.show = false; + $scope.editConfigCollectionOverlay = null; + }; + + $scope.editConfigCollectionOverlay.close = function(oldModel) { + $scope.editConfigCollectionOverlay.show = false; + $scope.editConfigCollectionOverlay = null; + }; + + }; + + $scope.editConfig = function() { + editConfigCollection($scope.model.value.config, "Settings", function(data) { + $scope.model.value.config = data; + }); + }; + + $scope.editStyles = function() { + editConfigCollection($scope.model.value.styles, "Styling", function(data){ + $scope.model.value.styles = data; + }); + }; + + /**************** + editors + *****************/ + gridService.getGridEditors().then(function(response){ + $scope.editors = response.data; + }); + + + /* init grid data */ + if (!$scope.model.value || $scope.model.value === "" || !$scope.model.value.templates) { + $scope.model.value = emptyModel; + } else { + + if (!$scope.model.value.columns) { + $scope.model.value.columns = emptyModel.columns; + } + + + if (!$scope.model.value.config) { + $scope.model.value.config = []; + } + + if (!$scope.model.value.styles) { + $scope.model.value.styles = []; + } + } + + /**************** + Clean up + *****************/ + var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { + var ts = $scope.model.value.templates; + var ls = $scope.model.value.layouts; + + _.each(ts, function(t){ + _.each(t.sections, function(section, index){ + if(section.grid === 0){ + t.sections.splice(index, 1); + } + }); + }); + + _.each(ls, function(l){ + _.each(l.areas, function(area, index){ + if(area.grid === 0){ + l.areas.splice(index, 1); + } + }); + }); + }); + + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); + + }); + +//this controller simply tells the dialogs service to open a mediaPicker window +//with a specified callback, this callback will receive an object with a selection on it +angular.module('umbraco') + .controller("Umbraco.PropertyEditors.ImageCropperController", + function ($rootScope, $routeParams, $scope, $log, mediaHelper, cropperHelper, $timeout, editorState, umbRequestHelper, fileManager, angularHelper) { + + var config = angular.copy($scope.model.config); + $scope.imageIsLoaded = false; + + //move previously saved value to the editor + if ($scope.model.value) { + //backwards compat with the old file upload (incase some-one swaps them..) + if (angular.isString($scope.model.value)) { + config.src = $scope.model.value; + $scope.model.value = config; + } else if ($scope.model.value.crops) { + //sync any config changes with the editor and drop outdated crops + _.each($scope.model.value.crops, function (saved) { + var configured = _.find(config.crops, function (item) { return item.alias === saved.alias }); + + if (configured && configured.height === saved.height && configured.width === saved.width) { + configured.coordinates = saved.coordinates; + } + }); + $scope.model.value.crops = config.crops; + + //restore focalpoint if missing + if (!$scope.model.value.focalPoint) { + $scope.model.value.focalPoint = { left: 0.5, top: 0.5 }; + } + } + + $scope.imageSrc = $scope.model.value.src; + } + + + //crop a specific crop + $scope.crop = function (crop) { + $scope.currentCrop = crop; + $scope.currentPoint = undefined; + }; + + //done cropping + $scope.done = function () { + $scope.currentCrop = undefined; + $scope.currentPoint = undefined; + }; + + //crop a specific crop + $scope.clear = function (crop) { + //clear current uploaded files + fileManager.setFiles($scope.model.alias, []); + + //clear the ui + $scope.imageSrc = undefined; + if ($scope.model.value) { + delete $scope.model.value; + } + + // set form to dirty to tricker discard changes dialog + var currForm = angularHelper.getCurrentForm($scope); + currForm.$setDirty(); + }; + + //show previews + $scope.togglePreviews = function () { + if ($scope.showPreviews) { + $scope.showPreviews = false; + $scope.tempShowPreviews = false; + } else { + $scope.showPreviews = true; + } + }; + + $scope.imageLoaded = function() { + $scope.imageIsLoaded = true; + }; + + //on image selected, update the cropper + $scope.$on("filesSelected", function (ev, args) { + $scope.model.value = config; + + if (args.files && args.files[0]) { + + fileManager.setFiles($scope.model.alias, args.files); + + var reader = new FileReader(); + reader.onload = function (e) { + + $scope.$apply(function () { + $scope.imageSrc = e.target.result; + }); + + }; + + reader.readAsDataURL(args.files[0]); + } + }); + + + //here we declare a special method which will be called whenever the value has changed from the server + $scope.model.onValueChanged = function (newVal, oldVal) { + //clear current uploaded files + fileManager.setFiles($scope.model.alias, []); + }; + + var unsubscribe = $scope.$on("formSubmitting", function () { + $scope.done(); + }); + + $scope.$on('$destroy', function () { + unsubscribe(); + }); + }) + .run(function (mediaHelper, umbRequestHelper) { + if (mediaHelper && mediaHelper.registerFileResolver) { + + //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource + // they contain different data structures so if we need to query against it we need to be aware of this. + mediaHelper.registerFileResolver("Umbraco.ImageCropper", function (property, entity, thumbnail) { + if (property.value && property.value.src) { + + if (thumbnail === true) { + return property.value.src + "?width=500&mode=max&animationprocessmode=first"; + } + else { + return property.value.src; + } + + //this is a fallback in case the cropper has been asssigned a upload field + } + else if (angular.isString(property.value)) { + if (thumbnail) { + + if (mediaHelper.detectIfImageByExtension(property.value)) { + + var thumbnailUrl = umbRequestHelper.getApiUrl( + "imagesApiBaseUrl", + "GetBigThumbnail", + [{ originalImagePath: property.value }]); + + return thumbnailUrl; + } + else { + return null; + } + + } + else { + return property.value; + } + } + + return null; + }); + } + }); + +angular.module("umbraco").controller("Umbraco.PrevalueEditors.CropSizesController", + function ($scope, $timeout) { + + if (!$scope.model.value) { + $scope.model.value = []; + } + + $scope.remove = function (item, evt) { + evt.preventDefault(); + $scope.model.value = _.reject($scope.model.value, function (x) { + return x.alias === item.alias; + }); + }; + + $scope.edit = function (item, evt) { + evt.preventDefault(); + $scope.newItem = item; + }; + + $scope.cancel = function (evt) { + evt.preventDefault(); + $scope.newItem = null; + }; + + $scope.add = function (evt) { + evt.preventDefault(); + + if ($scope.newItem && $scope.newItem.alias && + angular.isNumber($scope.newItem.width) && angular.isNumber($scope.newItem.height) && + $scope.newItem.width > 0 && $scope.newItem.height > 0) { + + var exists = _.find($scope.model.value, function (item) { return $scope.newItem.alias === item.alias; }); + if (!exists) { + $scope.model.value.push($scope.newItem); + $scope.newItem = {}; + $scope.hasError = false; + return; + } + } + + //there was an error, do the highlight (will be set back by the directive) + $scope.hasError = true; + }; }); -function includePropsPreValsController($rootScope, $scope, localizationService, contentTypeResource) { - - if (!$scope.model.value) { - $scope.model.value = []; - } - - $scope.propertyAliases = []; - $scope.selectedField = null; - $scope.systemFields = [ - { value: "sortOrder" }, - { value: "updateDate" }, - { value: "updater" }, - { value: "createDate" }, - { value: "owner" }, - { value: "published"}, - { value: "contentTypeAlias" }, - { value: "email" }, - { value: "username" } - ]; - - $scope.getLocalizedKey = function(alias) { - switch (alias) { - case "name": - return "general_name"; - case "sortOrder": - return "general_sort"; - case "updateDate": - return "content_updateDate"; - case "updater": - return "content_updatedBy"; - case "createDate": - return "content_createDate"; - case "owner": - return "content_createBy"; - case "published": - return "content_isPublished"; - case "contentTypeAlias": - //NOTE: This will just be 'Document' type even if it's for media/members since this is just a pre-val editor and we don't have a key for 'Content Type Alias' - return "content_documentType"; - case "email": - return "general_email"; - case "username": - return "general_username"; - } - return alias; - } - - $scope.removeField = function(e) { - $scope.model.value = _.reject($scope.model.value, function (x) { - return x.alias === e.alias; - }); - } - - //now we'll localize these strings, for some reason the directive doesn't work inside of the select group with an ng-model declared - _.each($scope.systemFields, function (e, i) { - var key = $scope.getLocalizedKey(e.value); - localizationService.localize(key).then(function (v) { - e.name = v; - - switch (e.value) { - case "updater": - e.name += " (Content only)"; - break; - case "published": - e.name += " (Content only)"; - break; - case "email": - e.name += " (Members only)"; - break; - case "username": - e.name += " (Members only)"; - break; - } - - }); - }); - - // Return a helper with preserved width of cells - var fixHelper = function (e, ui) { - var h = ui.clone(); - - h.children().each(function () { - $(this).width($(this).width()); - }); - h.css("background-color", "lightgray"); - - return h; - }; - - $scope.sortableOptions = { - helper: fixHelper, - handle: ".handle", - opacity: 0.5, - axis: 'y', - containment: 'parent', - cursor: 'move', - items: '> tr', - tolerance: 'pointer', - update: function (e, ui) { - - // Get the new and old index for the moved element (using the text as the identifier) - var newIndex = ui.item.index(); - var movedAlias = $('.alias-value', ui.item).text().trim(); - var originalIndex = getAliasIndexByText(movedAlias); - - // Move the element in the model - if (originalIndex > -1) { - var movedElement = $scope.model.value[originalIndex]; - $scope.model.value.splice(originalIndex, 1); - $scope.model.value.splice(newIndex, 0, movedElement); - } - } - }; - - contentTypeResource.getAllPropertyTypeAliases().then(function(data) { - $scope.propertyAliases = data; - }); - - $scope.addField = function () { - - var val = $scope.selectedField; - var isSystem = val.startsWith("_system_"); - if (isSystem) { - val = val.trimStart("_system_"); - } - - var exists = _.find($scope.model.value, function (i) { - return i.alias === val; - }); - if (!exists) { - $scope.model.value.push({ - alias: val, - isSystem: isSystem ? 1 : 0 - }); - } - } - - function getAliasIndexByText(value) { - for (var i = 0; i < $scope.model.value.length; i++) { - if ($scope.model.value[i].alias === value) { - return i; - } - } - - return -1; - } - -} - - +function includePropsPreValsController($rootScope, $scope, localizationService, contentTypeResource) { + + if (!$scope.model.value) { + $scope.model.value = []; + } + + $scope.propertyAliases = []; + $scope.selectedField = null; + $scope.systemFields = [ + { value: "sortOrder" }, + { value: "updateDate" }, + { value: "updater" }, + { value: "createDate" }, + { value: "owner" }, + { value: "published"}, + { value: "contentTypeAlias" }, + { value: "email" }, + { value: "username" } + ]; + + $scope.getLocalizedKey = function(alias) { + switch (alias) { + case "name": + return "general_name"; + case "sortOrder": + return "general_sort"; + case "updateDate": + return "content_updateDate"; + case "updater": + return "content_updatedBy"; + case "createDate": + return "content_createDate"; + case "owner": + return "content_createBy"; + case "published": + return "content_isPublished"; + case "contentTypeAlias": + //NOTE: This will just be 'Document' type even if it's for media/members since this is just a pre-val editor and we don't have a key for 'Content Type Alias' + return "content_documentType"; + case "email": + return "general_email"; + case "username": + return "general_username"; + } + return alias; + } + + $scope.removeField = function(e) { + $scope.model.value = _.reject($scope.model.value, function (x) { + return x.alias === e.alias; + }); + } + + //now we'll localize these strings, for some reason the directive doesn't work inside of the select group with an ng-model declared + _.each($scope.systemFields, function (e, i) { + var key = $scope.getLocalizedKey(e.value); + localizationService.localize(key).then(function (v) { + e.name = v; + + switch (e.value) { + case "updater": + e.name += " (Content only)"; + break; + case "published": + e.name += " (Content only)"; + break; + case "email": + e.name += " (Members only)"; + break; + case "username": + e.name += " (Members only)"; + break; + } + + }); + }); + + // Return a helper with preserved width of cells + var fixHelper = function (e, ui) { + var h = ui.clone(); + + h.children().each(function () { + $(this).width($(this).width()); + }); + h.css("background-color", "lightgray"); + + return h; + }; + + $scope.sortableOptions = { + helper: fixHelper, + handle: ".handle", + opacity: 0.5, + axis: 'y', + containment: 'parent', + cursor: 'move', + items: '> tr', + tolerance: 'pointer', + update: function (e, ui) { + + // Get the new and old index for the moved element (using the text as the identifier) + var newIndex = ui.item.index(); + var movedAlias = $('.alias-value', ui.item).text().trim(); + var originalIndex = getAliasIndexByText(movedAlias); + + // Move the element in the model + if (originalIndex > -1) { + var movedElement = $scope.model.value[originalIndex]; + $scope.model.value.splice(originalIndex, 1); + $scope.model.value.splice(newIndex, 0, movedElement); + } + } + }; + + contentTypeResource.getAllPropertyTypeAliases().then(function(data) { + $scope.propertyAliases = data; + }); + + $scope.addField = function () { + + var val = $scope.selectedField; + var isSystem = val.startsWith("_system_"); + if (isSystem) { + val = val.trimStart("_system_"); + } + + var exists = _.find($scope.model.value, function (i) { + return i.alias === val; + }); + if (!exists) { + $scope.model.value.push({ + alias: val, + isSystem: isSystem ? 1 : 0 + }); + } + } + + function getAliasIndexByText(value) { + for (var i = 0; i < $scope.model.value.length; i++) { + if ($scope.model.value[i].alias === value) { + return i; + } + } + + return -1; + } + +} + + angular.module("umbraco").controller("Umbraco.PrevalueEditors.IncludePropertiesListViewController", includePropsPreValsController); -/** - * @ngdoc controller - * @name Umbraco.PrevalueEditors.ListViewLayoutsPreValsController - * @function - * - * @description - * The controller for configuring layouts for list views - */ -(function() { - "use strict"; - - function ListViewLayoutsPreValsController($scope) { - - var vm = this; - vm.focusLayoutName = false; - - vm.layoutsSortableOptions = { - distance: 10, - tolerance: "pointer", - opacity: 0.7, - scroll: true, - cursor: "move", - handle: ".list-view-layout__sort-handle" - }; - - vm.addLayout = addLayout; - vm.showPrompt = showPrompt; - vm.hidePrompt = hidePrompt; - vm.removeLayout = removeLayout; - vm.openIconPicker = openIconPicker; - - function activate() { - - - - } - - function addLayout() { - - vm.focusLayoutName = false; - - var layout = { - "name": "", - "path": "", - "icon": "icon-stop", - "selected": true - }; - - $scope.model.value.push(layout); - - } - - function showPrompt(layout) { - layout.deletePrompt = true; - } - - function hidePrompt(layout) { - layout.deletePrompt = false; - } - - function removeLayout($index, layout) { - $scope.model.value.splice($index, 1); - } - - function openIconPicker(layout) { - vm.iconPickerDialog = { - view: "iconpicker", - show: true, - submit: function(model) { - if (model.color) { - layout.icon = model.icon + " " + model.color; - } else { - layout.icon = model.icon; - } - vm.focusLayoutName = true; - vm.iconPickerDialog.show = false; - vm.iconPickerDialog = null; - } - }; - } - - activate(); - - } - - angular.module("umbraco").controller("Umbraco.PrevalueEditors.ListViewLayoutsPreValsController", ListViewLayoutsPreValsController); - -})(); +/** + * @ngdoc controller + * @name Umbraco.PrevalueEditors.ListViewLayoutsPreValsController + * @function + * + * @description + * The controller for configuring layouts for list views + */ +(function() { + "use strict"; -/** - * @ngdoc controller - * @name Umbraco.Editors.DocumentType.EditController - * @function - * - * @description - * The controller for the content type editor - */ -(function() { - "use strict"; - - function ListViewGridLayoutController($scope, $routeParams, mediaHelper, mediaResource, $location, listViewHelper, mediaTypeHelper) { - - var vm = this; - - vm.nodeId = $scope.contentId; - //we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles - vm.acceptedFileTypes = !mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles); - vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; - vm.activeDrag = false; - vm.mediaDetailsTooltip = {}; - vm.itemsWithoutFolders = []; - vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; - vm.acceptedMediatypes = []; - - vm.dragEnter = dragEnter; - vm.dragLeave = dragLeave; - vm.onFilesQueue = onFilesQueue; - vm.onUploadComplete = onUploadComplete; - - vm.hoverMediaItemDetails = hoverMediaItemDetails; - vm.selectContentItem = selectContentItem; - vm.selectItem = selectItem; - vm.selectFolder = selectFolder; - vm.goToItem = goToItem; - - function activate() { - vm.itemsWithoutFolders = filterOutFolders($scope.items); - - if($scope.entityType === 'media') { - mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { - vm.acceptedMediatypes = types; - }); - } - - } - - function filterOutFolders(items) { - - var newArray = []; - - if(items && items.length ) { - - for (var i = 0; items.length > i; i++) { - var item = items[i]; - var isFolder = !mediaHelper.hasFilePropertyType(item); - - if (!isFolder) { - newArray.push(item); - } - } - - } - - return newArray; - } - - function dragEnter(el, event) { - vm.activeDrag = true; - } - - function dragLeave(el, event) { - vm.activeDrag = false; - } - - function onFilesQueue() { - vm.activeDrag = false; - } - - function onUploadComplete() { - $scope.getContent($scope.contentId); - } - - function hoverMediaItemDetails(item, event, hover) { - - if (hover && !vm.mediaDetailsTooltip.show) { - - vm.mediaDetailsTooltip.event = event; - vm.mediaDetailsTooltip.item = item; - vm.mediaDetailsTooltip.show = true; - - } else if (!hover && vm.mediaDetailsTooltip.show) { - - vm.mediaDetailsTooltip.show = false; - - } - - } - - function selectContentItem(item, $event, $index) { - listViewHelper.selectHandler(item, $index, $scope.items, $scope.selection, $event); - } - - function selectItem(item, $event, $index) { - listViewHelper.selectHandler(item, $index, vm.itemsWithoutFolders, $scope.selection, $event); - } - - function selectFolder(folder, $event, $index) { - listViewHelper.selectHandler(folder, $index, $scope.folders, $scope.selection, $event); - } - - function goToItem(item, $event, $index) { - $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + item.id); - } - - activate(); - - } - - angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.GridLayoutController", ListViewGridLayoutController); - -})(); + function ListViewLayoutsPreValsController($scope) { -(function () { - "use strict"; - - function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper, mediaTypeHelper) { - - var vm = this; - - vm.nodeId = $scope.contentId; - //we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles - vm.acceptedFileTypes = !mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles); - vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; - vm.activeDrag = false; - vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; - vm.acceptedMediatypes = []; - - vm.selectItem = selectItem; - vm.clickItem = clickItem; - vm.selectAll = selectAll; - vm.isSelectedAll = isSelectedAll; - vm.isSortDirection = isSortDirection; - vm.sort = sort; - vm.dragEnter = dragEnter; - vm.dragLeave = dragLeave; - vm.onFilesQueue = onFilesQueue; - vm.onUploadComplete = onUploadComplete; - - function activate() { - - if ($scope.entityType === 'media') { - mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { - vm.acceptedMediatypes = types; - }); - } - - } - - function selectAll($event) { - listViewHelper.selectAllItems($scope.items, $scope.selection, $event); - } - - function isSelectedAll() { - return listViewHelper.isSelectedAll($scope.items, $scope.selection); - } - - function selectItem(selectedItem, $index, $event) { - listViewHelper.selectHandler(selectedItem, $index, $scope.items, $scope.selection, $event); - } - - function clickItem(item) { - // if item.id is 2147483647 (int.MaxValue) use item.key - $location.path($scope.entityType + '/' +$scope.entityType + '/edit/' + (item.id === 2147483647 ? item.key : item.id)); - } - - function isSortDirection(col, direction) { - return listViewHelper.setSortingDirection(col, direction, $scope.options); - } - - function sort(field, allow, isSystem) { - if (allow) { - $scope.options.orderBySystemField = isSystem; - listViewHelper.setSorting(field, allow, $scope.options); - $scope.getContent($scope.contentId); - } - } - - // Dropzone upload functions - function dragEnter(el, event) { - vm.activeDrag = true; - } - - function dragLeave(el, event) { - vm.activeDrag = false; - } - - function onFilesQueue() { - vm.activeDrag = false; - } - - function onUploadComplete() { - $scope.getContent($scope.contentId); - } - - activate(); - - } - -angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.ListLayoutController", ListViewListLayoutController); - -}) (); + var vm = this; + vm.focusLayoutName = false; + + vm.layoutsSortableOptions = { + distance: 10, + tolerance: "pointer", + opacity: 0.7, + scroll: true, + cursor: "move", + handle: ".list-view-layout__sort-handle" + }; + + vm.addLayout = addLayout; + vm.showPrompt = showPrompt; + vm.hidePrompt = hidePrompt; + vm.removeLayout = removeLayout; + vm.openIconPicker = openIconPicker; + + function activate() { + + + + } + + function addLayout() { + + vm.focusLayoutName = false; + + var layout = { + "name": "", + "path": "", + "icon": "icon-stop", + "selected": true + }; + + $scope.model.value.push(layout); + + } + + function showPrompt(layout) { + layout.deletePrompt = true; + } + + function hidePrompt(layout) { + layout.deletePrompt = false; + } + + function removeLayout($index, layout) { + $scope.model.value.splice($index, 1); + } + + function openIconPicker(layout) { + vm.iconPickerDialog = { + view: "iconpicker", + show: true, + submit: function(model) { + if (model.color) { + layout.icon = model.icon + " " + model.color; + } else { + layout.icon = model.icon; + } + vm.focusLayoutName = true; + vm.iconPickerDialog.show = false; + vm.iconPickerDialog = null; + } + }; + } + + activate(); + + } + + angular.module("umbraco").controller("Umbraco.PrevalueEditors.ListViewLayoutsPreValsController", ListViewLayoutsPreValsController); + +})(); + +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.EditController + * @function + * + * @description + * The controller for the content type editor + */ +(function() { + "use strict"; + + function ListViewGridLayoutController($scope, $routeParams, mediaHelper, mediaResource, $location, listViewHelper, mediaTypeHelper) { + + var vm = this; + + vm.nodeId = $scope.contentId; + //we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles + vm.acceptedFileTypes = !mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles); + vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; + vm.activeDrag = false; + vm.mediaDetailsTooltip = {}; + vm.itemsWithoutFolders = []; + vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; + vm.acceptedMediatypes = []; + + vm.dragEnter = dragEnter; + vm.dragLeave = dragLeave; + vm.onFilesQueue = onFilesQueue; + vm.onUploadComplete = onUploadComplete; + + vm.hoverMediaItemDetails = hoverMediaItemDetails; + vm.selectContentItem = selectContentItem; + vm.selectItem = selectItem; + vm.selectFolder = selectFolder; + vm.goToItem = goToItem; + + function activate() { + vm.itemsWithoutFolders = filterOutFolders($scope.items); + + if($scope.entityType === 'media') { + mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { + vm.acceptedMediatypes = types; + }); + } + + } + + function filterOutFolders(items) { + + var newArray = []; + + if(items && items.length ) { + + for (var i = 0; items.length > i; i++) { + var item = items[i]; + var isFolder = !mediaHelper.hasFilePropertyType(item); + + if (!isFolder) { + newArray.push(item); + } + } + + } + + return newArray; + } + + function dragEnter(el, event) { + vm.activeDrag = true; + } + + function dragLeave(el, event) { + vm.activeDrag = false; + } + + function onFilesQueue() { + vm.activeDrag = false; + } + + function onUploadComplete() { + $scope.getContent($scope.contentId); + } + + function hoverMediaItemDetails(item, event, hover) { + + if (hover && !vm.mediaDetailsTooltip.show) { + + vm.mediaDetailsTooltip.event = event; + vm.mediaDetailsTooltip.item = item; + vm.mediaDetailsTooltip.show = true; + + } else if (!hover && vm.mediaDetailsTooltip.show) { + + vm.mediaDetailsTooltip.show = false; + + } + + } + + function selectContentItem(item, $event, $index) { + listViewHelper.selectHandler(item, $index, $scope.items, $scope.selection, $event); + } + + function selectItem(item, $event, $index) { + listViewHelper.selectHandler(item, $index, vm.itemsWithoutFolders, $scope.selection, $event); + } + + function selectFolder(folder, $event, $index) { + listViewHelper.selectHandler(folder, $index, $scope.folders, $scope.selection, $event); + } + + function goToItem(item, $event, $index) { + $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + item.id); + } + + activate(); + + } + + angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.GridLayoutController", ListViewGridLayoutController); + +})(); + +(function () { + "use strict"; + + function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper, mediaTypeHelper) { + + var vm = this; + + vm.nodeId = $scope.contentId; + //we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles + vm.acceptedFileTypes = !mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles); + vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; + vm.activeDrag = false; + vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; + vm.acceptedMediatypes = []; + + vm.selectItem = selectItem; + vm.clickItem = clickItem; + vm.selectAll = selectAll; + vm.isSelectedAll = isSelectedAll; + vm.isSortDirection = isSortDirection; + vm.sort = sort; + vm.dragEnter = dragEnter; + vm.dragLeave = dragLeave; + vm.onFilesQueue = onFilesQueue; + vm.onUploadComplete = onUploadComplete; + + function activate() { + + if ($scope.entityType === 'media') { + mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { + vm.acceptedMediatypes = types; + }); + } + + } + + function selectAll($event) { + listViewHelper.selectAllItems($scope.items, $scope.selection, $event); + } + + function isSelectedAll() { + return listViewHelper.isSelectedAll($scope.items, $scope.selection); + } + + function selectItem(selectedItem, $index, $event) { + listViewHelper.selectHandler(selectedItem, $index, $scope.items, $scope.selection, $event); + } + + function clickItem(item) { + // if item.id is 2147483647 (int.MaxValue) use item.key + $location.path($scope.entityType + '/' +$scope.entityType + '/edit/' + (item.id === 2147483647 ? item.key : item.id)); + } + + function isSortDirection(col, direction) { + return listViewHelper.setSortingDirection(col, direction, $scope.options); + } + + function sort(field, allow, isSystem) { + if (allow) { + $scope.options.orderBySystemField = isSystem; + listViewHelper.setSorting(field, allow, $scope.options); + $scope.getContent($scope.contentId); + } + } + + // Dropzone upload functions + function dragEnter(el, event) { + vm.activeDrag = true; + } + + function dragLeave(el, event) { + vm.activeDrag = false; + } + + function onFilesQueue() { + vm.activeDrag = false; + } + + function onUploadComplete() { + $scope.getContent($scope.contentId); + } + + activate(); + + } + +angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.ListLayoutController", ListViewListLayoutController); + +}) (); function listViewController($rootScope, $scope, $routeParams, $injector, $cookieStore, notificationsService, iconHelper, dialogService, editorState, localizationService, $location, appState, $timeout, $q, mediaResource, listViewHelper, userService, navigationService, treeService) { @@ -14115,238 +14115,238 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie angular.module("umbraco").controller("Umbraco.PropertyEditors.ListViewController", listViewController); -function sortByPreValsController($rootScope, $scope, localizationService, editorState, listViewPrevalueHelper) { - //Get the prevalue from the correct place - function getPrevalues() { - if (editorState.current.preValues) { - return editorState.current.preValues; - } - else { - return listViewPrevalueHelper.getPrevalues(); - } - } - - //Watch the prevalues - $scope.$watch(function () { - return _.findWhere(getPrevalues(), { key: "includeProperties" }).value; - }, function () { - populateFields(); - }, true); //Use deep watching, otherwise we won't pick up header changes - - function populateFields() { - // Helper to find a particular value from the list of sort by options - function findFromSortByFields(value) { - return _.find($scope.sortByFields, function (e) { - return e.value.toLowerCase() === value.toLowerCase(); - }); - } - - // Get list of properties assigned as columns of the list view - var propsPreValue = _.findWhere(getPrevalues(), { key: "includeProperties" }); - - // Populate list of options for the default sort (all the columns plus then node name) - $scope.sortByFields = []; - $scope.sortByFields.push({ value: "name", name: "Name", isSystem: 1 }); - if (propsPreValue != undefined) { - for (var i = 0; i < propsPreValue.value.length; i++) { - var value = propsPreValue.value[i]; - $scope.sortByFields.push({ - value: value.alias, - name: value.header, - isSystem: value.isSystem - }); - } - } - - // Localize the system fields, for some reason the directive doesn't work inside of the select group with an ng-model declared - var systemFields = [ - { value: "SortOrder", key: "general_sort" }, - { value: "Name", key: "general_name" }, - { value: "VersionDate", key: "content_updateDate" }, - { value: "Updater", key: "content_updatedBy" }, - { value: "CreateDate", key: "content_createDate" }, - { value: "Owner", key: "content_createBy" }, - { value: "ContentTypeAlias", key: "content_documentType" }, - { value: "Published", key: "content_isPublished" }, - { value: "Email", key: "general_email" }, - { value: "Username", key: "general_username" } - ]; - _.each(systemFields, function (e) { - localizationService.localize(e.key).then(function (v) { - - var sortByListValue = findFromSortByFields(e.value); - if (sortByListValue) { - sortByListValue.name = v; - switch (e.value) { - case "Updater": - e.name += " (Content only)"; - break; - case "Published": - e.name += " (Content only)"; - break; - case "Email": - e.name += " (Members only)"; - break; - case "Username": - e.name += " (Members only)"; - break; - } - } - }); - }); - - // Check existing model value is available in list and ensure a value is set - var existingValue = findFromSortByFields($scope.model.value); - if (existingValue) { - // Set the existing value - // The old implementation pre Umbraco 7.5 used PascalCase aliases, this uses camelCase, so this ensures that any previous value is set - $scope.model.value = existingValue.value; - } - else { - // Existing value not found, set to first value - $scope.model.value = $scope.sortByFields[0].value; - } - } -} - - -angular.module("umbraco").controller("Umbraco.PrevalueEditors.SortByListViewController", sortByPreValsController); -//DO NOT DELETE THIS, this is in use... -angular.module('umbraco') -.controller("Umbraco.PropertyEditors.MacroContainerController", - - function($scope, dialogService, entityResource, macroService){ - $scope.renderModel = []; - - if($scope.model.value){ - var macros = $scope.model.value.split('>'); - - angular.forEach(macros, function(syntax, key){ - if(syntax && syntax.length > 10){ - //re-add the char we split on - syntax = syntax + ">"; - var parsed = macroService.parseMacroSyntax(syntax); - if(!parsed){ - parsed = {}; - } - - parsed.syntax = syntax; - collectDetails(parsed); - $scope.renderModel.push(parsed); - } - }); - } - - - function collectDetails(macro){ - macro.details = ""; - if(macro.macroParamsDictionary){ - angular.forEach((macro.macroParamsDictionary), function(value, key){ - macro.details += key + ": " + value + " "; - }); - } - } - - function openDialog(index){ - var dialogData = { - allowedMacros: $scope.model.config.allowed - }; - - if(index !== null && $scope.renderModel[index]) { - var macro = $scope.renderModel[index]; - dialogData["macroData"] = macro; - } - - $scope.macroPickerOverlay = {}; - $scope.macroPickerOverlay.view = "macropicker"; - $scope.macroPickerOverlay.dialogData = dialogData; - $scope.macroPickerOverlay.show = true; - - $scope.macroPickerOverlay.submit = function(model) { - - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); - collectDetails(macroObject); - - //update the raw syntax and the list... - if(index !== null && $scope.renderModel[index]) { - $scope.renderModel[index] = macroObject; - } else { - $scope.renderModel.push(macroObject); - } - - $scope.macroPickerOverlay.show = false; - $scope.macroPickerOverlay = null; - }; - - $scope.macroPickerOverlay.close = function(oldModel) { - $scope.macroPickerOverlay.show = false; - $scope.macroPickerOverlay = null; - }; - - } - - - - $scope.edit =function(index){ - openDialog(index); - }; - - $scope.add = function () { - - if ($scope.model.config.max && $scope.model.config.max > 0 && $scope.renderModel.length >= $scope.model.config.max) { - //cannot add more than the max - return; - } - - openDialog(); - }; - - $scope.remove =function(index){ - $scope.renderModel.splice(index, 1); - }; - - $scope.clear = function() { - $scope.model.value = ""; - $scope.renderModel = []; - }; - - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - var syntax = []; - angular.forEach($scope.renderModel, function(value, key){ - syntax.push(value.syntax); - }); - - $scope.model.value = syntax.join(""); - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); - - - function trim(str, chr) { - var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^'+chr+'+|'+chr+'+$', 'g'); - return str.replace(rgxtrim, ''); - } - -}); +function sortByPreValsController($rootScope, $scope, localizationService, editorState, listViewPrevalueHelper) { + //Get the prevalue from the correct place + function getPrevalues() { + if (editorState.current.preValues) { + return editorState.current.preValues; + } + else { + return listViewPrevalueHelper.getPrevalues(); + } + } -function MacroListController($scope, entityResource) { - - $scope.items = []; - - entityResource.getAll("Macro").then(function(items) { - _.each(items, function(i) { - $scope.items.push({ name: i.name, alias: i.alias }); - }); - - }); - - -} - -angular.module("umbraco").controller("Umbraco.PrevalueEditors.MacroList", MacroListController); + //Watch the prevalues + $scope.$watch(function () { + return _.findWhere(getPrevalues(), { key: "includeProperties" }).value; + }, function () { + populateFields(); + }, true); //Use deep watching, otherwise we won't pick up header changes + + function populateFields() { + // Helper to find a particular value from the list of sort by options + function findFromSortByFields(value) { + return _.find($scope.sortByFields, function (e) { + return e.value.toLowerCase() === value.toLowerCase(); + }); + } + + // Get list of properties assigned as columns of the list view + var propsPreValue = _.findWhere(getPrevalues(), { key: "includeProperties" }); + + // Populate list of options for the default sort (all the columns plus then node name) + $scope.sortByFields = []; + $scope.sortByFields.push({ value: "name", name: "Name", isSystem: 1 }); + if (propsPreValue != undefined) { + for (var i = 0; i < propsPreValue.value.length; i++) { + var value = propsPreValue.value[i]; + $scope.sortByFields.push({ + value: value.alias, + name: value.header, + isSystem: value.isSystem + }); + } + } + + // Localize the system fields, for some reason the directive doesn't work inside of the select group with an ng-model declared + var systemFields = [ + { value: "SortOrder", key: "general_sort" }, + { value: "Name", key: "general_name" }, + { value: "VersionDate", key: "content_updateDate" }, + { value: "Updater", key: "content_updatedBy" }, + { value: "CreateDate", key: "content_createDate" }, + { value: "Owner", key: "content_createBy" }, + { value: "ContentTypeAlias", key: "content_documentType" }, + { value: "Published", key: "content_isPublished" }, + { value: "Email", key: "general_email" }, + { value: "Username", key: "general_username" } + ]; + _.each(systemFields, function (e) { + localizationService.localize(e.key).then(function (v) { + + var sortByListValue = findFromSortByFields(e.value); + if (sortByListValue) { + sortByListValue.name = v; + switch (e.value) { + case "Updater": + e.name += " (Content only)"; + break; + case "Published": + e.name += " (Content only)"; + break; + case "Email": + e.name += " (Members only)"; + break; + case "Username": + e.name += " (Members only)"; + break; + } + } + }); + }); + + // Check existing model value is available in list and ensure a value is set + var existingValue = findFromSortByFields($scope.model.value); + if (existingValue) { + // Set the existing value + // The old implementation pre Umbraco 7.5 used PascalCase aliases, this uses camelCase, so this ensures that any previous value is set + $scope.model.value = existingValue.value; + } + else { + // Existing value not found, set to first value + $scope.model.value = $scope.sortByFields[0].value; + } + } +} + + +angular.module("umbraco").controller("Umbraco.PrevalueEditors.SortByListViewController", sortByPreValsController); +//DO NOT DELETE THIS, this is in use... +angular.module('umbraco') +.controller("Umbraco.PropertyEditors.MacroContainerController", + + function($scope, dialogService, entityResource, macroService){ + $scope.renderModel = []; + + if($scope.model.value){ + var macros = $scope.model.value.split('>'); + + angular.forEach(macros, function(syntax, key){ + if(syntax && syntax.length > 10){ + //re-add the char we split on + syntax = syntax + ">"; + var parsed = macroService.parseMacroSyntax(syntax); + if(!parsed){ + parsed = {}; + } + + parsed.syntax = syntax; + collectDetails(parsed); + $scope.renderModel.push(parsed); + } + }); + } + + + function collectDetails(macro){ + macro.details = ""; + if(macro.macroParamsDictionary){ + angular.forEach((macro.macroParamsDictionary), function(value, key){ + macro.details += key + ": " + value + " "; + }); + } + } + + function openDialog(index){ + var dialogData = { + allowedMacros: $scope.model.config.allowed + }; + + if(index !== null && $scope.renderModel[index]) { + var macro = $scope.renderModel[index]; + dialogData["macroData"] = macro; + } + + $scope.macroPickerOverlay = {}; + $scope.macroPickerOverlay.view = "macropicker"; + $scope.macroPickerOverlay.dialogData = dialogData; + $scope.macroPickerOverlay.show = true; + + $scope.macroPickerOverlay.submit = function(model) { + + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); + collectDetails(macroObject); + + //update the raw syntax and the list... + if(index !== null && $scope.renderModel[index]) { + $scope.renderModel[index] = macroObject; + } else { + $scope.renderModel.push(macroObject); + } + + $scope.macroPickerOverlay.show = false; + $scope.macroPickerOverlay = null; + }; + + $scope.macroPickerOverlay.close = function(oldModel) { + $scope.macroPickerOverlay.show = false; + $scope.macroPickerOverlay = null; + }; + + } + + + + $scope.edit =function(index){ + openDialog(index); + }; + + $scope.add = function () { + + if ($scope.model.config.max && $scope.model.config.max > 0 && $scope.renderModel.length >= $scope.model.config.max) { + //cannot add more than the max + return; + } + + openDialog(); + }; + + $scope.remove =function(index){ + $scope.renderModel.splice(index, 1); + }; + + $scope.clear = function() { + $scope.model.value = ""; + $scope.renderModel = []; + }; + + var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { + var syntax = []; + angular.forEach($scope.renderModel, function(value, key){ + syntax.push(value.syntax); + }); + + $scope.model.value = syntax.join(""); + }); + + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); + + + function trim(str, chr) { + var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^'+chr+'+|'+chr+'+$', 'g'); + return str.replace(rgxtrim, ''); + } + +}); + +function MacroListController($scope, entityResource) { + + $scope.items = []; + + entityResource.getAll("Macro").then(function(items) { + _.each(items, function(i) { + $scope.items.push({ name: i.name, alias: i.alias }); + }); + + }); + + +} + +angular.module("umbraco").controller("Umbraco.PrevalueEditors.MacroList", MacroListController); //inject umbracos assetsServce and dialog service function MarkdownEditorController($scope, $element, assetsService, dialogService, angularHelper, $timeout) { @@ -14827,28 +14827,28 @@ function MultipleTextBoxController($scope) { angular.module("umbraco").controller("Umbraco.PropertyEditors.MultipleTextBoxController", MultipleTextBoxController); -angular.module("umbraco").controller("Umbraco.PropertyEditors.RadioButtonsController", - function($scope) { - - if (angular.isObject($scope.model.config.items)) { - - //now we need to format the items in the dictionary because we always want to have an array - var newItems = []; - var vals = _.values($scope.model.config.items); - var keys = _.keys($scope.model.config.items); - for (var i = 0; i < vals.length; i++) { - newItems.push({ id: keys[i], sortOrder: vals[i].sortOrder, value: vals[i].value }); - } - - //ensure the items are sorted by the provided sort order - newItems.sort(function (a, b) { return (a.sortOrder > b.sortOrder) ? 1 : ((b.sortOrder > a.sortOrder) ? -1 : 0); }); - - //re-assign - $scope.model.config.items = newItems; - - } - - }); +angular.module("umbraco").controller("Umbraco.PropertyEditors.RadioButtonsController", + function($scope) { + + if (angular.isObject($scope.model.config.items)) { + + //now we need to format the items in the dictionary because we always want to have an array + var newItems = []; + var vals = _.values($scope.model.config.items); + var keys = _.keys($scope.model.config.items); + for (var i = 0; i < vals.length; i++) { + newItems.push({ id: keys[i], sortOrder: vals[i].sortOrder, value: vals[i].value }); + } + + //ensure the items are sorted by the provided sort order + newItems.sort(function (a, b) { return (a.sortOrder > b.sortOrder) ? 1 : ((b.sortOrder > a.sortOrder) ? -1 : 0); }); + + //re-assign + $scope.model.config.items = newItems; + + } + + }); /** * @ngdoc controller @@ -15483,125 +15483,125 @@ angular.module("umbraco") }); -angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", - function ($scope, $timeout, $log, tinyMceService, stylesheetResource, assetsService) { - var cfg = tinyMceService.defaultPrevalues(); - - if($scope.model.value){ - if(angular.isString($scope.model.value)){ - $scope.model.value = cfg; - } - }else{ - $scope.model.value = cfg; - } - - if (!$scope.model.value.stylesheets) { - $scope.model.value.stylesheets = []; - } - if (!$scope.model.value.toolbar) { - $scope.model.value.toolbar = []; - } - if (!$scope.model.value.maxImageSize && $scope.model.value.maxImageSize != 0) { - $scope.model.value.maxImageSize = cfg.maxImageSize; - } - - tinyMceService.configuration().then(function(config){ - $scope.tinyMceConfig = config; - - // extend commands with properties for font-icon and if it is a custom command - $scope.tinyMceConfig.commands = _.map($scope.tinyMceConfig.commands, function (obj) { - var icon = getFontIcon(obj.frontEndCommand); - return angular.extend(obj, { - fontIcon: icon.name, - isCustom: icon.isCustom - }); - }); - }); - - stylesheetResource.getAll().then(function(stylesheets){ - $scope.stylesheets = stylesheets; - }); - - $scope.selected = function(cmd, alias, lookup){ - if (lookup && angular.isArray(lookup)) { - cmd.selected = lookup.indexOf(alias) >= 0; - return cmd.selected; - } - return false; - }; - - $scope.selectCommand = function(command){ - var index = $scope.model.value.toolbar.indexOf(command.frontEndCommand); - - if(command.selected && index === -1){ - $scope.model.value.toolbar.push(command.frontEndCommand); - }else if(index >= 0){ - $scope.model.value.toolbar.splice(index, 1); - } - }; - - $scope.selectStylesheet = function (css) { - - var index = $scope.model.value.stylesheets.indexOf(css.name); - - if(css.selected && index === -1){ - $scope.model.value.stylesheets.push(css.name); - }else if(index >= 0){ - $scope.model.value.stylesheets.splice(index, 1); - } - }; - - // map properties for specific commands - function getFontIcon(alias) { - var icon = { name: alias, isCustom: false }; - - switch (alias) { - case "codemirror": - icon.name = "code"; - icon.isCustom = false; - break; - case "styleselect": - icon.name = "icon-list"; - icon.isCustom = true; - break; - case "umbembeddialog": - icon.name = "icon-tv"; - icon.isCustom = true; - break; - case "umbmediapicker": - icon.name = "icon-picture"; - icon.isCustom = true; - break; - case "umbmacro": - icon.name = "icon-settings-alt"; - icon.isCustom = true; - break; - case "umbmacro": - icon.name = "icon-settings-alt"; - icon.isCustom = true; - break; - default: - icon.name = alias; - icon.isCustom = false; - } - - return icon; - } - - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - - var commands = _.where($scope.tinyMceConfig.commands, {selected: true}); - $scope.model.value.toolbar = _.pluck(commands, "frontEndCommand"); - - }); - - // when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); - - // load TinyMCE skin which contains css for font-icons - assetsService.loadCss("lib/tinymce/skins/umbraco/skin.min.css"); +angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", + function ($scope, $timeout, $log, tinyMceService, stylesheetResource, assetsService) { + var cfg = tinyMceService.defaultPrevalues(); + + if($scope.model.value){ + if(angular.isString($scope.model.value)){ + $scope.model.value = cfg; + } + }else{ + $scope.model.value = cfg; + } + + if (!$scope.model.value.stylesheets) { + $scope.model.value.stylesheets = []; + } + if (!$scope.model.value.toolbar) { + $scope.model.value.toolbar = []; + } + if (!$scope.model.value.maxImageSize && $scope.model.value.maxImageSize != 0) { + $scope.model.value.maxImageSize = cfg.maxImageSize; + } + + tinyMceService.configuration().then(function(config){ + $scope.tinyMceConfig = config; + + // extend commands with properties for font-icon and if it is a custom command + $scope.tinyMceConfig.commands = _.map($scope.tinyMceConfig.commands, function (obj) { + var icon = getFontIcon(obj.frontEndCommand); + return angular.extend(obj, { + fontIcon: icon.name, + isCustom: icon.isCustom + }); + }); + }); + + stylesheetResource.getAll().then(function(stylesheets){ + $scope.stylesheets = stylesheets; + }); + + $scope.selected = function(cmd, alias, lookup){ + if (lookup && angular.isArray(lookup)) { + cmd.selected = lookup.indexOf(alias) >= 0; + return cmd.selected; + } + return false; + }; + + $scope.selectCommand = function(command){ + var index = $scope.model.value.toolbar.indexOf(command.frontEndCommand); + + if(command.selected && index === -1){ + $scope.model.value.toolbar.push(command.frontEndCommand); + }else if(index >= 0){ + $scope.model.value.toolbar.splice(index, 1); + } + }; + + $scope.selectStylesheet = function (css) { + + var index = $scope.model.value.stylesheets.indexOf(css.name); + + if(css.selected && index === -1){ + $scope.model.value.stylesheets.push(css.name); + }else if(index >= 0){ + $scope.model.value.stylesheets.splice(index, 1); + } + }; + + // map properties for specific commands + function getFontIcon(alias) { + var icon = { name: alias, isCustom: false }; + + switch (alias) { + case "codemirror": + icon.name = "code"; + icon.isCustom = false; + break; + case "styleselect": + icon.name = "icon-list"; + icon.isCustom = true; + break; + case "umbembeddialog": + icon.name = "icon-tv"; + icon.isCustom = true; + break; + case "umbmediapicker": + icon.name = "icon-picture"; + icon.isCustom = true; + break; + case "umbmacro": + icon.name = "icon-settings-alt"; + icon.isCustom = true; + break; + case "umbmacro": + icon.name = "icon-settings-alt"; + icon.isCustom = true; + break; + default: + icon.name = alias; + icon.isCustom = false; + } + + return icon; + } + + var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { + + var commands = _.where($scope.tinyMceConfig.commands, {selected: true}); + $scope.model.value.toolbar = _.pluck(commands, "frontEndCommand"); + + }); + + // when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); + + // load TinyMCE skin which contains css for font-icons + assetsService.loadCss("lib/tinymce/skins/umbraco/skin.min.css"); }); function sliderController($scope, $log, $element, assetsService, angularHelper) { @@ -15823,7 +15823,7 @@ function sliderController($scope, $log, $element, assetsService, angularHelper) angular.module("umbraco").controller("Umbraco.PropertyEditors.SliderController", sliderController); angular.module("umbraco") .controller("Umbraco.PropertyEditors.TagsController", - function ($rootScope, $scope, $log, assetsService, umbRequestHelper, angularHelper, $timeout, $element, $sanitize) { + function ($rootScope, $scope, $log, assetsService, umbRequestHelper, angularHelper, $timeout, $element) { var $typeahead; @@ -15864,7 +15864,6 @@ angular.module("umbraco") //Helper method to add a tag on enter or on typeahead select function addTag(tagToAdd) { - tagToAdd = String(tagToAdd).htmlEncode(); if (tagToAdd != null && tagToAdd.length > 0) { if ($scope.model.value.indexOf(tagToAdd) < 0) { $scope.model.value.push(tagToAdd); diff --git a/OurUmbraco.Site/umbraco/js/umbraco.directives.js b/OurUmbraco.Site/umbraco/js/umbraco.directives.js index 2076f874..93ada1c5 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.directives.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.directives.js @@ -1,6 +1,6 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ @@ -10,129 +10,129 @@ angular.module("umbraco.directives", ["umbraco.directives.editors", "umbraco.dir angular.module("umbraco.directives.editors", []); angular.module("umbraco.directives.html", []); angular.module("umbraco.directives.validation", []); -/** -* @ngdoc directive -* @name umbraco.directives.directive:autoScale -* @element div -* @deprecated -* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. -* @function -* @description -* Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set -* So if you only want to scale the div to 70 pixels from the bottom you pass "70" - -* @example -* -* -*
-*
-*
-**/ - -angular.module("umbraco.directives") - .directive('autoScale', function ($window) { - return function (scope, el, attrs) { - - var totalOffset = 0; - var offsety = parseInt(attrs.autoScale, 10); - var window = angular.element($window); - if (offsety !== undefined){ - totalOffset += offsety; - } - - setTimeout(function () { - el.height(window.height() - (el.offset().top + totalOffset)); - }, 500); - - window.bind("resize", function () { - el.height(window.height() - (el.offset().top + totalOffset)); - }); - - }; - }); +/** +* @ngdoc directive +* @name umbraco.directives.directive:autoScale +* @element div +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* @function +* @description +* Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set +* So if you only want to scale the div to 70 pixels from the bottom you pass "70" -/** -* @ngdoc directive -* @name umbraco.directives.directive:detectFold -* @deprecated -* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. -* @description This is used for the editor buttons to ensure they are displayed correctly if the horizontal overflow of the editor -* exceeds the height of the window -**/ - -angular.module("umbraco.directives.html") - .directive('detectFold', function ($timeout, $log, windowResizeListener) { - return { - require: "^?umbTabs", - restrict: 'A', - link: function (scope, el, attrs, tabsCtrl) { - - var firstRun = false; - var parent = $(".umb-panel-body"); - var winHeight = $(window).height(); - var calculate = function () { - if (el && el.is(":visible") && !el.hasClass("umb-bottom-bar")) { - - //now that the element is visible, set the flag in a couple of seconds, - // this will ensure that loading time of a current tab get's completed and that - // we eventually stop watching to save on CPU time - $timeout(function() { - firstRun = true; - }, 4000); - - //var parent = el.parent(); - var hasOverflow = parent.innerHeight() < parent[0].scrollHeight; - //var belowFold = (el.offset().top + el.height()) > winHeight; - if (hasOverflow) { - el.addClass("umb-bottom-bar"); - - //I wish we didn't have to put this logic here but unfortunately we - // do. This needs to calculate the left offest to place the bottom bar - // depending on if the left column splitter has been moved by the user - // (based on the nav-resize directive) - var wrapper = $("#mainwrapper"); - var contentPanel = $("#leftcolumn").next(); - var contentPanelLeftPx = contentPanel.css("left"); - - el.css({ left: contentPanelLeftPx }); - } - } - return firstRun; - }; - - var resizeCallback = function(size) { - winHeight = size.height; - el.removeClass("umb-bottom-bar"); - calculate(); - }; - - windowResizeListener.register(resizeCallback); - - //Only execute the watcher if this tab is the active (first) tab on load, otherwise there's no reason to execute - // the watcher since it will be recalculated when the tab changes! - if (el.closest(".umb-tab-pane").index() === 0) { - //run a watcher to ensure that the calculation occurs until it's firstRun but ensure - // the calculations are throttled to save a bit of CPU - var listener = scope.$watch(_.throttle(calculate, 1000), function (newVal, oldVal) { - if (newVal !== oldVal) { - listener(); - } - }); - } - - //listen for tab changes - if (tabsCtrl != null) { - tabsCtrl.onTabShown(function (args) { - calculate(); - }); - } - - //ensure to unregister - scope.$on('$destroy', function() { - windowResizeListener.unregister(resizeCallback); - }); - } - }; +* @example +* +* +*
+*
+*
+**/ + +angular.module("umbraco.directives") + .directive('autoScale', function ($window) { + return function (scope, el, attrs) { + + var totalOffset = 0; + var offsety = parseInt(attrs.autoScale, 10); + var window = angular.element($window); + if (offsety !== undefined){ + totalOffset += offsety; + } + + setTimeout(function () { + el.height(window.height() - (el.offset().top + totalOffset)); + }, 500); + + window.bind("resize", function () { + el.height(window.height() - (el.offset().top + totalOffset)); + }); + + }; + }); + +/** +* @ngdoc directive +* @name umbraco.directives.directive:detectFold +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* @description This is used for the editor buttons to ensure they are displayed correctly if the horizontal overflow of the editor +* exceeds the height of the window +**/ + +angular.module("umbraco.directives.html") + .directive('detectFold', function ($timeout, $log, windowResizeListener) { + return { + require: "^?umbTabs", + restrict: 'A', + link: function (scope, el, attrs, tabsCtrl) { + + var firstRun = false; + var parent = $(".umb-panel-body"); + var winHeight = $(window).height(); + var calculate = function () { + if (el && el.is(":visible") && !el.hasClass("umb-bottom-bar")) { + + //now that the element is visible, set the flag in a couple of seconds, + // this will ensure that loading time of a current tab get's completed and that + // we eventually stop watching to save on CPU time + $timeout(function() { + firstRun = true; + }, 4000); + + //var parent = el.parent(); + var hasOverflow = parent.innerHeight() < parent[0].scrollHeight; + //var belowFold = (el.offset().top + el.height()) > winHeight; + if (hasOverflow) { + el.addClass("umb-bottom-bar"); + + //I wish we didn't have to put this logic here but unfortunately we + // do. This needs to calculate the left offest to place the bottom bar + // depending on if the left column splitter has been moved by the user + // (based on the nav-resize directive) + var wrapper = $("#mainwrapper"); + var contentPanel = $("#leftcolumn").next(); + var contentPanelLeftPx = contentPanel.css("left"); + + el.css({ left: contentPanelLeftPx }); + } + } + return firstRun; + }; + + var resizeCallback = function(size) { + winHeight = size.height; + el.removeClass("umb-bottom-bar"); + calculate(); + }; + + windowResizeListener.register(resizeCallback); + + //Only execute the watcher if this tab is the active (first) tab on load, otherwise there's no reason to execute + // the watcher since it will be recalculated when the tab changes! + if (el.closest(".umb-tab-pane").index() === 0) { + //run a watcher to ensure that the calculation occurs until it's firstRun but ensure + // the calculations are throttled to save a bit of CPU + var listener = scope.$watch(_.throttle(calculate, 1000), function (newVal, oldVal) { + if (newVal !== oldVal) { + listener(); + } + }); + } + + //listen for tab changes + if (tabsCtrl != null) { + tabsCtrl.onTabShown(function (args) { + calculate(); + }); + } + + //ensure to unregister + scope.$on('$destroy', function() { + windowResizeListener.unregister(resizeCallback); + }); + } + }; }); /** * @ngdoc directive @@ -297,70 +297,70 @@ angular.module("umbraco.directives") }; }); -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbHeader -* @deprecated -* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. -* @restrict E -* @function -* @description -* The header on an editor that contains tabs using bootstrap tabs - THIS IS OBSOLETE, use umbTabHeader instead -**/ - -angular.module("umbraco.directives") -.directive('umbHeader', function ($parse, $timeout) { - return { - restrict: 'E', - replace: true, - transclude: 'true', - templateUrl: 'views/directives/_obsolete/umb-header.html', - //create a new isolated scope assigning a tabs property from the attribute 'tabs' - //which is bound to the parent scope property passed in - scope: { - tabs: "=" - }, - link: function (scope, iElement, iAttrs) { - - scope.showTabs = iAttrs.tabs ? true : false; - scope.visibleTabs = []; - - //since tabs are loaded async, we need to put a watch on them to determine - // when they are loaded, then we can close the watch - var tabWatch = scope.$watch("tabs", function (newValue, oldValue) { - - angular.forEach(newValue, function(val, index){ - var tab = {id: val.id, label: val.label}; - scope.visibleTabs.push(tab); - }); - - //don't process if we cannot or have already done so - if (!newValue) {return;} - if (!newValue.length || newValue.length === 0){return;} - - //we need to do a timeout here so that the current sync operation can complete - // and update the UI, then this will fire and the UI elements will be available. - $timeout(function () { - - //use bootstrap tabs API to show the first one - iElement.find(".nav-tabs a:first").tab('show'); - - //enable the tab drop - iElement.find('.nav-pills, .nav-tabs').tabdrop(); - - //ensure to destroy tabdrop (unbinds window resize listeners) - scope.$on('$destroy', function () { - iElement.find('.nav-pills, .nav-tabs').tabdrop("destroy"); - }); - - //stop watching now - tabWatch(); - }, 200); - - }); - } - }; -}); +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbHeader +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* @restrict E +* @function +* @description +* The header on an editor that contains tabs using bootstrap tabs - THIS IS OBSOLETE, use umbTabHeader instead +**/ + +angular.module("umbraco.directives") +.directive('umbHeader', function ($parse, $timeout) { + return { + restrict: 'E', + replace: true, + transclude: 'true', + templateUrl: 'views/directives/_obsolete/umb-header.html', + //create a new isolated scope assigning a tabs property from the attribute 'tabs' + //which is bound to the parent scope property passed in + scope: { + tabs: "=" + }, + link: function (scope, iElement, iAttrs) { + + scope.showTabs = iAttrs.tabs ? true : false; + scope.visibleTabs = []; + + //since tabs are loaded async, we need to put a watch on them to determine + // when they are loaded, then we can close the watch + var tabWatch = scope.$watch("tabs", function (newValue, oldValue) { + + angular.forEach(newValue, function(val, index){ + var tab = {id: val.id, label: val.label}; + scope.visibleTabs.push(tab); + }); + + //don't process if we cannot or have already done so + if (!newValue) {return;} + if (!newValue.length || newValue.length === 0){return;} + + //we need to do a timeout here so that the current sync operation can complete + // and update the UI, then this will fire and the UI elements will be available. + $timeout(function () { + + //use bootstrap tabs API to show the first one + iElement.find(".nav-tabs a:first").tab('show'); + + //enable the tab drop + iElement.find('.nav-pills, .nav-tabs').tabdrop(); + + //ensure to destroy tabdrop (unbinds window resize listeners) + scope.$on('$destroy', function () { + iElement.find('.nav-pills, .nav-tabs').tabdrop("destroy"); + }); + + //stop watching now + tabWatch(); + }, 200); + + }); + } + }; +}); /** * @ngdoc directive @@ -382,55 +382,55 @@ function loginDirective() { angular.module('umbraco.directives').directive("umbLogin", loginDirective); -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbOptionsMenu -* @deprecated -* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. -* @function -* @element ANY -* @restrict E -**/ - -angular.module("umbraco.directives") -.directive('umbOptionsMenu', function ($injector, treeService, navigationService, umbModelMapper, appState) { - return { - scope: { - currentSection: "@", - currentNode: "=" - }, - restrict: 'E', - replace: true, - templateUrl: 'views/directives/_obsolete/umb-optionsmenu.html', - link: function (scope, element, attrs, ctrl) { - - //adds a handler to the context menu item click, we need to handle this differently - //depending on what the menu item is supposed to do. - scope.executeMenuItem = function (action) { - navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection); - }; - - //callback method to go and get the options async - scope.getOptions = function () { - - if (!scope.currentNode) { - return; - } - - //when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu) - appState.setMenuState("currentNode", scope.currentNode); - - if (!scope.actions) { - treeService.getMenu({ treeNode: scope.currentNode }) - .then(function (data) { - scope.actions = data.menuItems; - }); - } - }; - - } - }; -}); +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbOptionsMenu +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* @function +* @element ANY +* @restrict E +**/ + +angular.module("umbraco.directives") +.directive('umbOptionsMenu', function ($injector, treeService, navigationService, umbModelMapper, appState) { + return { + scope: { + currentSection: "@", + currentNode: "=" + }, + restrict: 'E', + replace: true, + templateUrl: 'views/directives/_obsolete/umb-optionsmenu.html', + link: function (scope, element, attrs, ctrl) { + + //adds a handler to the context menu item click, we need to handle this differently + //depending on what the menu item is supposed to do. + scope.executeMenuItem = function (action) { + navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection); + }; + + //callback method to go and get the options async + scope.getOptions = function () { + + if (!scope.currentNode) { + return; + } + + //when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu) + appState.setMenuState("currentNode", scope.currentNode); + + if (!scope.actions) { + treeService.getMenu({ treeNode: scope.currentNode }) + .then(function (data) { + scope.actions = data.menuItems; + }); + } + }; + + } + }; +}); /** * @ngdoc directive @@ -675,184 +675,184 @@ angular.module("umbraco.directives") }; }); -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbTabView -* @deprecated -* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. -* -* @restrict E -**/ - -angular.module("umbraco.directives") -.directive('umbTabView', function($timeout, $log){ - return { - restrict: 'E', - replace: true, - transclude: 'true', - templateUrl: 'views/directives/_obsolete/umb-tab-view.html' - }; -}); +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbTabView +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* +* @restrict E +**/ -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbUploadDropzone -* @deprecated -* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. -* -* @restrict E -**/ - -angular.module("umbraco.directives.html") - .directive('umbUploadDropzone', function(){ - return { - restrict: 'E', - replace: true, - templateUrl: 'views/directives/_obsolete/umb-upload-dropzone.html' - }; - }); +angular.module("umbraco.directives") +.directive('umbTabView', function($timeout, $log){ + return { + restrict: 'E', + replace: true, + transclude: 'true', + templateUrl: 'views/directives/_obsolete/umb-tab-view.html' + }; +}); -/** -* @ngdoc directive -* @name umbraco.directives.directive:navResize -* @restrict A - * - * @description - * Handles how the navigation responds to window resizing and controls how the draggable resize panel works -**/ -angular.module("umbraco.directives") - .directive('navResize', function (appState, eventsService, windowResizeListener) { - return { - restrict: 'A', - link: function (scope, element, attrs, ctrl) { - - var minScreenSize = 1100; - var resizeEnabled = false; - - function setTreeMode() { - appState.setGlobalState("showNavigation", appState.getGlobalState("isTablet") === false); - } - - function enableResize() { - //only enable when the size is correct and it's not already enabled - if (!resizeEnabled && appState.getGlobalState("isTablet") === false) { - element.resizable( - { - containment: $("#mainwrapper"), - autoHide: true, - handles: "e", - alsoResize: ".navigation-inner-container", - resize: function(e, ui) { - var wrapper = $("#mainwrapper"); - var contentPanel = $("#contentwrapper"); - var umbNotification = $("#umb-notifications-wrapper"); - var apps = $("#applications"); - var bottomBar = contentPanel.find(".umb-bottom-bar"); - var navOffeset = $("#navOffset"); - - var leftPanelWidth = ui.element.width() + apps.width(); - - contentPanel.css({ left: leftPanelWidth }); - bottomBar.css({ left: leftPanelWidth }); - umbNotification.css({ left: leftPanelWidth }); - - navOffeset.css({ "margin-left": ui.element.outerWidth() }); - }, - stop: function (e, ui) { - - } - }); - - resizeEnabled = true; - } - } - - function resetResize() { - if (resizeEnabled) { - //kill the resize - element.resizable("destroy"); - element.css("width", ""); - - var navInnerContainer = element.find(".navigation-inner-container"); - - navInnerContainer.css("width", ""); - $("#contentwrapper").css("left", ""); - $("#umb-notifications-wrapper").css("left", ""); - $("#navOffset").css("margin-left", ""); - - resizeEnabled = false; - } - } - - var evts = []; - - //Listen for global state changes - evts.push(eventsService.on("appState.globalState.changed", function (e, args) { - if (args.key === "showNavigation") { - if (args.value === false) { - resetResize(); - } - else { - enableResize(); - } - } - })); - - var resizeCallback = function(size) { - //set the global app state - appState.setGlobalState("isTablet", (size.width <= minScreenSize)); - setTreeMode(); - }; - - windowResizeListener.register(resizeCallback); - - //ensure to unregister from all events and kill jquery plugins - scope.$on('$destroy', function () { - windowResizeListener.unregister(resizeCallback); - for (var e in evts) { - eventsService.unsubscribe(evts[e]); - } - var navInnerContainer = element.find(".navigation-inner-container"); - navInnerContainer.resizable("destroy"); - }); - - //init - //set the global app state - appState.setGlobalState("isTablet", ($(window).width() <= minScreenSize)); - setTreeMode(); - } - }; - }); +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbUploadDropzone +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* +* @restrict E +**/ -angular.module("umbraco.directives") -.directive('sectionIcon', function ($compile, iconHelper) { - return { - restrict: 'E', - replace: true, - - link: function (scope, element, attrs) { - - var icon = attrs.icon; - - if (iconHelper.isLegacyIcon(icon)) { - //its a known legacy icon, convert to a new one - element.html(""); - } - else if (iconHelper.isFileBasedIcon(icon)) { - var convert = iconHelper.convertFromLegacyImage(icon); - if(convert){ - element.html(""); - }else{ - element.html(""); - } - //it's a file, normally legacy so look in the icon tray images - } - else { - //it's normal - element.html(""); - } - } - }; +angular.module("umbraco.directives.html") + .directive('umbUploadDropzone', function(){ + return { + restrict: 'E', + replace: true, + templateUrl: 'views/directives/_obsolete/umb-upload-dropzone.html' + }; + }); + +/** +* @ngdoc directive +* @name umbraco.directives.directive:navResize +* @restrict A + * + * @description + * Handles how the navigation responds to window resizing and controls how the draggable resize panel works +**/ +angular.module("umbraco.directives") + .directive('navResize', function (appState, eventsService, windowResizeListener) { + return { + restrict: 'A', + link: function (scope, element, attrs, ctrl) { + + var minScreenSize = 1100; + var resizeEnabled = false; + + function setTreeMode() { + appState.setGlobalState("showNavigation", appState.getGlobalState("isTablet") === false); + } + + function enableResize() { + //only enable when the size is correct and it's not already enabled + if (!resizeEnabled && appState.getGlobalState("isTablet") === false) { + element.resizable( + { + containment: $("#mainwrapper"), + autoHide: true, + handles: "e", + alsoResize: ".navigation-inner-container", + resize: function(e, ui) { + var wrapper = $("#mainwrapper"); + var contentPanel = $("#contentwrapper"); + var umbNotification = $("#umb-notifications-wrapper"); + var apps = $("#applications"); + var bottomBar = contentPanel.find(".umb-bottom-bar"); + var navOffeset = $("#navOffset"); + + var leftPanelWidth = ui.element.width() + apps.width(); + + contentPanel.css({ left: leftPanelWidth }); + bottomBar.css({ left: leftPanelWidth }); + umbNotification.css({ left: leftPanelWidth }); + + navOffeset.css({ "margin-left": ui.element.outerWidth() }); + }, + stop: function (e, ui) { + + } + }); + + resizeEnabled = true; + } + } + + function resetResize() { + if (resizeEnabled) { + //kill the resize + element.resizable("destroy"); + element.css("width", ""); + + var navInnerContainer = element.find(".navigation-inner-container"); + + navInnerContainer.css("width", ""); + $("#contentwrapper").css("left", ""); + $("#umb-notifications-wrapper").css("left", ""); + $("#navOffset").css("margin-left", ""); + + resizeEnabled = false; + } + } + + var evts = []; + + //Listen for global state changes + evts.push(eventsService.on("appState.globalState.changed", function (e, args) { + if (args.key === "showNavigation") { + if (args.value === false) { + resetResize(); + } + else { + enableResize(); + } + } + })); + + var resizeCallback = function(size) { + //set the global app state + appState.setGlobalState("isTablet", (size.width <= minScreenSize)); + setTreeMode(); + }; + + windowResizeListener.register(resizeCallback); + + //ensure to unregister from all events and kill jquery plugins + scope.$on('$destroy', function () { + windowResizeListener.unregister(resizeCallback); + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + var navInnerContainer = element.find(".navigation-inner-container"); + navInnerContainer.resizable("destroy"); + }); + + //init + //set the global app state + appState.setGlobalState("isTablet", ($(window).width() <= minScreenSize)); + setTreeMode(); + } + }; + }); + +angular.module("umbraco.directives") +.directive('sectionIcon', function ($compile, iconHelper) { + return { + restrict: 'E', + replace: true, + + link: function (scope, element, attrs) { + + var icon = attrs.icon; + + if (iconHelper.isLegacyIcon(icon)) { + //its a known legacy icon, convert to a new one + element.html(""); + } + else if (iconHelper.isFileBasedIcon(icon)) { + var convert = iconHelper.convertFromLegacyImage(icon); + if(convert){ + element.html(""); + }else{ + element.html(""); + } + //it's a file, normally legacy so look in the icon tray images + } + else { + //it's normal + element.html(""); + } + } + }; }); angular.module("umbraco.directives") .directive('umbContextMenu', function (navigationService) { @@ -1080,2718 +1080,2776 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se angular.module('umbraco.directives').directive("umbSections", sectionsDirective); -/** -@ngdoc directive -@name umbraco.directives.directive:umbButton -@restrict E -@scope - -@description -Use this directive to render an umbraco button. The directive can be used to generate all types of buttons, set type, style, translation, shortcut and much more. - -

Markup example

-
-    
- - - - -
-
- -

Controller example

-
-    (function () {
-        "use strict";
-
-        function Controller(myService) {
-
-            var vm = this;
-            vm.buttonState = "init";
-
-            vm.clickButton = clickButton;
-
-            function clickButton() {
-
-                vm.buttonState = "busy";
-
-                myService.clickButton().then(function() {
-                    vm.buttonState = "success";
-                }, function() {
-                    vm.buttonState = "error";
-                });
-
-            }
-        }
-
-        angular.module("umbraco").controller("My.Controller", Controller);
-
-    })();
-
- -@param {callback} action The button action which should be performed when the button is clicked. -@param {string=} href Url/Path to navigato to. -@param {string=} type Set the button type ("button" or "submit"). -@param {string=} buttonStyle Set the style of the button. The directive uses the default bootstrap styles ("primary", "info", "success", "warning", "danger", "inverse", "link"). -@param {string=} state Set a progress state on the button ("init", "busy", "success", "error"). -@param {string=} shortcut Set a keyboard shortcut for the button ("ctrl+c"). -@param {string=} label Set the button label. -@param {string=} labelKey Set a localization key to make a multi lingual button ("general_buttonText"). -@param {string=} icon Set a button icon. Can only be used when buttonStyle is "link". -@param {boolean=} disabled Set to true to disable the button. -**/ - -(function() { - 'use strict'; - - function ButtonDirective($timeout) { - - function link(scope, el, attr, ctrl) { - - scope.style = null; - - function activate() { - - if (!scope.state) { - scope.state = "init"; - } - - if (scope.buttonStyle) { - scope.style = "btn-" + scope.buttonStyle; - } - - } - - activate(); - - var unbindStateWatcher = scope.$watch('state', function(newValue, oldValue) { - - if (newValue === 'success' || newValue === 'error') { - $timeout(function() { - scope.state = 'init'; - }, 2000); - } - - }); - - scope.$on('$destroy', function() { - unbindStateWatcher(); - }); - - } - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/buttons/umb-button.html', - link: link, - scope: { - action: "&?", - href: "@?", - type: "@", - buttonStyle: "@?", - state: "=?", - shortcut: "@?", - shortcutWhenHidden: "@", - label: "@?", - labelKey: "@?", - icon: "@?", - disabled: "=" - } - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbButton', ButtonDirective); - -})(); +/** +@ngdoc directive +@name umbraco.directives.directive:umbButton +@restrict E +@scope -/** -@ngdoc directive -@name umbraco.directives.directive:umbButtonGroup -@restrict E -@scope - -@description -Use this directive to render a button with a dropdown of alternative actions. - -

Markup example

-
-    
- - - - -
-
- -

Controller example

-
-    (function () {
-        "use strict";
-
-        function Controller() {
-
-            var vm = this;
-
-            vm.buttonGroup = {
-                defaultButton: {
-                    labelKey: "general_defaultButton",
-                    hotKey: "ctrl+d",
-                    hotKeyWhenHidden: true,
-                    handler: function() {
-                        // do magic here
-                    }
-                },
-                subButtons: [
-                    {
-                        labelKey: "general_subButton",
-                        hotKey: "ctrl+b",
-                        hotKeyWhenHidden: true,
-                        handler: function() {
-                            // do magic here
-                        }
-                    }
-                ]
-            };
-        }
-
-        angular.module("umbraco").controller("My.Controller", Controller);
-
-    })();
-
- -

Button model description

-
    -
  • - labekKey - (string) - - Set a localization key to make a multi lingual button ("general_buttonText"). -
  • -
  • - hotKey - (array) - - Set a keyboard shortcut for the button ("ctrl+c"). -
  • -
  • - hotKeyWhenHidden - (boolean) - - As a default the hotkeys only works on elements visible in the UI. Set to true to set a hotkey on the hidden sub buttons. -
  • -
  • - handler - (callback) - - Set a callback to handle button click events. -
  • -
- -@param {object} defaultButton The model of the default button. -@param {array} subButtons Array of sub buttons. -@param {string=} state Set a progress state on the button ("init", "busy", "success", "error"). -@param {string=} direction Set the direction of the dropdown ("up", "down"). -@param {string=} float Set the float of the dropdown. ("left", "right"). -**/ - -(function() { - 'use strict'; - - function ButtonGroupDirective() { - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/buttons/umb-button-group.html', - scope: { - defaultButton: "=", - subButtons: "=", - state: "=?", - direction: "@?", - float: "@?" - } - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbButtonGroup', ButtonGroupDirective); - -})(); +@description +Use this directive to render an umbraco button. The directive can be used to generate all types of buttons, set type, style, translation, shortcut and much more. -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorSubHeader -@restrict E - -@description -Use this directive to construct a sub header in the main editor window. -The sub header is sticky and will follow along down the page when scrolling. - -

Markup example

-
-    
- -
- - - - - - - // sub header content here - - - - - - -
- -
-
- -

Use in combination with

-
    -
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
  • -
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
  • -
  • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
  • -
-**/ - -(function() { - 'use strict'; - - function EditorSubHeaderDirective() { - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/subheader/umb-editor-sub-header.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorSubHeader', EditorSubHeaderDirective); - -})(); +

Markup example

+
+    
-/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorSubHeaderContentLeft -@restrict E - -@description -Use this directive to left align content in a sub header in the main editor window. - -

Markup example

-
-    
- -
- - - - - - - - - // left content here - - - - // right content here - - - - - - - - -
- -
-
- -

Use in combination with

-
    -
  • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
  • -
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
  • -
  • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
  • -
-**/ - -(function() { - 'use strict'; - - function EditorSubHeaderContentLeftDirective() { - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-content-left.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorSubHeaderContentLeft', EditorSubHeaderContentLeftDirective); - -})(); + + -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorSubHeaderContentRight -@restrict E - -@description -Use this directive to rigt align content in a sub header in the main editor window. - -

Markup example

-
-    
- -
- - - - - - - - - // left content here - - - - // right content here - - - - - - - - -
- -
-
- -

Use in combination with

-
    -
  • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
  • -
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
  • -
  • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
  • -
-**/ - -(function() { - 'use strict'; - - function EditorSubHeaderContentRightDirective() { - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-content-right.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorSubHeaderContentRight', EditorSubHeaderContentRightDirective); - -})(); +
+
-/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorSubHeaderSection -@restrict E - -@description -Use this directive to create sections, divided by borders, in a sub header in the main editor window. - -

Markup example

-
-    
- -
- - - - - - - - - - - // section content here - - - - // section content here - - - - // section content here - - - - - - - - - - -
- -
-
- -

Use in combination with

-
    -
  • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
  • -
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
  • -
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
  • -
-**/ - -(function() { - 'use strict'; - - function EditorSubHeaderSectionDirective() { - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-section.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorSubHeaderSection', EditorSubHeaderSectionDirective); - -})(); +

Controller example

+
+    (function () {
+        "use strict";
 
-/**
-@ngdoc directive
-@name umbraco.directives.directive:umbBreadcrumbs
-@restrict E
-@scope
-
-@description
-Use this directive to generate a list of breadcrumbs.
-
-

Markup example

-
-    
- - -
-
- -

Controller example

-
-    (function () {
-        "use strict";
-
-        function Controller(myService) {
-
-            var vm = this;
-            vm.ancestors = [];
-
-            myService.getAncestors().then(function(ancestors){
-                vm.ancestors = ancestors;
-            });
-
-        }
-
-        angular.module("umbraco").controller("My.Controller", Controller);
-    })();
-
- -@param {array} ancestors Array of ancestors -@param {string} entityType The content entity type (member, media, content). -**/ - -(function() { - 'use strict'; - - function BreadcrumbsDirective() { - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-breadcrumbs.html', - scope: { - ancestors: "=", - entityType: "@" - } - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbBreadcrumbs', BreadcrumbsDirective); - -})(); + function Controller(myService) { -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorContainer -@restrict E - -@description -Use this directive to construct a main content area inside the main editor window. - -

Markup example

-
-    
- - - - - - - - // main content here - - - - // footer content here - - - - -
-
- -

Use in combination with

-
    -
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • -
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • -
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • -
-**/ - -(function() { - 'use strict'; - - function EditorContainerDirective(overlayHelper) { - - function link(scope, el, attr, ctrl) { - - scope.numberOfOverlays = 0; - - scope.$watch(function(){ - return overlayHelper.getNumberOfOverlays(); - }, function (newValue) { - scope.numberOfOverlays = newValue; - }); - - } - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-container.html', - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorContainer', EditorContainerDirective); - -})(); + var vm = this; + vm.buttonState = "init"; -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorFooter -@restrict E - -@description -Use this directive to construct a footer inside the main editor window. - -

Markup example

-
-    
- -
- - - - - - - - // main content here - - - - // footer content here - - - - -
- -
-
- -

Use in combination with

-
    -
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • -
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • -
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • -
  • {@link umbraco.directives.directive:umbEditorFooterContentLeft umbEditorFooterContentLeft}
  • -
  • {@link umbraco.directives.directive:umbEditorFooterContentRight umbEditorFooterContentRight}
  • -
-**/ - -(function() { - 'use strict'; - - function EditorFooterDirective() { - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-footer.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorFooter', EditorFooterDirective); - -})(); + vm.clickButton = clickButton; -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorFooterContentLeft -@restrict E - -@description -Use this directive to align content left inside the main editor footer. - -

Markup example

-
-    
- -
- - - - - - - // align content left - - - - // align content right - - - - - - -
- -
-
- -

Use in combination with

-
    -
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • -
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • -
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • -
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • -
  • {@link umbraco.directives.directive:umbEditorFooterContentRight umbEditorFooterContentRight}
  • -
-**/ - -(function() { - 'use strict'; - - function EditorFooterContentLeftDirective() { - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-footer-content-left.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorFooterContentLeft', EditorFooterContentLeftDirective); - -})(); + function clickButton() { -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorFooterContentRight -@restrict E - -@description -Use this directive to align content right inside the main editor footer. - -

Markup example

-
-    
- -
- - - - - - - // align content left - - - - // align content right - - - - - - -
- -
-
- -

Use in combination with

-
    -
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • -
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • -
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • -
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • -
  • {@link umbraco.directives.directive:umbEditorFooterContentLeft umbEditorFooterContentLeft}
  • -
-**/ - -(function() { - 'use strict'; - - function EditorFooterContentRightDirective() { - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-footer-content-right.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorFooterContentRight', EditorFooterContentRightDirective); - -})(); + vm.buttonState = "busy"; -/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorHeader -@restrict E -@scope - -@description -Use this directive to construct a header inside the main editor window. - -

Markup example

-
-    
- -
- - - - - - - - // main content here - - - - // footer content here - - - - -
- -
-
- -

Markup example - with tabs

-
-    
- -
- - - - - - - - - - -
- // tab 1 content -
- -
- // tab 2 content -
- -
-
-
- - - // footer content here - - -
- -
- -
-
- -

Controller example - with tabs

-
-    (function () {
-        "use strict";
-
-        function Controller() {
-
-            var vm = this;
-            vm.content = {
-                name: "",
-                tabs: [
-                    {
-                        id: 1,
-                        label: "Tab 1",
-                        alias: "tab1",
-                        active: true
-                    },
-                    {
-                        id: 2,
-                        label: "Tab 2",
-                        alias: "tab2",
-                        active: false
-                    }
-                ]
-            };
-
-        }
-
-        angular.module("umbraco").controller("MySection.Controller", Controller);
-    })();
-
- -

Markup example - with sub views

-
-    
- -
- - - - - - - - - - - - - - - // footer content here - - - - -
- -
-
- -

Controller example - with sub views

-
-    (function () {
-
-        "use strict";
-
-        function Controller() {
-
-            var vm = this;
-            vm.content = {
-                name: "",
-                navigation: [
-                    {
-                        "name": "Section 1",
-                        "icon": "icon-document-dashed-line",
-                        "view": "/App_Plugins/path/to/html.html",
-                        "active": true
-                    },
-                    {
-                        "name": "Section 2",
-                        "icon": "icon-list",
-                        "view": "/App_Plugins/path/to/html.html",
-                    }
-                ]
-            };
-
-        }
-
-        angular.module("umbraco").controller("MySection.Controller", Controller);
-    })();
-
- -

Use in combination with

-
    -
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • -
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • -
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • -
- -@param {string} name The content name. -@param {array=} tabs Array of tabs. See example above. -@param {array=} navigation Array of sub views. See example above. -@param {boolean=} nameLocked Set to true to lock the name. -@param {object=} menu Add a context menu to the editor. -@param {string=} icon Show and edit the content icon. Opens an overlay to change the icon. -@param {boolean=} hideIcon Set to true to hide icon. -@param {string=} alias show and edit the content alias. -@param {boolean=} hideAlias Set to true to hide alias. -@param {string=} description Add a description to the content. -@param {boolean=} hideDescription Set to true to hide description. - -**/ - -(function() { - 'use strict'; - - function EditorHeaderDirective(iconHelper) { - - function link(scope, el, attr, ctrl) { - - scope.openIconPicker = function() { - scope.dialogModel = { - view: "iconpicker", - show: true, - submit: function (model) { - - /* ensure an icon is selected, because on focus on close button - or an element in background no icon is submitted. So don't clear/update existing icon/preview. - */ - if (model.icon) { - - if (model.color) { - scope.icon = model.icon + " " + model.color; - } else { - scope.icon = model.icon; - } - - // set form to dirty - ctrl.$setDirty(); - } - - scope.dialogModel.show = false; - scope.dialogModel = null; - } - }; - }; - } - - var directive = { - require: '^form', - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-header.html', - scope: { - tabs: "=", - actions: "=", - name: "=", - nameLocked: "=", - menu: "=", - icon: "=", - hideIcon: "@", - alias: "=", - hideAlias: "@", - description: "=", - hideDescription: "@", - navigation: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorHeader', EditorHeaderDirective); - -})(); + myService.clickButton().then(function() { + vm.buttonState = "success"; + }, function() { + vm.buttonState = "error"; + }); -(function() { - 'use strict'; - - function EditorMenuDirective($injector, treeService, navigationService, umbModelMapper, appState) { - - function link(scope, el, attr, ctrl) { - - //adds a handler to the context menu item click, we need to handle this differently - //depending on what the menu item is supposed to do. - scope.executeMenuItem = function (action) { - navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection); - }; - - //callback method to go and get the options async - scope.getOptions = function () { - - if (!scope.currentNode) { - return; - } - - //when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu) - appState.setMenuState("currentNode", scope.currentNode); - - if (!scope.actions) { - treeService.getMenu({ treeNode: scope.currentNode }) - .then(function (data) { - scope.actions = data.menuItems; - }); - } - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-menu.html', - link: link, - scope: { - currentNode: "=", - currentSection: "@" - } - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorMenu', EditorMenuDirective); - -})(); + } + } -(function() { - 'use strict'; - - function EditorNavigationDirective() { - - function link(scope, el, attr, ctrl) { - - scope.showNavigation = true; - - scope.clickNavigationItem = function(selectedItem) { - setItemToActive(selectedItem); - runItemAction(selectedItem); - }; - - function runItemAction(selectedItem) { - if (selectedItem.action) { - selectedItem.action(selectedItem); - } - } - - function setItemToActive(selectedItem) { - // set all other views to inactive - if (selectedItem.view) { - - for (var index = 0; index < scope.navigation.length; index++) { - var item = scope.navigation[index]; - item.active = false; - } - - // set view to active - selectedItem.active = true; - - } - } - - function activate() { - - // hide navigation if there is only 1 item - if (scope.navigation.length <= 1) { - scope.showNavigation = false; - } - - } - - activate(); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-navigation.html', - scope: { - navigation: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives.html').directive('umbEditorNavigation', EditorNavigationDirective); - -})(); + angular.module("umbraco").controller("My.Controller", Controller); -(function() { - 'use strict'; - - function EditorSubViewsDirective() { - - function link(scope, el, attr, ctrl) { - - scope.activeView = {}; - - // set toolbar from selected navigation item - function setActiveView(items) { - - for (var index = 0; index < items.length; index++) { - - var item = items[index]; - - if (item.active && item.view) { - scope.activeView = item; - } - } - } - - // watch for navigation changes - scope.$watch('subViews', function(newValue, oldValue) { - if (newValue) { - setActiveView(newValue); - } - }, true); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-sub-views.html', - scope: { - subViews: "=", - model: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorSubViews', EditorSubViewsDirective); - -})(); + })(); +
-/** -@ngdoc directive -@name umbraco.directives.directive:umbEditorView -@restrict E -@scope - -@description -Use this directive to construct the main editor window. - -

Markup example

-
-    
- -
- - - - - - - - // main content here - - - - // footer content here - - - - -
- -
-
-

Controller example

-
-    (function () {
-
-        "use strict";
-
-        function Controller() {
-
-            var vm = this;
-
-        }
-
-        angular.module("umbraco").controller("MySection.Controller", Controller);
-    })();
-
- - -

Use in combination with

-
    -
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • -
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • -
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • -
-**/ - -(function() { - 'use strict'; - - function EditorViewDirective() { - - function link(scope, el, attr) { - - if(attr.footer) { - scope.footer = attr.footer; - } - - } - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-editor-view.html', - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEditorView', EditorViewDirective); - -})(); +@param {callback} action The button action which should be performed when the button is clicked. +@param {string=} href Url/Path to navigato to. +@param {string=} type Set the button type ("button" or "submit"). +@param {string=} buttonStyle Set the style of the button. The directive uses the default bootstrap styles ("primary", "info", "success", "warning", "danger", "inverse", "link"). +@param {string=} state Set a progress state on the button ("init", "busy", "success", "error"). +@param {string=} shortcut Set a keyboard shortcut for the button ("ctrl+c"). +@param {string=} label Set the button label. +@param {string=} labelKey Set a localization key to make a multi lingual button ("general_buttonText"). +@param {string=} icon Set a button icon. Can only be used when buttonStyle is "link". +@param {boolean=} disabled Set to true to disable the button. +**/ -/** -* @description Utillity directives for key and field events -**/ -angular.module('umbraco.directives') - -.directive('onKeyup', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onKeyup); - }; - elm.on("keyup", f); - scope.$on("$destroy", function(){ elm.off("keyup", f);} ); - } - }; -}) - -.directive('onKeydown', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onKeydown); - }; - elm.on("keydown", f); - scope.$on("$destroy", function(){ elm.off("keydown", f);} ); - } - }; -}) - -.directive('onBlur', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onBlur); - }; - elm.on("blur", f); - scope.$on("$destroy", function(){ elm.off("blur", f);} ); - } - }; -}) - -.directive('onFocus', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onFocus); - }; - elm.on("focus", f); - scope.$on("$destroy", function(){ elm.off("focus", f);} ); - } - }; -}) - -.directive('onDragEnter', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDragEnter); - }; - elm.on("dragenter", f); - scope.$on("$destroy", function(){ elm.off("dragenter", f);} ); - } - }; -}) - -.directive('onDragLeave', function () { - return function (scope, elm, attrs) { - var f = function (event) { - var rect = this.getBoundingClientRect(); - var getXY = function getCursorPosition(event) { - var x, y; - - if (typeof event.clientX === 'undefined') { - // try touch screen - x = event.pageX + document.documentElement.scrollLeft; - y = event.pageY + document.documentElement.scrollTop; - } else { - x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; - y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; - } - - return { x: x, y : y }; - }; - - var e = getXY(event.originalEvent); - - // Check the mouseEvent coordinates are outside of the rectangle - if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) { - scope.$apply(attrs.onDragLeave); - } - }; - - elm.on("dragleave", f); - scope.$on("$destroy", function(){ elm.off("dragleave", f);} ); - }; -}) - -.directive('onDragOver', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDragOver); - }; - elm.on("dragover", f); - scope.$on("$destroy", function(){ elm.off("dragover", f);} ); - } - }; -}) - -.directive('onDragStart', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDragStart); - }; - elm.on("dragstart", f); - scope.$on("$destroy", function(){ elm.off("dragstart", f);} ); - } - }; -}) - -.directive('onDragEnd', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDragEnd); - }; - elm.on("dragend", f); - scope.$on("$destroy", function(){ elm.off("dragend", f);} ); - } - }; -}) - -.directive('onDrop', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDrop); - }; - elm.on("drop", f); - scope.$on("$destroy", function(){ elm.off("drop", f);} ); - } - }; -}) - -.directive('onOutsideClick', function ($timeout) { - return function (scope, element, attrs) { - - var eventBindings = []; - - function oneTimeClick(event) { - var el = event.target.nodeName; - - //ignore link and button clicks - var els = ["INPUT","A","BUTTON"]; - if(els.indexOf(el) >= 0){return;} - - // ignore children of links and buttons - // ignore clicks on new overlay - var parents = $(event.target).parents("a,button,.umb-overlay"); - if(parents.length > 0){ - return; - } - - // ignore clicks on dialog from old dialog service - var oldDialog = $(el).parents("#old-dialog-service"); - if (oldDialog.length === 1) { - return; - } - - // ignore clicks in tinyMCE dropdown(floatpanel) - var floatpanel = $(el).parents(".mce-floatpanel"); - if (floatpanel.length === 1) { - return; - } - - //ignore clicks inside this element - if( $(element).has( $(event.target) ).length > 0 ){ - return; - } - - scope.$apply(attrs.onOutsideClick); - } - - - $timeout(function(){ - - if ("bindClickOn" in attrs) { - - eventBindings.push(scope.$watch(function() { - return attrs.bindClickOn; - }, function(newValue) { - if (newValue === "true") { - $(document).on("click", oneTimeClick); - } else { - $(document).off("click", oneTimeClick); - } - })); - - } else { - $(document).on("click", oneTimeClick); - } - - scope.$on("$destroy", function() { - $(document).off("click", oneTimeClick); - - // unbind watchers - for (var e in eventBindings) { - eventBindings[e](); - } - - }); - }); // Temp removal of 1 sec timeout to prevent bug where overlay does not open. We need to find a better solution. - - }; -}) - -.directive('onRightClick',function(){ - - document.oncontextmenu = function (e) { - if(e.target.hasAttribute('on-right-click')) { - e.preventDefault(); - e.stopPropagation(); - return false; - } - }; - - return function(scope,el,attrs){ - el.on('contextmenu',function(e){ - e.preventDefault(); - e.stopPropagation(); - scope.$apply(attrs.onRightClick); - return false; - }); - }; -}) - -.directive('onDelayedMouseleave', function ($timeout, $parse) { - return { - - restrict: 'A', - - link: function (scope, element, attrs, ctrl) { - var active = false; - var fn = $parse(attrs.onDelayedMouseleave); - - var leave_f = function(event) { - var callback = function() { - fn(scope, {$event:event}); - }; - - active = false; - $timeout(function(){ - if(active === false){ - scope.$apply(callback); - } - }, 650); - }; - - var enter_f = function(event, args){ - active = true; - }; - - - element.on("mouseleave", leave_f); - element.on("mouseenter", enter_f); - - //unsub events - scope.$on("$destroy", function(){ - element.off("mouseleave", leave_f); - element.off("mouseenter", enter_f); - }); - } - }; - }); +(function() { + 'use strict'; -/* - - http://vitalets.github.io/checklist-model/ - -*/ -angular.module('umbraco.directives') -.directive('checklistModel', ['$parse', '$compile', function($parse, $compile) { - // contains - function contains(arr, item) { - if (angular.isArray(arr)) { - for (var i = 0; i < arr.length; i++) { - if (angular.equals(arr[i], item)) { - return true; - } - } - } - return false; - } - - // add - function add(arr, item) { - arr = angular.isArray(arr) ? arr : []; - for (var i = 0; i < arr.length; i++) { - if (angular.equals(arr[i], item)) { - return arr; - } - } - arr.push(item); - return arr; - } - - // remove - function remove(arr, item) { - if (angular.isArray(arr)) { - for (var i = 0; i < arr.length; i++) { - if (angular.equals(arr[i], item)) { - arr.splice(i, 1); - break; - } - } - } - return arr; - } - - // http://stackoverflow.com/a/19228302/1458162 - function postLinkFn(scope, elem, attrs) { - // compile with `ng-model` pointing to `checked` - $compile(elem)(scope); - - // getter / setter for original model - var getter = $parse(attrs.checklistModel); - var setter = getter.assign; - - // value added to list - var value = $parse(attrs.checklistValue)(scope.$parent); - - // watch UI checked change - scope.$watch('checked', function(newValue, oldValue) { - if (newValue === oldValue) { - return; - } - var current = getter(scope.$parent); - if (newValue === true) { - setter(scope.$parent, add(current, value)); - } else { - setter(scope.$parent, remove(current, value)); - } - }); - - // watch original model change - scope.$parent.$watch(attrs.checklistModel, function(newArr, oldArr) { - scope.checked = contains(newArr, value); - }, true); - } - - return { - restrict: 'A', - priority: 1000, - terminal: true, - scope: true, - compile: function(tElement, tAttrs) { - if (tElement[0].tagName !== 'INPUT' || !tElement.attr('type', 'checkbox')) { - throw 'checklist-model should be applied to `input[type="checkbox"]`.'; - } - - if (!tAttrs.checklistValue) { - throw 'You should provide `checklist-value`.'; - } - - // exclude recursion - tElement.removeAttr('checklist-model'); - - // local scope var storing individual checkbox model - tElement.attr('ng-model', 'checked'); - - return postLinkFn; - } - }; -}]); -angular.module("umbraco.directives") -.directive("contenteditable", function() { - - return { - require: "ngModel", - link: function(scope, element, attrs, ngModel) { - - function read() { - ngModel.$setViewValue(element.html()); - } - - ngModel.$render = function() { - element.html(ngModel.$viewValue || ""); - }; - - - element.bind("focus", function(){ - - var range = document.createRange(); - range.selectNodeContents(element[0]); - - var sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - - }); - - element.bind("blur keyup change", function() { - scope.$apply(read); - }); - } - - }; - -}); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:fixNumber -* @restrict A -* @description Used in conjunction with type='number' input fields to ensure that the bound value is converted to a number when using ng-model -* because normally it thinks it's a string and also validation doesn't work correctly due to an angular bug. -**/ -function fixNumber($parse) { - return { - restrict: "A", - require: "ngModel", - - link: function (scope, elem, attrs, ctrl) { - - //parse ngModel onload - var modelVal = scope.$eval(attrs.ngModel); - if (modelVal) { - var asNum = parseFloat(modelVal, 10); - if (!isNaN(asNum)) { - $parse(attrs.ngModel).assign(scope, asNum); - } - } - - //always return an int to the model - ctrl.$parsers.push(function (value) { - if (value === 0) { - return 0; - } - return parseFloat(value || '', 10); - }); - - //always try to format the model value as an int - ctrl.$formatters.push(function (value) { - if (angular.isString(value)) { - return parseFloat(value, 10); - } - return value; - }); - - //This fixes this angular issue: - //https://github.com/angular/angular.js/issues/2144 - // which doesn't actually validate the number input properly since the model only changes when a real number is entered - // but the input box still allows non-numbers to be entered which do not validate (only via html5) - if (typeof elem.prop('validity') === 'undefined') { - return; - } - - elem.bind('input', function (e) { - var validity = elem.prop('validity'); - scope.$apply(function () { - ctrl.$setValidity('number', !validity.badInput); - }); - }); - } - }; -} -angular.module('umbraco.directives').directive("fixNumber", fixNumber); -angular.module("umbraco.directives").directive('focusWhen', function ($timeout) { - return { - restrict: 'A', - link: function (scope, elm, attrs, ctrl) { - attrs.$observe("focusWhen", function (newValue) { - if (newValue === "true") { - $timeout(function () { - elm.focus(); - }); - } - }); - } - }; -}); + function ButtonDirective($timeout) { - -/** -* @ngdoc directive -* @name umbraco.directives.directive:hexBgColor -* @restrict A -* @description Used to set a hex background color on an element, this will detect valid hex and when it is valid it will set the color, otherwise -* a color will not be set. -**/ -function hexBgColor() { - return { - restrict: "A", - link: function (scope, element, attr, formCtrl) { - - var origColor = null; - if (attr.hexBgOrig) { - //set the orig based on the attribute if there is one - origColor = attr.hexBgOrig; - } - - attr.$observe("hexBgColor", function (newVal) { - if (newVal) { - if (!origColor) { - //get the orig color before changing it - origColor = element.css("border-color"); - } - //validate it - test with and without the leading hash. - if (/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) { - element.css("background-color", "#" + newVal); - return; - } - if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) { - element.css("background-color", newVal); - return; - } - } - element.css("background-color", origColor); - }); - - } - }; -} -angular.module('umbraco.directives').directive("hexBgColor", hexBgColor); -/** -* @ngdoc directive -* @name umbraco.directives.directive:hotkey -**/ - -angular.module("umbraco.directives") - .directive('hotkey', function($window, keyboardService, $log) { - - return function(scope, el, attrs) { - - var options = {}; - var keyCombo = attrs.hotkey; - - if (!keyCombo) { - //support data binding - keyCombo = scope.$eval(attrs["hotkey"]); - } - - function activate() { - - if (keyCombo) { - - // disable shortcuts in input fields if keycombo is 1 character - if (keyCombo.length === 1) { - options = { - inputDisabled: true - }; - } - - keyboardService.bind(keyCombo, function() { - - var element = $(el); - var activeElementType = document.activeElement.tagName; - var clickableElements = ["A", "BUTTON"]; - - if (element.is("a,div,button,input[type='button'],input[type='submit'],input[type='checkbox']") && !element.is(':disabled')) { - - if (element.is(':visible') || attrs.hotkeyWhenHidden) { - - if (attrs.hotkeyWhen && attrs.hotkeyWhen === "false") { - return; - } - - // when keycombo is enter and a link or button has focus - click the link or button instead of using the hotkey - if (keyCombo === "enter" && clickableElements.indexOf(activeElementType) === 0) { - document.activeElement.click(); - } else { - element.click(); - } - - } - - } else { - element.focus(); - } - - }, options); - - el.on('$destroy', function() { - keyboardService.unbind(keyCombo); - }); - - } - - } - - activate(); - - }; - }); + function link(scope, el, attr, ctrl) { -/** -@ngdoc directive -@name umbraco.directives.directive:preventDefault - -@description -Use this directive to prevent default action of an element. Effectively implementing jQuery's preventdefault - -

Markup example

- -
-    Don't go to Umbraco.com
-
- -**/ -angular.module("umbraco.directives") - .directive('preventDefault', function() { - return function(scope, element, attrs) { - - var enabled = true; - //check if there's a value for the attribute, if there is and it's false then we conditionally don't - //prevent default. - if (attrs.preventDefault) { - attrs.$observe("preventDefault", function (newVal) { - enabled = (newVal === "false" || newVal === 0 || newVal === false) ? false : true; - }); - } - - $(element).click(function (event) { - if (event.metaKey || event.ctrlKey) { - return; - } - else { - if (enabled === true) { - event.preventDefault(); - } - } - }); - }; - }); + scope.style = null; -/** -* @ngdoc directive -* @name umbraco.directives.directive:preventEnterSubmit -* @description prevents a form from submitting when the enter key is pressed on an input field -**/ -angular.module("umbraco.directives") - .directive('preventEnterSubmit', function() { - return function(scope, element, attrs) { - - var enabled = true; - //check if there's a value for the attribute, if there is and it's false then we conditionally don't - //prevent default. - if (attrs.preventEnterSubmit) { - attrs.$observe("preventEnterSubmit", function (newVal) { - enabled = (newVal === "false" || newVal === 0 || newVal === false) ? false : true; - }); - } - - $(element).keypress(function (event) { - if (event.which === 13) { - event.preventDefault(); - } - }); - }; + function activate() { + + if (!scope.state) { + scope.state = "init"; + } + + if (scope.buttonStyle) { + scope.style = "btn-" + scope.buttonStyle; + } + + } + + activate(); + + var unbindStateWatcher = scope.$watch('state', function(newValue, oldValue) { + + if (newValue === 'success' || newValue === 'error') { + $timeout(function() { + scope.state = 'init'; + }, 2000); + } + + }); + + scope.$on('$destroy', function() { + unbindStateWatcher(); + }); + + } + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/buttons/umb-button.html', + link: link, + scope: { + action: "&?", + href: "@?", + type: "@", + buttonStyle: "@?", + state: "=?", + shortcut: "@?", + shortcutWhenHidden: "@", + label: "@?", + labelKey: "@?", + icon: "@?", + disabled: "=" + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbButton', ButtonDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbButtonGroup +@restrict E +@scope + +@description +Use this directive to render a button with a dropdown of alternative actions. + +

Markup example

+
+    
+ + + + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+            vm.buttonGroup = {
+                defaultButton: {
+                    labelKey: "general_defaultButton",
+                    hotKey: "ctrl+d",
+                    hotKeyWhenHidden: true,
+                    handler: function() {
+                        // do magic here
+                    }
+                },
+                subButtons: [
+                    {
+                        labelKey: "general_subButton",
+                        hotKey: "ctrl+b",
+                        hotKeyWhenHidden: true,
+                        handler: function() {
+                            // do magic here
+                        }
+                    }
+                ]
+            };
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+
+    })();
+
+ +

Button model description

+
    +
  • + labekKey + (string) - + Set a localization key to make a multi lingual button ("general_buttonText"). +
  • +
  • + hotKey + (array) - + Set a keyboard shortcut for the button ("ctrl+c"). +
  • +
  • + hotKeyWhenHidden + (boolean) - + As a default the hotkeys only works on elements visible in the UI. Set to true to set a hotkey on the hidden sub buttons. +
  • +
  • + handler + (callback) - + Set a callback to handle button click events. +
  • +
+ +@param {object} defaultButton The model of the default button. +@param {array} subButtons Array of sub buttons. +@param {string=} state Set a progress state on the button ("init", "busy", "success", "error"). +@param {string=} direction Set the direction of the dropdown ("up", "down"). +@param {string=} float Set the float of the dropdown. ("left", "right"). +**/ + +(function() { + 'use strict'; + + function ButtonGroupDirective() { + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/buttons/umb-button-group.html', + scope: { + defaultButton: "=", + subButtons: "=", + state: "=?", + direction: "@?", + float: "@?" + } + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbButtonGroup', ButtonGroupDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorSubHeader +@restrict E + +@description +Use this directive to construct a sub header in the main editor window. +The sub header is sticky and will follow along down the page when scrolling. + +

Markup example

+
+    
+ +
+ + + + + + + // sub header content here + + + + + + +
+ +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorSubHeaderDirective() { + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/subheader/umb-editor-sub-header.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorSubHeader', EditorSubHeaderDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorSubHeaderContentLeft +@restrict E + +@description +Use this directive to left align content in a sub header in the main editor window. + +

Markup example

+
+    
+ +
+ + + + + + + + + // left content here + + + + // right content here + + + + + + + + +
+ +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorSubHeaderContentLeftDirective() { + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-content-left.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorSubHeaderContentLeft', EditorSubHeaderContentLeftDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorSubHeaderContentRight +@restrict E + +@description +Use this directive to rigt align content in a sub header in the main editor window. + +

Markup example

+
+    
+ +
+ + + + + + + + + // left content here + + + + // right content here + + + + + + + + +
+ +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorSubHeaderContentRightDirective() { + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-content-right.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorSubHeaderContentRight', EditorSubHeaderContentRightDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorSubHeaderSection +@restrict E + +@description +Use this directive to create sections, divided by borders, in a sub header in the main editor window. + +

Markup example

+
+    
+ +
+ + + + + + + + + + + // section content here + + + + // section content here + + + + // section content here + + + + + + + + + + +
+ +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorSubHeaderSectionDirective() { + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-section.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorSubHeaderSection', EditorSubHeaderSectionDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbBreadcrumbs +@restrict E +@scope + +@description +Use this directive to generate a list of breadcrumbs. + +

Markup example

+
+    
+ + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller(myService) {
+
+            var vm = this;
+            vm.ancestors = [];
+
+            myService.getAncestors().then(function(ancestors){
+                vm.ancestors = ancestors;
+            });
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {array} ancestors Array of ancestors +@param {string} entityType The content entity type (member, media, content). +**/ + +(function() { + 'use strict'; + + function BreadcrumbsDirective() { + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-breadcrumbs.html', + scope: { + ancestors: "=", + entityType: "@" + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbBreadcrumbs', BreadcrumbsDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorContainer +@restrict E + +@description +Use this directive to construct a main content area inside the main editor window. + +

Markup example

+
+    
+ + + + + + + + // main content here + + + + // footer content here + + + + +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • +
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorContainerDirective(overlayHelper) { + + function link(scope, el, attr, ctrl) { + + scope.numberOfOverlays = 0; + + scope.$watch(function(){ + return overlayHelper.getNumberOfOverlays(); + }, function (newValue) { + scope.numberOfOverlays = newValue; + }); + + } + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-container.html', + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorContainer', EditorContainerDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorFooter +@restrict E + +@description +Use this directive to construct a footer inside the main editor window. + +

Markup example

+
+    
+ +
+ + + + + + + + // main content here + + + + // footer content here + + + + +
+ +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • +
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • +
  • {@link umbraco.directives.directive:umbEditorFooterContentLeft umbEditorFooterContentLeft}
  • +
  • {@link umbraco.directives.directive:umbEditorFooterContentRight umbEditorFooterContentRight}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorFooterDirective() { + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-footer.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorFooter', EditorFooterDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorFooterContentLeft +@restrict E + +@description +Use this directive to align content left inside the main editor footer. + +

Markup example

+
+    
+ +
+ + + + + + + // align content left + + + + // align content right + + + + + + +
+ +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • +
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • +
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • +
  • {@link umbraco.directives.directive:umbEditorFooterContentRight umbEditorFooterContentRight}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorFooterContentLeftDirective() { + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-footer-content-left.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorFooterContentLeft', EditorFooterContentLeftDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorFooterContentRight +@restrict E + +@description +Use this directive to align content right inside the main editor footer. + +

Markup example

+
+    
+ +
+ + + + + + + // align content left + + + + // align content right + + + + + + +
+ +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • +
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • +
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • +
  • {@link umbraco.directives.directive:umbEditorFooterContentLeft umbEditorFooterContentLeft}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorFooterContentRightDirective() { + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-footer-content-right.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorFooterContentRight', EditorFooterContentRightDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorHeader +@restrict E +@scope + +@description +Use this directive to construct a header inside the main editor window. + +

Markup example

+
+    
+ +
+ + + + + + + + // main content here + + + + // footer content here + + + + +
+ +
+
+ +

Markup example - with tabs

+
+    
+ +
+ + + + + + + + + + +
+ // tab 1 content +
+ +
+ // tab 2 content +
+ +
+
+
+ + + // footer content here + + +
+ +
+ +
+
+ +

Controller example - with tabs

+
+    (function () {
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+            vm.content = {
+                name: "",
+                tabs: [
+                    {
+                        id: 1,
+                        label: "Tab 1",
+                        alias: "tab1",
+                        active: true
+                    },
+                    {
+                        id: 2,
+                        label: "Tab 2",
+                        alias: "tab2",
+                        active: false
+                    }
+                ]
+            };
+
+        }
+
+        angular.module("umbraco").controller("MySection.Controller", Controller);
+    })();
+
+ +

Markup example - with sub views

+
+    
+ +
+ + + + + + + + + + + + + + + // footer content here + + + + +
+ +
+
+ +

Controller example - with sub views

+
+    (function () {
+
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+            vm.content = {
+                name: "",
+                navigation: [
+                    {
+                        "name": "Section 1",
+                        "icon": "icon-document-dashed-line",
+                        "view": "/App_Plugins/path/to/html.html",
+                        "active": true
+                    },
+                    {
+                        "name": "Section 2",
+                        "icon": "icon-list",
+                        "view": "/App_Plugins/path/to/html.html",
+                    }
+                ]
+            };
+
+        }
+
+        angular.module("umbraco").controller("MySection.Controller", Controller);
+    })();
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • +
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • +
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • +
+ +@param {string} name The content name. +@param {array=} tabs Array of tabs. See example above. +@param {array=} navigation Array of sub views. See example above. +@param {boolean=} nameLocked Set to true to lock the name. +@param {object=} menu Add a context menu to the editor. +@param {string=} icon Show and edit the content icon. Opens an overlay to change the icon. +@param {boolean=} hideIcon Set to true to hide icon. +@param {string=} alias show and edit the content alias. +@param {boolean=} hideAlias Set to true to hide alias. +@param {string=} description Add a description to the content. +@param {boolean=} hideDescription Set to true to hide description. + +**/ + +(function() { + 'use strict'; + + function EditorHeaderDirective(iconHelper) { + + function link(scope, el, attr, ctrl) { + + scope.openIconPicker = function() { + scope.dialogModel = { + view: "iconpicker", + show: true, + submit: function (model) { + + /* ensure an icon is selected, because on focus on close button + or an element in background no icon is submitted. So don't clear/update existing icon/preview. + */ + if (model.icon) { + + if (model.color) { + scope.icon = model.icon + " " + model.color; + } else { + scope.icon = model.icon; + } + + // set form to dirty + ctrl.$setDirty(); + } + + scope.dialogModel.show = false; + scope.dialogModel = null; + } + }; + }; + } + + var directive = { + require: '^form', + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-header.html', + scope: { + tabs: "=", + actions: "=", + name: "=", + nameLocked: "=", + menu: "=", + icon: "=", + hideIcon: "@", + alias: "=", + hideAlias: "@", + description: "=", + hideDescription: "@", + navigation: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorHeader', EditorHeaderDirective); + +})(); + +(function() { + 'use strict'; + + function EditorMenuDirective($injector, treeService, navigationService, umbModelMapper, appState) { + + function link(scope, el, attr, ctrl) { + + //adds a handler to the context menu item click, we need to handle this differently + //depending on what the menu item is supposed to do. + scope.executeMenuItem = function (action) { + navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection); + }; + + //callback method to go and get the options async + scope.getOptions = function () { + + if (!scope.currentNode) { + return; + } + + //when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu) + appState.setMenuState("currentNode", scope.currentNode); + + if (!scope.actions) { + treeService.getMenu({ treeNode: scope.currentNode }) + .then(function (data) { + scope.actions = data.menuItems; + }); + } + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-menu.html', + link: link, + scope: { + currentNode: "=", + currentSection: "@" + } + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorMenu', EditorMenuDirective); + +})(); + +(function() { + 'use strict'; + + function EditorNavigationDirective() { + + function link(scope, el, attr, ctrl) { + + scope.showNavigation = true; + + scope.clickNavigationItem = function(selectedItem) { + setItemToActive(selectedItem); + runItemAction(selectedItem); + }; + + function runItemAction(selectedItem) { + if (selectedItem.action) { + selectedItem.action(selectedItem); + } + } + + function setItemToActive(selectedItem) { + // set all other views to inactive + if (selectedItem.view) { + + for (var index = 0; index < scope.navigation.length; index++) { + var item = scope.navigation[index]; + item.active = false; + } + + // set view to active + selectedItem.active = true; + + } + } + + function activate() { + + // hide navigation if there is only 1 item + if (scope.navigation.length <= 1) { + scope.showNavigation = false; + } + + } + + activate(); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-navigation.html', + scope: { + navigation: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives.html').directive('umbEditorNavigation', EditorNavigationDirective); + +})(); + +(function() { + 'use strict'; + + function EditorSubViewsDirective() { + + function link(scope, el, attr, ctrl) { + + scope.activeView = {}; + + // set toolbar from selected navigation item + function setActiveView(items) { + + for (var index = 0; index < items.length; index++) { + + var item = items[index]; + + if (item.active && item.view) { + scope.activeView = item; + } + } + } + + // watch for navigation changes + scope.$watch('subViews', function(newValue, oldValue) { + if (newValue) { + setActiveView(newValue); + } + }, true); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-sub-views.html', + scope: { + subViews: "=", + model: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorSubViews', EditorSubViewsDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorView +@restrict E +@scope + +@description +Use this directive to construct the main editor window. + +

Markup example

+
+    
+ +
+ + + + + + + + // main content here + + + + // footer content here + + + + +
+ +
+
+

Controller example

+
+    (function () {
+
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+        }
+
+        angular.module("umbraco").controller("MySection.Controller", Controller);
+    })();
+
+ + +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • +
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorViewDirective() { + + function link(scope, el, attr) { + + if(attr.footer) { + scope.footer = attr.footer; + } + + } + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-view.html', + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorView', EditorViewDirective); + +})(); + +/** +* @description Utillity directives for key and field events +**/ +angular.module('umbraco.directives') + +.directive('onKeyup', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onKeyup); + }; + elm.on("keyup", f); + scope.$on("$destroy", function(){ elm.off("keyup", f);} ); + } + }; +}) + +.directive('onKeydown', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onKeydown); + }; + elm.on("keydown", f); + scope.$on("$destroy", function(){ elm.off("keydown", f);} ); + } + }; +}) + +.directive('onBlur', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onBlur); + }; + elm.on("blur", f); + scope.$on("$destroy", function(){ elm.off("blur", f);} ); + } + }; +}) + +.directive('onFocus', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onFocus); + }; + elm.on("focus", f); + scope.$on("$destroy", function(){ elm.off("focus", f);} ); + } + }; +}) + +.directive('onDragEnter', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragEnter); + }; + elm.on("dragenter", f); + scope.$on("$destroy", function(){ elm.off("dragenter", f);} ); + } + }; +}) + +.directive('onDragLeave', function () { + return function (scope, elm, attrs) { + var f = function (event) { + var rect = this.getBoundingClientRect(); + var getXY = function getCursorPosition(event) { + var x, y; + + if (typeof event.clientX === 'undefined') { + // try touch screen + x = event.pageX + document.documentElement.scrollLeft; + y = event.pageY + document.documentElement.scrollTop; + } else { + x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; + } + + return { x: x, y : y }; + }; + + var e = getXY(event.originalEvent); + + // Check the mouseEvent coordinates are outside of the rectangle + if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) { + scope.$apply(attrs.onDragLeave); + } + }; + + elm.on("dragleave", f); + scope.$on("$destroy", function(){ elm.off("dragleave", f);} ); + }; +}) + +.directive('onDragOver', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragOver); + }; + elm.on("dragover", f); + scope.$on("$destroy", function(){ elm.off("dragover", f);} ); + } + }; +}) + +.directive('onDragStart', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragStart); + }; + elm.on("dragstart", f); + scope.$on("$destroy", function(){ elm.off("dragstart", f);} ); + } + }; +}) + +.directive('onDragEnd', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragEnd); + }; + elm.on("dragend", f); + scope.$on("$destroy", function(){ elm.off("dragend", f);} ); + } + }; +}) + +.directive('onDrop', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDrop); + }; + elm.on("drop", f); + scope.$on("$destroy", function(){ elm.off("drop", f);} ); + } + }; +}) + +.directive('onOutsideClick', function ($timeout) { + return function (scope, element, attrs) { + + var eventBindings = []; + + function oneTimeClick(event) { + var el = event.target.nodeName; + + //ignore link and button clicks + var els = ["INPUT","A","BUTTON"]; + if(els.indexOf(el) >= 0){return;} + + // ignore children of links and buttons + // ignore clicks on new overlay + var parents = $(event.target).parents("a,button,.umb-overlay"); + if(parents.length > 0){ + return; + } + + // ignore clicks on dialog from old dialog service + var oldDialog = $(el).parents("#old-dialog-service"); + if (oldDialog.length === 1) { + return; + } + + // ignore clicks in tinyMCE dropdown(floatpanel) + var floatpanel = $(el).parents(".mce-floatpanel"); + if (floatpanel.length === 1) { + return; + } + + //ignore clicks inside this element + if( $(element).has( $(event.target) ).length > 0 ){ + return; + } + + scope.$apply(attrs.onOutsideClick); + } + + + $timeout(function(){ + + if ("bindClickOn" in attrs) { + + eventBindings.push(scope.$watch(function() { + return attrs.bindClickOn; + }, function(newValue) { + if (newValue === "true") { + $(document).on("click", oneTimeClick); + } else { + $(document).off("click", oneTimeClick); + } + })); + + } else { + $(document).on("click", oneTimeClick); + } + + scope.$on("$destroy", function() { + $(document).off("click", oneTimeClick); + + // unbind watchers + for (var e in eventBindings) { + eventBindings[e](); + } + + }); + }); // Temp removal of 1 sec timeout to prevent bug where overlay does not open. We need to find a better solution. + + }; +}) + +.directive('onRightClick',function(){ + + document.oncontextmenu = function (e) { + if(e.target.hasAttribute('on-right-click')) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + }; + + return function(scope,el,attrs){ + el.on('contextmenu',function(e){ + e.preventDefault(); + e.stopPropagation(); + scope.$apply(attrs.onRightClick); + return false; + }); + }; +}) + +.directive('onDelayedMouseleave', function ($timeout, $parse) { + return { + + restrict: 'A', + + link: function (scope, element, attrs, ctrl) { + var active = false; + var fn = $parse(attrs.onDelayedMouseleave); + + var leave_f = function(event) { + var callback = function() { + fn(scope, {$event:event}); + }; + + active = false; + $timeout(function(){ + if(active === false){ + scope.$apply(callback); + } + }, 650); + }; + + var enter_f = function(event, args){ + active = true; + }; + + + element.on("mouseleave", leave_f); + element.on("mouseenter", enter_f); + + //unsub events + scope.$on("$destroy", function(){ + element.off("mouseleave", leave_f); + element.off("mouseenter", enter_f); + }); + } + }; }); -/** - * @ngdoc directive - * @name umbraco.directives.directive:resizeToContent - * @element div - * @function - * - * @description - * Resize iframe's automatically to fit to the content they contain - * - * @example - - - - - - */ -angular.module("umbraco.directives") - .directive('resizeToContent', function ($window, $timeout) { - return function (scope, el, attrs) { - var iframe = el[0]; - var iframeWin = iframe.contentWindow || iframe.contentDocument.parentWindow; - if (iframeWin.document.body) { - - $timeout(function(){ - var height = iframeWin.document.documentElement.scrollHeight || iframeWin.document.body.scrollHeight; - el.height(height); - }, 3000); - } - }; - }); -angular.module("umbraco.directives") - .directive('selectOnFocus', function () { - return function (scope, el, attrs) { - $(el).bind("click", function () { - var editmode = $(el).data("editmode"); - //If editmode is true a click is handled like a normal click - if (!editmode) { - //Initial click, select entire text - this.select(); - //Set the edit mode so subsequent clicks work normally - $(el).data("editmode", true); - } - }). - bind("blur", function () { - //Reset on focus lost - $(el).data("editmode", false); - }); - }; - }); +/* + + http://vitalets.github.io/checklist-model/ + +*/ +angular.module('umbraco.directives') +.directive('checklistModel', ['$parse', '$compile', function($parse, $compile) { + // contains + function contains(arr, item) { + if (angular.isArray(arr)) { + for (var i = 0; i < arr.length; i++) { + if (angular.equals(arr[i], item)) { + return true; + } + } + } + return false; + } -angular.module("umbraco.directives") - .directive('umbAutoFocus', function($timeout) { - - return function(scope, element, attr){ - var update = function() { - //if it uses its default naming - if(element.val() === "" || attr.focusOnFilled){ - element.focus(); - } - }; - - $timeout(function() { - update(); - }); - }; -}); + // add + function add(arr, item) { + arr = angular.isArray(arr) ? arr : []; + for (var i = 0; i < arr.length; i++) { + if (angular.equals(arr[i], item)) { + return arr; + } + } + arr.push(item); + return arr; + } -angular.module("umbraco.directives") - .directive('umbAutoResize', function($timeout) { - return { - require: ["^?umbTabs", "ngModel"], - link: function(scope, element, attr, controllersArr) { - - var domEl = element[0]; - var domElType = domEl.type; - var umbTabsController = controllersArr[0]; - var ngModelController = controllersArr[1]; - - // IE elements - var isIEFlag = false; - var wrapper = angular.element('#umb-ie-resize-input-wrapper'); - var mirror = angular.element(''); - - function isIE() { - - var ua = window.navigator.userAgent; - var msie = ua.indexOf("MSIE "); - - if (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./) || navigator.userAgent.match(/Edge\/\d+/)) { - return true; - } else { - return false; - } - - } - - function activate() { - - // check if browser is Internet Explorere - isIEFlag = isIE(); - - // scrollWidth on element does not work in IE on inputs - // we have to do some dirty dom element copying. - if (isIEFlag === true && domElType === "text") { - setupInternetExplorerElements(); - } - - } - - function setupInternetExplorerElements() { - - if (!wrapper.length) { - wrapper = angular.element('
'); - angular.element('body').append(wrapper); - } - - angular.forEach(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', - 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent', - 'boxSizing', 'borderRightWidth', 'borderLeftWidth', 'borderLeftStyle', 'borderRightStyle', - 'paddingLeft', 'paddingRight', 'marginLeft', 'marginRight' - ], function(value) { - mirror.css(value, element.css(value)); - }); - - wrapper.append(mirror); - - } - - function resizeInternetExplorerInput() { - - mirror.text(element.val() || attr.placeholder); - element.css('width', mirror.outerWidth() + 1); - - } - - function resizeInput() { - - if (domEl.scrollWidth !== domEl.clientWidth) { - if (ngModelController.$modelValue) { - element.width(domEl.scrollWidth); - } - } - - if(!ngModelController.$modelValue && attr.placeholder) { - attr.$set('size', attr.placeholder.length); - element.width('auto'); - } - - } - - function resizeTextarea() { - - if(domEl.scrollHeight !== domEl.clientHeight) { - - element.height(domEl.scrollHeight); - - } - - } - - var update = function(force) { - - - if (force === true) { - - if (domElType === "textarea") { - element.height(0); - } else if (domElType === "text") { - element.width(0); - } - - } - - - if (isIEFlag === true && domElType === "text") { - - resizeInternetExplorerInput(); - - } else { - - if (domElType === "textarea") { - - resizeTextarea(); - - } else if (domElType === "text") { - - resizeInput(); - - } - - } - - }; - - activate(); - - //listen for tab changes - if (umbTabsController != null) { - umbTabsController.onTabShown(function(args) { - update(); - }); - } - - // listen for ng-model changes - var unbindModelWatcher = scope.$watch(function() { - return ngModelController.$modelValue; - }, function(newValue) { - update(true); - }); - - scope.$on('$destroy', function() { - element.unbind('keyup keydown keypress change', update); - element.unbind('blur', update(true)); - unbindModelWatcher(); - - // clean up IE dom element - if (isIEFlag === true && domElType === "text") { - mirror.remove(); - } - - }); - } - }; - }); + // remove + function remove(arr, item) { + if (angular.isArray(arr)) { + for (var i = 0; i < arr.length; i++) { + if (angular.equals(arr[i], item)) { + arr.splice(i, 1); + break; + } + } + } + return arr; + } -/* -example usage: - -jsonEditing is a string which we edit in a textarea. we try parsing to JSON with each change. when it is valid, propagate model changes via ngModelCtrl - -use isolate scope to prevent model propagation when invalid - will update manually. cannot replace with template, or will override ngModelCtrl, and not hide behind facade - -will override element type to textarea and add own attribute ngModel tied to jsonEditing - */ - -angular.module("umbraco.directives") - .directive('umbRawModel', function () { - return { - restrict: 'A', - require: 'ngModel', - template: '', - replace : true, - scope: { - model: '=umbRawModel', - validateOn:'=' - }, - link: function (scope, element, attrs, ngModelCtrl) { - - function setEditing (value) { - scope.jsonEditing = angular.copy( jsonToString(value)); - } - - function updateModel (value) { - scope.model = stringToJson(value); - } - - function setValid() { - ngModelCtrl.$setValidity('json', true); - } - - function setInvalid () { - ngModelCtrl.$setValidity('json', false); - } - - function stringToJson(text) { - try { - return angular.fromJson(text); - } catch (err) { - setInvalid(); - return text; - } - } - - function jsonToString(object) { - // better than JSON.stringify(), because it formats + filters $$hashKey etc. - // NOTE that this will remove all $-prefixed values - return angular.toJson(object, true); - } - - function isValidJson(model) { - var flag = true; - try { - angular.fromJson(model); - } catch (err) { - flag = false; - } - return flag; - } - - //init - setEditing(scope.model); - - var onInputChange = function(newval,oldval){ - if (newval !== oldval) { - if (isValidJson(newval)) { - setValid(); - updateModel(newval); - } else { - setInvalid(); - } - } - }; - - if(scope.validateOn){ - element.on(scope.validateOn, function(){ - scope.$apply(function(){ - onInputChange(scope.jsonEditing); - }); - }); - }else{ - //check for changes going out - scope.$watch('jsonEditing', onInputChange, true); - } - - //check for changes coming in - scope.$watch('model', function (newval, oldval) { - if (newval !== oldval) { - setEditing(newval); - } - }, true); - - } - }; - }); + // http://stackoverflow.com/a/19228302/1458162 + function postLinkFn(scope, elem, attrs) { + // compile with `ng-model` pointing to `checked` + $compile(elem)(scope); -(function() { - 'use strict'; - - function SelectWhen($timeout) { - - function link(scope, el, attr, ctrl) { - - attr.$observe("umbSelectWhen", function(newValue) { - if (newValue === "true") { - $timeout(function() { - el.select(); - }); - } - }); - - } - - var directive = { - restrict: 'A', - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbSelectWhen', SelectWhen); - -})(); + // getter / setter for original model + var getter = $parse(attrs.checklistModel); + var setter = getter.assign; -angular.module("umbraco.directives") - .directive('gridRte', function (tinyMceService, stylesheetResource, angularHelper, assetsService, $q, $timeout) { - return { - scope: { - uniqueId: '=', - value: '=', - onClick: '&', - onFocus: '&', - onBlur: '&', - configuration:"=", - onMediaPickerClick: "=", - onEmbedClick: "=", - onMacroPickerClick: "=", - onLinkPickerClick: "=" - }, - template: "", - replace: true, - link: function (scope, element, attrs) { - - var initTiny = function () { - - //we always fetch the default one, and then override parts with our own - tinyMceService.configuration().then(function (tinyMceConfig) { - - - - //config value from general tinymce.config file - var validElements = tinyMceConfig.validElements; - 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 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]"; - - var invalidElements = tinyMceConfig.inValidElements; - var plugins = _.map(tinyMceConfig.plugins, function (plugin) { - if (plugin.useOnFrontend) { - return plugin.name; - } - }).join(" ") + " autoresize"; - - //config value on the data type - var toolbar = ["code", "styleselect", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "link", "umbmediapicker", "umbembeddialog"].join(" | "); - var stylesheets = []; - - var styleFormats = []; - var await = []; - - //queue file loading - if (typeof (tinymce) === "undefined") { - await.push(assetsService.loadJs("lib/tinymce/tinymce.min.js", scope)); - } - - - if(scope.configuration && scope.configuration.toolbar){ - toolbar = scope.configuration.toolbar.join(' | '); - } - - - if(scope.configuration && scope.configuration.stylesheets){ - angular.forEach(scope.configuration.stylesheets, function(stylesheet, key){ - - stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + stylesheet + ".css"); - await.push(stylesheetResource.getRulesByName(stylesheet).then(function (rules) { - angular.forEach(rules, function (rule) { - var r = {}; - var split = ""; - r.title = rule.name; - if (rule.selector[0] === ".") { - r.inline = "span"; - r.classes = rule.selector.substring(1); - }else if (rule.selector[0] === "#") { - //Even though this will render in the style drop down, it will not actually be applied - // to the elements, don't think TinyMCE even supports this and it doesn't really make much sense - // since only one element can have one id. - r.inline = "span"; - r.attributes = { id: rule.selector.substring(1) }; - }else if (rule.selector[0] !== "." && rule.selector.indexOf(".") > -1) { - split = rule.selector.split("."); - r.block = split[0]; - r.classes = rule.selector.substring(rule.selector.indexOf(".") + 1).replace(".", " "); - }else if (rule.selector[0] !== "#" && rule.selector.indexOf("#") > -1) { - 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); - }); - })); - }); - }else{ - stylesheets.push("views/propertyeditors/grid/config/grid.default.rtestyles.css"); - styleFormats = fallbackStyles; - } - - //stores a reference to the editor - var tinyMceEditor = null; - $q.all(await).then(function () { - - var uniqueId = scope.uniqueId; - - //create a baseline Config to exten upon - var baseLineConfigObj = { - mode: "exact", - skin: "umbraco", - plugins: plugins, - valid_elements: validElements, - invalid_elements: invalidElements, - extended_valid_elements: extendedValidElements, - menubar: false, - statusbar: false, - relative_urls: false, - toolbar: toolbar, - content_css: stylesheets, - style_formats: styleFormats, - autoresize_bottom_margin: 0 - }; - - - 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 (angular.isArray(baseLineConfigObj[i]) && angular.isArray(tinyMceConfig.customConfig[i])) { - //concat it and below this concat'd array will overwrite the baseline in angular.extend - tinyMceConfig.customConfig[i] = baseLineConfigObj[i].concat(tinyMceConfig.customConfig[i]); - } - } - catch (e) { - //cannot parse, we'll just leave it - } - } - } - } - - angular.extend(baseLineConfigObj, tinyMceConfig.customConfig); - } - - //set all the things that user configs should not be able to override - baseLineConfigObj.elements = uniqueId; - baseLineConfigObj.setup = function (editor) { - - //set the reference - tinyMceEditor = editor; - - - //enable browser based spell checking - editor.on('init', function (e) { - - editor.getBody().setAttribute('spellcheck', true); - - //force overflow to hidden to prevent no needed scroll - editor.getBody().style.overflow = "hidden"; - - $timeout(function(){ - if(scope.value === null){ - editor.focus(); - } - }, 400); - - }); - - //when we leave the editor (maybe) - editor.on('blur', function (e) { - editor.save(); - angularHelper.safeApply(scope, function () { - scope.value = editor.getContent(); - - var _toolbar = $(editor.editorContainer) - .find(".mce-toolbar"); - - if(scope.onBlur){ - scope.onBlur(); - } - - }); - }); - - // Focus on editor - editor.on('focus', function (e) { - angularHelper.safeApply(scope, function () { - - if(scope.onFocus){ - scope.onFocus(); - } - - }); - }); - - // Click on editor - editor.on('click', function (e) { - angularHelper.safeApply(scope, function () { - - if(scope.onClick){ - scope.onClick(); - } - - }); - }); - - //when buttons modify content - editor.on('ExecCommand', function (e) { - editor.save(); - angularHelper.safeApply(scope, function () { - scope.value = editor.getContent(); - }); - }); - - // Update model on keypress - editor.on('KeyUp', function (e) { - editor.save(); - angularHelper.safeApply(scope, function () { - scope.value = editor.getContent(); - }); - }); - - // Update model on change, i.e. copy/pasted text, plugins altering content - editor.on('SetContent', function (e) { - if (!e.initial) { - editor.save(); - angularHelper.safeApply(scope, function () { - scope.value = editor.getContent(); - }); - } - }); - - editor.on('ObjectResized', function (e) { - var qs = "?width=" + e.width + "&height=" + e.height; - var srcAttr = $(e.target).attr("src"); - var path = srcAttr.split("?")[0]; - $(e.target).attr("data-mce-src", path + qs); - }); - - //Create the insert link plugin - tinyMceService.createLinkPicker(editor, scope, function(currentTarget, anchorElement){ - if(scope.onLinkPickerClick) { - scope.onLinkPickerClick(editor, currentTarget, anchorElement); - } - }); - - //Create the insert media plugin - tinyMceService.createMediaPicker(editor, scope, function(currentTarget, userData){ - if(scope.onMediaPickerClick) { - scope.onMediaPickerClick(editor, currentTarget, userData); - } - }); - - //Create the embedded plugin - tinyMceService.createInsertEmbeddedMedia(editor, scope, function(){ - if(scope.onEmbedClick) { - scope.onEmbedClick(editor); - } - }); - - //Create the insert macro plugin - tinyMceService.createInsertMacro(editor, scope, function(dialogData){ - if(scope.onMacroPickerClick) { - scope.onMacroPickerClick(editor, dialogData); - } - }); - - }; - - /** Loads in the editor */ - function loadTinyMce() { - - //we need to add a timeout here, to force a redraw so TinyMCE can find - //the elements needed - $timeout(function () { - tinymce.DOM.events.domLoaded = true; - tinymce.init(baseLineConfigObj); - }, 150, false); - } - - loadTinyMce(); - - //here we declare a special method which will be called whenever the value has changed from the server - //this is instead of doing a watch on the model.value = faster - //scope.model.onValueChanged = function (newVal, oldVal) { - // //update the display val again if it has changed from the server; - // tinyMceEditor.setContent(newVal, { format: 'raw' }); - // //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 - // tinyMceEditor.fire('LoadContent', null); - //}; - - //listen for formSubmitting event (the result is callback used to remove the event subscription) - var unsubscribe = scope.$on("formSubmitting", function () { - //TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer - // we do parse it out on the server side but would be nice to do that on the client side before as well. - scope.value = tinyMceEditor.getContent(); - }); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom - // element might still be there even after the modal has been hidden. - scope.$on('$destroy', function () { - unsubscribe(); - }); - - }); - - }); - - }; - - initTiny(); - - } - }; - }); + // value added to list + var value = $parse(attrs.checklistValue)(scope.$parent); + + // watch UI checked change + scope.$watch('checked', function(newValue, oldValue) { + if (newValue === oldValue) { + return; + } + var current = getter(scope.$parent); + if (newValue === true) { + setter(scope.$parent, add(current, value)); + } else { + setter(scope.$parent, remove(current, value)); + } + }); + + // watch original model change + scope.$parent.$watch(attrs.checklistModel, function(newArr, oldArr) { + scope.checked = contains(newArr, value); + }, true); + } + + return { + restrict: 'A', + priority: 1000, + terminal: true, + scope: true, + compile: function(tElement, tAttrs) { + if (tElement[0].tagName !== 'INPUT' || !tElement.attr('type', 'checkbox')) { + throw 'checklist-model should be applied to `input[type="checkbox"]`.'; + } + + if (!tAttrs.checklistValue) { + throw 'You should provide `checklist-value`.'; + } + + // exclude recursion + tElement.removeAttr('checklist-model'); + + // local scope var storing individual checkbox model + tElement.attr('ng-model', 'checked'); + + return postLinkFn; + } + }; +}]); +angular.module("umbraco.directives") +.directive("contenteditable", function() { + + return { + require: "ngModel", + link: function(scope, element, attrs, ngModel) { + + function read() { + ngModel.$setViewValue(element.html()); + } + + ngModel.$render = function() { + element.html(ngModel.$viewValue || ""); + }; + + + element.bind("focus", function(){ + + var range = document.createRange(); + range.selectNodeContents(element[0]); + + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + + }); + + element.bind("blur keyup change", function() { + scope.$apply(read); + }); + } + + }; + +}); + +/** +* @ngdoc directive +* @name umbraco.directives.directive:fixNumber +* @restrict A +* @description Used in conjunction with type='number' input fields to ensure that the bound value is converted to a number when using ng-model +* because normally it thinks it's a string and also validation doesn't work correctly due to an angular bug. +**/ +function fixNumber($parse) { + return { + restrict: "A", + require: "ngModel", + + link: function (scope, elem, attrs, ctrl) { + + //parse ngModel onload + var modelVal = scope.$eval(attrs.ngModel); + if (modelVal) { + var asNum = parseFloat(modelVal, 10); + if (!isNaN(asNum)) { + $parse(attrs.ngModel).assign(scope, asNum); + } + } + + //always return an int to the model + ctrl.$parsers.push(function (value) { + if (value === 0) { + return 0; + } + return parseFloat(value || '', 10); + }); + + //always try to format the model value as an int + ctrl.$formatters.push(function (value) { + if (angular.isString(value)) { + return parseFloat(value, 10); + } + return value; + }); + + //This fixes this angular issue: + //https://github.com/angular/angular.js/issues/2144 + // which doesn't actually validate the number input properly since the model only changes when a real number is entered + // but the input box still allows non-numbers to be entered which do not validate (only via html5) + if (typeof elem.prop('validity') === 'undefined') { + return; + } + + elem.bind('input', function (e) { + var validity = elem.prop('validity'); + scope.$apply(function () { + ctrl.$setValidity('number', !validity.badInput); + }); + }); + } + }; +} +angular.module('umbraco.directives').directive("fixNumber", fixNumber); +angular.module("umbraco.directives").directive('focusWhen', function ($timeout) { + return { + restrict: 'A', + link: function (scope, elm, attrs, ctrl) { + attrs.$observe("focusWhen", function (newValue) { + if (newValue === "true") { + $timeout(function () { + elm.focus(); + }); + } + }); + } + }; +}); + + +/** +* @ngdoc directive +* @name umbraco.directives.directive:hexBgColor +* @restrict A +* @description Used to set a hex background color on an element, this will detect valid hex and when it is valid it will set the color, otherwise +* a color will not be set. +**/ +function hexBgColor() { + return { + restrict: "A", + link: function (scope, element, attr, formCtrl) { + + var origColor = null; + if (attr.hexBgOrig) { + //set the orig based on the attribute if there is one + origColor = attr.hexBgOrig; + } + + attr.$observe("hexBgColor", function (newVal) { + if (newVal) { + if (!origColor) { + //get the orig color before changing it + origColor = element.css("border-color"); + } + //validate it - test with and without the leading hash. + if (/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) { + element.css("background-color", "#" + newVal); + return; + } + if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) { + element.css("background-color", newVal); + return; + } + } + element.css("background-color", origColor); + }); + + } + }; +} +angular.module('umbraco.directives').directive("hexBgColor", hexBgColor); +/** +* @ngdoc directive +* @name umbraco.directives.directive:hotkey +**/ + +angular.module("umbraco.directives") + .directive('hotkey', function($window, keyboardService, $log) { + + return function(scope, el, attrs) { + + var options = {}; + var keyCombo = attrs.hotkey; + + if (!keyCombo) { + //support data binding + keyCombo = scope.$eval(attrs["hotkey"]); + } + + function activate() { + + if (keyCombo) { + + // disable shortcuts in input fields if keycombo is 1 character + if (keyCombo.length === 1) { + options = { + inputDisabled: true + }; + } + + keyboardService.bind(keyCombo, function() { + + var element = $(el); + var activeElementType = document.activeElement.tagName; + var clickableElements = ["A", "BUTTON"]; + + if (element.is("a,div,button,input[type='button'],input[type='submit'],input[type='checkbox']") && !element.is(':disabled')) { + + if (element.is(':visible') || attrs.hotkeyWhenHidden) { + + if (attrs.hotkeyWhen && attrs.hotkeyWhen === "false") { + return; + } + + // when keycombo is enter and a link or button has focus - click the link or button instead of using the hotkey + if (keyCombo === "enter" && clickableElements.indexOf(activeElementType) === 0) { + document.activeElement.click(); + } else { + element.click(); + } + + } + + } else { + element.focus(); + } + + }, options); + + el.on('$destroy', function() { + keyboardService.unbind(keyCombo); + }); + + } + + } + + activate(); + + }; + }); + +/** +@ngdoc directive +@name umbraco.directives.directive:preventDefault + +@description +Use this directive to prevent default action of an element. Effectively implementing jQuery's preventdefault + +

Markup example

+ +
+    Don't go to Umbraco.com
+
+ +**/ +angular.module("umbraco.directives") + .directive('preventDefault', function() { + return function(scope, element, attrs) { + + var enabled = true; + //check if there's a value for the attribute, if there is and it's false then we conditionally don't + //prevent default. + if (attrs.preventDefault) { + attrs.$observe("preventDefault", function (newVal) { + enabled = (newVal === "false" || newVal === 0 || newVal === false) ? false : true; + }); + } + + $(element).click(function (event) { + if (event.metaKey || event.ctrlKey) { + return; + } + else { + if (enabled === true) { + event.preventDefault(); + } + } + }); + }; + }); + +/** +* @ngdoc directive +* @name umbraco.directives.directive:preventEnterSubmit +* @description prevents a form from submitting when the enter key is pressed on an input field +**/ +angular.module("umbraco.directives") + .directive('preventEnterSubmit', function() { + return function(scope, element, attrs) { + + var enabled = true; + //check if there's a value for the attribute, if there is and it's false then we conditionally don't + //prevent default. + if (attrs.preventEnterSubmit) { + attrs.$observe("preventEnterSubmit", function (newVal) { + enabled = (newVal === "false" || newVal === 0 || newVal === false) ? false : true; + }); + } + + $(element).keypress(function (event) { + if (event.which === 13) { + event.preventDefault(); + } + }); + }; + }); +/** + * @ngdoc directive + * @name umbraco.directives.directive:resizeToContent + * @element div + * @function + * + * @description + * Resize iframe's automatically to fit to the content they contain + * + * @example + + + + + + */ +angular.module("umbraco.directives") + .directive('resizeToContent', function ($window, $timeout) { + return function (scope, el, attrs) { + var iframe = el[0]; + var iframeWin = iframe.contentWindow || iframe.contentDocument.parentWindow; + if (iframeWin.document.body) { + + $timeout(function(){ + var height = iframeWin.document.documentElement.scrollHeight || iframeWin.document.body.scrollHeight; + el.height(height); + }, 3000); + } + }; + }); + +angular.module("umbraco.directives") + .directive('selectOnFocus', function () { + return function (scope, el, attrs) { + $(el).bind("click", function () { + var editmode = $(el).data("editmode"); + //If editmode is true a click is handled like a normal click + if (!editmode) { + //Initial click, select entire text + this.select(); + //Set the edit mode so subsequent clicks work normally + $(el).data("editmode", true); + } + }). + bind("blur", function () { + //Reset on focus lost + $(el).data("editmode", false); + }); + }; + }); + +angular.module("umbraco.directives") + .directive('umbAutoFocus', function($timeout) { + + return function(scope, element, attr){ + var update = function() { + //if it uses its default naming + if(element.val() === "" || attr.focusOnFilled){ + element.focus(); + } + }; + + $timeout(function() { + update(); + }); + }; +}); + +angular.module("umbraco.directives") + .directive('umbAutoResize', function($timeout) { + return { + require: ["^?umbTabs", "ngModel"], + link: function(scope, element, attr, controllersArr) { + + var domEl = element[0]; + var domElType = domEl.type; + var umbTabsController = controllersArr[0]; + var ngModelController = controllersArr[1]; + + // IE elements + var isIEFlag = false; + var wrapper = angular.element('#umb-ie-resize-input-wrapper'); + var mirror = angular.element(''); + + function isIE() { + + var ua = window.navigator.userAgent; + var msie = ua.indexOf("MSIE "); + + if (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./) || navigator.userAgent.match(/Edge\/\d+/)) { + return true; + } else { + return false; + } + + } + + function activate() { + + // check if browser is Internet Explorere + isIEFlag = isIE(); + + // scrollWidth on element does not work in IE on inputs + // we have to do some dirty dom element copying. + if (isIEFlag === true && domElType === "text") { + setupInternetExplorerElements(); + } + + } + + function setupInternetExplorerElements() { + + if (!wrapper.length) { + wrapper = angular.element('
'); + angular.element('body').append(wrapper); + } + + angular.forEach(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', + 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent', + 'boxSizing', 'borderRightWidth', 'borderLeftWidth', 'borderLeftStyle', 'borderRightStyle', + 'paddingLeft', 'paddingRight', 'marginLeft', 'marginRight' + ], function(value) { + mirror.css(value, element.css(value)); + }); + + wrapper.append(mirror); + + } + + function resizeInternetExplorerInput() { + + mirror.text(element.val() || attr.placeholder); + element.css('width', mirror.outerWidth() + 1); + + } + + function resizeInput() { + + if (domEl.scrollWidth !== domEl.clientWidth) { + if (ngModelController.$modelValue) { + element.width(domEl.scrollWidth); + } + } + + if(!ngModelController.$modelValue && attr.placeholder) { + attr.$set('size', attr.placeholder.length); + element.width('auto'); + } + + } + + function resizeTextarea() { + + if(domEl.scrollHeight !== domEl.clientHeight) { + + element.height(domEl.scrollHeight); + + } + + } + + var update = function(force) { + + + if (force === true) { + + if (domElType === "textarea") { + element.height(0); + } else if (domElType === "text") { + element.width(0); + } + + } + + + if (isIEFlag === true && domElType === "text") { + + resizeInternetExplorerInput(); + + } else { + + if (domElType === "textarea") { + + resizeTextarea(); + + } else if (domElType === "text") { + + resizeInput(); + + } + + } + + }; + + activate(); + + //listen for tab changes + if (umbTabsController != null) { + umbTabsController.onTabShown(function(args) { + update(); + }); + } + + // listen for ng-model changes + var unbindModelWatcher = scope.$watch(function() { + return ngModelController.$modelValue; + }, function(newValue) { + update(true); + }); + + scope.$on('$destroy', function() { + element.unbind('keyup keydown keypress change', update); + element.unbind('blur', update(true)); + unbindModelWatcher(); + + // clean up IE dom element + if (isIEFlag === true && domElType === "text") { + mirror.remove(); + } + + }); + } + }; + }); + +/* +example usage: + +jsonEditing is a string which we edit in a textarea. we try parsing to JSON with each change. when it is valid, propagate model changes via ngModelCtrl + +use isolate scope to prevent model propagation when invalid - will update manually. cannot replace with template, or will override ngModelCtrl, and not hide behind facade + +will override element type to textarea and add own attribute ngModel tied to jsonEditing + */ + +angular.module("umbraco.directives") + .directive('umbRawModel', function () { + return { + restrict: 'A', + require: 'ngModel', + template: '', + replace : true, + scope: { + model: '=umbRawModel', + validateOn:'=' + }, + link: function (scope, element, attrs, ngModelCtrl) { + + function setEditing (value) { + scope.jsonEditing = angular.copy( jsonToString(value)); + } + + function updateModel (value) { + scope.model = stringToJson(value); + } + + function setValid() { + ngModelCtrl.$setValidity('json', true); + } + + function setInvalid () { + ngModelCtrl.$setValidity('json', false); + } + + function stringToJson(text) { + try { + return angular.fromJson(text); + } catch (err) { + setInvalid(); + return text; + } + } + + function jsonToString(object) { + // better than JSON.stringify(), because it formats + filters $$hashKey etc. + // NOTE that this will remove all $-prefixed values + return angular.toJson(object, true); + } + + function isValidJson(model) { + var flag = true; + try { + angular.fromJson(model); + } catch (err) { + flag = false; + } + return flag; + } + + //init + setEditing(scope.model); + + var onInputChange = function(newval,oldval){ + if (newval !== oldval) { + if (isValidJson(newval)) { + setValid(); + updateModel(newval); + } else { + setInvalid(); + } + } + }; + + if(scope.validateOn){ + element.on(scope.validateOn, function(){ + scope.$apply(function(){ + onInputChange(scope.jsonEditing); + }); + }); + }else{ + //check for changes going out + scope.$watch('jsonEditing', onInputChange, true); + } + + //check for changes coming in + scope.$watch('model', function (newval, oldval) { + if (newval !== oldval) { + setEditing(newval); + } + }, true); + + } + }; + }); + +(function() { + 'use strict'; + + function SelectWhen($timeout) { + + function link(scope, el, attr, ctrl) { + + attr.$observe("umbSelectWhen", function(newValue) { + if (newValue === "true") { + $timeout(function() { + el.select(); + }); + } + }); + + } + + var directive = { + restrict: 'A', + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbSelectWhen', SelectWhen); + +})(); + +angular.module("umbraco.directives") + .directive('gridRte', function (tinyMceService, stylesheetResource, angularHelper, assetsService, $q, $timeout) { + return { + scope: { + uniqueId: '=', + value: '=', + onClick: '&', + onFocus: '&', + onBlur: '&', + configuration:"=", + onMediaPickerClick: "=", + onEmbedClick: "=", + onMacroPickerClick: "=", + onLinkPickerClick: "=" + }, + template: "", + replace: true, + link: function (scope, element, attrs) { + + var initTiny = function () { + + //we always fetch the default one, and then override parts with our own + tinyMceService.configuration().then(function (tinyMceConfig) { + + + + //config value from general tinymce.config file + var validElements = tinyMceConfig.validElements; + 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 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]"; + + var invalidElements = tinyMceConfig.inValidElements; + var plugins = _.map(tinyMceConfig.plugins, function (plugin) { + if (plugin.useOnFrontend) { + return plugin.name; + } + }).join(" ") + " autoresize"; + + //config value on the data type + var toolbar = ["code", "styleselect", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "link", "umbmediapicker", "umbembeddialog"].join(" | "); + var stylesheets = []; + + var styleFormats = []; + var await = []; + + //queue file loading + if (typeof (tinymce) === "undefined") { + await.push(assetsService.loadJs("lib/tinymce/tinymce.min.js", scope)); + } + + + if(scope.configuration && scope.configuration.toolbar){ + toolbar = scope.configuration.toolbar.join(' | '); + } + + + if(scope.configuration && scope.configuration.stylesheets){ + angular.forEach(scope.configuration.stylesheets, function(stylesheet, key){ + + stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + stylesheet + ".css"); + await.push(stylesheetResource.getRulesByName(stylesheet).then(function (rules) { + angular.forEach(rules, function (rule) { + var r = {}; + var split = ""; + r.title = rule.name; + if (rule.selector[0] === ".") { + r.inline = "span"; + r.classes = rule.selector.substring(1); + }else if (rule.selector[0] === "#") { + //Even though this will render in the style drop down, it will not actually be applied + // to the elements, don't think TinyMCE even supports this and it doesn't really make much sense + // since only one element can have one id. + r.inline = "span"; + r.attributes = { id: rule.selector.substring(1) }; + }else if (rule.selector[0] !== "." && rule.selector.indexOf(".") > -1) { + split = rule.selector.split("."); + r.block = split[0]; + r.classes = rule.selector.substring(rule.selector.indexOf(".") + 1).replace(".", " "); + }else if (rule.selector[0] !== "#" && rule.selector.indexOf("#") > -1) { + 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); + }); + })); + }); + }else{ + stylesheets.push("views/propertyeditors/grid/config/grid.default.rtestyles.css"); + styleFormats = fallbackStyles; + } + + //stores a reference to the editor + var tinyMceEditor = null; + $q.all(await).then(function () { + + var uniqueId = scope.uniqueId; + + //create a baseline Config to exten upon + var baseLineConfigObj = { + mode: "exact", + skin: "umbraco", + plugins: plugins, + valid_elements: validElements, + invalid_elements: invalidElements, + extended_valid_elements: extendedValidElements, + menubar: false, + statusbar: false, + relative_urls: false, + toolbar: toolbar, + content_css: stylesheets, + style_formats: styleFormats, + autoresize_bottom_margin: 0 + }; + + + 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 (angular.isArray(baseLineConfigObj[i]) && angular.isArray(tinyMceConfig.customConfig[i])) { + //concat it and below this concat'd array will overwrite the baseline in angular.extend + tinyMceConfig.customConfig[i] = baseLineConfigObj[i].concat(tinyMceConfig.customConfig[i]); + } + } + catch (e) { + //cannot parse, we'll just leave it + } + } + } + } + + angular.extend(baseLineConfigObj, tinyMceConfig.customConfig); + } + + //set all the things that user configs should not be able to override + baseLineConfigObj.elements = uniqueId; + baseLineConfigObj.setup = function (editor) { + + //set the reference + tinyMceEditor = editor; + + + //enable browser based spell checking + editor.on('init', function (e) { + + editor.getBody().setAttribute('spellcheck', true); + + //force overflow to hidden to prevent no needed scroll + editor.getBody().style.overflow = "hidden"; + + $timeout(function(){ + if(scope.value === null){ + editor.focus(); + } + }, 400); + + }); + + // pin toolbar to top of screen if we have focus and it scrolls off the screen + var pinToolbar = function () { + + var _toolbar = $(editor.editorContainer).find(".mce-toolbar"); + var toolbarHeight = _toolbar.height(); + + var _tinyMce = $(editor.editorContainer); + var tinyMceRect = _tinyMce[0].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 < 160 && ((160 + toolbarHeight) < tinyMceBottom)) { + _toolbar + .css("visibility", "visible") + .css("position", "fixed") + .css("top", "160px") + .css("margin-top", "0") + .css("width", tinyMceWidth); + } else { + _toolbar + .css("visibility", "visible") + .css("position", "absolute") + .css("top", "auto") + .css("margin-top", "0") + .css("width", tinyMceWidth); + } + + }; + + // unpin toolbar to top of screen + var unpinToolbar = function() { + + var _toolbar = $(editor.editorContainer).find(".mce-toolbar"); + var _tinyMce = $(editor.editorContainer); + 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"); + + }; + + //when we leave the editor (maybe) + editor.on('blur', function (e) { + editor.save(); + angularHelper.safeApply(scope, function () { + scope.value = editor.getContent(); + + var _toolbar = $(editor.editorContainer) + .find(".mce-toolbar"); + + if(scope.onBlur){ + scope.onBlur(); + } + + unpinToolbar(); + $('.umb-panel-body').off('scroll', pinToolbar); + + }); + }); + + // Focus on editor + editor.on('focus', function (e) { + angularHelper.safeApply(scope, function () { + + if(scope.onFocus){ + scope.onFocus(); + } + + pinToolbar(); + $('.umb-panel-body').on('scroll', pinToolbar); + + }); + }); + + // Click on editor + editor.on('click', function (e) { + angularHelper.safeApply(scope, function () { + + if(scope.onClick){ + scope.onClick(); + } + + pinToolbar(); + $('.umb-panel-body').on('scroll', pinToolbar); + + }); + }); + + //when buttons modify content + editor.on('ExecCommand', function (e) { + editor.save(); + angularHelper.safeApply(scope, function () { + scope.value = editor.getContent(); + }); + }); + + // Update model on keypress + editor.on('KeyUp', function (e) { + editor.save(); + angularHelper.safeApply(scope, function () { + scope.value = editor.getContent(); + }); + }); + + // Update model on change, i.e. copy/pasted text, plugins altering content + editor.on('SetContent', function (e) { + if (!e.initial) { + editor.save(); + angularHelper.safeApply(scope, function () { + scope.value = editor.getContent(); + }); + } + }); + + editor.on('ObjectResized', function (e) { + var qs = "?width=" + e.width + "&height=" + e.height; + var srcAttr = $(e.target).attr("src"); + var path = srcAttr.split("?")[0]; + $(e.target).attr("data-mce-src", path + qs); + }); + + //Create the insert link plugin + tinyMceService.createLinkPicker(editor, scope, function(currentTarget, anchorElement){ + if(scope.onLinkPickerClick) { + scope.onLinkPickerClick(editor, currentTarget, anchorElement); + } + }); + + //Create the insert media plugin + tinyMceService.createMediaPicker(editor, scope, function(currentTarget, userData){ + if(scope.onMediaPickerClick) { + scope.onMediaPickerClick(editor, currentTarget, userData); + } + }); + + //Create the embedded plugin + tinyMceService.createInsertEmbeddedMedia(editor, scope, function(){ + if(scope.onEmbedClick) { + scope.onEmbedClick(editor); + } + }); + + //Create the insert macro plugin + tinyMceService.createInsertMacro(editor, scope, function(dialogData){ + if(scope.onMacroPickerClick) { + scope.onMacroPickerClick(editor, dialogData); + } + }); + + }; + + /** Loads in the editor */ + function loadTinyMce() { + + //we need to add a timeout here, to force a redraw so TinyMCE can find + //the elements needed + $timeout(function () { + tinymce.DOM.events.domLoaded = true; + tinymce.init(baseLineConfigObj); + }, 150, false); + } + + loadTinyMce(); + + //here we declare a special method which will be called whenever the value has changed from the server + //this is instead of doing a watch on the model.value = faster + //scope.model.onValueChanged = function (newVal, oldVal) { + // //update the display val again if it has changed from the server; + // tinyMceEditor.setContent(newVal, { format: 'raw' }); + // //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 + // tinyMceEditor.fire('LoadContent', null); + //}; + + //listen for formSubmitting event (the result is callback used to remove the event subscription) + var unsubscribe = scope.$on("formSubmitting", function () { + //TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer + // we do parse it out on the server side but would be nice to do that on the client side before as well. + scope.value = tinyMceEditor.getContent(); + }); + + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom + // element might still be there even after the modal has been hidden. + scope.$on('$destroy', function () { + unsubscribe(); + }); + + }); + + }); + + }; + + initTiny(); + + } + }; + }); /** * @ngdoc directive @@ -3840,20 +3898,20 @@ angular.module("umbraco.directives.html") }; }); -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbPane -* @restrict E -**/ -angular.module("umbraco.directives.html") - .directive('umbPane', function () { - return { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/html/umb-pane.html' - }; - }); +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbPane +* @restrict E +**/ +angular.module("umbraco.directives.html") + .directive('umbPane', function () { + return { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/html/umb-pane.html' + }; + }); /** * @ngdoc directive @@ -3870,582 +3928,582 @@ angular.module("umbraco.directives.html") }; }); -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbImageCrop -* @restrict E -* @function -**/ -angular.module("umbraco.directives") - .directive('umbImageCrop', - function ($timeout, localizationService, cropperHelper, $log) { - return { - restrict: 'E', - replace: true, - templateUrl: 'views/components/imaging/umb-image-crop.html', - scope: { - src: '=', - width: '@', - height: '@', - crop: "=", - center: "=", - maxSize: '@' - }, - - link: function(scope, element, attrs) { - scope.width = 400; - scope.height = 320; - - scope.dimensions = { - image: {}, - cropper:{}, - viewport:{}, - margin: 20, - scale: { - min: 0.3, - max: 3, - current: 1 - } - }; - - - //live rendering of viewport and image styles - scope.style = function () { - return { - 'height': (parseInt(scope.dimensions.viewport.height, 10)) + 'px', - 'width': (parseInt(scope.dimensions.viewport.width, 10)) + 'px' - }; - }; - - - //elements - var $viewport = element.find(".viewport"); - var $image = element.find("img"); - var $overlay = element.find(".overlay"); - var $container = element.find(".crop-container"); - - //default constraints for drag n drop - var constraints = {left: {max: scope.dimensions.margin, min: scope.dimensions.margin}, top: {max: scope.dimensions.margin, min: scope.dimensions.margin}, }; - scope.constraints = constraints; - - - //set constaints for cropping drag and drop - var setConstraints = function(){ - constraints.left.min = scope.dimensions.margin + scope.dimensions.cropper.width - scope.dimensions.image.width; - constraints.top.min = scope.dimensions.margin + scope.dimensions.cropper.height - scope.dimensions.image.height; - }; - - - var setDimensions = function(originalImage){ - originalImage.width("auto"); - originalImage.height("auto"); - - var image = {}; - image.originalWidth = originalImage.width(); - image.originalHeight = originalImage.height(); - - image.width = image.originalWidth; - image.height = image.originalHeight; - image.left = originalImage[0].offsetLeft; - image.top = originalImage[0].offsetTop; - - scope.dimensions.image = image; - - //unscaled editor size - //var viewPortW = $viewport.width(); - //var viewPortH = $viewport.height(); - var _viewPortW = parseInt(scope.width, 10); - var _viewPortH = parseInt(scope.height, 10); - - //if we set a constraint we will scale it down if needed - if(scope.maxSize){ - var ratioCalculation = cropperHelper.scaleToMaxSize( - _viewPortW, - _viewPortH, - scope.maxSize); - - //so if we have a max size, override the thumb sizes - _viewPortW = ratioCalculation.width; - _viewPortH = ratioCalculation.height; - } - - scope.dimensions.viewport.width = _viewPortW + 2 * scope.dimensions.margin; - scope.dimensions.viewport.height = _viewPortH + 2 * scope.dimensions.margin; - scope.dimensions.cropper.width = _viewPortW; // scope.dimensions.viewport.width - 2 * scope.dimensions.margin; - scope.dimensions.cropper.height = _viewPortH; // scope.dimensions.viewport.height - 2 * scope.dimensions.margin; - }; - - - //when loading an image without any crop info, we center and fit it - var resizeImageToEditor = function(){ - //returns size fitting the cropper - var size = cropperHelper.calculateAspectRatioFit( - scope.dimensions.image.width, - scope.dimensions.image.height, - scope.dimensions.cropper.width, - scope.dimensions.cropper.height, - true); - - //sets the image size and updates the scope - scope.dimensions.image.width = size.width; - scope.dimensions.image.height = size.height; - - //calculate the best suited ratios - scope.dimensions.scale.min = size.ratio; - scope.dimensions.scale.max = 2; - scope.dimensions.scale.current = size.ratio; - - //center the image - var position = cropperHelper.centerInsideViewPort(scope.dimensions.image, scope.dimensions.cropper); - scope.dimensions.top = position.top; - scope.dimensions.left = position.left; - - setConstraints(); - }; - - //resize to a given ratio - var resizeImageToScale = function(ratio){ - //do stuff - var size = cropperHelper.calculateSizeToRatio(scope.dimensions.image.originalWidth, scope.dimensions.image.originalHeight, ratio); - scope.dimensions.image.width = size.width; - scope.dimensions.image.height = size.height; - - setConstraints(); - validatePosition(scope.dimensions.image.left, scope.dimensions.image.top); - }; - - //resize the image to a predefined crop coordinate - var resizeImageToCrop = function(){ - scope.dimensions.image = cropperHelper.convertToStyle( - scope.crop, - {width: scope.dimensions.image.originalWidth, height: scope.dimensions.image.originalHeight}, - scope.dimensions.cropper, - scope.dimensions.margin); - - var ratioCalculation = cropperHelper.calculateAspectRatioFit( - scope.dimensions.image.originalWidth, - scope.dimensions.image.originalHeight, - scope.dimensions.cropper.width, - scope.dimensions.cropper.height, - true); - - scope.dimensions.scale.current = scope.dimensions.image.ratio; - - //min max based on original width/height - scope.dimensions.scale.min = ratioCalculation.ratio; - scope.dimensions.scale.max = 2; - }; - - - - var validatePosition = function(left, top){ - if(left > constraints.left.max) - { - left = constraints.left.max; - } - - if(left <= constraints.left.min){ - left = constraints.left.min; - } - - if(top > constraints.top.max) - { - top = constraints.top.max; - } - if(top <= constraints.top.min){ - top = constraints.top.min; - } - - if(scope.dimensions.image.left !== left){ - scope.dimensions.image.left = left; - } - - if(scope.dimensions.image.top !== top){ - scope.dimensions.image.top = top; - } - }; - - - //sets scope.crop to the recalculated % based crop - var calculateCropBox = function(){ - scope.crop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, scope.dimensions.margin); - }; - - - //Drag and drop positioning, using jquery ui draggable - var onStartDragPosition, top, left; - $overlay.draggable({ - drag: function(event, ui) { - scope.$apply(function(){ - validatePosition(ui.position.left, ui.position.top); - }); - }, - stop: function(event, ui){ - scope.$apply(function(){ - //make sure that every validates one more time... - validatePosition(ui.position.left, ui.position.top); - - calculateCropBox(); - scope.dimensions.image.rnd = Math.random(); - }); - } - }); - - - - var init = function(image){ - scope.loaded = false; - - //set dimensions on image, viewport, cropper etc - setDimensions(image); - - //if we have a crop already position the image - if(scope.crop){ - resizeImageToCrop(); - }else{ - resizeImageToEditor(); - } - - //sets constaints for the cropper - setConstraints(); - scope.loaded = true; - }; - - - /// WATCHERS //// - scope.$watchCollection('[width, height]', function(newValues, oldValues){ - //we have to reinit the whole thing if - //one of the external params changes - if(newValues !== oldValues){ - setDimensions($image); - setConstraints(); - } - }); - - var throttledResizing = _.throttle(function(){ - resizeImageToScale(scope.dimensions.scale.current); - calculateCropBox(); - }, 100); - - - //happens when we change the scale - scope.$watch("dimensions.scale.current", function(){ - if(scope.loaded){ - throttledResizing(); - } - }); - - //ie hack - if(window.navigator.userAgent.indexOf("MSIE ")){ - var ranger = element.find("input"); - ranger.bind("change",function(){ - scope.$apply(function(){ - scope.dimensions.scale.current = ranger.val(); - }); - }); - } - - //// INIT ///// - $image.load(function(){ - $timeout(function(){ - init($image); - }); - }); - } - }; - }); +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbImageCrop +* @restrict E +* @function +**/ +angular.module("umbraco.directives") + .directive('umbImageCrop', + function ($timeout, localizationService, cropperHelper, $log) { + return { + restrict: 'E', + replace: true, + templateUrl: 'views/components/imaging/umb-image-crop.html', + scope: { + src: '=', + width: '@', + height: '@', + crop: "=", + center: "=", + maxSize: '@' + }, -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbImageGravity -* @restrict E -* @function -* @description -**/ -angular.module("umbraco.directives") - .directive('umbImageGravity', function ($timeout, localizationService, $log) { - return { - restrict: 'E', - replace: true, - templateUrl: 'views/components/imaging/umb-image-gravity.html', - scope: { - src: '=', - center: "=", - onImageLoaded: "=" - }, - link: function(scope, element, attrs) { - - //Internal values for keeping track of the dot and the size of the editor - scope.dimensions = { - width: 0, - height: 0, - left: 0, - top: 0 - }; - - scope.loaded = false; - - //elements - var $viewport = element.find(".viewport"); - var $image = element.find("img"); - var $overlay = element.find(".overlay"); - - scope.style = function () { - if(scope.dimensions.width <= 0){ - setDimensions(); - } - - return { - 'top': scope.dimensions.top + 'px', - 'left': scope.dimensions.left + 'px' - }; - }; - - scope.setFocalPoint = function(event) { - - scope.$emit("imageFocalPointStart"); - - var offsetX = event.offsetX - 10; - var offsetY = event.offsetY - 10; - - calculateGravity(offsetX, offsetY); - - lazyEndEvent(); - - }; - - var setDimensions = function(){ - scope.dimensions.width = $image.width(); - scope.dimensions.height = $image.height(); - - if(scope.center){ - scope.dimensions.left = scope.center.left * scope.dimensions.width -10; - scope.dimensions.top = scope.center.top * scope.dimensions.height -10; - }else{ - scope.center = { left: 0.5, top: 0.5 }; - } - }; - - var calculateGravity = function(offsetX, offsetY){ - scope.dimensions.left = offsetX; - scope.dimensions.top = offsetY; - - scope.center.left = (scope.dimensions.left+10) / scope.dimensions.width; - scope.center.top = (scope.dimensions.top+10) / scope.dimensions.height; - }; - - var lazyEndEvent = _.debounce(function(){ - scope.$apply(function(){ - scope.$emit("imageFocalPointStop"); - }); - }, 2000); - - - //Drag and drop positioning, using jquery ui draggable - //TODO ensure that the point doesnt go outside the box - $overlay.draggable({ - containment: "parent", - start: function(){ - scope.$apply(function(){ - scope.$emit("imageFocalPointStart"); - }); - }, - stop: function() { - scope.$apply(function(){ - var offsetX = $overlay[0].offsetLeft; - var offsetY = $overlay[0].offsetTop; - calculateGravity(offsetX, offsetY); - }); - - lazyEndEvent(); - } - }); - - //// INIT ///// - $image.load(function(){ - $timeout(function(){ - setDimensions(); - scope.loaded = true; - scope.onImageLoaded(); - }); - }); - - $(window).on('resize.umbImageGravity', function(){ - scope.$apply(function(){ - $timeout(function(){ - setDimensions(); - }); - // Make sure we can find the offset values for the overlay(dot) before calculating - // fixes issue with resize event when printing the page (ex. hitting ctrl+p inside the rte) - if($overlay.is(':visible')) { - var offsetX = $overlay[0].offsetLeft; - var offsetY = $overlay[0].offsetTop; - calculateGravity(offsetX, offsetY); - } - }); - }); - - scope.$on('$destroy', function() { - $(window).off('.umbImageGravity'); - }); - - } - }; - }); + link: function(scope, element, attrs) { + scope.width = 400; + scope.height = 320; -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbImageThumbnail -* @restrict E -* @function -* @description -**/ -angular.module("umbraco.directives") - .directive('umbImageThumbnail', - function ($timeout, localizationService, cropperHelper, $log) { - return { - restrict: 'E', - replace: true, - templateUrl: 'views/components/imaging/umb-image-thumbnail.html', - - scope: { - src: '=', - width: '@', - height: '@', - center: "=", - crop: "=", - maxSize: '@' - }, - - link: function(scope, element, attrs) { - //// INIT ///// - var $image = element.find("img"); - scope.loaded = false; - - $image.load(function(){ - $timeout(function(){ - $image.width("auto"); - $image.height("auto"); - - scope.image = {}; - scope.image.width = $image[0].width; - scope.image.height = $image[0].height; - - //we force a lower thumbnail size to fit the max size - //we do not compare to the image dimensions, but the thumbs - if(scope.maxSize){ - var ratioCalculation = cropperHelper.calculateAspectRatioFit( - scope.width, - scope.height, - scope.maxSize, - scope.maxSize, - false); - - //so if we have a max size, override the thumb sizes - scope.width = ratioCalculation.width; - scope.height = ratioCalculation.height; - } - - setPreviewStyle(); - scope.loaded = true; - }); - }); - - /// WATCHERS //// - scope.$watchCollection('[crop, center]', function(newValues, oldValues){ - //we have to reinit the whole thing if - //one of the external params changes - setPreviewStyle(); - }); - - scope.$watch("center", function(){ - setPreviewStyle(); - }, true); - - function setPreviewStyle(){ - if(scope.crop && scope.image){ - scope.preview = cropperHelper.convertToStyle( - scope.crop, - scope.image, - {width: scope.width, height: scope.height}, - 0); - }else if(scope.image){ - - //returns size fitting the cropper - var p = cropperHelper.calculateAspectRatioFit( - scope.image.width, - scope.image.height, - scope.width, - scope.height, - true); - - - if(scope.center){ - var xy = cropperHelper.alignToCoordinates(p, scope.center, {width: scope.width, height: scope.height}); - p.top = xy.top; - p.left = xy.left; - }else{ - - } - - p.position = "absolute"; - scope.preview = p; - } - } - } - }; - }); + scope.dimensions = { + image: {}, + cropper:{}, + viewport:{}, + margin: 20, + scale: { + min: 0.3, + max: 3, + current: 1 + } + }; -angular.module("umbraco.directives") - - /** - * @ngdoc directive - * @name umbraco.directives.directive:localize - * @restrict EA - * @function - * @description Localize directive - **/ - .directive('localize', function ($log, localizationService) { - return { - restrict: 'E', - scope:{ - key: '@' - }, - replace: true, - - link: function (scope, element, attrs) { - var key = scope.key; - localizationService.localize(key).then(function(value){ - element.html(value); - }); - } - }; - }) - - .directive('localize', function ($log, localizationService) { - return { - restrict: 'A', - link: function (scope, element, attrs) { - var keys = attrs.localize.split(','); - - angular.forEach(keys, function(value, key){ - var attr = element.attr(value); - - if(attr){ - if(attr[0] === '@'){ - - var t = localizationService.tokenize(attr.substring(1), scope); - localizationService.localize(t.key, t.tokens).then(function(val){ - element.attr(value, val); - }); - - } - } - }); - } - }; - - }); + + //live rendering of viewport and image styles + scope.style = function () { + return { + 'height': (parseInt(scope.dimensions.viewport.height, 10)) + 'px', + 'width': (parseInt(scope.dimensions.viewport.width, 10)) + 'px' + }; + }; + + + //elements + var $viewport = element.find(".viewport"); + var $image = element.find("img"); + var $overlay = element.find(".overlay"); + var $container = element.find(".crop-container"); + + //default constraints for drag n drop + var constraints = {left: {max: scope.dimensions.margin, min: scope.dimensions.margin}, top: {max: scope.dimensions.margin, min: scope.dimensions.margin}, }; + scope.constraints = constraints; + + + //set constaints for cropping drag and drop + var setConstraints = function(){ + constraints.left.min = scope.dimensions.margin + scope.dimensions.cropper.width - scope.dimensions.image.width; + constraints.top.min = scope.dimensions.margin + scope.dimensions.cropper.height - scope.dimensions.image.height; + }; + + + var setDimensions = function(originalImage){ + originalImage.width("auto"); + originalImage.height("auto"); + + var image = {}; + image.originalWidth = originalImage.width(); + image.originalHeight = originalImage.height(); + + image.width = image.originalWidth; + image.height = image.originalHeight; + image.left = originalImage[0].offsetLeft; + image.top = originalImage[0].offsetTop; + + scope.dimensions.image = image; + + //unscaled editor size + //var viewPortW = $viewport.width(); + //var viewPortH = $viewport.height(); + var _viewPortW = parseInt(scope.width, 10); + var _viewPortH = parseInt(scope.height, 10); + + //if we set a constraint we will scale it down if needed + if(scope.maxSize){ + var ratioCalculation = cropperHelper.scaleToMaxSize( + _viewPortW, + _viewPortH, + scope.maxSize); + + //so if we have a max size, override the thumb sizes + _viewPortW = ratioCalculation.width; + _viewPortH = ratioCalculation.height; + } + + scope.dimensions.viewport.width = _viewPortW + 2 * scope.dimensions.margin; + scope.dimensions.viewport.height = _viewPortH + 2 * scope.dimensions.margin; + scope.dimensions.cropper.width = _viewPortW; // scope.dimensions.viewport.width - 2 * scope.dimensions.margin; + scope.dimensions.cropper.height = _viewPortH; // scope.dimensions.viewport.height - 2 * scope.dimensions.margin; + }; + + + //when loading an image without any crop info, we center and fit it + var resizeImageToEditor = function(){ + //returns size fitting the cropper + var size = cropperHelper.calculateAspectRatioFit( + scope.dimensions.image.width, + scope.dimensions.image.height, + scope.dimensions.cropper.width, + scope.dimensions.cropper.height, + true); + + //sets the image size and updates the scope + scope.dimensions.image.width = size.width; + scope.dimensions.image.height = size.height; + + //calculate the best suited ratios + scope.dimensions.scale.min = size.ratio; + scope.dimensions.scale.max = 2; + scope.dimensions.scale.current = size.ratio; + + //center the image + var position = cropperHelper.centerInsideViewPort(scope.dimensions.image, scope.dimensions.cropper); + scope.dimensions.top = position.top; + scope.dimensions.left = position.left; + + setConstraints(); + }; + + //resize to a given ratio + var resizeImageToScale = function(ratio){ + //do stuff + var size = cropperHelper.calculateSizeToRatio(scope.dimensions.image.originalWidth, scope.dimensions.image.originalHeight, ratio); + scope.dimensions.image.width = size.width; + scope.dimensions.image.height = size.height; + + setConstraints(); + validatePosition(scope.dimensions.image.left, scope.dimensions.image.top); + }; + + //resize the image to a predefined crop coordinate + var resizeImageToCrop = function(){ + scope.dimensions.image = cropperHelper.convertToStyle( + scope.crop, + {width: scope.dimensions.image.originalWidth, height: scope.dimensions.image.originalHeight}, + scope.dimensions.cropper, + scope.dimensions.margin); + + var ratioCalculation = cropperHelper.calculateAspectRatioFit( + scope.dimensions.image.originalWidth, + scope.dimensions.image.originalHeight, + scope.dimensions.cropper.width, + scope.dimensions.cropper.height, + true); + + scope.dimensions.scale.current = scope.dimensions.image.ratio; + + //min max based on original width/height + scope.dimensions.scale.min = ratioCalculation.ratio; + scope.dimensions.scale.max = 2; + }; + + + + var validatePosition = function(left, top){ + if(left > constraints.left.max) + { + left = constraints.left.max; + } + + if(left <= constraints.left.min){ + left = constraints.left.min; + } + + if(top > constraints.top.max) + { + top = constraints.top.max; + } + if(top <= constraints.top.min){ + top = constraints.top.min; + } + + if(scope.dimensions.image.left !== left){ + scope.dimensions.image.left = left; + } + + if(scope.dimensions.image.top !== top){ + scope.dimensions.image.top = top; + } + }; + + + //sets scope.crop to the recalculated % based crop + var calculateCropBox = function(){ + scope.crop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, scope.dimensions.margin); + }; + + + //Drag and drop positioning, using jquery ui draggable + var onStartDragPosition, top, left; + $overlay.draggable({ + drag: function(event, ui) { + scope.$apply(function(){ + validatePosition(ui.position.left, ui.position.top); + }); + }, + stop: function(event, ui){ + scope.$apply(function(){ + //make sure that every validates one more time... + validatePosition(ui.position.left, ui.position.top); + + calculateCropBox(); + scope.dimensions.image.rnd = Math.random(); + }); + } + }); + + + + var init = function(image){ + scope.loaded = false; + + //set dimensions on image, viewport, cropper etc + setDimensions(image); + + //if we have a crop already position the image + if(scope.crop){ + resizeImageToCrop(); + }else{ + resizeImageToEditor(); + } + + //sets constaints for the cropper + setConstraints(); + scope.loaded = true; + }; + + + /// WATCHERS //// + scope.$watchCollection('[width, height]', function(newValues, oldValues){ + //we have to reinit the whole thing if + //one of the external params changes + if(newValues !== oldValues){ + setDimensions($image); + setConstraints(); + } + }); + + var throttledResizing = _.throttle(function(){ + resizeImageToScale(scope.dimensions.scale.current); + calculateCropBox(); + }, 100); + + + //happens when we change the scale + scope.$watch("dimensions.scale.current", function(){ + if(scope.loaded){ + throttledResizing(); + } + }); + + //ie hack + if(window.navigator.userAgent.indexOf("MSIE ")){ + var ranger = element.find("input"); + ranger.bind("change",function(){ + scope.$apply(function(){ + scope.dimensions.scale.current = ranger.val(); + }); + }); + } + + //// INIT ///// + $image.load(function(){ + $timeout(function(){ + init($image); + }); + }); + } + }; + }); + +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbImageGravity +* @restrict E +* @function +* @description +**/ +angular.module("umbraco.directives") + .directive('umbImageGravity', function ($timeout, localizationService, $log) { + return { + restrict: 'E', + replace: true, + templateUrl: 'views/components/imaging/umb-image-gravity.html', + scope: { + src: '=', + center: "=", + onImageLoaded: "=" + }, + link: function(scope, element, attrs) { + + //Internal values for keeping track of the dot and the size of the editor + scope.dimensions = { + width: 0, + height: 0, + left: 0, + top: 0 + }; + + scope.loaded = false; + + //elements + var $viewport = element.find(".viewport"); + var $image = element.find("img"); + var $overlay = element.find(".overlay"); + + scope.style = function () { + if(scope.dimensions.width <= 0){ + setDimensions(); + } + + return { + 'top': scope.dimensions.top + 'px', + 'left': scope.dimensions.left + 'px' + }; + }; + + scope.setFocalPoint = function(event) { + + scope.$emit("imageFocalPointStart"); + + var offsetX = event.offsetX - 10; + var offsetY = event.offsetY - 10; + + calculateGravity(offsetX, offsetY); + + lazyEndEvent(); + + }; + + var setDimensions = function(){ + scope.dimensions.width = $image.width(); + scope.dimensions.height = $image.height(); + + if(scope.center){ + scope.dimensions.left = scope.center.left * scope.dimensions.width -10; + scope.dimensions.top = scope.center.top * scope.dimensions.height -10; + }else{ + scope.center = { left: 0.5, top: 0.5 }; + } + }; + + var calculateGravity = function(offsetX, offsetY){ + scope.dimensions.left = offsetX; + scope.dimensions.top = offsetY; + + scope.center.left = (scope.dimensions.left+10) / scope.dimensions.width; + scope.center.top = (scope.dimensions.top+10) / scope.dimensions.height; + }; + + var lazyEndEvent = _.debounce(function(){ + scope.$apply(function(){ + scope.$emit("imageFocalPointStop"); + }); + }, 2000); + + + //Drag and drop positioning, using jquery ui draggable + //TODO ensure that the point doesnt go outside the box + $overlay.draggable({ + containment: "parent", + start: function(){ + scope.$apply(function(){ + scope.$emit("imageFocalPointStart"); + }); + }, + stop: function() { + scope.$apply(function(){ + var offsetX = $overlay[0].offsetLeft; + var offsetY = $overlay[0].offsetTop; + calculateGravity(offsetX, offsetY); + }); + + lazyEndEvent(); + } + }); + + //// INIT ///// + $image.load(function(){ + $timeout(function(){ + setDimensions(); + scope.loaded = true; + scope.onImageLoaded(); + }); + }); + + $(window).on('resize.umbImageGravity', function(){ + scope.$apply(function(){ + $timeout(function(){ + setDimensions(); + }); + // Make sure we can find the offset values for the overlay(dot) before calculating + // fixes issue with resize event when printing the page (ex. hitting ctrl+p inside the rte) + if($overlay.is(':visible')) { + var offsetX = $overlay[0].offsetLeft; + var offsetY = $overlay[0].offsetTop; + calculateGravity(offsetX, offsetY); + } + }); + }); + + scope.$on('$destroy', function() { + $(window).off('.umbImageGravity'); + }); + + } + }; + }); + +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbImageThumbnail +* @restrict E +* @function +* @description +**/ +angular.module("umbraco.directives") + .directive('umbImageThumbnail', + function ($timeout, localizationService, cropperHelper, $log) { + return { + restrict: 'E', + replace: true, + templateUrl: 'views/components/imaging/umb-image-thumbnail.html', + + scope: { + src: '=', + width: '@', + height: '@', + center: "=", + crop: "=", + maxSize: '@' + }, + + link: function(scope, element, attrs) { + //// INIT ///// + var $image = element.find("img"); + scope.loaded = false; + + $image.load(function(){ + $timeout(function(){ + $image.width("auto"); + $image.height("auto"); + + scope.image = {}; + scope.image.width = $image[0].width; + scope.image.height = $image[0].height; + + //we force a lower thumbnail size to fit the max size + //we do not compare to the image dimensions, but the thumbs + if(scope.maxSize){ + var ratioCalculation = cropperHelper.calculateAspectRatioFit( + scope.width, + scope.height, + scope.maxSize, + scope.maxSize, + false); + + //so if we have a max size, override the thumb sizes + scope.width = ratioCalculation.width; + scope.height = ratioCalculation.height; + } + + setPreviewStyle(); + scope.loaded = true; + }); + }); + + /// WATCHERS //// + scope.$watchCollection('[crop, center]', function(newValues, oldValues){ + //we have to reinit the whole thing if + //one of the external params changes + setPreviewStyle(); + }); + + scope.$watch("center", function(){ + setPreviewStyle(); + }, true); + + function setPreviewStyle(){ + if(scope.crop && scope.image){ + scope.preview = cropperHelper.convertToStyle( + scope.crop, + scope.image, + {width: scope.width, height: scope.height}, + 0); + }else if(scope.image){ + + //returns size fitting the cropper + var p = cropperHelper.calculateAspectRatioFit( + scope.image.width, + scope.image.height, + scope.width, + scope.height, + true); + + + if(scope.center){ + var xy = cropperHelper.alignToCoordinates(p, scope.center, {width: scope.width, height: scope.height}); + p.top = xy.top; + p.left = xy.left; + }else{ + + } + + p.position = "absolute"; + scope.preview = p; + } + } + } + }; + }); + +angular.module("umbraco.directives") + + /** + * @ngdoc directive + * @name umbraco.directives.directive:localize + * @restrict EA + * @function + * @description Localize directive + **/ + .directive('localize', function ($log, localizationService) { + return { + restrict: 'E', + scope:{ + key: '@' + }, + replace: true, + + link: function (scope, element, attrs) { + var key = scope.key; + localizationService.localize(key).then(function(value){ + element.html(value); + }); + } + }; + }) + + .directive('localize', function ($log, localizationService) { + return { + restrict: 'A', + link: function (scope, element, attrs) { + var keys = attrs.localize.split(','); + + angular.forEach(keys, function(value, key){ + var attr = element.attr(value); + + if(attr){ + if(attr[0] === '@'){ + + var t = localizationService.tokenize(attr.substring(1), scope); + localizationService.localize(t.key, t.tokens).then(function(val){ + element.attr(value, val); + }); + + } + } + }); + } + }; + + }); /** * @ngdoc directive @@ -4484,971 +4542,971 @@ angular.module("umbraco.directives") })(); -/** -@ngdoc directive -@name umbraco.directives.directive:umbOverlay -@restrict E -@scope - -@description - -

Markup example

-
-    
- - - - - - -
-
- -

Controller example

-
-    (function () {
-
-        "use strict";
-
-        function Controller() {
-
-            var vm = this;
-
-            vm.openOverlay = openOverlay;
-
-            function openOverlay() {
-
-                vm.overlay = {
-                    view: "mediapicker",
-                    show: true,
-                    submit: function(model) {
-
-                        vm.overlay.show = false;
-                        vm.overlay = null;
-                    },
-                    close: function(oldModel) {
-                        vm.overlay.show = false;
-                        vm.overlay = null;
-                    }
-                }
-
-            };
-
-        }
-
-        angular.module("umbraco").controller("My.Controller", Controller);
-    })();
-
- -

General Options

-Lorem ipsum dolor sit amet.. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParamTypeDetails
model.titleStringSet the title of the overlay.
model.subTitleStringSet the subtitle of the overlay.
model.submitButtonLabelStringSet an alternate submit button text
model.submitButtonLabelKeyStringSet an alternate submit button label key for localized texts
model.hideSubmitButtonBooleanHides the submit button
model.closeButtonLabelStringSet an alternate close button text
model.closeButtonLabelKeyStringSet an alternate close button label key for localized texts
model.showBooleanShow/hide the overlay
model.submitFunctionCallback function when the overlay submits. Returns the overlay model object
model.closeFunctionCallback function when the overlay closes. Returns a copy of the overlay model object before being modified
- - -

Content Picker

-Opens a content picker.
-view: contentpicker - - - - - - - - - - - - - -
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
- - - - - - - - - - - - - -
ReturnsTypeDetails
model.selectionArrayArray of content objects
- - -

Icon Picker

-Opens an icon picker.
-view: iconpicker - - - - - - - - - - - - - -
ReturnsTypeDetails
model.iconStringThe icon class
- -

Item Picker

-Opens an item picker.
-view: itempicker - - - - - - - - - - - - - - - - - - - - - - - - - -
ParamTypeDetails
model.availableItemsArrayArray of available items
model.selectedItemsArrayArray of selected items. When passed in the selected items will be filtered from the available items.
model.filterBooleanSet to false to hide the filter
- - - - - - - - - - - - - -
ReturnsTypeDetails
model.selectedItemObjectThe selected item
- -

Macro Picker

-Opens a media picker.
-view: macropicker - - - - - - - - - - - - - - - -
ParamTypeDetails
model.dialogDataObjectObject which contains array of allowedMacros. Set to null to allow all.
- - - - - - - - - - - - - - - - - - - - -
ReturnsTypeDetails
model.macroParamsArrayArray of macro params
model.selectedMacroObjectThe selected macro
- -

Media Picker

-Opens a media picker.
-view: mediapicker - - - - - - - - - - - - - - - - - - - - - - - - - -
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
model.onlyImagesBooleanOnly display files that have an image file-extension
model.disableFolderSelectBooleanDisable folder selection
- - - - - - - - - - - - - - - -
ReturnsTypeDetails
model.selectedImagesArrayArray of selected images
- -

Member Group Picker

-Opens a member group picker.
-view: membergrouppicker - - - - - - - - - - - - - - - -
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
- - - - - - - - - - - - - - - - - - - - -
ReturnsTypeDetails
model.selectedMemberGroupStringThe selected member group
model.selectedMemberGroups (multiPicker)ArrayThe selected member groups
- -

Member Picker

-Opens a member picker.
-view: memberpicker - - - - - - - - - - - - - - - -
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
- - - - - - - - - - - - - - -
ReturnsTypeDetails
model.selectionArrayArray of selected members/td> -
- -

YSOD

-Opens an overlay to show a custom YSOD.
-view: ysod - - - - - - - - - - - - - - - -
ParamTypeDetails
model.errorObjectError object
- -@param {object} model Overlay options. -@param {string} view Path to view or one of the default view names. -@param {string} position The overlay position ("left", "right", "center": "target"). -**/ - -(function() { - 'use strict'; - - function OverlayDirective($timeout, formHelper, overlayHelper, localizationService) { - - function link(scope, el, attr, ctrl) { - - scope.directive = { - enableConfirmButton: false - }; - - var overlayNumber = 0; - var numberOfOverlays = 0; - var isRegistered = false; - - var modelCopy = {}; - - function activate() { - - setView(); - - setButtonText(); - - modelCopy = makeModelCopy(scope.model); - - $timeout(function() { - - if (scope.position === "target") { - setTargetPosition(); - } - - // this has to be done inside a timeout to ensure the destroy - // event on other overlays is run before registering a new one - registerOverlay(); - - setOverlayIndent(); - - }); - - } - - function setView() { - - if (scope.view) { - - if (scope.view.indexOf(".html") === -1) { - var viewAlias = scope.view.toLowerCase(); - scope.view = "views/common/overlays/" + viewAlias + "/" + viewAlias + ".html"; - } - - } - - } - - function setButtonText() { - if (!scope.model.closeButtonLabelKey && !scope.model.closeButtonLabel) { - scope.model.closeButtonLabel = localizationService.localize("general_close"); - } - if (!scope.model.submitButtonLabelKey && !scope.model.submitButtonLabel) { - scope.model.submitButtonLabel = localizationService.localize("general_submit"); - } - } - - function registerOverlay() { - - overlayNumber = overlayHelper.registerOverlay(); - - $(document).bind("keydown.overlay-" + overlayNumber, function(event) { - - if (event.which === 27) { - - numberOfOverlays = overlayHelper.getNumberOfOverlays(); - - if(numberOfOverlays === overlayNumber) { - scope.closeOverLay(); - } - - event.preventDefault(); - } - - if (event.which === 13) { - - numberOfOverlays = overlayHelper.getNumberOfOverlays(); - - if(numberOfOverlays === overlayNumber) { - - var activeElementType = document.activeElement.tagName; - var clickableElements = ["A", "BUTTON"]; - var submitOnEnter = document.activeElement.hasAttribute("overlay-submit-on-enter"); - - if(clickableElements.indexOf(activeElementType) === 0) { - document.activeElement.click(); - event.preventDefault(); - } else if(activeElementType === "TEXTAREA" && !submitOnEnter) { - - - } else { - scope.$apply(function () { - scope.submitForm(scope.model); - }); - event.preventDefault(); - } - - } - - } - - }); - - isRegistered = true; - - } - - function unregisterOverlay() { - - if(isRegistered) { - - overlayHelper.unregisterOverlay(); - - $(document).unbind("keydown.overlay-" + overlayNumber); - - isRegistered = false; - } - - } - - function makeModelCopy(object) { - - var newObject = {}; - - for (var key in object) { - if (key !== "event") { - newObject[key] = angular.copy(object[key]); - } - } - - return newObject; - - } - - function setOverlayIndent() { - - var overlayIndex = overlayNumber - 1; - var indentSize = overlayIndex * 20; - var overlayWidth = el.context.clientWidth; - - el.css('width', overlayWidth - indentSize); - - if(scope.position === "center" || scope.position === "target") { - var overlayTopPosition = el.context.offsetTop; - el.css('top', overlayTopPosition + indentSize); - } - - } - - function setTargetPosition() { - - var container = $("#contentwrapper"); - var containerLeft = container[0].offsetLeft; - var containerRight = containerLeft + container[0].offsetWidth; - var containerTop = container[0].offsetTop; - var containerBottom = containerTop + container[0].offsetHeight; - - var mousePositionClickX = null; - var mousePositionClickY = null; - var elementHeight = null; - var elementWidth = null; - - var position = { - right: "inherit", - left: "inherit", - top: "inherit", - bottom: "inherit" - }; - - // if mouse click position is know place element with mouse in center - if (scope.model.event && scope.model.event) { - - // click position - mousePositionClickX = scope.model.event.pageX; - mousePositionClickY = scope.model.event.pageY; - - // element size - elementHeight = el.context.clientHeight; - elementWidth = el.context.clientWidth; - - // move element to this position - position.left = mousePositionClickX - (elementWidth / 2); - position.top = mousePositionClickY - (elementHeight / 2); - - // check to see if element is outside screen - // outside right - if (position.left + elementWidth > containerRight) { - position.right = 10; - position.left = "inherit"; - } - - // outside bottom - if (position.top + elementHeight > containerBottom) { - position.bottom = 10; - position.top = "inherit"; - } - - // outside left - if (position.left < containerLeft) { - position.left = containerLeft + 10; - position.right = "inherit"; - } - - // outside top - if (position.top < containerTop) { - position.top = 10; - position.bottom = "inherit"; - } - - el.css(position); - - } - - } - - scope.submitForm = function(model) { - if(scope.model.submit) { - if (formHelper.submitForm({scope: scope})) { - formHelper.resetForm({ scope: scope }); - - if(scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) { - scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton); - } else { - unregisterOverlay(); - scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton); - } - - } - } - }; - - scope.cancelConfirmSubmit = function() { - scope.model.confirmSubmit.show = false; - }; - - scope.closeOverLay = function() { - - unregisterOverlay(); - - if (scope.model.close) { - scope.model = modelCopy; - scope.model.close(scope.model); - } else { - scope.model.show = false; - scope.model = null; - } - - }; - - // angular does not support ng-show on custom directives - // width isolated scopes. So we have to make our own. - if (attr.hasOwnProperty("ngShow")) { - scope.$watch("ngShow", function(value) { - if (value) { - el.show(); - activate(); - } else { - unregisterOverlay(); - el.hide(); - } - }); - } else { - activate(); - } - - scope.$on('$destroy', function(){ - unregisterOverlay(); - }); - - } - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/overlays/umb-overlay.html', - scope: { - ngShow: "=", - model: "=", - view: "=", - position: "@" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbOverlay', OverlayDirective); - -})(); +/** +@ngdoc directive +@name umbraco.directives.directive:umbOverlay +@restrict E +@scope -(function() { - 'use strict'; - - function OverlayBackdropDirective(overlayHelper) { - - function link(scope, el, attr, ctrl) { - - scope.numberOfOverlays = 0; - - scope.$watch(function(){ - return overlayHelper.getNumberOfOverlays(); - }, function (newValue) { - scope.numberOfOverlays = newValue; - }); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/overlays/umb-overlay-backdrop.html', - link: link - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbOverlayBackdrop', OverlayBackdropDirective); - -})(); +@description -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbProperty -* @restrict E -**/ -angular.module("umbraco.directives") - .directive('umbProperty', function (umbPropEditorHelper) { - return { - scope: { - property: "=" - }, - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/property/umb-property.html', - link: function(scope) { - scope.propertyAlias = Umbraco.Sys.ServerVariables.isDebuggingEnabled === true ? scope.property.alias : null; - }, - //Define a controller for this directive to expose APIs to other directives - controller: function ($scope, $timeout) { - - var self = this; - - //set the API properties/methods - - self.property = $scope.property; - self.setPropertyError = function(errorMsg) { - $scope.property.propertyErrorMessage = errorMsg; - }; - } - }; +

Markup example

+
+    
+ + + + + + +
+
+ +

Controller example

+
+    (function () {
+
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+            vm.openOverlay = openOverlay;
+
+            function openOverlay() {
+
+                vm.overlay = {
+                    view: "mediapicker",
+                    show: true,
+                    submit: function(model) {
+
+                        vm.overlay.show = false;
+                        vm.overlay = null;
+                    },
+                    close: function(oldModel) {
+                        vm.overlay.show = false;
+                        vm.overlay = null;
+                    }
+                }
+
+            };
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +

General Options

+Lorem ipsum dolor sit amet.. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParamTypeDetails
model.titleStringSet the title of the overlay.
model.subTitleStringSet the subtitle of the overlay.
model.submitButtonLabelStringSet an alternate submit button text
model.submitButtonLabelKeyStringSet an alternate submit button label key for localized texts
model.hideSubmitButtonBooleanHides the submit button
model.closeButtonLabelStringSet an alternate close button text
model.closeButtonLabelKeyStringSet an alternate close button label key for localized texts
model.showBooleanShow/hide the overlay
model.submitFunctionCallback function when the overlay submits. Returns the overlay model object
model.closeFunctionCallback function when the overlay closes. Returns a copy of the overlay model object before being modified
+ + +

Content Picker

+Opens a content picker.
+view: contentpicker + + + + + + + + + + + + + +
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
+ + + + + + + + + + + + + +
ReturnsTypeDetails
model.selectionArrayArray of content objects
+ + +

Icon Picker

+Opens an icon picker.
+view: iconpicker + + + + + + + + + + + + + +
ReturnsTypeDetails
model.iconStringThe icon class
+ +

Item Picker

+Opens an item picker.
+view: itempicker + + + + + + + + + + + + + + + + + + + + + + + + + +
ParamTypeDetails
model.availableItemsArrayArray of available items
model.selectedItemsArrayArray of selected items. When passed in the selected items will be filtered from the available items.
model.filterBooleanSet to false to hide the filter
+ + + + + + + + + + + + + +
ReturnsTypeDetails
model.selectedItemObjectThe selected item
+ +

Macro Picker

+Opens a media picker.
+view: macropicker + + + + + + + + + + + + + + + +
ParamTypeDetails
model.dialogDataObjectObject which contains array of allowedMacros. Set to null to allow all.
+ + + + + + + + + + + + + + + + + + + + +
ReturnsTypeDetails
model.macroParamsArrayArray of macro params
model.selectedMacroObjectThe selected macro
+ +

Media Picker

+Opens a media picker.
+view: mediapicker + + + + + + + + + + + + + + + + + + + + + + + + + +
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
model.onlyImagesBooleanOnly display files that have an image file-extension
model.disableFolderSelectBooleanDisable folder selection
+ + + + + + + + + + + + + + + +
ReturnsTypeDetails
model.selectedImagesArrayArray of selected images
+ +

Member Group Picker

+Opens a member group picker.
+view: membergrouppicker + + + + + + + + + + + + + + + +
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
+ + + + + + + + + + + + + + + + + + + + +
ReturnsTypeDetails
model.selectedMemberGroupStringThe selected member group
model.selectedMemberGroups (multiPicker)ArrayThe selected member groups
+ +

Member Picker

+Opens a member picker.
+view: memberpicker + + + + + + + + + + + + + + + +
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
+ + + + + + + + + + + + + + +
ReturnsTypeDetails
model.selectionArrayArray of selected members/td> +
+ +

YSOD

+Opens an overlay to show a custom YSOD.
+view: ysod + + + + + + + + + + + + + + + +
ParamTypeDetails
model.errorObjectError object
+ +@param {object} model Overlay options. +@param {string} view Path to view or one of the default view names. +@param {string} position The overlay position ("left", "right", "center": "target"). +**/ + +(function() { + 'use strict'; + + function OverlayDirective($timeout, formHelper, overlayHelper, localizationService) { + + function link(scope, el, attr, ctrl) { + + scope.directive = { + enableConfirmButton: false + }; + + var overlayNumber = 0; + var numberOfOverlays = 0; + var isRegistered = false; + + var modelCopy = {}; + + function activate() { + + setView(); + + setButtonText(); + + modelCopy = makeModelCopy(scope.model); + + $timeout(function() { + + if (scope.position === "target") { + setTargetPosition(); + } + + // this has to be done inside a timeout to ensure the destroy + // event on other overlays is run before registering a new one + registerOverlay(); + + setOverlayIndent(); + + }); + + } + + function setView() { + + if (scope.view) { + + if (scope.view.indexOf(".html") === -1) { + var viewAlias = scope.view.toLowerCase(); + scope.view = "views/common/overlays/" + viewAlias + "/" + viewAlias + ".html"; + } + + } + + } + + function setButtonText() { + if (!scope.model.closeButtonLabelKey && !scope.model.closeButtonLabel) { + scope.model.closeButtonLabel = localizationService.localize("general_close"); + } + if (!scope.model.submitButtonLabelKey && !scope.model.submitButtonLabel) { + scope.model.submitButtonLabel = localizationService.localize("general_submit"); + } + } + + function registerOverlay() { + + overlayNumber = overlayHelper.registerOverlay(); + + $(document).bind("keydown.overlay-" + overlayNumber, function(event) { + + if (event.which === 27) { + + numberOfOverlays = overlayHelper.getNumberOfOverlays(); + + if(numberOfOverlays === overlayNumber) { + scope.closeOverLay(); + } + + event.preventDefault(); + } + + if (event.which === 13) { + + numberOfOverlays = overlayHelper.getNumberOfOverlays(); + + if(numberOfOverlays === overlayNumber) { + + var activeElementType = document.activeElement.tagName; + var clickableElements = ["A", "BUTTON"]; + var submitOnEnter = document.activeElement.hasAttribute("overlay-submit-on-enter"); + + if(clickableElements.indexOf(activeElementType) === 0) { + document.activeElement.click(); + event.preventDefault(); + } else if(activeElementType === "TEXTAREA" && !submitOnEnter) { + + + } else { + scope.$apply(function () { + scope.submitForm(scope.model); + }); + event.preventDefault(); + } + + } + + } + + }); + + isRegistered = true; + + } + + function unregisterOverlay() { + + if(isRegistered) { + + overlayHelper.unregisterOverlay(); + + $(document).unbind("keydown.overlay-" + overlayNumber); + + isRegistered = false; + } + + } + + function makeModelCopy(object) { + + var newObject = {}; + + for (var key in object) { + if (key !== "event") { + newObject[key] = angular.copy(object[key]); + } + } + + return newObject; + + } + + function setOverlayIndent() { + + var overlayIndex = overlayNumber - 1; + var indentSize = overlayIndex * 20; + var overlayWidth = el.context.clientWidth; + + el.css('width', overlayWidth - indentSize); + + if(scope.position === "center" || scope.position === "target") { + var overlayTopPosition = el.context.offsetTop; + el.css('top', overlayTopPosition + indentSize); + } + + } + + function setTargetPosition() { + + var container = $("#contentwrapper"); + var containerLeft = container[0].offsetLeft; + var containerRight = containerLeft + container[0].offsetWidth; + var containerTop = container[0].offsetTop; + var containerBottom = containerTop + container[0].offsetHeight; + + var mousePositionClickX = null; + var mousePositionClickY = null; + var elementHeight = null; + var elementWidth = null; + + var position = { + right: "inherit", + left: "inherit", + top: "inherit", + bottom: "inherit" + }; + + // if mouse click position is know place element with mouse in center + if (scope.model.event && scope.model.event) { + + // click position + mousePositionClickX = scope.model.event.pageX; + mousePositionClickY = scope.model.event.pageY; + + // element size + elementHeight = el.context.clientHeight; + elementWidth = el.context.clientWidth; + + // move element to this position + position.left = mousePositionClickX - (elementWidth / 2); + position.top = mousePositionClickY - (elementHeight / 2); + + // check to see if element is outside screen + // outside right + if (position.left + elementWidth > containerRight) { + position.right = 10; + position.left = "inherit"; + } + + // outside bottom + if (position.top + elementHeight > containerBottom) { + position.bottom = 10; + position.top = "inherit"; + } + + // outside left + if (position.left < containerLeft) { + position.left = containerLeft + 10; + position.right = "inherit"; + } + + // outside top + if (position.top < containerTop) { + position.top = 10; + position.bottom = "inherit"; + } + + el.css(position); + + } + + } + + scope.submitForm = function(model) { + if(scope.model.submit) { + if (formHelper.submitForm({scope: scope})) { + formHelper.resetForm({ scope: scope }); + + if(scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) { + scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton); + } else { + unregisterOverlay(); + scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton); + } + + } + } + }; + + scope.cancelConfirmSubmit = function() { + scope.model.confirmSubmit.show = false; + }; + + scope.closeOverLay = function() { + + unregisterOverlay(); + + if (scope.model.close) { + scope.model = modelCopy; + scope.model.close(scope.model); + } else { + scope.model.show = false; + scope.model = null; + } + + }; + + // angular does not support ng-show on custom directives + // width isolated scopes. So we have to make our own. + if (attr.hasOwnProperty("ngShow")) { + scope.$watch("ngShow", function(value) { + if (value) { + el.show(); + activate(); + } else { + unregisterOverlay(); + el.hide(); + } + }); + } else { + activate(); + } + + scope.$on('$destroy', function(){ + unregisterOverlay(); + }); + + } + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/overlays/umb-overlay.html', + scope: { + ngShow: "=", + model: "=", + view: "=", + position: "@" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbOverlay', OverlayDirective); + +})(); + +(function() { + 'use strict'; + + function OverlayBackdropDirective(overlayHelper) { + + function link(scope, el, attr, ctrl) { + + scope.numberOfOverlays = 0; + + scope.$watch(function(){ + return overlayHelper.getNumberOfOverlays(); + }, function (newValue) { + scope.numberOfOverlays = newValue; + }); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/overlays/umb-overlay-backdrop.html', + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbOverlayBackdrop', OverlayBackdropDirective); + +})(); + +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbProperty +* @restrict E +**/ +angular.module("umbraco.directives") + .directive('umbProperty', function (umbPropEditorHelper) { + return { + scope: { + property: "=" + }, + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/property/umb-property.html', + link: function(scope) { + scope.propertyAlias = Umbraco.Sys.ServerVariables.isDebuggingEnabled === true ? scope.property.alias : null; + }, + //Define a controller for this directive to expose APIs to other directives + controller: function ($scope, $timeout) { + + var self = this; + + //set the API properties/methods + + self.property = $scope.property; + self.setPropertyError = function(errorMsg) { + $scope.property.propertyErrorMessage = errorMsg; + }; + } + }; }); -/** -* @ngdoc directive -* @function -* @name umbraco.directives.directive:umbPropertyEditor -* @requires formController -* @restrict E -**/ - -//share property editor directive function -var _umbPropertyEditor = function (umbPropEditorHelper) { - return { - scope: { - model: "=", - isPreValue: "@", - preview: "@" - }, - - require: "^form", - restrict: 'E', - replace: true, - templateUrl: 'views/components/property/umb-property-editor.html', - link: function (scope, element, attrs, ctrl) { - - //we need to copy the form controller val to our isolated scope so that - //it get's carried down to the child scopes of this! - //we'll also maintain the current form name. - scope[ctrl.$name] = ctrl; - - if(!scope.model.alias){ - scope.model.alias = Math.random().toString(36).slice(2); - } - - scope.$watch("model.view", function(val){ - scope.propertyEditorView = umbPropEditorHelper.getViewPath(scope.model.view, scope.isPreValue); - }); - } - }; - }; - -//Preffered is the umb-property-editor as its more explicit - but we keep umb-editor for backwards compat -angular.module("umbraco.directives").directive('umbPropertyEditor', _umbPropertyEditor); -angular.module("umbraco.directives").directive('umbEditor', _umbPropertyEditor); +/** +* @ngdoc directive +* @function +* @name umbraco.directives.directive:umbPropertyEditor +* @requires formController +* @restrict E +**/ -angular.module("umbraco.directives.html") - .directive('umbPropertyGroup', function () { - return { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/property/umb-property-group.html' - }; +//share property editor directive function +var _umbPropertyEditor = function (umbPropEditorHelper) { + return { + scope: { + model: "=", + isPreValue: "@", + preview: "@" + }, + + require: "^form", + restrict: 'E', + replace: true, + templateUrl: 'views/components/property/umb-property-editor.html', + link: function (scope, element, attrs, ctrl) { + + //we need to copy the form controller val to our isolated scope so that + //it get's carried down to the child scopes of this! + //we'll also maintain the current form name. + scope[ctrl.$name] = ctrl; + + if(!scope.model.alias){ + scope.model.alias = Math.random().toString(36).slice(2); + } + + scope.$watch("model.view", function(val){ + scope.propertyEditorView = umbPropEditorHelper.getViewPath(scope.model.view, scope.isPreValue); + }); + } + }; + }; + +//Preffered is the umb-property-editor as its more explicit - but we keep umb-editor for backwards compat +angular.module("umbraco.directives").directive('umbPropertyEditor', _umbPropertyEditor); +angular.module("umbraco.directives").directive('umbEditor', _umbPropertyEditor); + +angular.module("umbraco.directives.html") + .directive('umbPropertyGroup', function () { + return { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/property/umb-property-group.html' + }; }); -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbTab -* @restrict E -**/ -angular.module("umbraco.directives") -.directive('umbTab', function ($parse, $timeout) { - return { - restrict: 'E', - replace: true, - transclude: 'true', - templateUrl: 'views/components/tabs/umb-tab.html' - }; -}); - -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbTabs -* @restrict A -* @description Used to bind to bootstrap tab events so that sub directives can use this API to listen to tab changes -**/ -angular.module("umbraco.directives") -.directive('umbTabs', function () { - return { - restrict: 'A', - controller: function ($scope, $element, $attrs) { - - var callbacks = []; - this.onTabShown = function(cb) { - callbacks.push(cb); - }; - - function tabShown(event) { - - var curr = $(event.target); // active tab - var prev = $(event.relatedTarget); // previous tab - - $scope.$apply(); - - for (var c in callbacks) { - callbacks[c].apply(this, [{current: curr, previous: prev}]); - } - } - - //NOTE: it MUST be done this way - binding to an ancestor element that exists - // in the DOM to bind to the dynamic elements that will be created. - // It would be nicer to create this event handler as a directive for which child - // directives can attach to. - $element.on('shown', '.nav-tabs a', tabShown); - - //ensure to unregister - $scope.$on('$destroy', function () { - $element.off('shown', '.nav-tabs a', tabShown); - - for (var c in callbacks) { - delete callbacks[c]; - } - callbacks = null; - }); - } - }; +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbTab +* @restrict E +**/ +angular.module("umbraco.directives") +.directive('umbTab', function ($parse, $timeout) { + return { + restrict: 'E', + replace: true, + transclude: 'true', + templateUrl: 'views/components/tabs/umb-tab.html' + }; }); -(function() { - 'use strict'; - - function UmbTabsContentDirective() { - - function link(scope, el, attr, ctrl) { - - scope.view = attr.view; - - } - - var directive = { - restrict: "E", - replace: true, - transclude: 'true', - templateUrl: "views/components/tabs/umb-tabs-content.html", - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbTabsContent', UmbTabsContentDirective); - -})(); -(function() { - 'use strict'; - - function UmbTabsNavDirective($timeout) { - - function link(scope, el, attr) { - - function activate() { - - $timeout(function () { - - //use bootstrap tabs API to show the first one - el.find("a:first").tab('show'); - - //enable the tab drop - el.tabdrop(); - - }); - - } - - var unbindModelWatch = scope.$watch('model', function(newValue, oldValue){ - - activate(); - - }); - - - scope.$on('$destroy', function () { - - //ensure to destroy tabdrop (unbinds window resize listeners) - el.tabdrop("destroy"); - - unbindModelWatch(); - - }); - - } - - var directive = { - restrict: "E", - replace: true, - templateUrl: "views/components/tabs/umb-tabs-nav.html", - scope: { - model: "=", - tabdrop: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbTabsNav', UmbTabsNavDirective); - -})(); +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbTabs +* @restrict A +* @description Used to bind to bootstrap tab events so that sub directives can use this API to listen to tab changes +**/ +angular.module("umbraco.directives") +.directive('umbTabs', function () { + return { + restrict: 'A', + controller: function ($scope, $element, $attrs) { + + var callbacks = []; + this.onTabShown = function(cb) { + callbacks.push(cb); + }; + + function tabShown(event) { + + var curr = $(event.target); // active tab + var prev = $(event.relatedTarget); // previous tab + + $scope.$apply(); + + for (var c in callbacks) { + callbacks[c].apply(this, [{current: curr, previous: prev}]); + } + } + + //NOTE: it MUST be done this way - binding to an ancestor element that exists + // in the DOM to bind to the dynamic elements that will be created. + // It would be nicer to create this event handler as a directive for which child + // directives can attach to. + $element.on('shown', '.nav-tabs a', tabShown); + + //ensure to unregister + $scope.$on('$destroy', function () { + $element.off('shown', '.nav-tabs a', tabShown); + + for (var c in callbacks) { + delete callbacks[c]; + } + callbacks = null; + }); + } + }; +}); +(function() { + 'use strict'; + + function UmbTabsContentDirective() { + + function link(scope, el, attr, ctrl) { + + scope.view = attr.view; + + } + + var directive = { + restrict: "E", + replace: true, + transclude: 'true', + templateUrl: "views/components/tabs/umb-tabs-content.html", + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbTabsContent', UmbTabsContentDirective); + +})(); + +(function() { + 'use strict'; + + function UmbTabsNavDirective($timeout) { + + function link(scope, el, attr) { + + function activate() { + + $timeout(function () { + + //use bootstrap tabs API to show the first one + el.find("a:first").tab('show'); + + //enable the tab drop + el.tabdrop(); + + }); + + } + + var unbindModelWatch = scope.$watch('model', function(newValue, oldValue){ + + activate(); + + }); + + + scope.$on('$destroy', function () { + + //ensure to destroy tabdrop (unbinds window resize listeners) + el.tabdrop("destroy"); + + unbindModelWatch(); + + }); + + } + + var directive = { + restrict: "E", + replace: true, + templateUrl: "views/components/tabs/umb-tabs-nav.html", + scope: { + model: "=", + tabdrop: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbTabsNav', UmbTabsNavDirective); + +})(); /** * @ngdoc directive @@ -6107,547 +6165,547 @@ angular.module("umbraco.directives") }; }); -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbTreeSearchBox -* @function -* @element ANY -* @restrict E -**/ -function treeSearchBox(localizationService, searchService, $q) { - return { - scope: { - searchFromId: "@", - searchFromName: "@", - showSearch: "@", - section: "@", - hideSearchCallback: "=", - searchCallback: "=" - }, - restrict: "E", // restrict to an element - replace: true, // replace the html element with the template - templateUrl: 'views/components/tree/umb-tree-search-box.html', - link: function (scope, element, attrs, ctrl) { - - scope.term = ""; - scope.hideSearch = function() { - scope.term = ""; - scope.hideSearchCallback(); - }; - - localizationService.localize("general_typeToSearch").then(function (value) { - scope.searchPlaceholderText = value; - }); - - if (!scope.showSearch) { - scope.showSearch = "false"; - } - - //used to cancel any request in progress if another one needs to take it's place - var canceler = null; - - function performSearch() { - if (scope.term) { - scope.results = []; - - //a canceler exists, so perform the cancelation operation and reset - if (canceler) { - canceler.resolve(); - canceler = $q.defer(); - } - else { - canceler = $q.defer(); - } - - var searchArgs = { - term: scope.term, - canceler: canceler - }; - - //append a start node context if there is one - if (scope.searchFromId) { - searchArgs["searchFrom"] = scope.searchFromId; - } - - searcher(searchArgs).then(function (data) { - scope.searchCallback(data); - //set back to null so it can be re-created - canceler = null; - }); - } - } - - scope.$watch("term", _.debounce(function(newVal, oldVal) { - scope.$apply(function() { - if (newVal !== null && newVal !== undefined && newVal !== oldVal) { - performSearch(); - } - }); - }, 200)); - - var searcher = searchService.searchContent; - //search - if (scope.section === "member") { - searcher = searchService.searchMembers; - } - else if (scope.section === "media") { - searcher = searchService.searchMedia; - } - } - }; -} -angular.module('umbraco.directives').directive("umbTreeSearchBox", treeSearchBox); +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbTreeSearchBox +* @function +* @element ANY +* @restrict E +**/ +function treeSearchBox(localizationService, searchService, $q) { + return { + scope: { + searchFromId: "@", + searchFromName: "@", + showSearch: "@", + section: "@", + hideSearchCallback: "=", + searchCallback: "=" + }, + restrict: "E", // restrict to an element + replace: true, // replace the html element with the template + templateUrl: 'views/components/tree/umb-tree-search-box.html', + link: function (scope, element, attrs, ctrl) { -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbTreeSearchResults -* @function -* @element ANY -* @restrict E -**/ -function treeSearchResults() { - return { - scope: { - results: "=", - selectResultCallback: "=" - }, - restrict: "E", // restrict to an element - replace: true, // replace the html element with the template - templateUrl: 'views/components/tree/umb-tree-search-results.html', - link: function (scope, element, attrs, ctrl) { - - } - }; -} -angular.module('umbraco.directives').directive("umbTreeSearchResults", treeSearchResults); + scope.term = ""; + scope.hideSearch = function() { + scope.term = ""; + scope.hideSearchCallback(); + }; -/** -@ngdoc directive -@name umbraco.directives.directive:umbGenerateAlias -@restrict E -@scope - -@description -Use this directive to generate a camelCased umbraco alias. -When the aliasFrom value is changed the directive will get a formatted alias from the server and update the alias model. If "enableLock" is set to true -the directive will use {@link umbraco.directives.directive:umbLockedField umbLockedField} to lock and unlock the alias. - -

Markup example

-
-    
- - - - - - -
-
- -

Controller example

-
-    (function () {
-        "use strict";
-
-        function Controller() {
-
-            var vm = this;
-
-            vm.name = "";
-            vm.alias = "";
-
-        }
-
-        angular.module("umbraco").controller("My.Controller", Controller);
-    })();
-
- -@param {string} alias (binding): The model where the alias is bound. -@param {string} aliasFrom (binding): The model to generate the alias from. -@param {boolean=} enableLock (binding): Set to true to add a lock next to the alias from where it can be unlocked and changed. -**/ - -angular.module("umbraco.directives") - .directive('umbGenerateAlias', function ($timeout, entityResource) { - return { - restrict: 'E', - templateUrl: 'views/components/umb-generate-alias.html', - replace: true, - scope: { - alias: '=', - aliasFrom: '=', - enableLock: '=?', - serverValidationField: '@' - }, - link: function (scope, element, attrs, ctrl) { - - var eventBindings = []; - var bindWatcher = true; - var generateAliasTimeout = ""; - var updateAlias = false; - - scope.locked = true; - scope.placeholderText = "Enter alias..."; - - function generateAlias(value) { - - if (generateAliasTimeout) { - $timeout.cancel(generateAliasTimeout); - } - - if( value !== undefined && value !== "" && value !== null) { - - scope.alias = ""; - scope.placeholderText = "Generating Alias..."; - - generateAliasTimeout = $timeout(function () { - updateAlias = true; - entityResource.getSafeAlias(value, true).then(function (safeAlias) { - if (updateAlias) { - scope.alias = safeAlias.alias; - } - }); - }, 500); - - } else { - updateAlias = true; - scope.alias = ""; - scope.placeholderText = "Enter alias..."; - } - - } - - // if alias gets unlocked - stop watching alias - eventBindings.push(scope.$watch('locked', function(newValue, oldValue){ - if(newValue === false) { - bindWatcher = false; - } - })); - - // validate custom entered alias - eventBindings.push(scope.$watch('alias', function(newValue, oldValue){ - - if(scope.alias === "" && bindWatcher === true || scope.alias === null && bindWatcher === true) { - // add watcher - eventBindings.push(scope.$watch('aliasFrom', function(newValue, oldValue) { - if(bindWatcher) { - generateAlias(newValue); - } - })); - } - - })); - - // clean up - scope.$on('$destroy', function(){ - // unbind watchers - for(var e in eventBindings) { - eventBindings[e](); - } - }); - - } - }; - }); + localizationService.localize("general_typeToSearch").then(function (value) { + scope.searchPlaceholderText = value; + }); -/** -@ngdoc directive -@name umbraco.directives.directive:umbAvatar -@restrict E -@scope - -@description -Use this directive to render an avatar. - -

Markup example

-
-	
- - - - -
-
- -

Controller example

-
-	(function () {
-		"use strict";
-
-		function Controller() {
-
-            var vm = this;
-
-            vm.avatar = [
-                { value: "assets/logo.png" },
-                { value: "assets/logo@2x.png" },
-                { value: "assets/logo@3x.png" }
-            ];
-
-        }
-
-		angular.module("umbraco").controller("My.Controller", Controller);
-
-	})();
-
- -@param {string} size (attribute): The size of the avatar (xs, s, m, l, xl). -@param {string} img-src (attribute): The image source to the avatar. -@param {string} img-srcset (atribute): Reponsive support for the image source. -**/ - -(function() { - 'use strict'; - - function AvatarDirective() { - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-avatar.html', - scope: { - size: "@", - imgSrc: "@", - imgSrcset: "@" - } - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbAvatar', AvatarDirective); - -})(); + if (!scope.showSearch) { + scope.showSearch = "false"; + } -/** -@ngdoc directive -@name umbraco.directives.directive:umbChildSelector -@restrict E -@scope - -@description -Use this directive to render a ui component for selecting child items to a parent node. - -

Markup example

-
-	
- - - - - - - - -
-
- -

Controller example

-
-	(function () {
-		"use strict";
-
-		function Controller() {
-
-            var vm = this;
-
-            vm.id = 1;
-            vm.name = "My Parent element";
-            vm.icon = "icon-document";
-            vm.selectedChildren = [];
-            vm.availableChildren = [
-                {
-                    id: 1,
-                    alias: "item1",
-                    name: "Item 1",
-                    icon: "icon-document"
-                },
-                {
-                    id: 2,
-                    alias: "item2",
-                    name: "Item 2",
-                    icon: "icon-document"
-                }
-            ];
-
-            vm.addChild = addChild;
-            vm.removeChild = removeChild;
-
-            function addChild($event) {
-                vm.overlay = {
-                    view: "itempicker",
-                    title: "Choose child",
-                    availableItems: vm.availableChildren,
-                    selectedItems: vm.selectedChildren,
-                    event: $event,
-                    show: true,
-                    submit: function(model) {
-
-                        // add selected child
-                        vm.selectedChildren.push(model.selectedItem);
-
-                        // close overlay
-                        vm.overlay.show = false;
-                        vm.overlay = null;
-                    }
-                };
-            }
-
-            function removeChild($index) {
-                vm.selectedChildren.splice($index, 1);
-            }
-
-        }
-
-		angular.module("umbraco").controller("My.Controller", Controller);
-
-	})();
-
- -@param {array} selectedChildren (binding): Array of selected children. -@param {array} availableChildren (binding: Array of items available for selection. -@param {string} parentName (binding): The parent name. -@param {string} parentIcon (binding): The parent icon. -@param {number} parentId (binding): The parent id. -@param {callback} onRemove (binding): Callback when the remove button is clicked on an item. -

The callback returns:

-
    -
  • child: The selected item.
  • -
  • $index: The selected item index.
  • -
-@param {callback} onAdd (binding): Callback when the add button is clicked. -

The callback returns:

-
    -
  • $event: The select event.
  • -
-**/ - -(function() { - 'use strict'; - - function ChildSelectorDirective() { - - function link(scope, el, attr, ctrl) { - - var eventBindings = []; - scope.dialogModel = {}; - scope.showDialog = false; - - scope.removeChild = function(selectedChild, $index) { - if(scope.onRemove) { - scope.onRemove(selectedChild, $index); - } - }; - - scope.addChild = function($event) { - if(scope.onAdd) { - scope.onAdd($event); - } - }; - - function syncParentName() { - - // update name on available item - angular.forEach(scope.availableChildren, function(availableChild){ - if(availableChild.id === scope.parentId) { - availableChild.name = scope.parentName; - } - }); - - // update name on selected child - angular.forEach(scope.selectedChildren, function(selectedChild){ - if(selectedChild.id === scope.parentId) { - selectedChild.name = scope.parentName; - } - }); - - } - - function syncParentIcon() { - - // update icon on available item - angular.forEach(scope.availableChildren, function(availableChild){ - if(availableChild.id === scope.parentId) { - availableChild.icon = scope.parentIcon; - } - }); - - // update icon on selected child - angular.forEach(scope.selectedChildren, function(selectedChild){ - if(selectedChild.id === scope.parentId) { - selectedChild.icon = scope.parentIcon; - } - }); - - } - - eventBindings.push(scope.$watch('parentName', function(newValue, oldValue){ - - if (newValue === oldValue) { return; } - if ( oldValue === undefined || newValue === undefined) { return; } - - syncParentName(); - - })); - - eventBindings.push(scope.$watch('parentIcon', function(newValue, oldValue){ - - if (newValue === oldValue) { return; } - if ( oldValue === undefined || newValue === undefined) { return; } - - syncParentIcon(); - })); - - // clean up - scope.$on('$destroy', function(){ - // unbind watchers - for(var e in eventBindings) { - eventBindings[e](); - } - }); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-child-selector.html', - scope: { - selectedChildren: '=', - availableChildren: "=", - parentName: "=", - parentIcon: "=", - parentId: "=", - onRemove: "=", - onAdd: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbChildSelector', ChildSelectorDirective); - -})(); + //used to cancel any request in progress if another one needs to take it's place + var canceler = null; + + function performSearch() { + if (scope.term) { + scope.results = []; + + //a canceler exists, so perform the cancelation operation and reset + if (canceler) { + canceler.resolve(); + canceler = $q.defer(); + } + else { + canceler = $q.defer(); + } + + var searchArgs = { + term: scope.term, + canceler: canceler + }; + + //append a start node context if there is one + if (scope.searchFromId) { + searchArgs["searchFrom"] = scope.searchFromId; + } + + searcher(searchArgs).then(function (data) { + scope.searchCallback(data); + //set back to null so it can be re-created + canceler = null; + }); + } + } + + scope.$watch("term", _.debounce(function(newVal, oldVal) { + scope.$apply(function() { + if (newVal !== null && newVal !== undefined && newVal !== oldVal) { + performSearch(); + } + }); + }, 200)); + + var searcher = searchService.searchContent; + //search + if (scope.section === "member") { + searcher = searchService.searchMembers; + } + else if (scope.section === "media") { + searcher = searchService.searchMedia; + } + } + }; +} +angular.module('umbraco.directives').directive("umbTreeSearchBox", treeSearchBox); + +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbTreeSearchResults +* @function +* @element ANY +* @restrict E +**/ +function treeSearchResults() { + return { + scope: { + results: "=", + selectResultCallback: "=" + }, + restrict: "E", // restrict to an element + replace: true, // replace the html element with the template + templateUrl: 'views/components/tree/umb-tree-search-results.html', + link: function (scope, element, attrs, ctrl) { + + } + }; +} +angular.module('umbraco.directives').directive("umbTreeSearchResults", treeSearchResults); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbGenerateAlias +@restrict E +@scope + +@description +Use this directive to generate a camelCased umbraco alias. +When the aliasFrom value is changed the directive will get a formatted alias from the server and update the alias model. If "enableLock" is set to true +the directive will use {@link umbraco.directives.directive:umbLockedField umbLockedField} to lock and unlock the alias. + +

Markup example

+
+    
+ + + + + + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+            vm.name = "";
+            vm.alias = "";
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {string} alias (binding): The model where the alias is bound. +@param {string} aliasFrom (binding): The model to generate the alias from. +@param {boolean=} enableLock (binding): Set to true to add a lock next to the alias from where it can be unlocked and changed. +**/ + +angular.module("umbraco.directives") + .directive('umbGenerateAlias', function ($timeout, entityResource) { + return { + restrict: 'E', + templateUrl: 'views/components/umb-generate-alias.html', + replace: true, + scope: { + alias: '=', + aliasFrom: '=', + enableLock: '=?', + serverValidationField: '@' + }, + link: function (scope, element, attrs, ctrl) { + + var eventBindings = []; + var bindWatcher = true; + var generateAliasTimeout = ""; + var updateAlias = false; + + scope.locked = true; + scope.placeholderText = "Enter alias..."; + + function generateAlias(value) { + + if (generateAliasTimeout) { + $timeout.cancel(generateAliasTimeout); + } + + if( value !== undefined && value !== "" && value !== null) { + + scope.alias = ""; + scope.placeholderText = "Generating Alias..."; + + generateAliasTimeout = $timeout(function () { + updateAlias = true; + entityResource.getSafeAlias(value, true).then(function (safeAlias) { + if (updateAlias) { + scope.alias = safeAlias.alias; + } + }); + }, 500); + + } else { + updateAlias = true; + scope.alias = ""; + scope.placeholderText = "Enter alias..."; + } + + } + + // if alias gets unlocked - stop watching alias + eventBindings.push(scope.$watch('locked', function(newValue, oldValue){ + if(newValue === false) { + bindWatcher = false; + } + })); + + // validate custom entered alias + eventBindings.push(scope.$watch('alias', function(newValue, oldValue){ + + if(scope.alias === "" && bindWatcher === true || scope.alias === null && bindWatcher === true) { + // add watcher + eventBindings.push(scope.$watch('aliasFrom', function(newValue, oldValue) { + if(bindWatcher) { + generateAlias(newValue); + } + })); + } + + })); + + // clean up + scope.$on('$destroy', function(){ + // unbind watchers + for(var e in eventBindings) { + eventBindings[e](); + } + }); + + } + }; + }); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbAvatar +@restrict E +@scope + +@description +Use this directive to render an avatar. + +

Markup example

+
+	
+ + + + +
+
+ +

Controller example

+
+	(function () {
+		"use strict";
+
+		function Controller() {
+
+            var vm = this;
+
+            vm.avatar = [
+                { value: "assets/logo.png" },
+                { value: "assets/logo@2x.png" },
+                { value: "assets/logo@3x.png" }
+            ];
+
+        }
+
+		angular.module("umbraco").controller("My.Controller", Controller);
+
+	})();
+
+ +@param {string} size (attribute): The size of the avatar (xs, s, m, l, xl). +@param {string} img-src (attribute): The image source to the avatar. +@param {string} img-srcset (atribute): Reponsive support for the image source. +**/ + +(function() { + 'use strict'; + + function AvatarDirective() { + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-avatar.html', + scope: { + size: "@", + imgSrc: "@", + imgSrcset: "@" + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbAvatar', AvatarDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbChildSelector +@restrict E +@scope + +@description +Use this directive to render a ui component for selecting child items to a parent node. + +

Markup example

+
+	
+ + + + + + + + +
+
+ +

Controller example

+
+	(function () {
+		"use strict";
+
+		function Controller() {
+
+            var vm = this;
+
+            vm.id = 1;
+            vm.name = "My Parent element";
+            vm.icon = "icon-document";
+            vm.selectedChildren = [];
+            vm.availableChildren = [
+                {
+                    id: 1,
+                    alias: "item1",
+                    name: "Item 1",
+                    icon: "icon-document"
+                },
+                {
+                    id: 2,
+                    alias: "item2",
+                    name: "Item 2",
+                    icon: "icon-document"
+                }
+            ];
+
+            vm.addChild = addChild;
+            vm.removeChild = removeChild;
+
+            function addChild($event) {
+                vm.overlay = {
+                    view: "itempicker",
+                    title: "Choose child",
+                    availableItems: vm.availableChildren,
+                    selectedItems: vm.selectedChildren,
+                    event: $event,
+                    show: true,
+                    submit: function(model) {
+
+                        // add selected child
+                        vm.selectedChildren.push(model.selectedItem);
+
+                        // close overlay
+                        vm.overlay.show = false;
+                        vm.overlay = null;
+                    }
+                };
+            }
+
+            function removeChild($index) {
+                vm.selectedChildren.splice($index, 1);
+            }
+
+        }
+
+		angular.module("umbraco").controller("My.Controller", Controller);
+
+	})();
+
+ +@param {array} selectedChildren (binding): Array of selected children. +@param {array} availableChildren (binding: Array of items available for selection. +@param {string} parentName (binding): The parent name. +@param {string} parentIcon (binding): The parent icon. +@param {number} parentId (binding): The parent id. +@param {callback} onRemove (binding): Callback when the remove button is clicked on an item. +

The callback returns:

+
    +
  • child: The selected item.
  • +
  • $index: The selected item index.
  • +
+@param {callback} onAdd (binding): Callback when the add button is clicked. +

The callback returns:

+
    +
  • $event: The select event.
  • +
+**/ + +(function() { + 'use strict'; + + function ChildSelectorDirective() { + + function link(scope, el, attr, ctrl) { + + var eventBindings = []; + scope.dialogModel = {}; + scope.showDialog = false; + + scope.removeChild = function(selectedChild, $index) { + if(scope.onRemove) { + scope.onRemove(selectedChild, $index); + } + }; + + scope.addChild = function($event) { + if(scope.onAdd) { + scope.onAdd($event); + } + }; + + function syncParentName() { + + // update name on available item + angular.forEach(scope.availableChildren, function(availableChild){ + if(availableChild.id === scope.parentId) { + availableChild.name = scope.parentName; + } + }); + + // update name on selected child + angular.forEach(scope.selectedChildren, function(selectedChild){ + if(selectedChild.id === scope.parentId) { + selectedChild.name = scope.parentName; + } + }); + + } + + function syncParentIcon() { + + // update icon on available item + angular.forEach(scope.availableChildren, function(availableChild){ + if(availableChild.id === scope.parentId) { + availableChild.icon = scope.parentIcon; + } + }); + + // update icon on selected child + angular.forEach(scope.selectedChildren, function(selectedChild){ + if(selectedChild.id === scope.parentId) { + selectedChild.icon = scope.parentIcon; + } + }); + + } + + eventBindings.push(scope.$watch('parentName', function(newValue, oldValue){ + + if (newValue === oldValue) { return; } + if ( oldValue === undefined || newValue === undefined) { return; } + + syncParentName(); + + })); + + eventBindings.push(scope.$watch('parentIcon', function(newValue, oldValue){ + + if (newValue === oldValue) { return; } + if ( oldValue === undefined || newValue === undefined) { return; } + + syncParentIcon(); + })); + + // clean up + scope.$on('$destroy', function(){ + // unbind watchers + for(var e in eventBindings) { + eventBindings[e](); + } + }); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-child-selector.html', + scope: { + selectedChildren: '=', + availableChildren: "=", + parentName: "=", + parentIcon: "=", + parentId: "=", + onRemove: "=", + onAdd: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbChildSelector', ChildSelectorDirective); + +})(); /** * @ngdoc directive @@ -6675,2806 +6733,2806 @@ function confirmDirective() { } angular.module('umbraco.directives').directive("umbConfirm", confirmDirective); -/** -@ngdoc directive -@name umbraco.directives.directive:umbConfirmAction -@restrict E -@scope - -@description -

Use this directive to toggle a confirmation prompt for an action. -The prompt consists of a checkmark and a cross to confirm or cancel the action. -The prompt can be opened in four direction up, down, left or right.

- -

Markup example

-
-    
- -
- - - -
- -
-
- -

Controller example

-
-    (function () {
-
-        "use strict";
-
-        function Controller() {
-
-            var vm = this;
-            vm.promptIsVisible = false;
-
-            vm.confirmAction = confirmAction;
-            vm.showPrompt = showPrompt;
-            vm.hidePrompt = hidePrompt;
-
-            function confirmAction() {
-                // confirm logic here
-            }
-
-            function showPrompt() {
-                vm.promptIsVisible = true;
-            }
-
-            function hidePrompt() {
-                vm.promptIsVisible = false;
-            }
-
-        }
-
-        angular.module("umbraco").controller("My.Controller", Controller);
-    })();
-
- -@param {string} direction The direction the prompt opens ("up", "down", "left", "right"). -@param {callback} onConfirm Callback when the checkmark is clicked. -@param {callback} onCancel Callback when the cross is clicked. -**/ - -(function() { - 'use strict'; - - function ConfirmAction() { - - function link(scope, el, attr, ctrl) { - - scope.clickConfirm = function() { - if(scope.onConfirm) { - scope.onConfirm(); - } - }; - - scope.clickCancel = function() { - if(scope.onCancel) { - scope.onCancel(); - } - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-confirm-action.html', - scope: { - direction: "@", - onConfirm: "&", - onCancel: "&" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbConfirmAction', ConfirmAction); - -})(); +/** +@ngdoc directive +@name umbraco.directives.directive:umbConfirmAction +@restrict E +@scope -/** -@ngdoc directive -@name umbraco.directives.directive:umbContentGrid -@restrict E -@scope - -@description -Use this directive to generate a list of content items presented as a flexbox grid. - -

Markup example

-
-    
- - - - -
-
- -

Controller example

-
-    (function () {
-        "use strict";
-
-        function Controller() {
-
-            var vm = this;
-            vm.contentItems = [
-                {
-                    "name": "Cape",
-                    "published": true,
-                    "icon": "icon-document",
-                    "updateDate": "15-02-2016",
-                    "owner": "Mr. Batman",
-                    "selected": false
-                },
-                {
-                    "name": "Utility Belt",
-                    "published": true,
-                    "icon": "icon-document",
-                    "updateDate": "15-02-2016",
-                    "owner": "Mr. Batman",
-                    "selected": false
-                },
-                {
-                    "name": "Cave",
-                    "published": true,
-                    "icon": "icon-document",
-                    "updateDate": "15-02-2016",
-                    "owner": "Mr. Batman",
-                    "selected": false
-                }
-            ];
-            vm.includeProperties = [
-                {
-                  "alias": "updateDate",
-                  "header": "Last edited"
-                },
-                {
-                  "alias": "owner",
-                  "header": "Created by"
-                }
-            ];
-
-            vm.clickItem = clickItem;
-            vm.selectItem = selectItem;
-
-
-            function clickItem(item, $event, $index){
-                // do magic here
-            }
-
-            function selectItem(item, $event, $index) {
-                // set item.selected = true; to select the item
-                // do magic here
-            }
-
-        }
-
-        angular.module("umbraco").controller("My.Controller", Controller);
-    })();
-
- -@param {array} content (binding): Array of content items. -@param {array=} contentProperties (binding): Array of content item properties to include in the item. If left empty the item will only show the item icon and name. -@param {callback=} onClick (binding): Callback method to handle click events on the content item. -

The callback returns:

-
    -
  • item: The clicked item
  • -
  • $event: The select event
  • -
  • $index: The item index
  • -
-@param {callback=} onClickName (binding): Callback method to handle click events on the checkmark icon. -

The callback returns:

-
    -
  • item: The selected item
  • -
  • $event: The select event
  • -
  • $index: The item index
  • -
-**/ - -(function() { - 'use strict'; - - function ContentGridDirective() { - - function link(scope, el, attr, ctrl) { - - scope.clickItem = function(item, $event, $index) { - if(scope.onClick) { - scope.onClick(item, $event, $index); - } - }; - - scope.clickItemName = function(item, $event, $index) { - if(scope.onClickName) { - scope.onClickName(item, $event, $index); - } - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-content-grid.html', - scope: { - content: '=', - contentProperties: "=", - onClick: "=", - onClickName: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbContentGrid', ContentGridDirective); - -})(); +@description +

Use this directive to toggle a confirmation prompt for an action. +The prompt consists of a checkmark and a cross to confirm or cancel the action. +The prompt can be opened in four direction up, down, left or right.

-(function() { - 'use strict'; - - function UmbDisableFormValidation() { - - var directive = { - restrict: 'A', - require: '?form', - link: function (scope, elm, attrs, ctrl) { - //override the $setValidity function of the form to disable validation - ctrl.$setValidity = function () { }; - } - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbDisableFormValidation', UmbDisableFormValidation); - -})(); +

Markup example

+
+    
-/** -@ngdoc directive -@name umbraco.directives.directive:umbEmptyState -@restrict E -@scope - -@description -Use this directive to show an empty state message. - -

Markup example

-
-    
- - - // Empty state content - - -
-
- -@param {string=} size Set the size of the text ("small", "large"). -@param {string=} position Set the position of the text ("center"). -**/ - -(function() { - 'use strict'; - - function EmptyStateDirective() { - - var directive = { - restrict: 'E', - replace: true, - transclude: true, - templateUrl: 'views/components/umb-empty-state.html', - scope: { - size: '@', - position: '@' - } - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbEmptyState', EmptyStateDirective); - -})(); +
+ + + +
-/** -@ngdoc directive -@name umbraco.directives.directive:umbFolderGrid -@restrict E -@scope - -@description -Use this directive to generate a list of folders presented as a flexbox grid. - -

Markup example

-
-    
- - -
-
- -

Controller example

-
-    (function () {
-        "use strict";
-
-        function Controller(myService) {
-
-            var vm = this;
-            vm.folders = [
-                {
-                    "name": "Folder 1",
-                    "icon": "icon-folder",
-                    "selected": false
-                },
-                {
-                    "name": "Folder 2",
-                    "icon": "icon-folder",
-                    "selected": false
-                }
-
-            ];
-
-            vm.clickFolder = clickFolder;
-            vm.selectFolder = selectFolder;
-
-            myService.getFolders().then(function(folders){
-                vm.folders = folders;
-            });
-
-            function clickFolder(folder){
-                // Execute when clicking folder name/link
-            }
-
-            function selectFolder(folder, event, index) {
-                // Execute when clicking folder
-                // set folder.selected = true; to show checkmark icon
-            }
-
-        }
-
-        angular.module("umbraco").controller("My.Controller", Controller);
-    })();
-
- -@param {array} folders (binding): Array of folders -@param {callback=} onClick (binding): Callback method to handle click events on the folder. -

The callback returns:

-
    -
  • folder: The selected folder
  • -
-@param {callback=} onSelect (binding): Callback method to handle click events on the checkmark icon. -

The callback returns:

-
    -
  • folder: The selected folder
  • -
  • $event: The select event
  • -
  • $index: The folder index
  • -
-**/ - -(function() { - 'use strict'; - - function FolderGridDirective() { - - function link(scope, el, attr, ctrl) { - - scope.clickFolder = function(folder, $event, $index) { - if(scope.onClick) { - scope.onClick(folder, $event, $index); - } - }; - - scope.clickFolderName = function(folder, $event, $index) { - if(scope.onClickName) { - scope.onClickName(folder, $event, $index); - $event.stopPropagation(); - } - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-folder-grid.html', - scope: { - folders: '=', - onClick: "=", - onClickName: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbFolderGrid', FolderGridDirective); - -})(); +
+
-(function() { - 'use strict'; - - function GridSelector() { - - function link(scope, el, attr, ctrl) { - - var eventBindings = []; - scope.dialogModel = {}; - scope.showDialog = false; - scope.itemLabel = ""; - - // set default item name - if(!scope.itemName){ - scope.itemLabel = "item"; - } else { - scope.itemLabel = scope.itemName; - } - - scope.removeItem = function(selectedItem) { - var selectedItemIndex = scope.selectedItems.indexOf(selectedItem); - scope.selectedItems.splice(selectedItemIndex, 1); - }; - - scope.removeDefaultItem = function() { - - // it will be the last item so we can clear the array - scope.selectedItems = []; - - // remove as default item - scope.defaultItem = null; - - }; - - scope.openItemPicker = function($event){ - scope.dialogModel = { - view: "itempicker", - title: "Choose " + scope.itemLabel, - availableItems: scope.availableItems, - selectedItems: scope.selectedItems, - event: $event, - show: true, - submit: function(model) { - scope.selectedItems.push(model.selectedItem); - - // if no default item - set item as default - if(scope.defaultItem === null) { - scope.setAsDefaultItem(model.selectedItem); - } - - scope.dialogModel.show = false; - scope.dialogModel = null; - } - }; - }; - - scope.setAsDefaultItem = function(selectedItem) { - - // clear default item - scope.defaultItem = {}; - - // set as default item - scope.defaultItem = selectedItem; - }; - - function updatePlaceholders() { - - // update default item - if(scope.defaultItem !== null && scope.defaultItem.placeholder) { - - scope.defaultItem.name = scope.name; - - if(scope.alias !== null && scope.alias !== undefined) { - scope.defaultItem.alias = scope.alias; - } - - } - - // update selected items - angular.forEach(scope.selectedItems, function(selectedItem) { - if(selectedItem.placeholder) { - - selectedItem.name = scope.name; - - if(scope.alias !== null && scope.alias !== undefined) { - selectedItem.alias = scope.alias; - } - - } - }); - - // update availableItems - angular.forEach(scope.availableItems, function(availableItem) { - if(availableItem.placeholder) { - - availableItem.name = scope.name; - - if(scope.alias !== null && scope.alias !== undefined) { - availableItem.alias = scope.alias; - } - - } - }); - - } - - function activate() { - - // add watchers for updating placeholde name and alias - if(scope.updatePlaceholder) { - eventBindings.push(scope.$watch('name', function(newValue, oldValue){ - updatePlaceholders(); - })); - - eventBindings.push(scope.$watch('alias', function(newValue, oldValue){ - updatePlaceholders(); - })); - } - - } - - activate(); - - // clean up - scope.$on('$destroy', function(){ - - // clear watchers - for(var e in eventBindings) { - eventBindings[e](); - } - - }); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-grid-selector.html', - scope: { - name: "=", - alias: "=", - selectedItems: '=', - availableItems: "=", - defaultItem: "=", - itemName: "@", - updatePlaceholder: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbGridSelector', GridSelector); - -})(); +

Controller example

+
+    (function () {
 
-(function() {
-  'use strict';
-
-  function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, localizationService) {
-
-    function link(scope, el, attr, ctrl) {
-
-        var validationTranslated = "";
-        var tabNoSortOrderTranslated = "";
-
-      scope.sortingMode = false;
-      scope.toolbar = [];
-      scope.sortableOptionsGroup = {};
-      scope.sortableOptionsProperty = {};
-      scope.sortingButtonKey = "general_reorder";
-
-      function activate() {
-
-          setSortingOptions();
-
-          // set placeholder property on each group
-          if (scope.model.groups.length !== 0) {
-            angular.forEach(scope.model.groups, function(group) {
-              addInitProperty(group);
-            });
-          }
-
-          // add init tab
-          addInitGroup(scope.model.groups);
-
-          activateFirstGroup(scope.model.groups);
-
-          // localize texts
-          localizationService.localize("validation_validation").then(function(value) {
-              validationTranslated = value;
-          });
-
-          localizationService.localize("contentTypeEditor_tabHasNoSortOrder").then(function(value) {
-              tabNoSortOrderTranslated = value;
-          });
-      }
-
-      function setSortingOptions() {
-
-        scope.sortableOptionsGroup = {
-          distance: 10,
-          tolerance: "pointer",
-          opacity: 0.7,
-          scroll: true,
-          cursor: "move",
-          placeholder: "umb-group-builder__group-sortable-placeholder",
-          zIndex: 6000,
-          handle: ".umb-group-builder__group-handle",
-          items: ".umb-group-builder__group-sortable",
-          start: function(e, ui) {
-            ui.placeholder.height(ui.item.height());
-          },
-          stop: function(e, ui) {
-            updateTabsSortOrder();
-          },
-        };
-
-        scope.sortableOptionsProperty = {
-          distance: 10,
-          tolerance: "pointer",
-          connectWith: ".umb-group-builder__properties",
-          opacity: 0.7,
-          scroll: true,
-          cursor: "move",
-          placeholder: "umb-group-builder__property_sortable-placeholder",
-          zIndex: 6000,
-          handle: ".umb-group-builder__property-handle",
-          items: ".umb-group-builder__property-sortable",
-          start: function(e, ui) {
-            ui.placeholder.height(ui.item.height());
-          },
-          stop: function(e, ui) {
-            updatePropertiesSortOrder();
-          }
-        };
-
-      }
-
-      function updateTabsSortOrder() {
-
-        var first = true;
-        var prevSortOrder = 0;
-
-        scope.model.groups.map(function(group){
-
-          var index = scope.model.groups.indexOf(group);
-
-          if(group.tabState !== "init") {
-
-            // set the first not inherited tab to sort order 0
-            if(!group.inherited && first) {
-
-              // set the first tab sort order to 0 if prev is 0
-              if( prevSortOrder === 0 ) {
-                group.sortOrder = 0;
-              // when the first tab is inherited and sort order is not 0
-              } else {
-                group.sortOrder = prevSortOrder + 1;
-              }
-
-              first = false;
-
-            } else if(!group.inherited && !first) {
-
-                // find next group
-                var nextGroup = scope.model.groups[index + 1];
-
-                // if a groups is dropped in the middle of to groups with
-                // same sort order. Give it the dropped group same sort order
-                if( prevSortOrder === nextGroup.sortOrder ) {
-                  group.sortOrder = prevSortOrder;
-                } else {
-                  group.sortOrder = prevSortOrder + 1;
-                }
-
-            }
-
-            // store this tabs sort order as reference for the next
-            prevSortOrder = group.sortOrder;
-
-          }
-
-        });
-
-      }
-
-        function filterAvailableCompositions(selectedContentType, selecting) {
-
-            //selecting = true if the user has check the item, false if the user has unchecked the item
-
-            var selectedContentTypeAliases = selecting ?
-                //the user has selected the item so add to the current list
-                _.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) :
-                //the user has unselected the item so remove from the current list
-                _.reject(scope.compositionsDialogModel.compositeContentTypes, function(i) {
-                    return i === selectedContentType.alias;
-                });
-
-            //get the currently assigned property type aliases - ensure we pass these to the server side filer
-            var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) {
-                return _.map(g.properties, function(p) {
-                    return p.alias;
-                });
-            })), function (f) {
-                return f !== null && f !== undefined;
-            });
-
-            //use a different resource lookup depending on the content type type
-            var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes;
-
-            return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) {
-                _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) {
-                    //reset first
-                    current.allowed = true;
-                    //see if this list item is found in the response (allowed) list
-                    var found = _.find(filteredAvailableCompositeTypes, function (f) {
-                        return current.contentType.alias === f.contentType.alias;
-                    });
-
-                    //allow if the item was  found in the response (allowed) list -
-                    // and ensure its set to allowed if it is currently checked,
-                    // DO not allow if it's a locked content type.
-                    current.allowed = scope.model.lockedCompositeContentTypes.indexOf(current.contentType.alias) === -1 &&
-                        (selectedContentTypeAliases.indexOf(current.contentType.alias) !== -1) || ((found !== null && found !== undefined) ? found.allowed : false);
-
-                });
-            });
-        }
-
-      function updatePropertiesSortOrder() {
-
-        angular.forEach(scope.model.groups, function(group){
-          if( group.tabState !== "init" ) {
-            group.properties = contentTypeHelper.updatePropertiesSortOrder(group.properties);
-          }
-        });
-
-      }
-
-        function setupAvailableContentTypesModel(result) {
-            scope.compositionsDialogModel.availableCompositeContentTypes = result;
-            //iterate each one and set it up
-            _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (c) {
-                //enable it if it's part of the selected model
-                if (scope.compositionsDialogModel.compositeContentTypes.indexOf(c.contentType.alias) !== -1) {
-                    c.allowed = true;
-                }
-
-                //set the inherited flags
-                c.inherited = false;
-                if (scope.model.lockedCompositeContentTypes.indexOf(c.contentType.alias) > -1) {
-                    c.inherited = true;
-                }
-                // convert icons for composite content types
-                iconHelper.formatContentTypeIcons([c.contentType]);
-            });
-        }
-
-      /* ---------- DELETE PROMT ---------- */
-
-      scope.togglePrompt = function (object) {
-          object.deletePrompt = !object.deletePrompt;
-      };
-
-      scope.hidePrompt = function (object) {
-          object.deletePrompt = false;
-      };
-
-      /* ---------- TOOLBAR ---------- */
-
-      scope.toggleSortingMode = function(tool) {
-
-          if (scope.sortingMode === true) {
-
-              var sortOrderMissing = false;
-
-              for (var i = 0; i < scope.model.groups.length; i++) {
-                  var group = scope.model.groups[i];
-                  if (group.tabState !== "init" && group.sortOrder === undefined) {
-                      sortOrderMissing = true;
-                      group.showSortOrderMissing = true;
-                      notificationsService.error(validationTranslated + ": " + group.name + " " + tabNoSortOrderTranslated);
-                  }
-              }
-
-              if (!sortOrderMissing) {
-                  scope.sortingMode = false;
-                  scope.sortingButtonKey = "general_reorder";
-              }
-
-          } else {
-
-              scope.sortingMode = true;
-              scope.sortingButtonKey = "general_reorderDone";
-
-          }
-
-      };
-
-      scope.openCompositionsDialog = function() {
-
-        scope.compositionsDialogModel = {
-            title: "Compositions",
-            contentType: scope.model,
-            compositeContentTypes: scope.model.compositeContentTypes,
-            view: "views/common/overlays/contenttypeeditor/compositions/compositions.html",
-            confirmSubmit: {
-                title: "Warning",
-                description: "Removing a composition will delete all the associated property data. Once you save the document type there's no way back, are you sure?",
-                checkboxLabel: "I know what I'm doing",
-                enable: true
-            },
-            submit: function(model, oldModel, confirmed) {
-
-                var compositionRemoved = false;
-
-                // check if any compositions has been removed
-                for(var i = 0; oldModel.compositeContentTypes.length > i; i++) {
-
-                    var oldComposition = oldModel.compositeContentTypes[i];
-
-                    if(_.contains(model.compositeContentTypes, oldComposition) === false) {
-                        compositionRemoved = true;
-                    }
-
-                }
-
-                // show overlay confirm box if compositions has been removed.
-                if(compositionRemoved && confirmed === false) {
-
-                    scope.compositionsDialogModel.confirmSubmit.show = true;
-
-                // submit overlay if no compositions has been removed
-                // or the action has been confirmed
-                } else {
-
-                    // make sure that all tabs has an init property
-                    if (scope.model.groups.length !== 0) {
-                      angular.forEach(scope.model.groups, function(group) {
-                        addInitProperty(group);
-                      });
-                    }
-
-                    // remove overlay
-                    scope.compositionsDialogModel.show = false;
-                    scope.compositionsDialogModel = null;
-                }
-
-            },
-            close: function(oldModel) {
-
-                // reset composition changes
-                scope.model.groups = oldModel.contentType.groups;
-                scope.model.compositeContentTypes = oldModel.contentType.compositeContentTypes;
-
-                // remove overlay
-                scope.compositionsDialogModel.show = false;
-                scope.compositionsDialogModel = null;
-
-            },
-            selectCompositeContentType: function (selectedContentType) {
-
-                //first check if this is a new selection - we need to store this value here before any further digests/async
-                // because after that the scope.model.compositeContentTypes will be populated with the selected value.
-                var newSelection = scope.model.compositeContentTypes.indexOf(selectedContentType.alias) === -1;
-
-                if (newSelection) {
-                    //merge composition with content type
-
-                    //use a different resource lookup depending on the content type type
-                    var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getById : mediaTypeResource.getById;
-
-                    resourceLookup(selectedContentType.id).then(function (composition) {
-                        //based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and
-                        // double check here.
-                        var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition);
-                        if (overlappingAliases.length > 0) {
-                            //this will create an invalid composition, need to uncheck it
-                            scope.compositionsDialogModel.compositeContentTypes.splice(
-                                scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1);
-                            //dissallow this until something else is unchecked
-                            selectedContentType.allowed = false;
-                        }
-                        else {
-                            contentTypeHelper.mergeCompositeContentType(scope.model, composition);
-                        }
-
-                        //based on the selection, we need to filter the available composite types list
-                        filterAvailableCompositions(selectedContentType, newSelection).then(function () {
-                            //TODO: Here we could probably re-enable selection if we previously showed a throbber or something
-                        });
-                    });
-                }
-                else {
-                    // split composition from content type
-                    contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType);
-
-                    //based on the selection, we need to filter the available composite types list
-                    filterAvailableCompositions(selectedContentType, newSelection).then(function () {
-                        //TODO: Here we could probably re-enable selection if we previously showed a throbber or something
-                    });
-                }
-
-            }
-        };
-
-        var availableContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes;
-        var countContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getCount : mediaTypeResource.getCount;
-
-          //get the currently assigned property type aliases - ensure we pass these to the server side filer
-          var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) {
-              return _.map(g.properties, function(p) {
-                  return p.alias;
-              });
-          })), function(f) {
-              return f !== null && f !== undefined;
-          });
-          $q.all([
-              //get available composite types
-              availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) {
-                  setupAvailableContentTypesModel(result);
-              }),
-              //get content type count
-              countContentTypeResource().then(function(result) {
-                  scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10);
-              })
-          ]).then(function() {
-              //resolves when both other promises are done, now show it
-              scope.compositionsDialogModel.show = true;
-          });
-
-      };
-
-
-      /* ---------- GROUPS ---------- */
-
-      scope.addGroup = function(group) {
-
-        // set group sort order
-        var index = scope.model.groups.indexOf(group);
-        var prevGroup = scope.model.groups[index - 1];
-
-        if( index > 0) {
-          // set index to 1 higher than the previous groups sort order
-          group.sortOrder = prevGroup.sortOrder + 1;
-
-        } else {
-          // first group - sort order will be 0
-          group.sortOrder = 0;
-        }
-
-        // activate group
-        scope.activateGroup(group);
-
-      };
-
-      scope.activateGroup = function(selectedGroup) {
-
-        // set all other groups that are inactive to active
-        angular.forEach(scope.model.groups, function(group) {
-          // skip init tab
-          if (group.tabState !== "init") {
-            group.tabState = "inActive";
-          }
-        });
-
-        selectedGroup.tabState = "active";
-
-      };
-
-      scope.removeGroup = function(groupIndex) {
-        scope.model.groups.splice(groupIndex, 1);
-        addInitGroup(scope.model.groups);
-      };
-
-      scope.updateGroupTitle = function(group) {
-        if (group.properties.length === 0) {
-          addInitProperty(group);
-        }
-      };
-
-      scope.changeSortOrderValue = function(group) {
-
-          if (group.sortOrder !== undefined) {
-              group.showSortOrderMissing = false;
-          }
-          scope.model.groups = $filter('orderBy')(scope.model.groups, 'sortOrder');
-      };
-
-      function addInitGroup(groups) {
-
-        // check i init tab already exists
-        var addGroup = true;
-
-        angular.forEach(groups, function(group) {
-          if (group.tabState === "init") {
-            addGroup = false;
-          }
-        });
-
-        if (addGroup) {
-          groups.push({
-            properties: [],
-            parentTabContentTypes: [],
-            parentTabContentTypeNames: [],
-            name: "",
-            tabState: "init"
-          });
-        }
-
-        return groups;
-      }
-
-      function activateFirstGroup(groups) {
-          if (groups && groups.length > 0) {
-              var firstGroup = groups[0];
-              if(!firstGroup.tabState || firstGroup.tabState === "inActive") {
-                  firstGroup.tabState = "active";
-              }
-          }
-      }
-
-      /* ---------- PROPERTIES ---------- */
-
-      scope.addProperty = function(property, group) {
-
-        // set property sort order
-        var index = group.properties.indexOf(property);
-        var prevProperty = group.properties[index - 1];
-
-        if( index > 0) {
-          // set index to 1 higher than the previous property sort order
-          property.sortOrder = prevProperty.sortOrder + 1;
-
-        } else {
-          // first property - sort order will be 0
-          property.sortOrder = 0;
-        }
-
-        // open property settings dialog
-        scope.editPropertyTypeSettings(property, group);
-
-      };
-
-      scope.editPropertyTypeSettings = function(property, group) {
-
-        if (!property.inherited && !property.locked) {
-
-          scope.propertySettingsDialogModel = {};
-          scope.propertySettingsDialogModel.title = "Property settings";
-          scope.propertySettingsDialogModel.property = property;
-          scope.propertySettingsDialogModel.contentType = scope.contentType;
-          scope.propertySettingsDialogModel.contentTypeName = scope.model.name;
-          scope.propertySettingsDialogModel.view = "views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html";
-          scope.propertySettingsDialogModel.show = true;
-
-          // set state to active to access the preview
-          property.propertyState = "active";
-
-          // set property states
-          property.dialogIsOpen = true;
-
-          scope.propertySettingsDialogModel.submit = function(model) {
-
-            property.inherited = false;
-            property.dialogIsOpen = false;
-
-            // update existing data types
-            if(model.updateSameDataTypes) {
-              updateSameDataTypes(property);
-            }
-
-            // remove dialog
-            scope.propertySettingsDialogModel.show = false;
-            scope.propertySettingsDialogModel = null;
-
-            // push new init property to group
-            addInitProperty(group);
-
-            // set focus on init property
-            var numberOfProperties = group.properties.length;
-            group.properties[numberOfProperties - 1].focus = true;
-
-            // push new init tab to the scope
-            addInitGroup(scope.model.groups);
-
-          };
-
-          scope.propertySettingsDialogModel.close = function(oldModel) {
-
-            // reset all property changes
-            property.label = oldModel.property.label;
-            property.alias = oldModel.property.alias;
-            property.description = oldModel.property.description;
-            property.config = oldModel.property.config;
-            property.editor = oldModel.property.editor;
-            property.view = oldModel.property.view;
-            property.dataTypeId = oldModel.property.dataTypeId;
-            property.dataTypeIcon = oldModel.property.dataTypeIcon;
-            property.dataTypeName = oldModel.property.dataTypeName;
-            property.validation.mandatory = oldModel.property.validation.mandatory;
-            property.validation.pattern = oldModel.property.validation.pattern;
-            property.showOnMemberProfile = oldModel.property.showOnMemberProfile;
-            property.memberCanEdit = oldModel.property.memberCanEdit;
-
-            // because we set state to active, to show a preview, we have to check if has been filled out
-            // label is required so if it is not filled we know it is a placeholder
-            if(oldModel.property.editor === undefined || oldModel.property.editor === null || oldModel.property.editor === "") {
-              property.propertyState = "init";
-            } else {
-              property.propertyState = oldModel.property.propertyState;
-            }
-
-            // remove dialog
-            scope.propertySettingsDialogModel.show = false;
-            scope.propertySettingsDialogModel = null;
-
-          };
-
-        }
-      };
-
-      scope.deleteProperty = function(tab, propertyIndex) {
-
-        // remove property
-        tab.properties.splice(propertyIndex, 1);
-
-        // if the last property in group is an placeholder - remove add new tab placeholder
-        if(tab.properties.length === 1 && tab.properties[0].propertyState === "init") {
-
-          angular.forEach(scope.model.groups, function(group, index, groups){
-            if(group.tabState === 'init') {
-              groups.splice(index, 1);
-            }
-          });
-
-        }
-
-      };
-
-      function addInitProperty(group) {
-
-        var addInitPropertyBool = true;
-        var initProperty = {
-          label: null,
-          alias: null,
-          propertyState: "init",
-          validation: {
-            mandatory: false,
-            pattern: null
-          }
-        };
-
-        // check if there already is an init property
-        angular.forEach(group.properties, function(property) {
-          if (property.propertyState === "init") {
-            addInitPropertyBool = false;
-          }
-        });
-
-        if (addInitPropertyBool) {
-          group.properties.push(initProperty);
-        }
-
-        return group;
-      }
-
-      function updateSameDataTypes(newProperty) {
-
-        // find each property
-        angular.forEach(scope.model.groups, function(group){
-          angular.forEach(group.properties, function(property){
-
-            if(property.dataTypeId === newProperty.dataTypeId) {
-
-              // update property data
-              property.config = newProperty.config;
-              property.editor = newProperty.editor;
-              property.view = newProperty.view;
-              property.dataTypeId = newProperty.dataTypeId;
-              property.dataTypeIcon = newProperty.dataTypeIcon;
-              property.dataTypeName = newProperty.dataTypeName;
-
-            }
-
-          });
-        });
-      }
-
-
-      var unbindModelWatcher = scope.$watch('model', function(newValue, oldValue) {
-        if (newValue !== undefined && newValue.groups !== undefined) {
-          activate();
-        }
-      });
-
-      // clean up
-      scope.$on('$destroy', function(){
-        unbindModelWatcher();
-      });
-
-    }
-
-    var directive = {
-      restrict: "E",
-      replace: true,
-      templateUrl: "views/components/umb-groups-builder.html",
-      scope: {
-        model: "=",
-        compositions: "=",
-        sorting: "=",
-        contentType: "@"
-      },
-      link: link
-    };
-
-    return directive;
-  }
-
-  angular.module('umbraco.directives').directive('umbGroupsBuilder', GroupsBuilderDirective);
-
-})();
+        "use strict";
 
-/**
-@ngdoc directive
-@name umbraco.directives.directive:umbkeyboardShortcutsOverview
-@restrict E
-@scope
-
-@description
-
-

Use this directive to show an overview of keyboard shortcuts in an editor. -The directive will render an overview trigger wich shows how the overview is opened. -When this combination is hit an overview is opened with shortcuts based on the model sent to the directive.

- -

Markup example

-
-    
- - - - -
-
- -

Controller example

-
-    (function () {
-
-        "use strict";
-
-        function Controller() {
-
-            var vm = this;
-
-            vm.keyboardShortcutsOverview = [
-                {
-                    "name": "Sections",
-                    "shortcuts": [
-                        {
-                            "description": "Navigate sections",
-                            "keys": [
-                                {"key": "1"},
-                                {"key": "4"}
-                            ],
-                            "keyRange": true
-                        }
-                    ]
-                },
-                {
-                    "name": "Design",
-                    "shortcuts": [
-                        {
-                            "description": "Add tab",
-                            "keys": [
-                                {"key": "alt"},
-                                {"key": "shift"},
-                                {"key": "t"}
-                            ]
-                        }
-                    ]
-                }
-            ];
-
-        }
-
-        angular.module("umbraco").controller("My.Controller", Controller);
-    })();
-
- -

Model description

-
    -
  • - name - (string) - - Sets the shortcut section name. -
  • -
  • - shortcuts - (array) - - Array of available shortcuts in the section. -
  • -
      -
    • - description - (string) - - Short description of the shortcut. -
    • -
    • - keys - (array) - - Array of keys in the shortcut. -
    • -
        -
      • - key - (string) - - The invidual key in the shortcut. -
      • -
      -
    • - keyRange - (boolean) - - Set to true to show a key range. It combines the shortcut keys with "-" instead of "+". -
    • -
    -
- -@param {object} model keyboard shortcut model. See description and example above. -**/ - -(function() { - 'use strict'; - - function KeyboardShortcutsOverviewDirective() { - - function link(scope, el, attr, ctrl) { - - scope.shortcutOverlay = false; - - scope.toggleShortcutsOverlay = function() { - scope.shortcutOverlay = !scope.shortcutOverlay; - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-keyboard-shortcuts-overview.html', - link: link, - scope: { - model: "=" - } - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbKeyboardShortcutsOverview', KeyboardShortcutsOverviewDirective); - -})(); + function Controller() { -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbLaunchMiniEditor -* @restrict E -* @function -* @description -* Used on a button to launch a mini content editor editor dialog -**/ -angular.module("umbraco.directives") - .directive('umbLaunchMiniEditor', function (dialogService, editorState, fileManager, contentEditingHelper) { - return { - restrict: 'A', - replace: false, - scope: { - node: '=umbLaunchMiniEditor', - }, - link: function(scope, element, attrs) { - - var launched = false; - - element.click(function() { - - if (launched === true) { - return; - } - - launched = true; - - //We need to store the current files selected in the file manager locally because the fileManager - // is a singleton and is shared globally. The mini dialog will also be referencing the fileManager - // and we don't want it to be sharing the same files as the main editor. So we'll store the current files locally here, - // clear them out and then launch the dialog. When the dialog closes, we'll reset the fileManager to it's previous state. - var currFiles = _.groupBy(fileManager.getFiles(), "alias"); - fileManager.clearFiles(); - - //We need to store the original editorState entity because it will need to change when the mini editor is loaded so that - // any property editors that are working with editorState get given the correct entity, otherwise strange things will - // start happening. - var currEditorState = editorState.getCurrent(); - - dialogService.open({ - template: "views/common/dialogs/content/edit.html", - id: scope.node.id, - closeOnSave: true, - tabFilter: ["Generic properties"], - callback: function (data) { - - //set the node name back - scope.node.name = data.name; - - //reset the fileManager to what it was - fileManager.clearFiles(); - _.each(currFiles, function (val, key) { - fileManager.setFiles(key, _.map(currFiles['upload'], function (i) { return i.file; })); - }); - - //reset the editor state - editorState.set(currEditorState); - - //Now we need to check if the content item that was edited was actually the same content item - // as the main content editor and if so, update all property data - if (data.id === currEditorState.id) { - var changed = contentEditingHelper.reBindChangedProperties(currEditorState, data); - } - - launched = false; - }, - closeCallback: function () { - //reset the fileManager to what it was - fileManager.clearFiles(); - _.each(currFiles, function (val, key) { - fileManager.setFiles(key, _.map(currFiles['upload'], function (i) { return i.file; })); - }); - - //reset the editor state - editorState.set(currEditorState); - - launched = false; - } - }); - - }); - - } - }; + var vm = this; + vm.promptIsVisible = false; + + vm.confirmAction = confirmAction; + vm.showPrompt = showPrompt; + vm.hidePrompt = hidePrompt; + + function confirmAction() { + // confirm logic here + } + + function showPrompt() { + vm.promptIsVisible = true; + } + + function hidePrompt() { + vm.promptIsVisible = false; + } + + } + + angular.module("umbraco").controller("My.Controller", Controller); + })(); +
+ +@param {string} direction The direction the prompt opens ("up", "down", "left", "right"). +@param {callback} onConfirm Callback when the checkmark is clicked. +@param {callback} onCancel Callback when the cross is clicked. +**/ + +(function() { + 'use strict'; + + function ConfirmAction() { + + function link(scope, el, attr, ctrl) { + + scope.clickConfirm = function() { + if(scope.onConfirm) { + scope.onConfirm(); + } + }; + + scope.clickCancel = function() { + if(scope.onCancel) { + scope.onCancel(); + } + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-confirm-action.html', + scope: { + direction: "@", + onConfirm: "&", + onCancel: "&" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbConfirmAction', ConfirmAction); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbContentGrid +@restrict E +@scope + +@description +Use this directive to generate a list of content items presented as a flexbox grid. + +

Markup example

+
+    
+ + + + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+            vm.contentItems = [
+                {
+                    "name": "Cape",
+                    "published": true,
+                    "icon": "icon-document",
+                    "updateDate": "15-02-2016",
+                    "owner": "Mr. Batman",
+                    "selected": false
+                },
+                {
+                    "name": "Utility Belt",
+                    "published": true,
+                    "icon": "icon-document",
+                    "updateDate": "15-02-2016",
+                    "owner": "Mr. Batman",
+                    "selected": false
+                },
+                {
+                    "name": "Cave",
+                    "published": true,
+                    "icon": "icon-document",
+                    "updateDate": "15-02-2016",
+                    "owner": "Mr. Batman",
+                    "selected": false
+                }
+            ];
+            vm.includeProperties = [
+                {
+                  "alias": "updateDate",
+                  "header": "Last edited"
+                },
+                {
+                  "alias": "owner",
+                  "header": "Created by"
+                }
+            ];
+
+            vm.clickItem = clickItem;
+            vm.selectItem = selectItem;
+
+
+            function clickItem(item, $event, $index){
+                // do magic here
+            }
+
+            function selectItem(item, $event, $index) {
+                // set item.selected = true; to select the item
+                // do magic here
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {array} content (binding): Array of content items. +@param {array=} contentProperties (binding): Array of content item properties to include in the item. If left empty the item will only show the item icon and name. +@param {callback=} onClick (binding): Callback method to handle click events on the content item. +

The callback returns:

+
    +
  • item: The clicked item
  • +
  • $event: The select event
  • +
  • $index: The item index
  • +
+@param {callback=} onClickName (binding): Callback method to handle click events on the checkmark icon. +

The callback returns:

+
    +
  • item: The selected item
  • +
  • $event: The select event
  • +
  • $index: The item index
  • +
+**/ + +(function() { + 'use strict'; + + function ContentGridDirective() { + + function link(scope, el, attr, ctrl) { + + scope.clickItem = function(item, $event, $index) { + if(scope.onClick) { + scope.onClick(item, $event, $index); + } + }; + + scope.clickItemName = function(item, $event, $index) { + if(scope.onClickName) { + scope.onClickName(item, $event, $index); + } + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-content-grid.html', + scope: { + content: '=', + contentProperties: "=", + onClick: "=", + onClickName: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbContentGrid', ContentGridDirective); + +})(); + +(function() { + 'use strict'; + + function UmbDisableFormValidation() { + + var directive = { + restrict: 'A', + require: '?form', + link: function (scope, elm, attrs, ctrl) { + //override the $setValidity function of the form to disable validation + ctrl.$setValidity = function () { }; + } + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbDisableFormValidation', UmbDisableFormValidation); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbEmptyState +@restrict E +@scope + +@description +Use this directive to show an empty state message. + +

Markup example

+
+    
+ + + // Empty state content + + +
+
+ +@param {string=} size Set the size of the text ("small", "large"). +@param {string=} position Set the position of the text ("center"). +**/ + +(function() { + 'use strict'; + + function EmptyStateDirective() { + + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/umb-empty-state.html', + scope: { + size: '@', + position: '@' + } + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEmptyState', EmptyStateDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbFolderGrid +@restrict E +@scope + +@description +Use this directive to generate a list of folders presented as a flexbox grid. + +

Markup example

+
+    
+ + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller(myService) {
+
+            var vm = this;
+            vm.folders = [
+                {
+                    "name": "Folder 1",
+                    "icon": "icon-folder",
+                    "selected": false
+                },
+                {
+                    "name": "Folder 2",
+                    "icon": "icon-folder",
+                    "selected": false
+                }
+
+            ];
+
+            vm.clickFolder = clickFolder;
+            vm.selectFolder = selectFolder;
+
+            myService.getFolders().then(function(folders){
+                vm.folders = folders;
+            });
+
+            function clickFolder(folder){
+                // Execute when clicking folder name/link
+            }
+
+            function selectFolder(folder, event, index) {
+                // Execute when clicking folder
+                // set folder.selected = true; to show checkmark icon
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {array} folders (binding): Array of folders +@param {callback=} onClick (binding): Callback method to handle click events on the folder. +

The callback returns:

+
    +
  • folder: The selected folder
  • +
+@param {callback=} onSelect (binding): Callback method to handle click events on the checkmark icon. +

The callback returns:

+
    +
  • folder: The selected folder
  • +
  • $event: The select event
  • +
  • $index: The folder index
  • +
+**/ + +(function() { + 'use strict'; + + function FolderGridDirective() { + + function link(scope, el, attr, ctrl) { + + scope.clickFolder = function(folder, $event, $index) { + if(scope.onClick) { + scope.onClick(folder, $event, $index); + } + }; + + scope.clickFolderName = function(folder, $event, $index) { + if(scope.onClickName) { + scope.onClickName(folder, $event, $index); + $event.stopPropagation(); + } + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-folder-grid.html', + scope: { + folders: '=', + onClick: "=", + onClickName: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbFolderGrid', FolderGridDirective); + +})(); + +(function() { + 'use strict'; + + function GridSelector() { + + function link(scope, el, attr, ctrl) { + + var eventBindings = []; + scope.dialogModel = {}; + scope.showDialog = false; + scope.itemLabel = ""; + + // set default item name + if(!scope.itemName){ + scope.itemLabel = "item"; + } else { + scope.itemLabel = scope.itemName; + } + + scope.removeItem = function(selectedItem) { + var selectedItemIndex = scope.selectedItems.indexOf(selectedItem); + scope.selectedItems.splice(selectedItemIndex, 1); + }; + + scope.removeDefaultItem = function() { + + // it will be the last item so we can clear the array + scope.selectedItems = []; + + // remove as default item + scope.defaultItem = null; + + }; + + scope.openItemPicker = function($event){ + scope.dialogModel = { + view: "itempicker", + title: "Choose " + scope.itemLabel, + availableItems: scope.availableItems, + selectedItems: scope.selectedItems, + event: $event, + show: true, + submit: function(model) { + scope.selectedItems.push(model.selectedItem); + + // if no default item - set item as default + if(scope.defaultItem === null) { + scope.setAsDefaultItem(model.selectedItem); + } + + scope.dialogModel.show = false; + scope.dialogModel = null; + } + }; + }; + + scope.setAsDefaultItem = function(selectedItem) { + + // clear default item + scope.defaultItem = {}; + + // set as default item + scope.defaultItem = selectedItem; + }; + + function updatePlaceholders() { + + // update default item + if(scope.defaultItem !== null && scope.defaultItem.placeholder) { + + scope.defaultItem.name = scope.name; + + if(scope.alias !== null && scope.alias !== undefined) { + scope.defaultItem.alias = scope.alias; + } + + } + + // update selected items + angular.forEach(scope.selectedItems, function(selectedItem) { + if(selectedItem.placeholder) { + + selectedItem.name = scope.name; + + if(scope.alias !== null && scope.alias !== undefined) { + selectedItem.alias = scope.alias; + } + + } + }); + + // update availableItems + angular.forEach(scope.availableItems, function(availableItem) { + if(availableItem.placeholder) { + + availableItem.name = scope.name; + + if(scope.alias !== null && scope.alias !== undefined) { + availableItem.alias = scope.alias; + } + + } + }); + + } + + function activate() { + + // add watchers for updating placeholde name and alias + if(scope.updatePlaceholder) { + eventBindings.push(scope.$watch('name', function(newValue, oldValue){ + updatePlaceholders(); + })); + + eventBindings.push(scope.$watch('alias', function(newValue, oldValue){ + updatePlaceholders(); + })); + } + + } + + activate(); + + // clean up + scope.$on('$destroy', function(){ + + // clear watchers + for(var e in eventBindings) { + eventBindings[e](); + } + + }); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-grid-selector.html', + scope: { + name: "=", + alias: "=", + selectedItems: '=', + availableItems: "=", + defaultItem: "=", + itemName: "@", + updatePlaceholder: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbGridSelector', GridSelector); + +})(); + +(function() { + 'use strict'; + + function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, localizationService) { + + function link(scope, el, attr, ctrl) { + + var validationTranslated = ""; + var tabNoSortOrderTranslated = ""; + + scope.sortingMode = false; + scope.toolbar = []; + scope.sortableOptionsGroup = {}; + scope.sortableOptionsProperty = {}; + scope.sortingButtonKey = "general_reorder"; + + function activate() { + + setSortingOptions(); + + // set placeholder property on each group + if (scope.model.groups.length !== 0) { + angular.forEach(scope.model.groups, function(group) { + addInitProperty(group); + }); + } + + // add init tab + addInitGroup(scope.model.groups); + + activateFirstGroup(scope.model.groups); + + // localize texts + localizationService.localize("validation_validation").then(function(value) { + validationTranslated = value; + }); + + localizationService.localize("contentTypeEditor_tabHasNoSortOrder").then(function(value) { + tabNoSortOrderTranslated = value; + }); + } + + function setSortingOptions() { + + scope.sortableOptionsGroup = { + distance: 10, + tolerance: "pointer", + opacity: 0.7, + scroll: true, + cursor: "move", + placeholder: "umb-group-builder__group-sortable-placeholder", + zIndex: 6000, + handle: ".umb-group-builder__group-handle", + items: ".umb-group-builder__group-sortable", + start: function(e, ui) { + ui.placeholder.height(ui.item.height()); + }, + stop: function(e, ui) { + updateTabsSortOrder(); + }, + }; + + scope.sortableOptionsProperty = { + distance: 10, + tolerance: "pointer", + connectWith: ".umb-group-builder__properties", + opacity: 0.7, + scroll: true, + cursor: "move", + placeholder: "umb-group-builder__property_sortable-placeholder", + zIndex: 6000, + handle: ".umb-group-builder__property-handle", + items: ".umb-group-builder__property-sortable", + start: function(e, ui) { + ui.placeholder.height(ui.item.height()); + }, + stop: function(e, ui) { + updatePropertiesSortOrder(); + } + }; + + } + + function updateTabsSortOrder() { + + var first = true; + var prevSortOrder = 0; + + scope.model.groups.map(function(group){ + + var index = scope.model.groups.indexOf(group); + + if(group.tabState !== "init") { + + // set the first not inherited tab to sort order 0 + if(!group.inherited && first) { + + // set the first tab sort order to 0 if prev is 0 + if( prevSortOrder === 0 ) { + group.sortOrder = 0; + // when the first tab is inherited and sort order is not 0 + } else { + group.sortOrder = prevSortOrder + 1; + } + + first = false; + + } else if(!group.inherited && !first) { + + // find next group + var nextGroup = scope.model.groups[index + 1]; + + // if a groups is dropped in the middle of to groups with + // same sort order. Give it the dropped group same sort order + if( prevSortOrder === nextGroup.sortOrder ) { + group.sortOrder = prevSortOrder; + } else { + group.sortOrder = prevSortOrder + 1; + } + + } + + // store this tabs sort order as reference for the next + prevSortOrder = group.sortOrder; + + } + + }); + + } + + function filterAvailableCompositions(selectedContentType, selecting) { + + //selecting = true if the user has check the item, false if the user has unchecked the item + + var selectedContentTypeAliases = selecting ? + //the user has selected the item so add to the current list + _.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) : + //the user has unselected the item so remove from the current list + _.reject(scope.compositionsDialogModel.compositeContentTypes, function(i) { + return i === selectedContentType.alias; + }); + + //get the currently assigned property type aliases - ensure we pass these to the server side filer + var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })), function (f) { + return f !== null && f !== undefined; + }); + + //use a different resource lookup depending on the content type type + var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; + + return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) { + _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) { + //reset first + current.allowed = true; + //see if this list item is found in the response (allowed) list + var found = _.find(filteredAvailableCompositeTypes, function (f) { + return current.contentType.alias === f.contentType.alias; + }); + + //allow if the item was found in the response (allowed) list - + // and ensure its set to allowed if it is currently checked, + // DO not allow if it's a locked content type. + current.allowed = scope.model.lockedCompositeContentTypes.indexOf(current.contentType.alias) === -1 && + (selectedContentTypeAliases.indexOf(current.contentType.alias) !== -1) || ((found !== null && found !== undefined) ? found.allowed : false); + + }); + }); + } + + function updatePropertiesSortOrder() { + + angular.forEach(scope.model.groups, function(group){ + if( group.tabState !== "init" ) { + group.properties = contentTypeHelper.updatePropertiesSortOrder(group.properties); + } + }); + + } + + function setupAvailableContentTypesModel(result) { + scope.compositionsDialogModel.availableCompositeContentTypes = result; + //iterate each one and set it up + _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (c) { + //enable it if it's part of the selected model + if (scope.compositionsDialogModel.compositeContentTypes.indexOf(c.contentType.alias) !== -1) { + c.allowed = true; + } + + //set the inherited flags + c.inherited = false; + if (scope.model.lockedCompositeContentTypes.indexOf(c.contentType.alias) > -1) { + c.inherited = true; + } + // convert icons for composite content types + iconHelper.formatContentTypeIcons([c.contentType]); + }); + } + + /* ---------- DELETE PROMT ---------- */ + + scope.togglePrompt = function (object) { + object.deletePrompt = !object.deletePrompt; + }; + + scope.hidePrompt = function (object) { + object.deletePrompt = false; + }; + + /* ---------- TOOLBAR ---------- */ + + scope.toggleSortingMode = function(tool) { + + if (scope.sortingMode === true) { + + var sortOrderMissing = false; + + for (var i = 0; i < scope.model.groups.length; i++) { + var group = scope.model.groups[i]; + if (group.tabState !== "init" && group.sortOrder === undefined) { + sortOrderMissing = true; + group.showSortOrderMissing = true; + notificationsService.error(validationTranslated + ": " + group.name + " " + tabNoSortOrderTranslated); + } + } + + if (!sortOrderMissing) { + scope.sortingMode = false; + scope.sortingButtonKey = "general_reorder"; + } + + } else { + + scope.sortingMode = true; + scope.sortingButtonKey = "general_reorderDone"; + + } + + }; + + scope.openCompositionsDialog = function() { + + scope.compositionsDialogModel = { + title: "Compositions", + contentType: scope.model, + compositeContentTypes: scope.model.compositeContentTypes, + view: "views/common/overlays/contenttypeeditor/compositions/compositions.html", + confirmSubmit: { + title: "Warning", + description: "Removing a composition will delete all the associated property data. Once you save the document type there's no way back, are you sure?", + checkboxLabel: "I know what I'm doing", + enable: true + }, + submit: function(model, oldModel, confirmed) { + + var compositionRemoved = false; + + // check if any compositions has been removed + for(var i = 0; oldModel.compositeContentTypes.length > i; i++) { + + var oldComposition = oldModel.compositeContentTypes[i]; + + if(_.contains(model.compositeContentTypes, oldComposition) === false) { + compositionRemoved = true; + } + + } + + // show overlay confirm box if compositions has been removed. + if(compositionRemoved && confirmed === false) { + + scope.compositionsDialogModel.confirmSubmit.show = true; + + // submit overlay if no compositions has been removed + // or the action has been confirmed + } else { + + // make sure that all tabs has an init property + if (scope.model.groups.length !== 0) { + angular.forEach(scope.model.groups, function(group) { + addInitProperty(group); + }); + } + + // remove overlay + scope.compositionsDialogModel.show = false; + scope.compositionsDialogModel = null; + } + + }, + close: function(oldModel) { + + // reset composition changes + scope.model.groups = oldModel.contentType.groups; + scope.model.compositeContentTypes = oldModel.contentType.compositeContentTypes; + + // remove overlay + scope.compositionsDialogModel.show = false; + scope.compositionsDialogModel = null; + + }, + selectCompositeContentType: function (selectedContentType) { + + //first check if this is a new selection - we need to store this value here before any further digests/async + // because after that the scope.model.compositeContentTypes will be populated with the selected value. + var newSelection = scope.model.compositeContentTypes.indexOf(selectedContentType.alias) === -1; + + if (newSelection) { + //merge composition with content type + + //use a different resource lookup depending on the content type type + var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getById : mediaTypeResource.getById; + + resourceLookup(selectedContentType.id).then(function (composition) { + //based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and + // double check here. + var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition); + if (overlappingAliases.length > 0) { + //this will create an invalid composition, need to uncheck it + scope.compositionsDialogModel.compositeContentTypes.splice( + scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1); + //dissallow this until something else is unchecked + selectedContentType.allowed = false; + } + else { + contentTypeHelper.mergeCompositeContentType(scope.model, composition); + } + + //based on the selection, we need to filter the available composite types list + filterAvailableCompositions(selectedContentType, newSelection).then(function () { + //TODO: Here we could probably re-enable selection if we previously showed a throbber or something + }); + }); + } + else { + // split composition from content type + contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType); + + //based on the selection, we need to filter the available composite types list + filterAvailableCompositions(selectedContentType, newSelection).then(function () { + //TODO: Here we could probably re-enable selection if we previously showed a throbber or something + }); + } + + } + }; + + var availableContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; + var countContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getCount : mediaTypeResource.getCount; + + //get the currently assigned property type aliases - ensure we pass these to the server side filer + var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })), function(f) { + return f !== null && f !== undefined; + }); + $q.all([ + //get available composite types + availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) { + setupAvailableContentTypesModel(result); + }), + //get content type count + countContentTypeResource().then(function(result) { + scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10); + }) + ]).then(function() { + //resolves when both other promises are done, now show it + scope.compositionsDialogModel.show = true; + }); + + }; + + + /* ---------- GROUPS ---------- */ + + scope.addGroup = function(group) { + + // set group sort order + var index = scope.model.groups.indexOf(group); + var prevGroup = scope.model.groups[index - 1]; + + if( index > 0) { + // set index to 1 higher than the previous groups sort order + group.sortOrder = prevGroup.sortOrder + 1; + + } else { + // first group - sort order will be 0 + group.sortOrder = 0; + } + + // activate group + scope.activateGroup(group); + + }; + + scope.activateGroup = function(selectedGroup) { + + // set all other groups that are inactive to active + angular.forEach(scope.model.groups, function(group) { + // skip init tab + if (group.tabState !== "init") { + group.tabState = "inActive"; + } + }); + + selectedGroup.tabState = "active"; + + }; + + scope.removeGroup = function(groupIndex) { + scope.model.groups.splice(groupIndex, 1); + addInitGroup(scope.model.groups); + }; + + scope.updateGroupTitle = function(group) { + if (group.properties.length === 0) { + addInitProperty(group); + } + }; + + scope.changeSortOrderValue = function(group) { + + if (group.sortOrder !== undefined) { + group.showSortOrderMissing = false; + } + scope.model.groups = $filter('orderBy')(scope.model.groups, 'sortOrder'); + }; + + function addInitGroup(groups) { + + // check i init tab already exists + var addGroup = true; + + angular.forEach(groups, function(group) { + if (group.tabState === "init") { + addGroup = false; + } + }); + + if (addGroup) { + groups.push({ + properties: [], + parentTabContentTypes: [], + parentTabContentTypeNames: [], + name: "", + tabState: "init" + }); + } + + return groups; + } + + function activateFirstGroup(groups) { + if (groups && groups.length > 0) { + var firstGroup = groups[0]; + if(!firstGroup.tabState || firstGroup.tabState === "inActive") { + firstGroup.tabState = "active"; + } + } + } + + /* ---------- PROPERTIES ---------- */ + + scope.addProperty = function(property, group) { + + // set property sort order + var index = group.properties.indexOf(property); + var prevProperty = group.properties[index - 1]; + + if( index > 0) { + // set index to 1 higher than the previous property sort order + property.sortOrder = prevProperty.sortOrder + 1; + + } else { + // first property - sort order will be 0 + property.sortOrder = 0; + } + + // open property settings dialog + scope.editPropertyTypeSettings(property, group); + + }; + + scope.editPropertyTypeSettings = function(property, group) { + + if (!property.inherited && !property.locked) { + + scope.propertySettingsDialogModel = {}; + scope.propertySettingsDialogModel.title = "Property settings"; + scope.propertySettingsDialogModel.property = property; + scope.propertySettingsDialogModel.contentType = scope.contentType; + scope.propertySettingsDialogModel.contentTypeName = scope.model.name; + scope.propertySettingsDialogModel.view = "views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html"; + scope.propertySettingsDialogModel.show = true; + + // set state to active to access the preview + property.propertyState = "active"; + + // set property states + property.dialogIsOpen = true; + + scope.propertySettingsDialogModel.submit = function(model) { + + property.inherited = false; + property.dialogIsOpen = false; + + // update existing data types + if(model.updateSameDataTypes) { + updateSameDataTypes(property); + } + + // remove dialog + scope.propertySettingsDialogModel.show = false; + scope.propertySettingsDialogModel = null; + + // push new init property to group + addInitProperty(group); + + // set focus on init property + var numberOfProperties = group.properties.length; + group.properties[numberOfProperties - 1].focus = true; + + // push new init tab to the scope + addInitGroup(scope.model.groups); + + }; + + scope.propertySettingsDialogModel.close = function(oldModel) { + + // reset all property changes + property.label = oldModel.property.label; + property.alias = oldModel.property.alias; + property.description = oldModel.property.description; + property.config = oldModel.property.config; + property.editor = oldModel.property.editor; + property.view = oldModel.property.view; + property.dataTypeId = oldModel.property.dataTypeId; + property.dataTypeIcon = oldModel.property.dataTypeIcon; + property.dataTypeName = oldModel.property.dataTypeName; + property.validation.mandatory = oldModel.property.validation.mandatory; + property.validation.pattern = oldModel.property.validation.pattern; + property.showOnMemberProfile = oldModel.property.showOnMemberProfile; + property.memberCanEdit = oldModel.property.memberCanEdit; + + // because we set state to active, to show a preview, we have to check if has been filled out + // label is required so if it is not filled we know it is a placeholder + if(oldModel.property.editor === undefined || oldModel.property.editor === null || oldModel.property.editor === "") { + property.propertyState = "init"; + } else { + property.propertyState = oldModel.property.propertyState; + } + + // remove dialog + scope.propertySettingsDialogModel.show = false; + scope.propertySettingsDialogModel = null; + + }; + + } + }; + + scope.deleteProperty = function(tab, propertyIndex) { + + // remove property + tab.properties.splice(propertyIndex, 1); + + // if the last property in group is an placeholder - remove add new tab placeholder + if(tab.properties.length === 1 && tab.properties[0].propertyState === "init") { + + angular.forEach(scope.model.groups, function(group, index, groups){ + if(group.tabState === 'init') { + groups.splice(index, 1); + } + }); + + } + + }; + + function addInitProperty(group) { + + var addInitPropertyBool = true; + var initProperty = { + label: null, + alias: null, + propertyState: "init", + validation: { + mandatory: false, + pattern: null + } + }; + + // check if there already is an init property + angular.forEach(group.properties, function(property) { + if (property.propertyState === "init") { + addInitPropertyBool = false; + } + }); + + if (addInitPropertyBool) { + group.properties.push(initProperty); + } + + return group; + } + + function updateSameDataTypes(newProperty) { + + // find each property + angular.forEach(scope.model.groups, function(group){ + angular.forEach(group.properties, function(property){ + + if(property.dataTypeId === newProperty.dataTypeId) { + + // update property data + property.config = newProperty.config; + property.editor = newProperty.editor; + property.view = newProperty.view; + property.dataTypeId = newProperty.dataTypeId; + property.dataTypeIcon = newProperty.dataTypeIcon; + property.dataTypeName = newProperty.dataTypeName; + + } + + }); + }); + } + + + var unbindModelWatcher = scope.$watch('model', function(newValue, oldValue) { + if (newValue !== undefined && newValue.groups !== undefined) { + activate(); + } + }); + + // clean up + scope.$on('$destroy', function(){ + unbindModelWatcher(); + }); + + } + + var directive = { + restrict: "E", + replace: true, + templateUrl: "views/components/umb-groups-builder.html", + scope: { + model: "=", + compositions: "=", + sorting: "=", + contentType: "@" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbGroupsBuilder', GroupsBuilderDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbkeyboardShortcutsOverview +@restrict E +@scope + +@description + +

Use this directive to show an overview of keyboard shortcuts in an editor. +The directive will render an overview trigger wich shows how the overview is opened. +When this combination is hit an overview is opened with shortcuts based on the model sent to the directive.

+ +

Markup example

+
+    
+ + + + +
+
+ +

Controller example

+
+    (function () {
+
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+            vm.keyboardShortcutsOverview = [
+                {
+                    "name": "Sections",
+                    "shortcuts": [
+                        {
+                            "description": "Navigate sections",
+                            "keys": [
+                                {"key": "1"},
+                                {"key": "4"}
+                            ],
+                            "keyRange": true
+                        }
+                    ]
+                },
+                {
+                    "name": "Design",
+                    "shortcuts": [
+                        {
+                            "description": "Add tab",
+                            "keys": [
+                                {"key": "alt"},
+                                {"key": "shift"},
+                                {"key": "t"}
+                            ]
+                        }
+                    ]
+                }
+            ];
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +

Model description

+
    +
  • + name + (string) - + Sets the shortcut section name. +
  • +
  • + shortcuts + (array) - + Array of available shortcuts in the section. +
  • +
      +
    • + description + (string) - + Short description of the shortcut. +
    • +
    • + keys + (array) - + Array of keys in the shortcut. +
    • +
        +
      • + key + (string) - + The invidual key in the shortcut. +
      • +
      +
    • + keyRange + (boolean) - + Set to true to show a key range. It combines the shortcut keys with "-" instead of "+". +
    • +
    +
+ +@param {object} model keyboard shortcut model. See description and example above. +**/ + +(function() { + 'use strict'; + + function KeyboardShortcutsOverviewDirective() { + + function link(scope, el, attr, ctrl) { + + scope.shortcutOverlay = false; + + scope.toggleShortcutsOverlay = function() { + scope.shortcutOverlay = !scope.shortcutOverlay; + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-keyboard-shortcuts-overview.html', + link: link, + scope: { + model: "=" + } + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbKeyboardShortcutsOverview', KeyboardShortcutsOverviewDirective); + +})(); + +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbLaunchMiniEditor +* @restrict E +* @function +* @description +* Used on a button to launch a mini content editor editor dialog +**/ +angular.module("umbraco.directives") + .directive('umbLaunchMiniEditor', function (dialogService, editorState, fileManager, contentEditingHelper) { + return { + restrict: 'A', + replace: false, + scope: { + node: '=umbLaunchMiniEditor', + }, + link: function(scope, element, attrs) { + + var launched = false; + + element.click(function() { + + if (launched === true) { + return; + } + + launched = true; + + //We need to store the current files selected in the file manager locally because the fileManager + // is a singleton and is shared globally. The mini dialog will also be referencing the fileManager + // and we don't want it to be sharing the same files as the main editor. So we'll store the current files locally here, + // clear them out and then launch the dialog. When the dialog closes, we'll reset the fileManager to it's previous state. + var currFiles = _.groupBy(fileManager.getFiles(), "alias"); + fileManager.clearFiles(); + + //We need to store the original editorState entity because it will need to change when the mini editor is loaded so that + // any property editors that are working with editorState get given the correct entity, otherwise strange things will + // start happening. + var currEditorState = editorState.getCurrent(); + + dialogService.open({ + template: "views/common/dialogs/content/edit.html", + id: scope.node.id, + closeOnSave: true, + tabFilter: ["Generic properties"], + callback: function (data) { + + //set the node name back + scope.node.name = data.name; + + //reset the fileManager to what it was + fileManager.clearFiles(); + _.each(currFiles, function (val, key) { + fileManager.setFiles(key, _.map(currFiles['upload'], function (i) { return i.file; })); + }); + + //reset the editor state + editorState.set(currEditorState); + + //Now we need to check if the content item that was edited was actually the same content item + // as the main content editor and if so, update all property data + if (data.id === currEditorState.id) { + var changed = contentEditingHelper.reBindChangedProperties(currEditorState, data); + } + + launched = false; + }, + closeCallback: function () { + //reset the fileManager to what it was + fileManager.clearFiles(); + _.each(currFiles, function (val, key) { + fileManager.setFiles(key, _.map(currFiles['upload'], function (i) { return i.file; })); + }); + + //reset the editor state + editorState.set(currEditorState); + + launched = false; + } + }); + + }); + + } + }; }); -(function() { - 'use strict'; - - function LayoutSelectorDirective() { - - function link(scope, el, attr, ctrl) { - - scope.layoutDropDownIsOpen = false; - scope.showLayoutSelector = true; - - function activate() { - - setVisibility(); - - setActiveLayout(scope.layouts); - - } - - function setVisibility() { - - var numberOfAllowedLayouts = getNumberOfAllowedLayouts(scope.layouts); - - if(numberOfAllowedLayouts === 1) { - scope.showLayoutSelector = false; - } - - } - - function getNumberOfAllowedLayouts(layouts) { - - var allowedLayouts = 0; - - for (var i = 0; layouts.length > i; i++) { - - var layout = layouts[i]; - - if(layout.selected === true) { - allowedLayouts++; - } - - } - - return allowedLayouts; - } - - function setActiveLayout(layouts) { - - for (var i = 0; layouts.length > i; i++) { - var layout = layouts[i]; - if(layout.path === scope.activeLayout.path) { - layout.active = true; - } - } - - } - - scope.pickLayout = function(selectedLayout) { - if(scope.onLayoutSelect) { - scope.onLayoutSelect(selectedLayout); - scope.layoutDropDownIsOpen = false; - } - }; - - scope.toggleLayoutDropdown = function() { - scope.layoutDropDownIsOpen = !scope.layoutDropDownIsOpen; - }; - - scope.closeLayoutDropdown = function() { - scope.layoutDropDownIsOpen = false; - }; - - activate(); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-layout-selector.html', - scope: { - layouts: '=', - activeLayout: '=', - onLayoutSelect: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbLayoutSelector', LayoutSelectorDirective); - -})(); +(function() { + 'use strict'; -/** -@ngdoc directive -@name umbraco.directives.directive:umbLightbox -@restrict E -@scope - -@description -

Use this directive to open a gallery in a lightbox overlay.

- -

Markup example

-
-    
- - - - - - -
-
- -

Controller example

-
-    (function () {
-
-        "use strict";
-
-        function Controller() {
-
-            var vm = this;
-
-            vm.images = [
-                {
-                    "source": "linkToImage"
-                },
-                {
-                    "source": "linkToImage"
-                }
-            ]
-
-            vm.openLightbox = openLightbox;
-            vm.closeLightbox = closeLightbox;
-
-            function openLightbox(itemIndex, items) {
-                vm.lightbox = {
-                    show: true,
-                    items: items,
-                    activeIndex: itemIndex
-                };
-            }
-
-            function closeLightbox() {
-                vm.lightbox.show = false;
-                vm.lightbox = null;
-            }
-
-        }
-
-        angular.module("umbraco").controller("My.Controller", Controller);
-    })();
-
- -@param {array} items Array of gallery items. -@param {callback} onClose Callback when the lightbox is closed. -@param {number} activeItemIndex Index of active item. -**/ - - -(function() { - 'use strict'; - - function LightboxDirective() { - - function link(scope, el, attr, ctrl) { - - - function activate() { - - var eventBindings = []; - - el.appendTo("body"); - - // clean up - scope.$on('$destroy', function() { - // unbind watchers - for (var e in eventBindings) { - eventBindings[e](); - } - }); - } - - scope.next = function() { - - var nextItemIndex = scope.activeItemIndex + 1; - - if( nextItemIndex < scope.items.length) { - scope.items[scope.activeItemIndex].active = false; - scope.items[nextItemIndex].active = true; - scope.activeItemIndex = nextItemIndex; - } - }; - - scope.prev = function() { - - var prevItemIndex = scope.activeItemIndex - 1; - - if( prevItemIndex >= 0) { - scope.items[scope.activeItemIndex].active = false; - scope.items[prevItemIndex].active = true; - scope.activeItemIndex = prevItemIndex; - } - - }; - - scope.close = function() { - if(scope.onClose) { - scope.onClose(); - } - }; - - activate(); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-lightbox.html', - scope: { - items: '=', - onClose: "=", - activeItemIndex: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbLightbox', LightboxDirective); - -})(); + function LayoutSelectorDirective() { -(function() { - 'use strict'; - - function ListViewLayoutDirective() { - - function link(scope, el, attr, ctrl) { - - scope.getContent = function(contentId) { - if(scope.onGetContent) { - scope.onGetContent(contentId); - } - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-list-view-layout.html', - scope: { - contentId: '=', - folders: '=', - items: '=', - selection: '=', - options: '=', - entityType: '@', - onGetContent: '=' - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbListViewLayout', ListViewLayoutDirective); - -})(); + function link(scope, el, attr, ctrl) { -(function() { - 'use strict'; - - function ListViewSettingsDirective(contentTypeResource, dataTypeResource, dataTypeHelper, listViewPrevalueHelper) { - - function link(scope, el, attr, ctrl) { - - scope.dataType = {}; - scope.editDataTypeSettings = false; - scope.customListViewCreated = false; - - /* ---------- INIT ---------- */ - - function activate() { - - if(scope.enableListView) { - - dataTypeResource.getByName(scope.listViewName) - .then(function(dataType) { - - scope.dataType = dataType; - - listViewPrevalueHelper.setPrevalues(dataType.preValues); - scope.customListViewCreated = checkForCustomListView(); - - }); - - } else { - - scope.dataType = {}; - - } - - } - - /* ----------- LIST VIEW SETTINGS --------- */ - - scope.toggleEditListViewDataTypeSettings = function() { - scope.editDataTypeSettings = !scope.editDataTypeSettings; - }; - - scope.saveListViewDataType = function() { - - var preValues = dataTypeHelper.createPreValueProps(scope.dataType.preValues); - - dataTypeResource.save(scope.dataType, preValues, false).then(function(dataType) { - - // store data type - scope.dataType = dataType; - - // hide settings panel - scope.editDataTypeSettings = false; - - }); - - }; - - - /* ---------- CUSTOM LIST VIEW ---------- */ - - scope.createCustomListViewDataType = function() { - - dataTypeResource.createCustomListView(scope.modelAlias).then(function(dataType) { - - // store data type - scope.dataType = dataType; - - // set list view name on scope - scope.listViewName = dataType.name; - - // change state to custom list view - scope.customListViewCreated = true; - - // show settings panel - scope.editDataTypeSettings = true; - - }); - - }; - - scope.removeCustomListDataType = function() { - - scope.editDataTypeSettings = false; - - // delete custom list view data type - dataTypeResource.deleteById(scope.dataType.id).then(function(dataType) { - - // set list view name on scope - if(scope.contentType === "documentType") { - - scope.listViewName = "List View - Content"; - - } else if(scope.contentType === "mediaType") { - - scope.listViewName = "List View - Media"; - - } - - // get default data type - dataTypeResource.getByName(scope.listViewName) - .then(function(dataType) { - - // store data type - scope.dataType = dataType; - - // change state to default list view - scope.customListViewCreated = false; - - }); - }); - - }; - - /* ----------- SCOPE WATCHERS ----------- */ - var unbindEnableListViewWatcher = scope.$watch('enableListView', function(newValue, oldValue){ - - if(newValue !== undefined) { - activate(); - } - - }); - - // clean up - scope.$on('$destroy', function(){ - unbindEnableListViewWatcher(); - }); - - /* ----------- METHODS ---------- */ - - function checkForCustomListView() { - return scope.dataType.name === "List View - " + scope.modelAlias; - } - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-list-view-settings.html', - scope: { - enableListView: "=", - listViewName: "=", - modelAlias: "=", - contentType: "@" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbListViewSettings', ListViewSettingsDirective); - -})(); + scope.layoutDropDownIsOpen = false; + scope.showLayoutSelector = true; -/** -@ngdoc directive -@name umbraco.directives.directive:umbLoadIndicator -@restrict E - -@description -Use this directive to generate a loading indicator. - -

Markup example

-
-    
- - - - -
-

{{content}}

-
- -
-
- -

Controller example

-
-    (function () {
-        "use strict";
-
-        function Controller(myService) {
-
-            var vm = this;
-
-            vm.content = "";
-            vm.loading = true;
-
-            myService.getContent().then(function(content){
-                vm.content = content;
-                vm.loading = false;
-            });
-
-        }
-½
-        angular.module("umbraco").controller("My.Controller", Controller);
-    })();
-
-**/ - -(function() { - 'use strict'; - - function UmbLoadIndicatorDirective() { - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-load-indicator.html' - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbLoadIndicator', UmbLoadIndicatorDirective); - -})(); + function activate() { -/** -@ngdoc directive -@name umbraco.directives.directive:umbLockedField -@restrict E -@scope - -@description -Use this directive to render a value with a lock next to it. When the lock is clicked the value gets unlocked and can be edited. - -

Markup example

-
-	
- - - - -
-
- -

Controller example

-
-	(function () {
-		"use strict";
-
-		function Controller() {
-
-			var vm = this;
-			vm.value = "My locked text";
-
-        }
-
-		angular.module("umbraco").controller("My.Controller", Controller);
-
-	})();
-
- -@param {string} ngModel (binding): The locked text. -@param {boolean=} locked (binding): true by default. Set to false to unlock the text. -@param {string=} placeholderText (binding): If ngModel is empty this text will be shown. -@param {string=} regexValidation (binding): Set a regex expression for validation of the field. -@param {string=} serverValidationField (attribute): Set a server validation field. -**/ - -(function() { - 'use strict'; - - function LockedFieldDirective($timeout, localizationService) { - - function link(scope, el, attr, ngModelCtrl) { - - function activate() { - - // if locked state is not defined as an attr set default state - if (scope.locked === undefined || scope.locked === null) { - scope.locked = true; - } - - // if regex validation is not defined as an attr set default state - // if this is set to an empty string then regex validation can be ignored. - if (scope.regexValidation === undefined || scope.regexValidation === null) { - scope.regexValidation = "^[a-zA-Z]\\w.*$"; - } - - if (scope.serverValidationField === undefined || scope.serverValidationField === null) { - scope.serverValidationField = ""; - } - - // if locked state is not defined as an attr set default state - if (scope.placeholderText === undefined || scope.placeholderText === null) { - scope.placeholderText = "Enter value..."; - } - - } - - scope.lock = function() { - scope.locked = true; - }; - - scope.unlock = function() { - scope.locked = false; - }; - - activate(); - - } - - var directive = { - require: "ngModel", - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-locked-field.html', - scope: { - ngModel: "=", - locked: "=?", - placeholderText: "=?", - regexValidation: "=?", - serverValidationField: "@" - }, - link: link - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbLockedField', LockedFieldDirective); - -})(); + setVisibility(); -/** -@ngdoc directive -@name umbraco.directives.directive:umbMediaGrid -@restrict E -@scope - -@description -Use this directive to generate a thumbnail grid of media items. - -

Markup example

-
-    
- - - - -
-
- -

Controller example

-
-    (function () {
-        "use strict";
-
-        function Controller() {
-
-            var vm = this;
-            vm.mediaItems = [];
-
-            vm.clickItem = clickItem;
-            vm.clickItemName = clickItemName;
-
-            myService.getMediaItems().then(function (mediaItems) {
-                vm.mediaItems = mediaItems;
-            });
-
-            function clickItem(item, $event, $index){
-                // do magic here
-            }
-
-            function clickItemName(item, $event, $index) {
-                // set item.selected = true; to select the item
-                // do magic here
-            }
-
-        }
-
-        angular.module("umbraco").controller("My.Controller", Controller);
-    })();
-
- -@param {array} items (binding): Array of media items. -@param {callback=} onDetailsHover (binding): Callback method when the details icon is hovered. -

The callback returns:

-
    -
  • item: The hovered item
  • -
  • $event: The hover event
  • -
  • hover: Boolean to tell if the item is hovered or not
  • -
-@param {callback=} onClick (binding): Callback method to handle click events on the media item. -

The callback returns:

-
    -
  • item: The clicked item
  • -
  • $event: The click event
  • -
  • $index: The item index
  • -
-@param {callback=} onClickName (binding): Callback method to handle click events on the media item name. -

The callback returns:

-
    -
  • item: The clicked item
  • -
  • $event: The click event
  • -
  • $index: The item index
  • -
-@param {string=} filterBy (binding): String to filter media items by -@param {string=} itemMaxWidth (attribute): Sets a max width on the media item thumbnails. -@param {string=} itemMaxHeight (attribute): Sets a max height on the media item thumbnails. -@param {string=} itemMinWidth (attribute): Sets a min width on the media item thumbnails. -@param {string=} itemMinHeight (attribute): Sets a min height on the media item thumbnails. - -**/ - -(function() { - 'use strict'; - - function MediaGridDirective($filter, mediaHelper) { - - function link(scope, el, attr, ctrl) { - - var itemDefaultHeight = 200; - var itemDefaultWidth = 200; - var itemMaxWidth = 200; - var itemMaxHeight = 200; - var itemMinWidth = 125; - var itemMinHeight = 125; - - function activate() { - - if (scope.itemMaxWidth) { - itemMaxWidth = scope.itemMaxWidth; - } - - if (scope.itemMaxHeight) { - itemMaxHeight = scope.itemMaxHeight; - } - - if (scope.itemMinWidth) { - itemMinWidth = scope.itemMinWidth; - } - - if (scope.itemMinWidth) { - itemMinHeight = scope.itemMinHeight; - } - - for (var i = 0; scope.items.length > i; i++) { - var item = scope.items[i]; - setItemData(item); - setOriginalSize(item, itemMaxHeight); - - // remove non images when onlyImages is set to true - if(scope.onlyImages === "true" && !item.isFolder && !item.thumbnail){ - scope.items.splice(i, 1); - i--; - } - - } - - if (scope.items.length > 0) { - setFlexValues(scope.items); - } - - } - - function setItemData(item) { - item.isFolder = !mediaHelper.hasFilePropertyType(item); - if (!item.isFolder) { - item.thumbnail = mediaHelper.resolveFile(item, true); - item.image = mediaHelper.resolveFile(item, false); - - var fileProp = _.find(item.properties, function (v) { - return (v.alias === "umbracoFile"); - }); - - if (fileProp && fileProp.value) { - item.file = fileProp.value; - } - - var extensionProp = _.find(item.properties, function (v) { - return (v.alias === "umbracoExtension"); - }); - - if (extensionProp && extensionProp.value) { - item.extension = extensionProp.value; - } - } - } - - function setOriginalSize(item, maxHeight) { - - //set to a square by default - item.width = itemDefaultWidth; - item.height = itemDefaultHeight; - item.aspectRatio = 1; - - var widthProp = _.find(item.properties, function(v) { - return (v.alias === "umbracoWidth"); - }); - - if (widthProp && widthProp.value) { - item.width = parseInt(widthProp.value, 10); - if (isNaN(item.width)) { - item.width = itemDefaultWidth; - } - } - - var heightProp = _.find(item.properties, function(v) { - return (v.alias === "umbracoHeight"); - }); - - if (heightProp && heightProp.value) { - item.height = parseInt(heightProp.value, 10); - if (isNaN(item.height)) { - item.height = itemDefaultWidth; - } - } - - item.aspectRatio = item.width / item.height; - - // set max width and height - // landscape - if (item.aspectRatio >= 1) { - if (item.width > itemMaxWidth) { - item.width = itemMaxWidth; - item.height = itemMaxWidth / item.aspectRatio; - } - // portrait - } else { - if (item.height > itemMaxHeight) { - item.height = itemMaxHeight; - item.width = itemMaxHeight * item.aspectRatio; - } - } - - } - - function setFlexValues(mediaItems) { - - var flexSortArray = mediaItems; - var smallestImageWidth = null; - var widestImageAspectRatio = null; - - // sort array after image width with the widest image first - flexSortArray = $filter('orderBy')(flexSortArray, 'width', true); - - // find widest image aspect ratio - widestImageAspectRatio = flexSortArray[0].aspectRatio; - - // find smallest image width - smallestImageWidth = flexSortArray[flexSortArray.length - 1].width; - - for (var i = 0; flexSortArray.length > i; i++) { - - var mediaItem = flexSortArray[i]; - var flex = 1 / (widestImageAspectRatio / mediaItem.aspectRatio); - - if (flex === 0) { - flex = 1; - } - - var imageMinFlexWidth = smallestImageWidth * flex; - - var flexStyle = { - "flex": flex + " 1 " + imageMinFlexWidth + "px", - "max-width": mediaItem.width + "px", - "min-width": itemMinWidth + "px", - "min-height": itemMinHeight + "px" - }; - - mediaItem.flexStyle = flexStyle; - - } - - } - - scope.clickItem = function(item, $event, $index) { - if (scope.onClick) { - scope.onClick(item, $event, $index); - } - }; - - scope.clickItemName = function(item, $event, $index) { - if (scope.onClickName) { - scope.onClickName(item, $event, $index); - $event.stopPropagation(); - } - }; - - scope.hoverItemDetails = function(item, $event, hover) { - if (scope.onDetailsHover) { - scope.onDetailsHover(item, $event, hover); - } - }; - - var unbindItemsWatcher = scope.$watch('items', function(newValue, oldValue) { - if (angular.isArray(newValue)) { - activate(); - } - }); - - scope.$on('$destroy', function() { - unbindItemsWatcher(); - }); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-media-grid.html', - scope: { - items: '=', - onDetailsHover: "=", - onClick: '=', - onClickName: "=", - filterBy: "=", - itemMaxWidth: "@", - itemMaxHeight: "@", - itemMinWidth: "@", - itemMinHeight: "@", - onlyImages: "@" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbMediaGrid', MediaGridDirective); - -})(); + setActiveLayout(scope.layouts); -/** -@ngdoc directive -@name umbraco.directives.directive:umbPagination -@restrict E -@scope - -@description -Use this directive to generate a pagination. - -

Markup example

-
-    
- - - - -
-
- -

Controller example

-
-    (function () {
-        "use strict";
-
-        function Controller() {
-
-            var vm = this;
-
-            vm.pagination = {
-                pageNumber: 1,
-                totalPages: 10
-            }
-
-            vm.nextPage = nextPage;
-            vm.prevPage = prevPage;
-            vm.goToPage = goToPage;
-
-            function nextPage(pageNumber) {
-                // do magic here
-                console.log(pageNumber);
-                alert("nextpage");
-            }
-
-            function prevPage(pageNumber) {
-                // do magic here
-                console.log(pageNumber);
-                alert("prevpage");
-            }
-
-            function goToPage(pageNumber) {
-                // do magic here
-                console.log(pageNumber);
-                alert("go to");
-            }
-
-        }
-
-        angular.module("umbraco").controller("My.Controller", Controller);
-    })();
-
- -@param {number} pageNumber (binding): Current page number. -@param {number} totalPages (binding): The total number of pages. -@param {callback} onNext (binding): Callback method to go to the next page. -

The callback returns:

-
    -
  • pageNumber: The page number
  • -
-@param {callback=} onPrev (binding): Callback method to go to the previous page. -

The callback returns:

-
    -
  • pageNumber: The page number
  • -
-@param {callback=} onGoToPage (binding): Callback method to go to a specific page. -

The callback returns:

-
    -
  • pageNumber: The page number
  • -
-**/ - -(function() { - 'use strict'; - - function PaginationDirective() { - - function link(scope, el, attr, ctrl) { - - function activate() { - - scope.pagination = []; - - var i = 0; - - if (scope.totalPages <= 10) { - for (i = 0; i < scope.totalPages; i++) { - scope.pagination.push({ - val: (i + 1), - isActive: scope.pageNumber === (i + 1) - }); - } - } - else { - //if there is more than 10 pages, we need to do some fancy bits - - //get the max index to start - var maxIndex = scope.totalPages - 10; - //set the start, but it can't be below zero - var start = Math.max(scope.pageNumber - 5, 0); - //ensure that it's not too far either - start = Math.min(maxIndex, start); - - for (i = start; i < (10 + start) ; i++) { - scope.pagination.push({ - val: (i + 1), - isActive: scope.pageNumber === (i + 1) - }); - } - - //now, if the start is greater than 0 then '1' will not be displayed, so do the elipses thing - if (start > 0) { - scope.pagination.unshift({ name: "First", val: 1, isActive: false }, {val: "...",isActive: false}); - } - - //same for the end - if (start < maxIndex) { - scope.pagination.push({ val: "...", isActive: false }, { name: "Last", val: scope.totalPages, isActive: false }); - } - } - - } - - scope.next = function() { - if (scope.onNext && scope.pageNumber < scope.totalPages) { - scope.pageNumber++; - scope.onNext(scope.pageNumber); - } - }; - - scope.prev = function(pageNumber) { - if (scope.onPrev && scope.pageNumber > 1) { - scope.pageNumber--; - scope.onPrev(scope.pageNumber); - } - }; - - scope.goToPage = function(pageNumber) { - if(scope.onGoToPage) { - scope.pageNumber = pageNumber + 1; - scope.onGoToPage(scope.pageNumber); - } - }; - - var unbindPageNumberWatcher = scope.$watch('pageNumber', function(newValue, oldValue){ - activate(); - }); - - scope.$on('$destroy', function(){ - unbindPageNumberWatcher(); - }); - - activate(); - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-pagination.html', - scope: { - pageNumber: "=", - totalPages: "=", - onNext: "=", - onPrev: "=", - onGoToPage: "=" - }, - link: link - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbPagination', PaginationDirective); - -})(); + } - -/** -@ngdoc directive -@name umbraco.directives.directive:umbProgressBar -@restrict E -@scope - -@description -Use this directive to generate a progress bar. - -

Markup example

-
-    
-    
-
- -@param {number} percentage (attribute): The progress in percentage. -**/ - -(function() { - 'use strict'; - - function ProgressBarDirective() { - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-progress-bar.html', - scope: { - percentage: "@" - } - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbProgressBar', ProgressBarDirective); - -})(); + function setVisibility() { -/** -@ngdoc directive -@name umbraco.directives.directive:umbStickyBar -@restrict A - -@description -Use this directive make an element sticky and follow the page when scrolling. - -

Markup example

-
-    
- -
-
- -
-
- -

CSS example

-
-    .my-sticky-bar {
-        padding: 15px 0;
-        background: #000000;
-        position: relative;
-        top: 0;
-    }
-
-    .my-sticky-bar.-umb-sticky-bar {
-        top: 100px;
-    }
-
- -@param {string} scrollableContainer Set the class (".element") or the id ("#element") of the scrollable container element. -**/ - -(function() { - 'use strict'; - - function StickyBarDirective($rootScope) { - - function link(scope, el, attr, ctrl) { - - var bar = $(el); - var scrollableContainer = null; - var clonedBar = null; - var cloneIsMade = false; - var barTop = bar.context.offsetTop; - - function activate() { - - if (attr.scrollableContainer) { - scrollableContainer = $(attr.scrollableContainer); - } else { - scrollableContainer = $(window); - } - - scrollableContainer.on('scroll.umbStickyBar', determineVisibility).trigger("scroll"); - $(window).on('resize.umbStickyBar', determineVisibility); - - scope.$on('$destroy', function() { - scrollableContainer.off('.umbStickyBar'); - $(window).off('.umbStickyBar'); - }); - - } - - function determineVisibility() { - - var scrollTop = scrollableContainer.scrollTop(); - - if (scrollTop > barTop) { - - if (!cloneIsMade) { - - createClone(); - - clonedBar.css({ - 'visibility': 'visible' - }); - - } else { - - calculateSize(); - - } - - } else { - - if (cloneIsMade) { - - //remove cloned element (switched places with original on creation) - bar.remove(); - bar = clonedBar; - clonedBar = null; - - bar.removeClass('-umb-sticky-bar'); - bar.css({ - position: 'relative', - 'width': 'auto', - 'height': 'auto', - 'z-index': 'auto', - 'visibility': 'visible' - }); - - cloneIsMade = false; - - } - - } - - } - - function calculateSize() { - clonedBar.css({ - width: bar.outerWidth(), - height: bar.height() - }); - } - - function createClone() { - //switch place with cloned element, to keep binding intact - clonedBar = bar; - bar = clonedBar.clone(); - clonedBar.after(bar); - clonedBar.addClass('-umb-sticky-bar'); - clonedBar.css({ - 'position': 'fixed', - 'z-index': 500, - 'visibility': 'hidden' - }); - - cloneIsMade = true; - calculateSize(); - - } - - activate(); - - } - - var directive = { - restrict: 'A', - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbStickyBar', StickyBarDirective); - -})(); + var numberOfAllowedLayouts = getNumberOfAllowedLayouts(scope.layouts); + + if(numberOfAllowedLayouts === 1) { + scope.showLayoutSelector = false; + } + + } + + function getNumberOfAllowedLayouts(layouts) { + + var allowedLayouts = 0; + + for (var i = 0; layouts.length > i; i++) { + + var layout = layouts[i]; + + if(layout.selected === true) { + allowedLayouts++; + } + + } + + return allowedLayouts; + } + + function setActiveLayout(layouts) { + + for (var i = 0; layouts.length > i; i++) { + var layout = layouts[i]; + if(layout.path === scope.activeLayout.path) { + layout.active = true; + } + } + + } + + scope.pickLayout = function(selectedLayout) { + if(scope.onLayoutSelect) { + scope.onLayoutSelect(selectedLayout); + scope.layoutDropDownIsOpen = false; + } + }; + + scope.toggleLayoutDropdown = function() { + scope.layoutDropDownIsOpen = !scope.layoutDropDownIsOpen; + }; + + scope.closeLayoutDropdown = function() { + scope.layoutDropDownIsOpen = false; + }; + + activate(); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-layout-selector.html', + scope: { + layouts: '=', + activeLayout: '=', + onLayoutSelect: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbLayoutSelector', LayoutSelectorDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbLightbox +@restrict E +@scope + +@description +

Use this directive to open a gallery in a lightbox overlay.

+ +

Markup example

+
+    
+ + + + + + +
+
+ +

Controller example

+
+    (function () {
+
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+            vm.images = [
+                {
+                    "source": "linkToImage"
+                },
+                {
+                    "source": "linkToImage"
+                }
+            ]
+
+            vm.openLightbox = openLightbox;
+            vm.closeLightbox = closeLightbox;
+
+            function openLightbox(itemIndex, items) {
+                vm.lightbox = {
+                    show: true,
+                    items: items,
+                    activeIndex: itemIndex
+                };
+            }
+
+            function closeLightbox() {
+                vm.lightbox.show = false;
+                vm.lightbox = null;
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {array} items Array of gallery items. +@param {callback} onClose Callback when the lightbox is closed. +@param {number} activeItemIndex Index of active item. +**/ + + +(function() { + 'use strict'; + + function LightboxDirective() { + + function link(scope, el, attr, ctrl) { + + + function activate() { + + var eventBindings = []; + + el.appendTo("body"); + + // clean up + scope.$on('$destroy', function() { + // unbind watchers + for (var e in eventBindings) { + eventBindings[e](); + } + }); + } + + scope.next = function() { + + var nextItemIndex = scope.activeItemIndex + 1; + + if( nextItemIndex < scope.items.length) { + scope.items[scope.activeItemIndex].active = false; + scope.items[nextItemIndex].active = true; + scope.activeItemIndex = nextItemIndex; + } + }; + + scope.prev = function() { + + var prevItemIndex = scope.activeItemIndex - 1; + + if( prevItemIndex >= 0) { + scope.items[scope.activeItemIndex].active = false; + scope.items[prevItemIndex].active = true; + scope.activeItemIndex = prevItemIndex; + } + + }; + + scope.close = function() { + if(scope.onClose) { + scope.onClose(); + } + }; + + activate(); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-lightbox.html', + scope: { + items: '=', + onClose: "=", + activeItemIndex: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbLightbox', LightboxDirective); + +})(); + +(function() { + 'use strict'; + + function ListViewLayoutDirective() { + + function link(scope, el, attr, ctrl) { + + scope.getContent = function(contentId) { + if(scope.onGetContent) { + scope.onGetContent(contentId); + } + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-list-view-layout.html', + scope: { + contentId: '=', + folders: '=', + items: '=', + selection: '=', + options: '=', + entityType: '@', + onGetContent: '=' + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbListViewLayout', ListViewLayoutDirective); + +})(); + +(function() { + 'use strict'; + + function ListViewSettingsDirective(contentTypeResource, dataTypeResource, dataTypeHelper, listViewPrevalueHelper) { + + function link(scope, el, attr, ctrl) { + + scope.dataType = {}; + scope.editDataTypeSettings = false; + scope.customListViewCreated = false; + + /* ---------- INIT ---------- */ + + function activate() { + + if(scope.enableListView) { + + dataTypeResource.getByName(scope.listViewName) + .then(function(dataType) { + + scope.dataType = dataType; + + listViewPrevalueHelper.setPrevalues(dataType.preValues); + scope.customListViewCreated = checkForCustomListView(); + + }); + + } else { + + scope.dataType = {}; + + } + + } + + /* ----------- LIST VIEW SETTINGS --------- */ + + scope.toggleEditListViewDataTypeSettings = function() { + scope.editDataTypeSettings = !scope.editDataTypeSettings; + }; + + scope.saveListViewDataType = function() { + + var preValues = dataTypeHelper.createPreValueProps(scope.dataType.preValues); + + dataTypeResource.save(scope.dataType, preValues, false).then(function(dataType) { + + // store data type + scope.dataType = dataType; + + // hide settings panel + scope.editDataTypeSettings = false; + + }); + + }; + + + /* ---------- CUSTOM LIST VIEW ---------- */ + + scope.createCustomListViewDataType = function() { + + dataTypeResource.createCustomListView(scope.modelAlias).then(function(dataType) { + + // store data type + scope.dataType = dataType; + + // set list view name on scope + scope.listViewName = dataType.name; + + // change state to custom list view + scope.customListViewCreated = true; + + // show settings panel + scope.editDataTypeSettings = true; + + }); + + }; + + scope.removeCustomListDataType = function() { + + scope.editDataTypeSettings = false; + + // delete custom list view data type + dataTypeResource.deleteById(scope.dataType.id).then(function(dataType) { + + // set list view name on scope + if(scope.contentType === "documentType") { + + scope.listViewName = "List View - Content"; + + } else if(scope.contentType === "mediaType") { + + scope.listViewName = "List View - Media"; + + } + + // get default data type + dataTypeResource.getByName(scope.listViewName) + .then(function(dataType) { + + // store data type + scope.dataType = dataType; + + // change state to default list view + scope.customListViewCreated = false; + + }); + }); + + }; + + /* ----------- SCOPE WATCHERS ----------- */ + var unbindEnableListViewWatcher = scope.$watch('enableListView', function(newValue, oldValue){ + + if(newValue !== undefined) { + activate(); + } + + }); + + // clean up + scope.$on('$destroy', function(){ + unbindEnableListViewWatcher(); + }); + + /* ----------- METHODS ---------- */ + + function checkForCustomListView() { + return scope.dataType.name === "List View - " + scope.modelAlias; + } + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-list-view-settings.html', + scope: { + enableListView: "=", + listViewName: "=", + modelAlias: "=", + contentType: "@" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbListViewSettings', ListViewSettingsDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbLoadIndicator +@restrict E + +@description +Use this directive to generate a loading indicator. + +

Markup example

+
+    
+ + + + +
+

{{content}}

+
+ +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller(myService) {
+
+            var vm = this;
+
+            vm.content = "";
+            vm.loading = true;
+
+            myService.getContent().then(function(content){
+                vm.content = content;
+                vm.loading = false;
+            });
+
+        }
+½
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+**/ + +(function() { + 'use strict'; + + function UmbLoadIndicatorDirective() { + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-load-indicator.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbLoadIndicator', UmbLoadIndicatorDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbLockedField +@restrict E +@scope + +@description +Use this directive to render a value with a lock next to it. When the lock is clicked the value gets unlocked and can be edited. + +

Markup example

+
+	
+ + + + +
+
+ +

Controller example

+
+	(function () {
+		"use strict";
+
+		function Controller() {
+
+			var vm = this;
+			vm.value = "My locked text";
+
+        }
+
+		angular.module("umbraco").controller("My.Controller", Controller);
+
+	})();
+
+ +@param {string} ngModel (binding): The locked text. +@param {boolean=} locked (binding): true by default. Set to false to unlock the text. +@param {string=} placeholderText (binding): If ngModel is empty this text will be shown. +@param {string=} regexValidation (binding): Set a regex expression for validation of the field. +@param {string=} serverValidationField (attribute): Set a server validation field. +**/ + +(function() { + 'use strict'; + + function LockedFieldDirective($timeout, localizationService) { + + function link(scope, el, attr, ngModelCtrl) { + + function activate() { + + // if locked state is not defined as an attr set default state + if (scope.locked === undefined || scope.locked === null) { + scope.locked = true; + } + + // if regex validation is not defined as an attr set default state + // if this is set to an empty string then regex validation can be ignored. + if (scope.regexValidation === undefined || scope.regexValidation === null) { + scope.regexValidation = "^[a-zA-Z]\\w.*$"; + } + + if (scope.serverValidationField === undefined || scope.serverValidationField === null) { + scope.serverValidationField = ""; + } + + // if locked state is not defined as an attr set default state + if (scope.placeholderText === undefined || scope.placeholderText === null) { + scope.placeholderText = "Enter value..."; + } + + } + + scope.lock = function() { + scope.locked = true; + }; + + scope.unlock = function() { + scope.locked = false; + }; + + activate(); + + } + + var directive = { + require: "ngModel", + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-locked-field.html', + scope: { + ngModel: "=", + locked: "=?", + placeholderText: "=?", + regexValidation: "=?", + serverValidationField: "@" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbLockedField', LockedFieldDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbMediaGrid +@restrict E +@scope + +@description +Use this directive to generate a thumbnail grid of media items. + +

Markup example

+
+    
+ + + + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+            vm.mediaItems = [];
+
+            vm.clickItem = clickItem;
+            vm.clickItemName = clickItemName;
+
+            myService.getMediaItems().then(function (mediaItems) {
+                vm.mediaItems = mediaItems;
+            });
+
+            function clickItem(item, $event, $index){
+                // do magic here
+            }
+
+            function clickItemName(item, $event, $index) {
+                // set item.selected = true; to select the item
+                // do magic here
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {array} items (binding): Array of media items. +@param {callback=} onDetailsHover (binding): Callback method when the details icon is hovered. +

The callback returns:

+
    +
  • item: The hovered item
  • +
  • $event: The hover event
  • +
  • hover: Boolean to tell if the item is hovered or not
  • +
+@param {callback=} onClick (binding): Callback method to handle click events on the media item. +

The callback returns:

+
    +
  • item: The clicked item
  • +
  • $event: The click event
  • +
  • $index: The item index
  • +
+@param {callback=} onClickName (binding): Callback method to handle click events on the media item name. +

The callback returns:

+
    +
  • item: The clicked item
  • +
  • $event: The click event
  • +
  • $index: The item index
  • +
+@param {string=} filterBy (binding): String to filter media items by +@param {string=} itemMaxWidth (attribute): Sets a max width on the media item thumbnails. +@param {string=} itemMaxHeight (attribute): Sets a max height on the media item thumbnails. +@param {string=} itemMinWidth (attribute): Sets a min width on the media item thumbnails. +@param {string=} itemMinHeight (attribute): Sets a min height on the media item thumbnails. + +**/ + +(function() { + 'use strict'; + + function MediaGridDirective($filter, mediaHelper) { + + function link(scope, el, attr, ctrl) { + + var itemDefaultHeight = 200; + var itemDefaultWidth = 200; + var itemMaxWidth = 200; + var itemMaxHeight = 200; + var itemMinWidth = 125; + var itemMinHeight = 125; + + function activate() { + + if (scope.itemMaxWidth) { + itemMaxWidth = scope.itemMaxWidth; + } + + if (scope.itemMaxHeight) { + itemMaxHeight = scope.itemMaxHeight; + } + + if (scope.itemMinWidth) { + itemMinWidth = scope.itemMinWidth; + } + + if (scope.itemMinWidth) { + itemMinHeight = scope.itemMinHeight; + } + + for (var i = 0; scope.items.length > i; i++) { + var item = scope.items[i]; + setItemData(item); + setOriginalSize(item, itemMaxHeight); + + // remove non images when onlyImages is set to true + if(scope.onlyImages === "true" && !item.isFolder && !item.thumbnail){ + scope.items.splice(i, 1); + i--; + } + + } + + if (scope.items.length > 0) { + setFlexValues(scope.items); + } + + } + + function setItemData(item) { + item.isFolder = !mediaHelper.hasFilePropertyType(item); + if (!item.isFolder) { + item.thumbnail = mediaHelper.resolveFile(item, true); + item.image = mediaHelper.resolveFile(item, false); + + var fileProp = _.find(item.properties, function (v) { + return (v.alias === "umbracoFile"); + }); + + if (fileProp && fileProp.value) { + item.file = fileProp.value; + } + + var extensionProp = _.find(item.properties, function (v) { + return (v.alias === "umbracoExtension"); + }); + + if (extensionProp && extensionProp.value) { + item.extension = extensionProp.value; + } + } + } + + function setOriginalSize(item, maxHeight) { + + //set to a square by default + item.width = itemDefaultWidth; + item.height = itemDefaultHeight; + item.aspectRatio = 1; + + var widthProp = _.find(item.properties, function(v) { + return (v.alias === "umbracoWidth"); + }); + + if (widthProp && widthProp.value) { + item.width = parseInt(widthProp.value, 10); + if (isNaN(item.width)) { + item.width = itemDefaultWidth; + } + } + + var heightProp = _.find(item.properties, function(v) { + return (v.alias === "umbracoHeight"); + }); + + if (heightProp && heightProp.value) { + item.height = parseInt(heightProp.value, 10); + if (isNaN(item.height)) { + item.height = itemDefaultWidth; + } + } + + item.aspectRatio = item.width / item.height; + + // set max width and height + // landscape + if (item.aspectRatio >= 1) { + if (item.width > itemMaxWidth) { + item.width = itemMaxWidth; + item.height = itemMaxWidth / item.aspectRatio; + } + // portrait + } else { + if (item.height > itemMaxHeight) { + item.height = itemMaxHeight; + item.width = itemMaxHeight * item.aspectRatio; + } + } + + } + + function setFlexValues(mediaItems) { + + var flexSortArray = mediaItems; + var smallestImageWidth = null; + var widestImageAspectRatio = null; + + // sort array after image width with the widest image first + flexSortArray = $filter('orderBy')(flexSortArray, 'width', true); + + // find widest image aspect ratio + widestImageAspectRatio = flexSortArray[0].aspectRatio; + + // find smallest image width + smallestImageWidth = flexSortArray[flexSortArray.length - 1].width; + + for (var i = 0; flexSortArray.length > i; i++) { + + var mediaItem = flexSortArray[i]; + var flex = 1 / (widestImageAspectRatio / mediaItem.aspectRatio); + + if (flex === 0) { + flex = 1; + } + + var imageMinFlexWidth = smallestImageWidth * flex; + + var flexStyle = { + "flex": flex + " 1 " + imageMinFlexWidth + "px", + "max-width": mediaItem.width + "px", + "min-width": itemMinWidth + "px", + "min-height": itemMinHeight + "px" + }; + + mediaItem.flexStyle = flexStyle; + + } + + } + + scope.clickItem = function(item, $event, $index) { + if (scope.onClick) { + scope.onClick(item, $event, $index); + } + }; + + scope.clickItemName = function(item, $event, $index) { + if (scope.onClickName) { + scope.onClickName(item, $event, $index); + $event.stopPropagation(); + } + }; + + scope.hoverItemDetails = function(item, $event, hover) { + if (scope.onDetailsHover) { + scope.onDetailsHover(item, $event, hover); + } + }; + + var unbindItemsWatcher = scope.$watch('items', function(newValue, oldValue) { + if (angular.isArray(newValue)) { + activate(); + } + }); + + scope.$on('$destroy', function() { + unbindItemsWatcher(); + }); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-media-grid.html', + scope: { + items: '=', + onDetailsHover: "=", + onClick: '=', + onClickName: "=", + filterBy: "=", + itemMaxWidth: "@", + itemMaxHeight: "@", + itemMinWidth: "@", + itemMinHeight: "@", + onlyImages: "@" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbMediaGrid', MediaGridDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbPagination +@restrict E +@scope + +@description +Use this directive to generate a pagination. + +

Markup example

+
+    
+ + + + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+            vm.pagination = {
+                pageNumber: 1,
+                totalPages: 10
+            }
+
+            vm.nextPage = nextPage;
+            vm.prevPage = prevPage;
+            vm.goToPage = goToPage;
+
+            function nextPage(pageNumber) {
+                // do magic here
+                console.log(pageNumber);
+                alert("nextpage");
+            }
+
+            function prevPage(pageNumber) {
+                // do magic here
+                console.log(pageNumber);
+                alert("prevpage");
+            }
+
+            function goToPage(pageNumber) {
+                // do magic here
+                console.log(pageNumber);
+                alert("go to");
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {number} pageNumber (binding): Current page number. +@param {number} totalPages (binding): The total number of pages. +@param {callback} onNext (binding): Callback method to go to the next page. +

The callback returns:

+
    +
  • pageNumber: The page number
  • +
+@param {callback=} onPrev (binding): Callback method to go to the previous page. +

The callback returns:

+
    +
  • pageNumber: The page number
  • +
+@param {callback=} onGoToPage (binding): Callback method to go to a specific page. +

The callback returns:

+
    +
  • pageNumber: The page number
  • +
+**/ + +(function() { + 'use strict'; + + function PaginationDirective() { + + function link(scope, el, attr, ctrl) { + + function activate() { + + scope.pagination = []; + + var i = 0; + + if (scope.totalPages <= 10) { + for (i = 0; i < scope.totalPages; i++) { + scope.pagination.push({ + val: (i + 1), + isActive: scope.pageNumber === (i + 1) + }); + } + } + else { + //if there is more than 10 pages, we need to do some fancy bits + + //get the max index to start + var maxIndex = scope.totalPages - 10; + //set the start, but it can't be below zero + var start = Math.max(scope.pageNumber - 5, 0); + //ensure that it's not too far either + start = Math.min(maxIndex, start); + + for (i = start; i < (10 + start) ; i++) { + scope.pagination.push({ + val: (i + 1), + isActive: scope.pageNumber === (i + 1) + }); + } + + //now, if the start is greater than 0 then '1' will not be displayed, so do the elipses thing + if (start > 0) { + scope.pagination.unshift({ name: "First", val: 1, isActive: false }, {val: "...",isActive: false}); + } + + //same for the end + if (start < maxIndex) { + scope.pagination.push({ val: "...", isActive: false }, { name: "Last", val: scope.totalPages, isActive: false }); + } + } + + } + + scope.next = function() { + if (scope.onNext && scope.pageNumber < scope.totalPages) { + scope.pageNumber++; + scope.onNext(scope.pageNumber); + } + }; + + scope.prev = function(pageNumber) { + if (scope.onPrev && scope.pageNumber > 1) { + scope.pageNumber--; + scope.onPrev(scope.pageNumber); + } + }; + + scope.goToPage = function(pageNumber) { + if(scope.onGoToPage) { + scope.pageNumber = pageNumber + 1; + scope.onGoToPage(scope.pageNumber); + } + }; + + var unbindPageNumberWatcher = scope.$watch('pageNumber', function(newValue, oldValue){ + activate(); + }); + + scope.$on('$destroy', function(){ + unbindPageNumberWatcher(); + }); + + activate(); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-pagination.html', + scope: { + pageNumber: "=", + totalPages: "=", + onNext: "=", + onPrev: "=", + onGoToPage: "=" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbPagination', PaginationDirective); + +})(); + + +/** +@ngdoc directive +@name umbraco.directives.directive:umbProgressBar +@restrict E +@scope + +@description +Use this directive to generate a progress bar. + +

Markup example

+
+    
+    
+
+ +@param {number} percentage (attribute): The progress in percentage. +**/ + +(function() { + 'use strict'; + + function ProgressBarDirective() { + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-progress-bar.html', + scope: { + percentage: "@" + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbProgressBar', ProgressBarDirective); + +})(); + +/** +@ngdoc directive +@name umbraco.directives.directive:umbStickyBar +@restrict A + +@description +Use this directive make an element sticky and follow the page when scrolling. + +

Markup example

+
+    
+ +
+
+ +
+
+ +

CSS example

+
+    .my-sticky-bar {
+        padding: 15px 0;
+        background: #000000;
+        position: relative;
+        top: 0;
+    }
+
+    .my-sticky-bar.-umb-sticky-bar {
+        top: 100px;
+    }
+
+ +@param {string} scrollableContainer Set the class (".element") or the id ("#element") of the scrollable container element. +**/ + +(function() { + 'use strict'; + + function StickyBarDirective($rootScope) { + + function link(scope, el, attr, ctrl) { + + var bar = $(el); + var scrollableContainer = null; + var clonedBar = null; + var cloneIsMade = false; + var barTop = bar.context.offsetTop; + + function activate() { + + if (attr.scrollableContainer) { + scrollableContainer = $(attr.scrollableContainer); + } else { + scrollableContainer = $(window); + } + + scrollableContainer.on('scroll.umbStickyBar', determineVisibility).trigger("scroll"); + $(window).on('resize.umbStickyBar', determineVisibility); + + scope.$on('$destroy', function() { + scrollableContainer.off('.umbStickyBar'); + $(window).off('.umbStickyBar'); + }); + + } + + function determineVisibility() { + + var scrollTop = scrollableContainer.scrollTop(); + + if (scrollTop > barTop) { + + if (!cloneIsMade) { + + createClone(); + + clonedBar.css({ + 'visibility': 'visible' + }); + + } else { + + calculateSize(); + + } + + } else { + + if (cloneIsMade) { + + //remove cloned element (switched places with original on creation) + bar.remove(); + bar = clonedBar; + clonedBar = null; + + bar.removeClass('-umb-sticky-bar'); + bar.css({ + position: 'relative', + 'width': 'auto', + 'height': 'auto', + 'z-index': 'auto', + 'visibility': 'visible' + }); + + cloneIsMade = false; + + } + + } + + } + + function calculateSize() { + clonedBar.css({ + width: bar.outerWidth(), + height: bar.height() + }); + } + + function createClone() { + //switch place with cloned element, to keep binding intact + clonedBar = bar; + bar = clonedBar.clone(); + clonedBar.after(bar); + clonedBar.addClass('-umb-sticky-bar'); + clonedBar.css({ + 'position': 'fixed', + 'z-index': 500, + 'visibility': 'hidden' + }); + + cloneIsMade = true; + calculateSize(); + + } + + activate(); + + } + + var directive = { + restrict: 'A', + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbStickyBar', StickyBarDirective); + +})(); (function () { 'use strict'; @@ -9548,403 +9606,403 @@ Use this directive make an element sticky and follow the page when scrolling. })(); -/** -@ngdoc directive -@name umbraco.directives.directive:umbTooltip -@restrict E -@scope - -@description -Use this directive to render a tooltip. - -

Markup example

-
-    
- -
- Hover me -
- - - // tooltip content here - - -
-
- -

Controller example

-
-    (function () {
-        "use strict";
-
-        function Controller() {
-
-            var vm = this;
-            vm.tooltip = {
-                show: false,
-                event: null
-            };
-
-            vm.mouseOver = mouseOver;
-            vm.mouseLeave = mouseLeave;
-
-            function mouseOver($event) {
-                vm.tooltip = {
-                    show: true,
-                    event: $event
-                };
-            }
-
-            function mouseLeave() {
-                vm.tooltip = {
-                    show: false,
-                    event: null
-                };
-            }
-
-        }
-
-        angular.module("umbraco").controller("My.Controller", Controller);
-
-    })();
-
- -@param {string} event Set the $event from the target element to position the tooltip relative to the mouse cursor. -**/ - -(function() { - 'use strict'; - - function TooltipDirective($timeout) { - - function link(scope, el, attr, ctrl) { - - scope.tooltipStyles = {}; - scope.tooltipStyles.left = 0; - scope.tooltipStyles.top = 0; - - function activate() { - - $timeout(function() { - setTooltipPosition(scope.event); - }); - - } - - function setTooltipPosition(event) { - - var container = $("#contentwrapper"); - var containerLeft = container[0].offsetLeft; - var containerRight = containerLeft + container[0].offsetWidth; - var containerTop = container[0].offsetTop; - var containerBottom = containerTop + container[0].offsetHeight; - - var elementHeight = null; - var elementWidth = null; - - var position = { - right: "inherit", - left: "inherit", - top: "inherit", - bottom: "inherit" - }; - - // element size - elementHeight = el.context.clientHeight; - elementWidth = el.context.clientWidth; - - position.left = event.pageX - (elementWidth / 2); - position.top = event.pageY; - - // check to see if element is outside screen - // outside right - if (position.left + elementWidth > containerRight) { - position.right = 10; - position.left = "inherit"; - } - - // outside bottom - if (position.top + elementHeight > containerBottom) { - position.bottom = 10; - position.top = "inherit"; - } - - // outside left - if (position.left < containerLeft) { - position.left = containerLeft + 10; - position.right = "inherit"; - } - - // outside top - if (position.top < containerTop) { - position.top = 10; - position.bottom = "inherit"; - } - - scope.tooltipStyles = position; - - el.css(position); - - } - - activate(); - - } - - var directive = { - restrict: 'E', - transclude: true, - replace: true, - templateUrl: 'views/components/umb-tooltip.html', - scope: { - event: "=" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbTooltip', TooltipDirective); - -})(); +/** +@ngdoc directive +@name umbraco.directives.directive:umbTooltip +@restrict E +@scope -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbFileDropzone -* @restrict E -* @function -* @description -* Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form. -**/ - -/* -TODO -.directive("umbFileDrop", function ($timeout, $upload, localizationService, umbRequestHelper){ - return{ - restrict: "A", - link: function(scope, element, attrs){ - //load in the options model - } - } -}) -*/ - -angular.module("umbraco.directives") - .directive('umbFileDropzone', - function($timeout, Upload, localizationService, umbRequestHelper) { - return { - restrict: 'E', - replace: true, - templateUrl: 'views/components/upload/umb-file-dropzone.html', - scope: { - parentId: '@', - contentTypeAlias: '@', - propertyAlias: '@', - accept: '@', - maxFileSize: '@', - - compact: '@', - hideDropzone: '@', - acceptedMediatypes: '=', - - filesQueued: '=', - handleFile: '=', - filesUploaded: '=' - }, - link: function(scope, element, attrs) { - scope.queue = []; - scope.done = []; - scope.rejected = []; - scope.currentFile = undefined; - - function _filterFile(file) { - var ignoreFileNames = ['Thumbs.db']; - var ignoreFileTypes = ['directory']; - - // ignore files with names from the list - // ignore files with types from the list - // ignore files which starts with "." - if (ignoreFileNames.indexOf(file.name) === -1 && - ignoreFileTypes.indexOf(file.type) === -1 && - file.name.indexOf(".") !== 0) { - return true; - } else { - return false; - } - } - - function _filesQueued(files, event) { - //Push into the queue - angular.forEach(files, - function(file) { - - if (_filterFile(file) === true) { - - if (file.$error) { - scope.rejected.push(file); - } else { - scope.queue.push(file); - } - } - }); - - //when queue is done, kick the uploader - if (!scope.working) { - // Upload not allowed - if (!scope.acceptedMediatypes || !scope.acceptedMediatypes.length) { - files.map(function(file) { - file.uploadStatus = "error"; - file.serverErrorMessage = "File type is not allowed here"; - scope.rejected.push(file); - }); - scope.queue = []; - } - // One allowed type - if (scope.acceptedMediatypes && scope.acceptedMediatypes.length === 1) { - // Standard setup - set alias to auto select to let the server best decide which media type to use - if (scope.acceptedMediatypes[0].alias === 'Image') { - scope.contentTypeAlias = "umbracoAutoSelect"; - } else { - scope.contentTypeAlias = scope.acceptedMediatypes[0].alias; - } - - _processQueueItem(); - } - // More than one, open dialog - if (scope.acceptedMediatypes && scope.acceptedMediatypes.length > 1) { - _chooseMediaType(); - } - } - } - - function _processQueueItem() { - if (scope.queue.length > 0) { - scope.currentFile = scope.queue.shift(); - _upload(scope.currentFile); - } else if (scope.done.length > 0) { - if (scope.filesUploaded) { - //queue is empty, trigger the done action - scope.filesUploaded(scope.done); - } - - //auto-clear the done queue after 3 secs - var currentLength = scope.done.length; - $timeout(function() { - scope.done.splice(0, currentLength); - }, - 3000); - } - } - - function _upload(file) { - - scope.propertyAlias = scope.propertyAlias ? scope.propertyAlias : "umbracoFile"; - scope.contentTypeAlias = scope.contentTypeAlias ? scope.contentTypeAlias : "Image"; - - Upload.upload({ - url: umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostAddFile"), - fields: { - 'currentFolder': scope.parentId, - 'contentTypeAlias': scope.contentTypeAlias, - 'propertyAlias': scope.propertyAlias, - 'path': file.path - }, - file: file - }) - .progress(function(evt) { - // calculate progress in percentage - var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); - // set percentage property on file - file.uploadProgress = progressPercentage; - // set uploading status on file - file.uploadStatus = "uploading"; - }) - .success(function(data, status, headers, config) { - if (data.notifications && data.notifications.length > 0) { - // set error status on file - file.uploadStatus = "error"; - // Throw message back to user with the cause of the error - file.serverErrorMessage = data.notifications[0].message; - // Put the file in the rejected pool - scope.rejected.push(file); - } else { - // set done status on file - file.uploadStatus = "done"; - // set date/time for when done - used for sorting - file.doneDate = new Date(); - // Put the file in the done pool - scope.done.push(file); - } - scope.currentFile = undefined; - //after processing, test if everthing is done - _processQueueItem(); - }) - .error(function(evt, status, headers, config) { - // set status done - file.uploadStatus = "error"; - //if the service returns a detailed error - if (evt.InnerException) { - file.serverErrorMessage = evt.InnerException.ExceptionMessage; - //Check if its the common "too large file" exception - if (evt.InnerException.StackTrace && - evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { - file.serverErrorMessage = "File too large to upload"; - } - } else if (evt.Message) { - file.serverErrorMessage = evt.Message; - } - // If file not found, server will return a 404 and display this message - if (status === 404) { - file.serverErrorMessage = "File not found"; - } - //after processing, test if everthing is done - scope.rejected.push(file); - scope.currentFile = undefined; - _processQueueItem(); - }); - } - - function _chooseMediaType() { - scope.mediatypepickerOverlay = { - view: "mediatypepicker", - title: "Choose media type", - acceptedMediatypes: scope.acceptedMediatypes, - hideSubmitButton: true, - show: true, - submit: function(model) { - scope.contentTypeAlias = model.selectedType.alias; - scope.mediatypepickerOverlay.show = false; - scope.mediatypepickerOverlay = null; - _processQueueItem(); - }, - close: function(oldModel) { - - scope.queue.map(function(file) { - file.uploadStatus = "error"; - file.serverErrorMessage = "Cannot upload this file, no mediatype selected"; - scope.rejected.push(file); - }); - scope.queue = []; - scope.mediatypepickerOverlay.show = false; - scope.mediatypepickerOverlay = null; - } - }; - } - - scope.handleFiles = function(files, event) { - if (scope.filesQueued) { - scope.filesQueued(files, event); - } - _filesQueued(files, event); - }; - } - }; - }); +@description +Use this directive to render a tooltip. + +

Markup example

+
+    
+ +
+ Hover me +
+ + + // tooltip content here + + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+            vm.tooltip = {
+                show: false,
+                event: null
+            };
+
+            vm.mouseOver = mouseOver;
+            vm.mouseLeave = mouseLeave;
+
+            function mouseOver($event) {
+                vm.tooltip = {
+                    show: true,
+                    event: $event
+                };
+            }
+
+            function mouseLeave() {
+                vm.tooltip = {
+                    show: false,
+                    event: null
+                };
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+
+    })();
+
+ +@param {string} event Set the $event from the target element to position the tooltip relative to the mouse cursor. +**/ + +(function() { + 'use strict'; + + function TooltipDirective($timeout) { + + function link(scope, el, attr, ctrl) { + + scope.tooltipStyles = {}; + scope.tooltipStyles.left = 0; + scope.tooltipStyles.top = 0; + + function activate() { + + $timeout(function() { + setTooltipPosition(scope.event); + }); + + } + + function setTooltipPosition(event) { + + var container = $("#contentwrapper"); + var containerLeft = container[0].offsetLeft; + var containerRight = containerLeft + container[0].offsetWidth; + var containerTop = container[0].offsetTop; + var containerBottom = containerTop + container[0].offsetHeight; + + var elementHeight = null; + var elementWidth = null; + + var position = { + right: "inherit", + left: "inherit", + top: "inherit", + bottom: "inherit" + }; + + // element size + elementHeight = el.context.clientHeight; + elementWidth = el.context.clientWidth; + + position.left = event.pageX - (elementWidth / 2); + position.top = event.pageY; + + // check to see if element is outside screen + // outside right + if (position.left + elementWidth > containerRight) { + position.right = 10; + position.left = "inherit"; + } + + // outside bottom + if (position.top + elementHeight > containerBottom) { + position.bottom = 10; + position.top = "inherit"; + } + + // outside left + if (position.left < containerLeft) { + position.left = containerLeft + 10; + position.right = "inherit"; + } + + // outside top + if (position.top < containerTop) { + position.top = 10; + position.bottom = "inherit"; + } + + scope.tooltipStyles = position; + + el.css(position); + + } + + activate(); + + } + + var directive = { + restrict: 'E', + transclude: true, + replace: true, + templateUrl: 'views/components/umb-tooltip.html', + scope: { + event: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbTooltip', TooltipDirective); + +})(); + +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbFileDropzone +* @restrict E +* @function +* @description +* Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form. +**/ + +/* +TODO +.directive("umbFileDrop", function ($timeout, $upload, localizationService, umbRequestHelper){ + return{ + restrict: "A", + link: function(scope, element, attrs){ + //load in the options model + } + } +}) +*/ + +angular.module("umbraco.directives") + .directive('umbFileDropzone', + function($timeout, Upload, localizationService, umbRequestHelper) { + return { + restrict: 'E', + replace: true, + templateUrl: 'views/components/upload/umb-file-dropzone.html', + scope: { + parentId: '@', + contentTypeAlias: '@', + propertyAlias: '@', + accept: '@', + maxFileSize: '@', + + compact: '@', + hideDropzone: '@', + acceptedMediatypes: '=', + + filesQueued: '=', + handleFile: '=', + filesUploaded: '=' + }, + link: function(scope, element, attrs) { + scope.queue = []; + scope.done = []; + scope.rejected = []; + scope.currentFile = undefined; + + function _filterFile(file) { + var ignoreFileNames = ['Thumbs.db']; + var ignoreFileTypes = ['directory']; + + // ignore files with names from the list + // ignore files with types from the list + // ignore files which starts with "." + if (ignoreFileNames.indexOf(file.name) === -1 && + ignoreFileTypes.indexOf(file.type) === -1 && + file.name.indexOf(".") !== 0) { + return true; + } else { + return false; + } + } + + function _filesQueued(files, event) { + //Push into the queue + angular.forEach(files, + function(file) { + + if (_filterFile(file) === true) { + + if (file.$error) { + scope.rejected.push(file); + } else { + scope.queue.push(file); + } + } + }); + + //when queue is done, kick the uploader + if (!scope.working) { + // Upload not allowed + if (!scope.acceptedMediatypes || !scope.acceptedMediatypes.length) { + files.map(function(file) { + file.uploadStatus = "error"; + file.serverErrorMessage = "File type is not allowed here"; + scope.rejected.push(file); + }); + scope.queue = []; + } + // One allowed type + if (scope.acceptedMediatypes && scope.acceptedMediatypes.length === 1) { + // Standard setup - set alias to auto select to let the server best decide which media type to use + if (scope.acceptedMediatypes[0].alias === 'Image') { + scope.contentTypeAlias = "umbracoAutoSelect"; + } else { + scope.contentTypeAlias = scope.acceptedMediatypes[0].alias; + } + + _processQueueItem(); + } + // More than one, open dialog + if (scope.acceptedMediatypes && scope.acceptedMediatypes.length > 1) { + _chooseMediaType(); + } + } + } + + function _processQueueItem() { + if (scope.queue.length > 0) { + scope.currentFile = scope.queue.shift(); + _upload(scope.currentFile); + } else if (scope.done.length > 0) { + if (scope.filesUploaded) { + //queue is empty, trigger the done action + scope.filesUploaded(scope.done); + } + + //auto-clear the done queue after 3 secs + var currentLength = scope.done.length; + $timeout(function() { + scope.done.splice(0, currentLength); + }, + 3000); + } + } + + function _upload(file) { + + scope.propertyAlias = scope.propertyAlias ? scope.propertyAlias : "umbracoFile"; + scope.contentTypeAlias = scope.contentTypeAlias ? scope.contentTypeAlias : "Image"; + + Upload.upload({ + url: umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostAddFile"), + fields: { + 'currentFolder': scope.parentId, + 'contentTypeAlias': scope.contentTypeAlias, + 'propertyAlias': scope.propertyAlias, + 'path': file.path + }, + file: file + }) + .progress(function(evt) { + // calculate progress in percentage + var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); + // set percentage property on file + file.uploadProgress = progressPercentage; + // set uploading status on file + file.uploadStatus = "uploading"; + }) + .success(function(data, status, headers, config) { + if (data.notifications && data.notifications.length > 0) { + // set error status on file + file.uploadStatus = "error"; + // Throw message back to user with the cause of the error + file.serverErrorMessage = data.notifications[0].message; + // Put the file in the rejected pool + scope.rejected.push(file); + } else { + // set done status on file + file.uploadStatus = "done"; + // set date/time for when done - used for sorting + file.doneDate = new Date(); + // Put the file in the done pool + scope.done.push(file); + } + scope.currentFile = undefined; + //after processing, test if everthing is done + _processQueueItem(); + }) + .error(function(evt, status, headers, config) { + // set status done + file.uploadStatus = "error"; + //if the service returns a detailed error + if (evt.InnerException) { + file.serverErrorMessage = evt.InnerException.ExceptionMessage; + //Check if its the common "too large file" exception + if (evt.InnerException.StackTrace && + evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { + file.serverErrorMessage = "File too large to upload"; + } + } else if (evt.Message) { + file.serverErrorMessage = evt.Message; + } + // If file not found, server will return a 404 and display this message + if (status === 404) { + file.serverErrorMessage = "File not found"; + } + //after processing, test if everthing is done + scope.rejected.push(file); + scope.currentFile = undefined; + _processQueueItem(); + }); + } + + function _chooseMediaType() { + scope.mediatypepickerOverlay = { + view: "mediatypepicker", + title: "Choose media type", + acceptedMediatypes: scope.acceptedMediatypes, + hideSubmitButton: true, + show: true, + submit: function(model) { + scope.contentTypeAlias = model.selectedType.alias; + scope.mediatypepickerOverlay.show = false; + scope.mediatypepickerOverlay = null; + _processQueueItem(); + }, + close: function(oldModel) { + + scope.queue.map(function(file) { + file.uploadStatus = "error"; + file.serverErrorMessage = "Cannot upload this file, no mediatype selected"; + scope.rejected.push(file); + }); + scope.queue = []; + scope.mediatypepickerOverlay.show = false; + scope.mediatypepickerOverlay = null; + } + }; + } + + scope.handleFiles = function(files, event) { + if (scope.filesQueued) { + scope.filesQueued(files, event); + } + _filesQueued(files, event); + }; + } + }; + }); /** * @ngdoc directive @@ -9970,1243 +10028,1243 @@ function umbFileUpload() { } angular.module('umbraco.directives').directive("umbFileUpload", umbFileUpload); -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbSingleFileUpload -* @function -* @restrict A -* @scope -* @description -* A single file upload field that will reset itself based on the object passed in for the rebuild parameter. This -* is required because the only way to reset an upload control is to replace it's html. -**/ -function umbSingleFileUpload($compile) { - return { - restrict: "E", - scope: { - rebuild: "=" - }, - replace: true, - template: "
", - link: function (scope, el, attrs) { - - scope.$watch("rebuild", function (newVal, oldVal) { - if (newVal && newVal !== oldVal) { - //recompile it! - el.html(""); - $compile(el.contents())(scope); - } - }); - - } - }; -} - +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbSingleFileUpload +* @function +* @restrict A +* @scope +* @description +* A single file upload field that will reset itself based on the object passed in for the rebuild parameter. This +* is required because the only way to reset an upload control is to replace it's html. +**/ +function umbSingleFileUpload($compile) { + return { + restrict: "E", + scope: { + rebuild: "=" + }, + replace: true, + template: "
", + link: function (scope, el, attrs) { + + scope.$watch("rebuild", function (newVal, oldVal) { + if (newVal && newVal !== oldVal) { + //recompile it! + el.html(""); + $compile(el.contents())(scope); + } + }); + + } + }; +} + angular.module('umbraco.directives').directive("umbSingleFileUpload", umbSingleFileUpload); -/** - * Konami Code directive for AngularJS - * @version v0.0.1 - * @license MIT License, http://www.opensource.org/licenses/MIT - */ - -angular.module('umbraco.directives') - .directive('konamiCode', ['$document', function ($document) { - var konamiKeysDefault = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]; - - return { - restrict: 'A', - link: function (scope, element, attr) { - - if (!attr.konamiCode) { - throw ('Konami directive must receive an expression as value.'); - } - - // Let user define a custom code. - var konamiKeys = attr.konamiKeys || konamiKeysDefault; - var keyIndex = 0; - - /** - * Fired when konami code is type. - */ - function activated() { - if ('konamiOnce' in attr) { - stopListening(); - } - // Execute expression. - scope.$eval(attr.konamiCode); - } - - /** - * Handle keydown events. - */ - function keydown(e) { - if (e.keyCode === konamiKeys[keyIndex++]) { - if (keyIndex === konamiKeys.length) { - keyIndex = 0; - activated(); - } - } else { - keyIndex = 0; - } - } - - /** - * Stop to listen typing. - */ - function stopListening() { - $document.off('keydown', keydown); - } - - // Start listening to key typing. - $document.on('keydown', keydown); - - // Stop listening when scope is destroyed. - scope.$on('$destroy', stopListening); - } - }; +/** + * Konami Code directive for AngularJS + * @version v0.0.1 + * @license MIT License, http://www.opensource.org/licenses/MIT + */ + +angular.module('umbraco.directives') + .directive('konamiCode', ['$document', function ($document) { + var konamiKeysDefault = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]; + + return { + restrict: 'A', + link: function (scope, element, attr) { + + if (!attr.konamiCode) { + throw ('Konami directive must receive an expression as value.'); + } + + // Let user define a custom code. + var konamiKeys = attr.konamiKeys || konamiKeysDefault; + var keyIndex = 0; + + /** + * Fired when konami code is type. + */ + function activated() { + if ('konamiOnce' in attr) { + stopListening(); + } + // Execute expression. + scope.$eval(attr.konamiCode); + } + + /** + * Handle keydown events. + */ + function keydown(e) { + if (e.keyCode === konamiKeys[keyIndex++]) { + if (keyIndex === konamiKeys.length) { + keyIndex = 0; + activated(); + } + } else { + keyIndex = 0; + } + } + + /** + * Stop to listen typing. + */ + function stopListening() { + $document.off('keydown', keydown); + } + + // Start listening to key typing. + $document.on('keydown', keydown); + + // Stop listening when scope is destroyed. + scope.$on('$destroy', stopListening); + } + }; }]); -/** -* @ngdoc directive -* @name umbraco.directives.directive:noDirtyCheck -* @restrict A -* @description Can be attached to form inputs to prevent them from setting the form as dirty (http://stackoverflow.com/questions/17089090/prevent-input-from-setting-form-dirty-angularjs) -**/ -function noDirtyCheck() { - return { - restrict: 'A', - require: 'ngModel', - link: function (scope, elm, attrs, ctrl) { - elm.focus(function () { - ctrl.$pristine = false; - }); - } - }; -} +/** +* @ngdoc directive +* @name umbraco.directives.directive:noDirtyCheck +* @restrict A +* @description Can be attached to form inputs to prevent them from setting the form as dirty (http://stackoverflow.com/questions/17089090/prevent-input-from-setting-form-dirty-angularjs) +**/ +function noDirtyCheck() { + return { + restrict: 'A', + require: 'ngModel', + link: function (scope, elm, attrs, ctrl) { + elm.focus(function () { + ctrl.$pristine = false; + }); + } + }; +} angular.module('umbraco.directives.validation').directive("noDirtyCheck", noDirtyCheck); -(function() { - 'use strict'; - - function SetDirtyOnChange() { - - function link(scope, el, attr, ctrl) { - - var initValue = attr.umbSetDirtyOnChange; - - attr.$observe("umbSetDirtyOnChange", function (newValue) { - if(newValue !== initValue) { - ctrl.$setDirty(); - } - }); - - } - - var directive = { - require: "^form", - restrict: 'A', - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbSetDirtyOnChange', SetDirtyOnChange); - -})(); +(function() { + 'use strict'; -/** - * General-purpose validator for ngModel. - * angular.js comes with several built-in validation mechanism for input fields (ngRequired, ngPattern etc.) but using - * an arbitrary validation function requires creation of a custom formatters and / or parsers. - * The ui-validate directive makes it easy to use any function(s) defined in scope as a validator function(s). - * A validator function will trigger validation on both model and input changes. - * - * @example - * @example - * @example - * @example - * - * @param val-custom {string|object literal} If strings is passed it should be a scope's function to be used as a validator. - * If an object literal is passed a key denotes a validation error key while a value should be a validator function. - * In both cases validator function should take a value to validate as its argument and should return true/false indicating a validation result. - */ - - /* - This code comes from the angular UI project, we had to change the directive name and module - but other then that its unmodified - */ -angular.module('umbraco.directives.validation') -.directive('valCustom', function () { - - return { - restrict: 'A', - require: 'ngModel', - link: function (scope, elm, attrs, ctrl) { - var validateFn, watch, validators = {}, - validateExpr = scope.$eval(attrs.valCustom); - - if (!validateExpr){ return;} - - if (angular.isString(validateExpr)) { - validateExpr = { validator: validateExpr }; - } - - angular.forEach(validateExpr, function (exprssn, key) { - validateFn = function (valueToValidate) { - var expression = scope.$eval(exprssn, { '$value' : valueToValidate }); - if (angular.isObject(expression) && angular.isFunction(expression.then)) { - // expression is a promise - expression.then(function(){ - ctrl.$setValidity(key, true); - }, function(){ - ctrl.$setValidity(key, false); - }); - return valueToValidate; - } else if (expression) { - // expression is true - ctrl.$setValidity(key, true); - return valueToValidate; - } else { - // expression is false - ctrl.$setValidity(key, false); - return undefined; - } - }; - validators[key] = validateFn; - - ctrl.$parsers.push(validateFn); - }); - - function apply_watch(watch) - { - //string - update all validators on expression change - if (angular.isString(watch)) - { - scope.$watch(watch, function(){ - angular.forEach(validators, function(validatorFn){ - validatorFn(ctrl.$modelValue); - }); - }); - return; - } - - //array - update all validators on change of any expression - if (angular.isArray(watch)) - { - angular.forEach(watch, function(expression){ - scope.$watch(expression, function() - { - angular.forEach(validators, function(validatorFn){ - validatorFn(ctrl.$modelValue); - }); - }); - }); - return; - } - - //object - update appropriate validator - if (angular.isObject(watch)) - { - angular.forEach(watch, function(expression, validatorKey) - { - //value is string - look after one expression - if (angular.isString(expression)) - { - scope.$watch(expression, function(){ - validators[validatorKey](ctrl.$modelValue); - }); - } - - //value is array - look after all expressions in array - if (angular.isArray(expression)) - { - angular.forEach(expression, function(intExpression) - { - scope.$watch(intExpression, function(){ - validators[validatorKey](ctrl.$modelValue); - }); - }); - } - }); - } - } - // Support for val-custom-watch - if (attrs.valCustomWatch){ - apply_watch( scope.$eval(attrs.valCustomWatch) ); - } - } - }; -}); -/** -* @ngdoc directive -* @name umbraco.directives.directive:valHighlight -* @restrict A -* @description Used on input fields when you want to signal that they are in error, this will highlight the item for 1 second -**/ -function valHighlight($timeout) { - return { - restrict: "A", - link: function (scope, element, attrs, ctrl) { - - attrs.$observe("valHighlight", function (newVal) { - if (newVal === "true") { - element.addClass("highlight-error"); - $timeout(function () { - //set the bound scope property to false - scope[attrs.valHighlight] = false; - }, 1000); - } - else { - element.removeClass("highlight-error"); - } - }); - - } - }; -} -angular.module('umbraco.directives.validation').directive("valHighlight", valHighlight); + function SetDirtyOnChange() { -angular.module('umbraco.directives.validation') - .directive('valCompare',function () { - return { - require: "ngModel", - link: function (scope, elem, attrs, ctrl) { - - //TODO: Pretty sure this should be done using a requires ^form in the directive declaration - var otherInput = elem.inheritedData("$formController")[attrs.valCompare]; - - ctrl.$parsers.push(function(value) { - if(value === otherInput.$viewValue) { - ctrl.$setValidity("valCompare", true); - return value; - } - ctrl.$setValidity("valCompare", false); - }); - - otherInput.$parsers.push(function(value) { - ctrl.$setValidity("valCompare", value === ctrl.$viewValue); - return value; - }); - } - }; + function link(scope, el, attr, ctrl) { + + var initValue = attr.umbSetDirtyOnChange; + + attr.$observe("umbSetDirtyOnChange", function (newValue) { + if(newValue !== initValue) { + ctrl.$setDirty(); + } + }); + + } + + var directive = { + require: "^form", + restrict: 'A', + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbSetDirtyOnChange', SetDirtyOnChange); + +})(); + +/** + * General-purpose validator for ngModel. + * angular.js comes with several built-in validation mechanism for input fields (ngRequired, ngPattern etc.) but using + * an arbitrary validation function requires creation of a custom formatters and / or parsers. + * The ui-validate directive makes it easy to use any function(s) defined in scope as a validator function(s). + * A validator function will trigger validation on both model and input changes. + * + * @example + * @example + * @example + * @example + * + * @param val-custom {string|object literal} If strings is passed it should be a scope's function to be used as a validator. + * If an object literal is passed a key denotes a validation error key while a value should be a validator function. + * In both cases validator function should take a value to validate as its argument and should return true/false indicating a validation result. + */ + + /* + This code comes from the angular UI project, we had to change the directive name and module + but other then that its unmodified + */ +angular.module('umbraco.directives.validation') +.directive('valCustom', function () { + + return { + restrict: 'A', + require: 'ngModel', + link: function (scope, elm, attrs, ctrl) { + var validateFn, watch, validators = {}, + validateExpr = scope.$eval(attrs.valCustom); + + if (!validateExpr){ return;} + + if (angular.isString(validateExpr)) { + validateExpr = { validator: validateExpr }; + } + + angular.forEach(validateExpr, function (exprssn, key) { + validateFn = function (valueToValidate) { + var expression = scope.$eval(exprssn, { '$value' : valueToValidate }); + if (angular.isObject(expression) && angular.isFunction(expression.then)) { + // expression is a promise + expression.then(function(){ + ctrl.$setValidity(key, true); + }, function(){ + ctrl.$setValidity(key, false); + }); + return valueToValidate; + } else if (expression) { + // expression is true + ctrl.$setValidity(key, true); + return valueToValidate; + } else { + // expression is false + ctrl.$setValidity(key, false); + return undefined; + } + }; + validators[key] = validateFn; + + ctrl.$parsers.push(validateFn); + }); + + function apply_watch(watch) + { + //string - update all validators on expression change + if (angular.isString(watch)) + { + scope.$watch(watch, function(){ + angular.forEach(validators, function(validatorFn){ + validatorFn(ctrl.$modelValue); + }); + }); + return; + } + + //array - update all validators on change of any expression + if (angular.isArray(watch)) + { + angular.forEach(watch, function(expression){ + scope.$watch(expression, function() + { + angular.forEach(validators, function(validatorFn){ + validatorFn(ctrl.$modelValue); + }); + }); + }); + return; + } + + //object - update appropriate validator + if (angular.isObject(watch)) + { + angular.forEach(watch, function(expression, validatorKey) + { + //value is string - look after one expression + if (angular.isString(expression)) + { + scope.$watch(expression, function(){ + validators[validatorKey](ctrl.$modelValue); + }); + } + + //value is array - look after all expressions in array + if (angular.isArray(expression)) + { + angular.forEach(expression, function(intExpression) + { + scope.$watch(intExpression, function(){ + validators[validatorKey](ctrl.$modelValue); + }); + }); + } + }); + } + } + // Support for val-custom-watch + if (attrs.valCustomWatch){ + apply_watch( scope.$eval(attrs.valCustomWatch) ); + } + } + }; }); -/** - * @ngdoc directive - * @name umbraco.directives.directive:valEmail - * @restrict A - * @description A custom directive to validate an email address string, this is required because angular's default validator is incorrect. - **/ -function valEmail(valEmailExpression) { - - return { - require: 'ngModel', - restrict: "A", - link: function (scope, elm, attrs, ctrl) { - - var patternValidator = function (viewValue) { - //NOTE: we don't validate on empty values, use required validator for that - if (!viewValue || valEmailExpression.EMAIL_REGEXP.test(viewValue)) { - // it is valid - ctrl.$setValidity('valEmail', true); - //assign a message to the validator - ctrl.errorMsg = ""; - return viewValue; - } - else { - // it is invalid, return undefined (no model update) - ctrl.$setValidity('valEmail', false); - //assign a message to the validator - ctrl.errorMsg = "Invalid email"; - return undefined; - } - }; - - //if there is an attribute: type="email" then we need to remove those formatters and parsers - if (attrs.type === "email") { - //we need to remove the existing parsers = the default angular one which is created by - // type="email", but this has a regex issue, so we'll remove that and add our custom one - ctrl.$parsers.pop(); - //we also need to remove the existing formatter - the default angular one will not render - // what it thinks is an invalid email address, so it will just be blank - ctrl.$formatters.pop(); - } - - ctrl.$parsers.push(patternValidator); - } - }; -} - -angular.module('umbraco.directives.validation') - .directive("valEmail", valEmail) - .factory('valEmailExpression', function () { - //NOTE: This is the fixed regex which is part of the newer angular - return { - EMAIL_REGEXP: /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i - }; +/** +* @ngdoc directive +* @name umbraco.directives.directive:valHighlight +* @restrict A +* @description Used on input fields when you want to signal that they are in error, this will highlight the item for 1 second +**/ +function valHighlight($timeout) { + return { + restrict: "A", + link: function (scope, element, attrs, ctrl) { + + attrs.$observe("valHighlight", function (newVal) { + if (newVal === "true") { + element.addClass("highlight-error"); + $timeout(function () { + //set the bound scope property to false + scope[attrs.valHighlight] = false; + }, 1000); + } + else { + element.removeClass("highlight-error"); + } + }); + + } + }; +} +angular.module('umbraco.directives.validation').directive("valHighlight", valHighlight); + +angular.module('umbraco.directives.validation') + .directive('valCompare',function () { + return { + require: "ngModel", + link: function (scope, elem, attrs, ctrl) { + + //TODO: Pretty sure this should be done using a requires ^form in the directive declaration + var otherInput = elem.inheritedData("$formController")[attrs.valCompare]; + + ctrl.$parsers.push(function(value) { + if(value === otherInput.$viewValue) { + ctrl.$setValidity("valCompare", true); + return value; + } + ctrl.$setValidity("valCompare", false); + }); + + otherInput.$parsers.push(function(value) { + ctrl.$setValidity("valCompare", value === ctrl.$viewValue); + return value; + }); + } + }; +}); +/** + * @ngdoc directive + * @name umbraco.directives.directive:valEmail + * @restrict A + * @description A custom directive to validate an email address string, this is required because angular's default validator is incorrect. + **/ +function valEmail(valEmailExpression) { + + return { + require: 'ngModel', + restrict: "A", + link: function (scope, elm, attrs, ctrl) { + + var patternValidator = function (viewValue) { + //NOTE: we don't validate on empty values, use required validator for that + if (!viewValue || valEmailExpression.EMAIL_REGEXP.test(viewValue)) { + // it is valid + ctrl.$setValidity('valEmail', true); + //assign a message to the validator + ctrl.errorMsg = ""; + return viewValue; + } + else { + // it is invalid, return undefined (no model update) + ctrl.$setValidity('valEmail', false); + //assign a message to the validator + ctrl.errorMsg = "Invalid email"; + return undefined; + } + }; + + //if there is an attribute: type="email" then we need to remove those formatters and parsers + if (attrs.type === "email") { + //we need to remove the existing parsers = the default angular one which is created by + // type="email", but this has a regex issue, so we'll remove that and add our custom one + ctrl.$parsers.pop(); + //we also need to remove the existing formatter - the default angular one will not render + // what it thinks is an invalid email address, so it will just be blank + ctrl.$formatters.pop(); + } + + ctrl.$parsers.push(patternValidator); + } + }; +} + +angular.module('umbraco.directives.validation') + .directive("valEmail", valEmail) + .factory('valEmailExpression', function () { + //NOTE: This is the fixed regex which is part of the newer angular + return { + EMAIL_REGEXP: /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i + }; }); -/** -* @ngdoc directive -* @name umbraco.directives.directive:valFormManager -* @restrict A -* @require formController -* @description Used to broadcast an event to all elements inside this one to notify that form validation has -* changed. If we don't use this that means you have to put a watch for each directive on a form's validation -* changing which would result in much higher processing. We need to actually watch the whole $error collection of a form -* because just watching $valid or $invalid doesn't acurrately trigger form validation changing. -* This also sets the show-validation (or a custom) css class on the element when the form is invalid - this lets -* us css target elements to be displayed when the form is submitting/submitted. -* Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will -* be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly. -**/ -function valFormManager(serverValidationManager, $rootScope, $log, $timeout, notificationsService, eventsService, $routeParams) { - return { - require: "form", - restrict: "A", - controller: function($scope) { - //This exposes an API for direct use with this directive - - var unsubscribe = []; - var self = this; - - //This is basically the same as a directive subscribing to an event but maybe a little - // nicer since the other directive can use this directive's API instead of a magical event - this.onValidationStatusChanged = function (cb) { - unsubscribe.push($scope.$on("valStatusChanged", function(evt, args) { - cb.apply(self, [evt, args]); - })); - }; - - //Ensure to remove the event handlers when this instance is destroyted - $scope.$on('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - }, - link: function (scope, element, attr, formCtrl) { - - scope.$watch(function () { - return formCtrl.$error; - }, function (e) { - scope.$broadcast("valStatusChanged", { form: formCtrl }); - - //find all invalid elements' .control-group's and apply the error class - var inError = element.find(".control-group .ng-invalid").closest(".control-group"); - inError.addClass("error"); - - //find all control group's that have no error and ensure the class is removed - var noInError = element.find(".control-group .ng-valid").closest(".control-group").not(inError); - noInError.removeClass("error"); - - }, true); - - var className = attr.valShowValidation ? attr.valShowValidation : "show-validation"; - var savingEventName = attr.savingEvent ? attr.savingEvent : "formSubmitting"; - var savedEvent = attr.savedEvent ? attr.savingEvent : "formSubmitted"; - - //This tracks if the user is currently saving a new item, we use this to determine - // if we should display the warning dialog that they are leaving the page - if a new item - // is being saved we never want to display that dialog, this will also cause problems when there - // are server side validation issues. - var isSavingNewItem = false; - - //we should show validation if there are any msgs in the server validation collection - if (serverValidationManager.items.length > 0) { - element.addClass(className); - } - - var unsubscribe = []; - - //listen for the forms saving event - unsubscribe.push(scope.$on(savingEventName, function(ev, args) { - element.addClass(className); - - //set the flag so we can check to see if we should display the error. - isSavingNewItem = $routeParams.create; - })); - - //listen for the forms saved event - unsubscribe.push(scope.$on(savedEvent, function(ev, args) { - //remove validation class - element.removeClass(className); - - //clear form state as at this point we retrieve new data from the server - //and all validation will have cleared at this point - formCtrl.$setPristine(); - })); - - //This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but - // the form has pending changes - var locationEvent = $rootScope.$on('$locationChangeStart', function(event, nextLocation, currentLocation) { - if (!formCtrl.$dirty || isSavingNewItem) { - return; - } - - var path = nextLocation.split("#")[1]; - if (path) { - if (path.indexOf("%253") || path.indexOf("%252")) { - path = decodeURIComponent(path); - } - - if (!notificationsService.hasView()) { - var msg = { view: "confirmroutechange", args: { path: path, listener: locationEvent } }; - notificationsService.add(msg); - } - - //prevent the route! - event.preventDefault(); - - //raise an event - eventsService.emit("valFormManager.pendingChanges", true); - } - - }); - unsubscribe.push(locationEvent); - - //Ensure to remove the event handler when this instance is destroyted - scope.$on('$destroy', function() { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - - $timeout(function(){ - formCtrl.$setPristine(); - }, 1000); - } - }; -} +/** +* @ngdoc directive +* @name umbraco.directives.directive:valFormManager +* @restrict A +* @require formController +* @description Used to broadcast an event to all elements inside this one to notify that form validation has +* changed. If we don't use this that means you have to put a watch for each directive on a form's validation +* changing which would result in much higher processing. We need to actually watch the whole $error collection of a form +* because just watching $valid or $invalid doesn't acurrately trigger form validation changing. +* This also sets the show-validation (or a custom) css class on the element when the form is invalid - this lets +* us css target elements to be displayed when the form is submitting/submitted. +* Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will +* be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly. +**/ +function valFormManager(serverValidationManager, $rootScope, $log, $timeout, notificationsService, eventsService, $routeParams) { + return { + require: "form", + restrict: "A", + controller: function($scope) { + //This exposes an API for direct use with this directive + + var unsubscribe = []; + var self = this; + + //This is basically the same as a directive subscribing to an event but maybe a little + // nicer since the other directive can use this directive's API instead of a magical event + this.onValidationStatusChanged = function (cb) { + unsubscribe.push($scope.$on("valStatusChanged", function(evt, args) { + cb.apply(self, [evt, args]); + })); + }; + + //Ensure to remove the event handlers when this instance is destroyted + $scope.$on('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + }, + link: function (scope, element, attr, formCtrl) { + + scope.$watch(function () { + return formCtrl.$error; + }, function (e) { + scope.$broadcast("valStatusChanged", { form: formCtrl }); + + //find all invalid elements' .control-group's and apply the error class + var inError = element.find(".control-group .ng-invalid").closest(".control-group"); + inError.addClass("error"); + + //find all control group's that have no error and ensure the class is removed + var noInError = element.find(".control-group .ng-valid").closest(".control-group").not(inError); + noInError.removeClass("error"); + + }, true); + + var className = attr.valShowValidation ? attr.valShowValidation : "show-validation"; + var savingEventName = attr.savingEvent ? attr.savingEvent : "formSubmitting"; + var savedEvent = attr.savedEvent ? attr.savingEvent : "formSubmitted"; + + //This tracks if the user is currently saving a new item, we use this to determine + // if we should display the warning dialog that they are leaving the page - if a new item + // is being saved we never want to display that dialog, this will also cause problems when there + // are server side validation issues. + var isSavingNewItem = false; + + //we should show validation if there are any msgs in the server validation collection + if (serverValidationManager.items.length > 0) { + element.addClass(className); + } + + var unsubscribe = []; + + //listen for the forms saving event + unsubscribe.push(scope.$on(savingEventName, function(ev, args) { + element.addClass(className); + + //set the flag so we can check to see if we should display the error. + isSavingNewItem = $routeParams.create; + })); + + //listen for the forms saved event + unsubscribe.push(scope.$on(savedEvent, function(ev, args) { + //remove validation class + element.removeClass(className); + + //clear form state as at this point we retrieve new data from the server + //and all validation will have cleared at this point + formCtrl.$setPristine(); + })); + + //This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but + // the form has pending changes + var locationEvent = $rootScope.$on('$locationChangeStart', function(event, nextLocation, currentLocation) { + if (!formCtrl.$dirty || isSavingNewItem) { + return; + } + + var path = nextLocation.split("#")[1]; + if (path) { + if (path.indexOf("%253") || path.indexOf("%252")) { + path = decodeURIComponent(path); + } + + if (!notificationsService.hasView()) { + var msg = { view: "confirmroutechange", args: { path: path, listener: locationEvent } }; + notificationsService.add(msg); + } + + //prevent the route! + event.preventDefault(); + + //raise an event + eventsService.emit("valFormManager.pendingChanges", true); + } + + }); + unsubscribe.push(locationEvent); + + //Ensure to remove the event handler when this instance is destroyted + scope.$on('$destroy', function() { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + + $timeout(function(){ + formCtrl.$setPristine(); + }, 1000); + } + }; +} angular.module('umbraco.directives.validation').directive("valFormManager", valFormManager); -/** -* @ngdoc directive -* @name umbraco.directives.directive:valPropertyMsg -* @restrict A -* @element textarea -* @requires formController -* @description This directive is used to control the display of the property level validation message. -* We will listen for server side validation changes -* and when an error is detected for this property we'll show the error message. -* In order for this directive to work, the valStatusChanged directive must be placed on the containing form. -**/ -function valPropertyMsg(serverValidationManager) { - - return { - scope: { - property: "=" - }, - require: "^form", //require that this directive is contained within an ngForm - replace: true, //replace the element with the template - restrict: "E", //restrict to element - template: "
{{errorMsg}}
", - - /** - Our directive requries a reference to a form controller - which gets passed in to this parameter - */ - link: function (scope, element, attrs, formCtrl) { - - var watcher = null; - - // Gets the error message to display - function getErrorMsg() { - //this can be null if no property was assigned - if (scope.property) { - //first try to get the error msg from the server collection - var err = serverValidationManager.getPropertyError(scope.property.alias, ""); - //if there's an error message use it - if (err && err.errorMsg) { - return err.errorMsg; - } - else { - return scope.property.propertyErrorMessage ? scope.property.propertyErrorMessage : "Property has errors"; - } - - } - return "Property has errors"; - } - - // We need to subscribe to any changes to our model (based on user input) - // This is required because when we have a server error we actually invalidate - // the form which means it cannot be resubmitted. - // So once a field is changed that has a server error assigned to it - // we need to re-validate it for the server side validator so the user can resubmit - // the form. Of course normal client-side validators will continue to execute. - function startWatch() { - //if there's not already a watch - if (!watcher) { - watcher = scope.$watch("property.value", function (newValue, oldValue) { - - if (!newValue || angular.equals(newValue, oldValue)) { - return; - } - - var errCount = 0; - for (var e in formCtrl.$error) { - if (angular.isArray(formCtrl.$error[e])) { - errCount++; - } - } - - //we are explicitly checking for valServer errors here, since we shouldn't auto clear - // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg - // is the only one, then we'll clear. - - if ((errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) { - scope.errorMsg = ""; - formCtrl.$setValidity('valPropertyMsg', true); - stopWatch(); - } - }, true); - } - } - - //clear the watch when the property validator is valid again - function stopWatch() { - if (watcher) { - watcher(); - watcher = null; - } - } - - //if there's any remaining errors in the server validation service then we should show them. - var showValidation = serverValidationManager.items.length > 0; - var hasError = false; - - //create properties on our custom scope so we can use it in our template - scope.errorMsg = ""; - - var unsubscribe = []; - - //listen for form error changes - unsubscribe.push(scope.$on("valStatusChanged", function(evt, args) { - if (args.form.$invalid) { - - //first we need to check if the valPropertyMsg validity is invalid - if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) { - //since we already have an error we'll just return since this means we've already set the - // hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe - return; - } - else if (element.closest(".umb-control-group").find(".ng-invalid").length > 0) { - //check if it's one of the properties that is invalid in the current content property - hasError = true; - //update the validation message if we don't already have one assigned. - if (showValidation && scope.errorMsg === "") { - scope.errorMsg = getErrorMsg(); - } - } - else { - hasError = false; - scope.errorMsg = ""; - } - } - else { - hasError = false; - scope.errorMsg = ""; - } - }, true)); - - //listen for the forms saving event - unsubscribe.push(scope.$on("formSubmitting", function(ev, args) { - showValidation = true; - if (hasError && scope.errorMsg === "") { - scope.errorMsg = getErrorMsg(); - } - else if (!hasError) { - scope.errorMsg = ""; - stopWatch(); - } - })); - - //listen for the forms saved event - unsubscribe.push(scope.$on("formSubmitted", function(ev, args) { - showValidation = false; - scope.errorMsg = ""; - formCtrl.$setValidity('valPropertyMsg', true); - stopWatch(); - })); - - //listen for server validation changes - // NOTE: we pass in "" in order to listen for all validation changes to the content property, not for - // validation changes to fields in the property this is because some server side validators may not - // return the field name for which the error belongs too, just the property for which it belongs. - // It's important to note that we need to subscribe to server validation changes here because we always must - // indicate that a content property is invalid at the property level since developers may not actually implement - // the correct field validation in their property editors. - - if (scope.property) { //this can be null if no property was assigned - serverValidationManager.subscribe(scope.property.alias, "", function (isValid, propertyErrors, allErrors) { - hasError = !isValid; - if (hasError) { - //set the error message to the server message - scope.errorMsg = propertyErrors[0].errorMsg; - //flag that the current validator is invalid - formCtrl.$setValidity('valPropertyMsg', false); - startWatch(); - } - else { - scope.errorMsg = ""; - //flag that the current validator is valid - formCtrl.$setValidity('valPropertyMsg', true); - stopWatch(); - } - }); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain - // but they are a different callback instance than the above. - element.bind('$destroy', function () { - stopWatch(); - serverValidationManager.unsubscribe(scope.property.alias, ""); - }); - } - - //when the scope is disposed we need to unsubscribe - scope.$on('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - } - - - }; -} +/** +* @ngdoc directive +* @name umbraco.directives.directive:valPropertyMsg +* @restrict A +* @element textarea +* @requires formController +* @description This directive is used to control the display of the property level validation message. +* We will listen for server side validation changes +* and when an error is detected for this property we'll show the error message. +* In order for this directive to work, the valStatusChanged directive must be placed on the containing form. +**/ +function valPropertyMsg(serverValidationManager) { + + return { + scope: { + property: "=" + }, + require: "^form", //require that this directive is contained within an ngForm + replace: true, //replace the element with the template + restrict: "E", //restrict to element + template: "
{{errorMsg}}
", + + /** + Our directive requries a reference to a form controller + which gets passed in to this parameter + */ + link: function (scope, element, attrs, formCtrl) { + + var watcher = null; + + // Gets the error message to display + function getErrorMsg() { + //this can be null if no property was assigned + if (scope.property) { + //first try to get the error msg from the server collection + var err = serverValidationManager.getPropertyError(scope.property.alias, ""); + //if there's an error message use it + if (err && err.errorMsg) { + return err.errorMsg; + } + else { + return scope.property.propertyErrorMessage ? scope.property.propertyErrorMessage : "Property has errors"; + } + + } + return "Property has errors"; + } + + // We need to subscribe to any changes to our model (based on user input) + // This is required because when we have a server error we actually invalidate + // the form which means it cannot be resubmitted. + // So once a field is changed that has a server error assigned to it + // we need to re-validate it for the server side validator so the user can resubmit + // the form. Of course normal client-side validators will continue to execute. + function startWatch() { + //if there's not already a watch + if (!watcher) { + watcher = scope.$watch("property.value", function (newValue, oldValue) { + + if (!newValue || angular.equals(newValue, oldValue)) { + return; + } + + var errCount = 0; + for (var e in formCtrl.$error) { + if (angular.isArray(formCtrl.$error[e])) { + errCount++; + } + } + + //we are explicitly checking for valServer errors here, since we shouldn't auto clear + // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg + // is the only one, then we'll clear. + + if ((errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) { + scope.errorMsg = ""; + formCtrl.$setValidity('valPropertyMsg', true); + stopWatch(); + } + }, true); + } + } + + //clear the watch when the property validator is valid again + function stopWatch() { + if (watcher) { + watcher(); + watcher = null; + } + } + + //if there's any remaining errors in the server validation service then we should show them. + var showValidation = serverValidationManager.items.length > 0; + var hasError = false; + + //create properties on our custom scope so we can use it in our template + scope.errorMsg = ""; + + var unsubscribe = []; + + //listen for form error changes + unsubscribe.push(scope.$on("valStatusChanged", function(evt, args) { + if (args.form.$invalid) { + + //first we need to check if the valPropertyMsg validity is invalid + if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) { + //since we already have an error we'll just return since this means we've already set the + // hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe + return; + } + else if (element.closest(".umb-control-group").find(".ng-invalid").length > 0) { + //check if it's one of the properties that is invalid in the current content property + hasError = true; + //update the validation message if we don't already have one assigned. + if (showValidation && scope.errorMsg === "") { + scope.errorMsg = getErrorMsg(); + } + } + else { + hasError = false; + scope.errorMsg = ""; + } + } + else { + hasError = false; + scope.errorMsg = ""; + } + }, true)); + + //listen for the forms saving event + unsubscribe.push(scope.$on("formSubmitting", function(ev, args) { + showValidation = true; + if (hasError && scope.errorMsg === "") { + scope.errorMsg = getErrorMsg(); + } + else if (!hasError) { + scope.errorMsg = ""; + stopWatch(); + } + })); + + //listen for the forms saved event + unsubscribe.push(scope.$on("formSubmitted", function(ev, args) { + showValidation = false; + scope.errorMsg = ""; + formCtrl.$setValidity('valPropertyMsg', true); + stopWatch(); + })); + + //listen for server validation changes + // NOTE: we pass in "" in order to listen for all validation changes to the content property, not for + // validation changes to fields in the property this is because some server side validators may not + // return the field name for which the error belongs too, just the property for which it belongs. + // It's important to note that we need to subscribe to server validation changes here because we always must + // indicate that a content property is invalid at the property level since developers may not actually implement + // the correct field validation in their property editors. + + if (scope.property) { //this can be null if no property was assigned + serverValidationManager.subscribe(scope.property.alias, "", function (isValid, propertyErrors, allErrors) { + hasError = !isValid; + if (hasError) { + //set the error message to the server message + scope.errorMsg = propertyErrors[0].errorMsg; + //flag that the current validator is invalid + formCtrl.$setValidity('valPropertyMsg', false); + startWatch(); + } + else { + scope.errorMsg = ""; + //flag that the current validator is valid + formCtrl.$setValidity('valPropertyMsg', true); + stopWatch(); + } + }); + + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain + // but they are a different callback instance than the above. + element.bind('$destroy', function () { + stopWatch(); + serverValidationManager.unsubscribe(scope.property.alias, ""); + }); + } + + //when the scope is disposed we need to unsubscribe + scope.$on('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + } + + + }; +} angular.module('umbraco.directives.validation').directive("valPropertyMsg", valPropertyMsg); -/** -* @ngdoc directive -* @name umbraco.directives.directive:valPropertyValidator -* @restrict A -* @description Performs any custom property value validation checks on the client side. This allows property editors to be highly flexible when it comes to validation - on the client side. Typically if a property editor stores a primitive value (i.e. string) then the client side validation can easily be taken care of - with standard angular directives such as ng-required. However since some property editors store complex data such as JSON, a given property editor - might require custom validation. This directive can be used to validate an Umbraco property in any way that a developer would like by specifying a - callback method to perform the validation. The result of this method must return an object in the format of - {isValid: true, errorKey: 'required', errorMsg: 'Something went wrong' } - The error message returned will also be displayed for the property level validation message. - This directive should only be used when dealing with complex models, if custom validation needs to be performed with primitive values, use the simpler - angular validation directives instead since this will watch the entire model. -**/ - -function valPropertyValidator(serverValidationManager) { - return { - scope: { - valPropertyValidator: "=" - }, - - // The element must have ng-model attribute and be inside an umbProperty directive - require: ['ngModel', '?^umbProperty'], - - restrict: "A", - - link: function (scope, element, attrs, ctrls) { - - var modelCtrl = ctrls[0]; - var propCtrl = ctrls.length > 1 ? ctrls[1] : null; - - // Check whether the scope has a valPropertyValidator method - if (!scope.valPropertyValidator || !angular.isFunction(scope.valPropertyValidator)) { - throw new Error('val-property-validator directive must specify a function to call'); - } - - var initResult = scope.valPropertyValidator(); - - // Validation method - var validate = function (viewValue) { - // Calls the validition method - var result = scope.valPropertyValidator(); - if (!result.errorKey || result.isValid === undefined || !result.errorMsg) { - throw "The result object from valPropertyValidator does not contain required properties: isValid, errorKey, errorMsg"; - } - if (result.isValid === true) { - // Tell the controller that the value is valid - modelCtrl.$setValidity(result.errorKey, true); - if (propCtrl) { - propCtrl.setPropertyError(null); - } - } - else { - // Tell the controller that the value is invalid - modelCtrl.$setValidity(result.errorKey, false); - if (propCtrl) { - propCtrl.setPropertyError(result.errorMsg); - } - } - }; - - // Parsers are called as soon as the value in the form input is modified - modelCtrl.$parsers.push(validate); - - } - }; -} -angular.module('umbraco.directives.validation').directive("valPropertyValidator", valPropertyValidator); +/** +* @ngdoc directive +* @name umbraco.directives.directive:valPropertyValidator +* @restrict A +* @description Performs any custom property value validation checks on the client side. This allows property editors to be highly flexible when it comes to validation + on the client side. Typically if a property editor stores a primitive value (i.e. string) then the client side validation can easily be taken care of + with standard angular directives such as ng-required. However since some property editors store complex data such as JSON, a given property editor + might require custom validation. This directive can be used to validate an Umbraco property in any way that a developer would like by specifying a + callback method to perform the validation. The result of this method must return an object in the format of + {isValid: true, errorKey: 'required', errorMsg: 'Something went wrong' } + The error message returned will also be displayed for the property level validation message. + This directive should only be used when dealing with complex models, if custom validation needs to be performed with primitive values, use the simpler + angular validation directives instead since this will watch the entire model. +**/ -/** - * @ngdoc directive - * @name umbraco.directives.directive:valRegex - * @restrict A - * @description A custom directive to allow for matching a value against a regex string. - * NOTE: there's already an ng-pattern but this requires that a regex expression is set, not a regex string - **/ -function valRegex() { - - return { - require: 'ngModel', - restrict: "A", - link: function (scope, elm, attrs, ctrl) { - - var flags = ""; - var regex; - var eventBindings = []; - - attrs.$observe("valRegexFlags", function (newVal) { - if (newVal) { - flags = newVal; - } - }); - - attrs.$observe("valRegex", function (newVal) { - if (newVal) { - try { - var resolved = newVal; - if (resolved) { - regex = new RegExp(resolved, flags); - } - else { - regex = new RegExp(attrs.valRegex, flags); - } - } - catch (e) { - regex = new RegExp(attrs.valRegex, flags); - } - } - }); - - eventBindings.push(scope.$watch('ngModel', function(newValue, oldValue){ - if(newValue && newValue !== oldValue) { - patternValidator(newValue); - } - })); - - var patternValidator = function (viewValue) { - if (regex) { - //NOTE: we don't validate on empty values, use required validator for that - if (!viewValue || regex.test(viewValue.toString())) { - // it is valid - ctrl.$setValidity('valRegex', true); - //assign a message to the validator - ctrl.errorMsg = ""; - return viewValue; - } - else { - // it is invalid, return undefined (no model update) - ctrl.$setValidity('valRegex', false); - //assign a message to the validator - ctrl.errorMsg = "Value is invalid, it does not match the correct pattern"; - return undefined; - } - } - }; - - scope.$on('$destroy', function(){ - // unbind watchers - for(var e in eventBindings) { - eventBindings[e](); - } - }); - - } - }; -} -angular.module('umbraco.directives.validation').directive("valRegex", valRegex); +function valPropertyValidator(serverValidationManager) { + return { + scope: { + valPropertyValidator: "=" + }, -(function() { - 'use strict'; - - function ValRequireComponentDirective() { - - function link(scope, el, attr, ngModel) { - - var unbindModelWatcher = scope.$watch(function () { - return ngModel.$modelValue; - }, function(newValue) { - - if(newValue === undefined || newValue === null || newValue === "") { - ngModel.$setValidity("valRequiredComponent", false); - } else { - ngModel.$setValidity("valRequiredComponent", true); - } - - }); - - // clean up - scope.$on('$destroy', function(){ - unbindModelWatcher(); - }); - - } - - var directive = { - require: 'ngModel', - restrict: "A", - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('valRequireComponent', ValRequireComponentDirective); - -})(); + // The element must have ng-model attribute and be inside an umbProperty directive + require: ['ngModel', '?^umbProperty'], -/** - * @ngdoc directive - * @name umbraco.directives.directive:valServer - * @restrict A - * @description This directive is used to associate a content property with a server-side validation response - * so that the validators in angular are updated based on server-side feedback. - **/ -function valServer(serverValidationManager) { - return { - require: ['ngModel', '?^umbProperty'], - restrict: "A", - link: function (scope, element, attr, ctrls) { - - var modelCtrl = ctrls[0]; - var umbPropCtrl = ctrls.length > 1 ? ctrls[1] : null; - if (!umbPropCtrl) { - //we cannot proceed, this validator will be disabled - return; - } - - var watcher = null; - - //Need to watch the value model for it to change, previously we had subscribed to - //modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that - // doesn't specifically have a 2 way ng binding. This is required because when we - // have a server error we actually invalidate the form which means it cannot be - // resubmitted. So once a field is changed that has a server error assigned to it - // we need to re-validate it for the server side validator so the user can resubmit - // the form. Of course normal client-side validators will continue to execute. - function startWatch() { - //if there's not already a watch - if (!watcher) { - watcher = scope.$watch(function () { - return modelCtrl.$modelValue; - }, function (newValue, oldValue) { - - if (!newValue || angular.equals(newValue, oldValue)) { - return; - } - - if (modelCtrl.$invalid) { - modelCtrl.$setValidity('valServer', true); - stopWatch(); - } - }, true); - } - } - - function stopWatch() { - if (watcher) { - watcher(); - watcher = null; - } - } - - var currentProperty = umbPropCtrl.property; - - //default to 'value' if nothing is set - var fieldName = "value"; - if (attr.valServer) { - fieldName = scope.$eval(attr.valServer); - if (!fieldName) { - //eval returned nothing so just use the string - fieldName = attr.valServer; - } - } - - //subscribe to the server validation changes - serverValidationManager.subscribe(currentProperty.alias, fieldName, function (isValid, propertyErrors, allErrors) { - if (!isValid) { - modelCtrl.$setValidity('valServer', false); - //assign an error msg property to the current validator - modelCtrl.errorMsg = propertyErrors[0].errorMsg; - startWatch(); - } - else { - modelCtrl.$setValidity('valServer', true); - //reset the error message - modelCtrl.errorMsg = ""; - stopWatch(); - } - }); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain - // but they are a different callback instance than the above. - element.bind('$destroy', function () { - stopWatch(); - serverValidationManager.unsubscribe(currentProperty.alias, fieldName); - }); - } - }; -} + restrict: "A", + + link: function (scope, element, attrs, ctrls) { + + var modelCtrl = ctrls[0]; + var propCtrl = ctrls.length > 1 ? ctrls[1] : null; + + // Check whether the scope has a valPropertyValidator method + if (!scope.valPropertyValidator || !angular.isFunction(scope.valPropertyValidator)) { + throw new Error('val-property-validator directive must specify a function to call'); + } + + var initResult = scope.valPropertyValidator(); + + // Validation method + var validate = function (viewValue) { + // Calls the validition method + var result = scope.valPropertyValidator(); + if (!result.errorKey || result.isValid === undefined || !result.errorMsg) { + throw "The result object from valPropertyValidator does not contain required properties: isValid, errorKey, errorMsg"; + } + if (result.isValid === true) { + // Tell the controller that the value is valid + modelCtrl.$setValidity(result.errorKey, true); + if (propCtrl) { + propCtrl.setPropertyError(null); + } + } + else { + // Tell the controller that the value is invalid + modelCtrl.$setValidity(result.errorKey, false); + if (propCtrl) { + propCtrl.setPropertyError(result.errorMsg); + } + } + }; + + // Parsers are called as soon as the value in the form input is modified + modelCtrl.$parsers.push(validate); + + } + }; +} +angular.module('umbraco.directives.validation').directive("valPropertyValidator", valPropertyValidator); + +/** + * @ngdoc directive + * @name umbraco.directives.directive:valRegex + * @restrict A + * @description A custom directive to allow for matching a value against a regex string. + * NOTE: there's already an ng-pattern but this requires that a regex expression is set, not a regex string + **/ +function valRegex() { + + return { + require: 'ngModel', + restrict: "A", + link: function (scope, elm, attrs, ctrl) { + + var flags = ""; + var regex; + var eventBindings = []; + + attrs.$observe("valRegexFlags", function (newVal) { + if (newVal) { + flags = newVal; + } + }); + + attrs.$observe("valRegex", function (newVal) { + if (newVal) { + try { + var resolved = newVal; + if (resolved) { + regex = new RegExp(resolved, flags); + } + else { + regex = new RegExp(attrs.valRegex, flags); + } + } + catch (e) { + regex = new RegExp(attrs.valRegex, flags); + } + } + }); + + eventBindings.push(scope.$watch('ngModel', function(newValue, oldValue){ + if(newValue && newValue !== oldValue) { + patternValidator(newValue); + } + })); + + var patternValidator = function (viewValue) { + if (regex) { + //NOTE: we don't validate on empty values, use required validator for that + if (!viewValue || regex.test(viewValue.toString())) { + // it is valid + ctrl.$setValidity('valRegex', true); + //assign a message to the validator + ctrl.errorMsg = ""; + return viewValue; + } + else { + // it is invalid, return undefined (no model update) + ctrl.$setValidity('valRegex', false); + //assign a message to the validator + ctrl.errorMsg = "Value is invalid, it does not match the correct pattern"; + return undefined; + } + } + }; + + scope.$on('$destroy', function(){ + // unbind watchers + for(var e in eventBindings) { + eventBindings[e](); + } + }); + + } + }; +} +angular.module('umbraco.directives.validation').directive("valRegex", valRegex); + +(function() { + 'use strict'; + + function ValRequireComponentDirective() { + + function link(scope, el, attr, ngModel) { + + var unbindModelWatcher = scope.$watch(function () { + return ngModel.$modelValue; + }, function(newValue) { + + if(newValue === undefined || newValue === null || newValue === "") { + ngModel.$setValidity("valRequiredComponent", false); + } else { + ngModel.$setValidity("valRequiredComponent", true); + } + + }); + + // clean up + scope.$on('$destroy', function(){ + unbindModelWatcher(); + }); + + } + + var directive = { + require: 'ngModel', + restrict: "A", + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('valRequireComponent', ValRequireComponentDirective); + +})(); + +/** + * @ngdoc directive + * @name umbraco.directives.directive:valServer + * @restrict A + * @description This directive is used to associate a content property with a server-side validation response + * so that the validators in angular are updated based on server-side feedback. + **/ +function valServer(serverValidationManager) { + return { + require: ['ngModel', '?^umbProperty'], + restrict: "A", + link: function (scope, element, attr, ctrls) { + + var modelCtrl = ctrls[0]; + var umbPropCtrl = ctrls.length > 1 ? ctrls[1] : null; + if (!umbPropCtrl) { + //we cannot proceed, this validator will be disabled + return; + } + + var watcher = null; + + //Need to watch the value model for it to change, previously we had subscribed to + //modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that + // doesn't specifically have a 2 way ng binding. This is required because when we + // have a server error we actually invalidate the form which means it cannot be + // resubmitted. So once a field is changed that has a server error assigned to it + // we need to re-validate it for the server side validator so the user can resubmit + // the form. Of course normal client-side validators will continue to execute. + function startWatch() { + //if there's not already a watch + if (!watcher) { + watcher = scope.$watch(function () { + return modelCtrl.$modelValue; + }, function (newValue, oldValue) { + + if (!newValue || angular.equals(newValue, oldValue)) { + return; + } + + if (modelCtrl.$invalid) { + modelCtrl.$setValidity('valServer', true); + stopWatch(); + } + }, true); + } + } + + function stopWatch() { + if (watcher) { + watcher(); + watcher = null; + } + } + + var currentProperty = umbPropCtrl.property; + + //default to 'value' if nothing is set + var fieldName = "value"; + if (attr.valServer) { + fieldName = scope.$eval(attr.valServer); + if (!fieldName) { + //eval returned nothing so just use the string + fieldName = attr.valServer; + } + } + + //subscribe to the server validation changes + serverValidationManager.subscribe(currentProperty.alias, fieldName, function (isValid, propertyErrors, allErrors) { + if (!isValid) { + modelCtrl.$setValidity('valServer', false); + //assign an error msg property to the current validator + modelCtrl.errorMsg = propertyErrors[0].errorMsg; + startWatch(); + } + else { + modelCtrl.$setValidity('valServer', true); + //reset the error message + modelCtrl.errorMsg = ""; + stopWatch(); + } + }); + + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain + // but they are a different callback instance than the above. + element.bind('$destroy', function () { + stopWatch(); + serverValidationManager.unsubscribe(currentProperty.alias, fieldName); + }); + } + }; +} angular.module('umbraco.directives.validation').directive("valServer", valServer); -/** - * @ngdoc directive - * @name umbraco.directives.directive:valServerField - * @restrict A - * @description This directive is used to associate a content field (not user defined) with a server-side validation response - * so that the validators in angular are updated based on server-side feedback. - **/ -function valServerField(serverValidationManager) { - return { - require: 'ngModel', - restrict: "A", - link: function (scope, element, attr, ctrl) { - - var fieldName = null; - var eventBindings = []; - - attr.$observe("valServerField", function (newVal) { - if (newVal && fieldName === null) { - fieldName = newVal; - - //subscribe to the changed event of the view model. This is required because when we - // have a server error we actually invalidate the form which means it cannot be - // resubmitted. So once a field is changed that has a server error assigned to it - // we need to re-validate it for the server side validator so the user can resubmit - // the form. Of course normal client-side validators will continue to execute. - eventBindings.push(scope.$watch('ngModel', function(newValue){ - if (ctrl.$invalid) { - ctrl.$setValidity('valServerField', true); - } - })); - - //subscribe to the server validation changes - serverValidationManager.subscribe(null, fieldName, function (isValid, fieldErrors, allErrors) { - if (!isValid) { - ctrl.$setValidity('valServerField', false); - //assign an error msg property to the current validator - ctrl.errorMsg = fieldErrors[0].errorMsg; - } - else { - ctrl.$setValidity('valServerField', true); - //reset the error message - ctrl.errorMsg = ""; - } - }); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain - // but they are a different callback instance than the above. - element.bind('$destroy', function () { - serverValidationManager.unsubscribe(null, fieldName); - }); - } - }); - - scope.$on('$destroy', function(){ - // unbind watchers - for(var e in eventBindings) { - eventBindings[e](); - } - }); - - } - }; -} -angular.module('umbraco.directives.validation').directive("valServerField", valServerField); +/** + * @ngdoc directive + * @name umbraco.directives.directive:valServerField + * @restrict A + * @description This directive is used to associate a content field (not user defined) with a server-side validation response + * so that the validators in angular are updated based on server-side feedback. + **/ +function valServerField(serverValidationManager) { + return { + require: 'ngModel', + restrict: "A", + link: function (scope, element, attr, ctrl) { + + var fieldName = null; + var eventBindings = []; -/** -* @ngdoc directive -* @name umbraco.directives.directive:valSubView -* @restrict A -* @description Used to show validation warnings for a editor sub view to indicate that the section content has validation errors in its data. -* In order for this directive to work, the valFormManager directive must be placed on the containing form. -**/ -(function() { - 'use strict'; - - function valSubViewDirective() { - - function link(scope, el, attr, ctrl) { - - var valFormManager = ctrl[1]; - scope.subView.hasError = false; - - //listen for form validation changes - valFormManager.onValidationStatusChanged(function (evt, args) { - if (!args.form.$valid) { - - var subViewContent = el.find(".ng-invalid"); - - if (subViewContent.length > 0) { - scope.subView.hasError = true; - } else { - scope.subView.hasError = false; - } - - } - else { - scope.subView.hasError = false; - } - }); - - } - - var directive = { - require: ['^form', '^valFormManager'], - restrict: "A", - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('valSubView', valSubViewDirective); - -})(); + attr.$observe("valServerField", function (newVal) { + if (newVal && fieldName === null) { + fieldName = newVal; - -/** -* @ngdoc directive -* @name umbraco.directives.directive:valTab -* @restrict A -* @description Used to show validation warnings for a tab to indicate that the tab content has validations errors in its data. -* In order for this directive to work, the valFormManager directive must be placed on the containing form. -**/ -function valTab() { - return { - require: ['^form', '^valFormManager'], - restrict: "A", - link: function (scope, element, attr, ctrs) { - - var valFormManager = ctrs[1]; - var tabId = "tab" + scope.tab.id; - scope.tabHasError = false; - - //listen for form validation changes - valFormManager.onValidationStatusChanged(function (evt, args) { - if (!args.form.$valid) { - var tabContent = element.closest(".umb-panel").find("#" + tabId); - //check if the validation messages are contained inside of this tabs - if (tabContent.find(".ng-invalid").length > 0) { - scope.tabHasError = true; - } else { - scope.tabHasError = false; - } - } - else { - scope.tabHasError = false; - } - }); - - } - }; -} + //subscribe to the changed event of the view model. This is required because when we + // have a server error we actually invalidate the form which means it cannot be + // resubmitted. So once a field is changed that has a server error assigned to it + // we need to re-validate it for the server side validator so the user can resubmit + // the form. Of course normal client-side validators will continue to execute. + eventBindings.push(scope.$watch('ngModel', function(newValue){ + if (ctrl.$invalid) { + ctrl.$setValidity('valServerField', true); + } + })); + + //subscribe to the server validation changes + serverValidationManager.subscribe(null, fieldName, function (isValid, fieldErrors, allErrors) { + if (!isValid) { + ctrl.$setValidity('valServerField', false); + //assign an error msg property to the current validator + ctrl.errorMsg = fieldErrors[0].errorMsg; + } + else { + ctrl.$setValidity('valServerField', true); + //reset the error message + ctrl.errorMsg = ""; + } + }); + + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain + // but they are a different callback instance than the above. + element.bind('$destroy', function () { + serverValidationManager.unsubscribe(null, fieldName); + }); + } + }); + + scope.$on('$destroy', function(){ + // unbind watchers + for(var e in eventBindings) { + eventBindings[e](); + } + }); + + } + }; +} +angular.module('umbraco.directives.validation').directive("valServerField", valServerField); + +/** +* @ngdoc directive +* @name umbraco.directives.directive:valSubView +* @restrict A +* @description Used to show validation warnings for a editor sub view to indicate that the section content has validation errors in its data. +* In order for this directive to work, the valFormManager directive must be placed on the containing form. +**/ +(function() { + 'use strict'; + + function valSubViewDirective() { + + function link(scope, el, attr, ctrl) { + + var valFormManager = ctrl[1]; + scope.subView.hasError = false; + + //listen for form validation changes + valFormManager.onValidationStatusChanged(function (evt, args) { + if (!args.form.$valid) { + + var subViewContent = el.find(".ng-invalid"); + + if (subViewContent.length > 0) { + scope.subView.hasError = true; + } else { + scope.subView.hasError = false; + } + + } + else { + scope.subView.hasError = false; + } + }); + + } + + var directive = { + require: ['^form', '^valFormManager'], + restrict: "A", + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('valSubView', valSubViewDirective); + +})(); + + +/** +* @ngdoc directive +* @name umbraco.directives.directive:valTab +* @restrict A +* @description Used to show validation warnings for a tab to indicate that the tab content has validations errors in its data. +* In order for this directive to work, the valFormManager directive must be placed on the containing form. +**/ +function valTab() { + return { + require: ['^form', '^valFormManager'], + restrict: "A", + link: function (scope, element, attr, ctrs) { + + var valFormManager = ctrs[1]; + var tabId = "tab" + scope.tab.id; + scope.tabHasError = false; + + //listen for form validation changes + valFormManager.onValidationStatusChanged(function (evt, args) { + if (!args.form.$valid) { + var tabContent = element.closest(".umb-panel").find("#" + tabId); + //check if the validation messages are contained inside of this tabs + if (tabContent.find(".ng-invalid").length > 0) { + scope.tabHasError = true; + } else { + scope.tabHasError = false; + } + } + else { + scope.tabHasError = false; + } + }); + + } + }; +} angular.module('umbraco.directives.validation').directive("valTab", valTab); -function valToggleMsg(serverValidationManager) { - return { - require: "^form", - restrict: "A", - - /** - Our directive requries a reference to a form controller which gets passed in to this parameter - */ - link: function (scope, element, attr, formCtrl) { - - if (!attr.valToggleMsg){ - throw "valToggleMsg requires that a reference to a validator is specified"; - } - if (!attr.valMsgFor){ - throw "valToggleMsg requires that the attribute valMsgFor exists on the element"; - } - if (!formCtrl[attr.valMsgFor]) { - throw "valToggleMsg cannot find field " + attr.valMsgFor + " on form " + formCtrl.$name; - } - - //if there's any remaining errors in the server validation service then we should show them. - var showValidation = serverValidationManager.items.length > 0; - var hasCustomMsg = element.contents().length > 0; - - //add a watch to the validator for the value (i.e. myForm.value.$error.required ) - scope.$watch(function () { - //sometimes if a dialog closes in the middle of digest we can get null references here - - return (formCtrl && formCtrl[attr.valMsgFor]) ? formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] : null; - }, function () { - //sometimes if a dialog closes in the middle of digest we can get null references here - if ((formCtrl && formCtrl[attr.valMsgFor])) { - if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] && showValidation) { - element.show(); - //display the error message if this element has no contents - if (!hasCustomMsg) { - element.html(formCtrl[attr.valMsgFor].errorMsg); - } - } - else { - element.hide(); - } - } - }); - - var unsubscribe = []; - - //listen for the saving event (the result is a callback method which is called to unsubscribe) - unsubscribe.push(scope.$on("formSubmitting", function(ev, args) { - showValidation = true; - if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg]) { - element.show(); - //display the error message if this element has no contents - if (!hasCustomMsg) { - element.html(formCtrl[attr.valMsgFor].errorMsg); - } - } - else { - element.hide(); - } - })); - - //listen for the saved event (the result is a callback method which is called to unsubscribe) - unsubscribe.push(scope.$on("formSubmitted", function(ev, args) { - showValidation = false; - element.hide(); - })); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise if this directive is part of a modal, the listener still exists because the dom - // element might still be there even after the modal has been hidden. - element.bind('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - - } - }; -} - -/** -* @ngdoc directive -* @name umbraco.directives.directive:valToggleMsg -* @restrict A -* @element input -* @requires formController -* @description This directive will show/hide an error based on: is the value + the given validator invalid? AND, has the form been submitted ? -**/ +function valToggleMsg(serverValidationManager) { + return { + require: "^form", + restrict: "A", + + /** + Our directive requries a reference to a form controller which gets passed in to this parameter + */ + link: function (scope, element, attr, formCtrl) { + + if (!attr.valToggleMsg){ + throw "valToggleMsg requires that a reference to a validator is specified"; + } + if (!attr.valMsgFor){ + throw "valToggleMsg requires that the attribute valMsgFor exists on the element"; + } + if (!formCtrl[attr.valMsgFor]) { + throw "valToggleMsg cannot find field " + attr.valMsgFor + " on form " + formCtrl.$name; + } + + //if there's any remaining errors in the server validation service then we should show them. + var showValidation = serverValidationManager.items.length > 0; + var hasCustomMsg = element.contents().length > 0; + + //add a watch to the validator for the value (i.e. myForm.value.$error.required ) + scope.$watch(function () { + //sometimes if a dialog closes in the middle of digest we can get null references here + + return (formCtrl && formCtrl[attr.valMsgFor]) ? formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] : null; + }, function () { + //sometimes if a dialog closes in the middle of digest we can get null references here + if ((formCtrl && formCtrl[attr.valMsgFor])) { + if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] && showValidation) { + element.show(); + //display the error message if this element has no contents + if (!hasCustomMsg) { + element.html(formCtrl[attr.valMsgFor].errorMsg); + } + } + else { + element.hide(); + } + } + }); + + var unsubscribe = []; + + //listen for the saving event (the result is a callback method which is called to unsubscribe) + unsubscribe.push(scope.$on("formSubmitting", function(ev, args) { + showValidation = true; + if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg]) { + element.show(); + //display the error message if this element has no contents + if (!hasCustomMsg) { + element.html(formCtrl[attr.valMsgFor].errorMsg); + } + } + else { + element.hide(); + } + })); + + //listen for the saved event (the result is a callback method which is called to unsubscribe) + unsubscribe.push(scope.$on("formSubmitted", function(ev, args) { + showValidation = false; + element.hide(); + })); + + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise if this directive is part of a modal, the listener still exists because the dom + // element might still be there even after the modal has been hidden. + element.bind('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + + } + }; +} + +/** +* @ngdoc directive +* @name umbraco.directives.directive:valToggleMsg +* @restrict A +* @element input +* @requires formController +* @description This directive will show/hide an error based on: is the value + the given validator invalid? AND, has the form been submitted ? +**/ angular.module('umbraco.directives.validation').directive("valToggleMsg", valToggleMsg); -angular.module('umbraco.directives.validation') -.directive('valTriggerChange', function($sniffer) { - return { - link : function(scope, elem, attrs) { - elem.bind('click', function(){ - $(attrs.valTriggerChange).trigger($sniffer.hasEvent('input') ? 'input' : 'change'); - }); - }, - priority : 1 - }; +angular.module('umbraco.directives.validation') +.directive('valTriggerChange', function($sniffer) { + return { + link : function(scope, elem, attrs) { + elem.bind('click', function(){ + $(attrs.valTriggerChange).trigger($sniffer.hasEvent('input') ? 'input' : 'change'); + }); + }, + priority : 1 + }; }); })(); \ No newline at end of file diff --git a/OurUmbraco.Site/umbraco/js/umbraco.filters.js b/OurUmbraco.Site/umbraco/js/umbraco.filters.js index d9bfd464..7cc29d1f 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.filters.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.filters.js @@ -1,53 +1,53 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ (function() { angular.module('umbraco.filters', []); -angular.module("umbraco.filters") - .filter('compareArrays', function() { - return function inArray(array, compareArray, compareProperty) { - - var result = []; - - angular.forEach(array, function(arrayItem){ - - var exists = false; - - angular.forEach(compareArray, function(compareItem){ - if( arrayItem[compareProperty] === compareItem[compareProperty]) { - exists = true; - } - }); - - if(!exists) { - result.push(arrayItem); - } - - }); - - return result; - - }; -}); +angular.module("umbraco.filters") + .filter('compareArrays', function() { + return function inArray(array, compareArray, compareProperty) { -angular.module("umbraco.filters").filter('timespan', function() { - return function(input) { - var sec_num = parseInt(input, 10); - var hours = Math.floor(sec_num / 3600); - var minutes = Math.floor((sec_num - (hours * 3600)) / 60); - var seconds = sec_num - (hours * 3600) - (minutes * 60); - - if (hours < 10) {hours = "0"+hours;} - if (minutes < 10) {minutes = "0"+minutes;} - if (seconds < 10) {seconds = "0"+seconds;} - var time = hours+':'+minutes+':'+seconds; - return time; - }; - }); + var result = []; + + angular.forEach(array, function(arrayItem){ + + var exists = false; + + angular.forEach(compareArray, function(compareItem){ + if( arrayItem[compareProperty] === compareItem[compareProperty]) { + exists = true; + } + }); + + if(!exists) { + result.push(arrayItem); + } + + }); + + return result; + + }; +}); + +angular.module("umbraco.filters").filter('timespan', function() { + return function(input) { + var sec_num = parseInt(input, 10); + var hours = Math.floor(sec_num / 3600); + var minutes = Math.floor((sec_num - (hours * 3600)) / 60); + var seconds = sec_num - (hours * 3600) - (minutes * 60); + + if (hours < 10) {hours = "0"+hours;} + if (minutes < 10) {minutes = "0"+minutes;} + if (seconds < 10) {seconds = "0"+seconds;} + var time = hours+':'+minutes+':'+seconds; + return time; + }; + }); })(); \ No newline at end of file diff --git a/OurUmbraco.Site/umbraco/js/umbraco.installer.js b/OurUmbraco.Site/umbraco/js/umbraco.installer.js index 90538bf3..19e2702b 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.installer.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.installer.js @@ -1,468 +1,468 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ (function() { angular.module('umbraco.install', []); -angular.module("umbraco.install").controller("Umbraco.InstallerController", - function ($scope, installerService) { - - //TODO: Decouple the service from the controller - the controller should be responsible - // for the model (state) and the service should be responsible for helping the controller, - // the controller should be passing the model into it's methods for manipulation and not hold - // state. We should not be assigning properties from a service to a controller's scope. - // see: https://github.com/umbraco/Umbraco-CMS/commit/b86ef0d7ac83f699aee35d807f7f7ebb6dd0ed2c#commitcomment-5721204 - - $scope.stepIndex = 0; - //comment this out if you just want to see tips - installerService.init(); - - //uncomment this to see tips - //installerService.switchToFeedback(); - - $scope.installer = installerService.status; - - $scope.forward = function () { - installerService.forward(); - }; - - $scope.backward = function () { - installerService.backward(); - }; - - $scope.install = function () { - installerService.install(); - }; - - $scope.gotoStep = function (step) { - installerService.gotoNamedStep(step); - }; - - $scope.restart = function () { - installerService.gotoStep(0); - }; - }); - -//this ensure that we start with a clean slate on every install and upgrade -angular.module("umbraco.install").run(function ($templateCache) { - $templateCache.removeAll(); -}); -angular.module("umbraco.install").factory('installerService', function($rootScope, $q, $timeout, $http, $location, $log){ - - var _status = { - index: 0, - current: undefined, - steps: undefined, - loading: true, - progress: "100%" - }; - - var factTimer = undefined; - var _installerModel = { - installId: undefined, - instructions: { - } - }; - - //add to umbraco installer facts here - var facts = ['Umbraco helped millions of people watch a man jump from the edge of space', - 'Over 370 000 websites are currently powered by Umbraco', - "At least 2 people have named their cat 'Umbraco'", - 'On an average day, more than 1000 people download Umbraco', - 'umbraco.tv is the premier source of Umbraco video tutorials to get you started', - 'You can find the world\'s friendliest CMS community at our.umbraco.org', - 'You can become a certified Umbraco developer by attending one of the official courses', - 'Umbraco works really well on tablets', - 'You have 100% control over your markup and design when crafting a website in Umbraco', - 'Umbraco is the best of both worlds: 100% free and open source, and backed by a professional and profitable company', - "There's a pretty big chance, you've visited a website powered by Umbraco today", - "'Umbraco-spotting' is the game of spotting big brands running Umbraco", - "At least 4 people have the Umbraco logo tattooed on them", - "'Umbraco' is the danish name for an allen key", - "Umbraco has been around since 2005, that's a looong time in IT", - "More than 400 people from all over the world meet each year in Denmark in June for our annual conference CodeGarden", - "While you are installing Umbraco someone else on the other side of the planet is probably doing it too", - "You can extend Umbraco without modifying the source code using either JavaScript or C#", - "Umbraco was installed in more than 165 countries in 2015" - ]; - - /** - Returns the description for the step at a given index based on the order of the serverOrder of steps - Since they don't execute on the server in the order that they are displayed in the UI. - */ - function getDescriptionForStepAtIndex(steps, index) { - var sorted = _.sortBy(steps, "serverOrder"); - if (sorted[index]) { - return sorted[index].description; - } - return null; - } - /* Returns the description for the given step name */ - function getDescriptionForStepName(steps, name) { - var found = _.find(steps, function(i) { - return i.name == name; - }); - return (found) ? found.description : null; - } - - //calculates the offset of the progressbar on the installer - function calculateProgress(steps, next) { - var sorted = _.sortBy(steps, "serverOrder"); - - var pct = "100%"; - for (var i = sorted.length - 1; i >= 0; i--) { - if(sorted[i].name == next){ - pct = Math.floor((i+1) / steps.length * 100) + "%"; - break; - } - } - return pct; - } - - //helpful defaults for the view loading - function resolveView(view){ - - if(view.indexOf(".html") < 0){ - view = view + ".html"; - } - if(view.indexOf("/") < 0){ - view = "views/install/" + view; - } - - return view; - } - - /** Have put this here because we are not referencing our other modules */ - function safeApply (scope, fn) { - if (scope.$$phase || scope.$root.$$phase) { - if (angular.isFunction(fn)) { - fn(); - } - } - else { - if (angular.isFunction(fn)) { - scope.$apply(fn); - } - else { - scope.$apply(); - } - } - } - - var service = { - - status : _status, - //loads the needed steps and sets the intial state - init : function(){ - service.status.loading = true; - if(!_status.all){ - service.getSteps().then(function(response){ - service.status.steps = response.data.steps; - service.status.index = 0; - _installerModel.installId = response.data.installId; - service.findNextStep(); - - $timeout(function(){ - service.status.loading = false; - service.status.configuring = true; - }, 2000); - }); - } - }, - - //loads available packages from our.umbraco.org - getPackages : function(){ - return $http.get(Umbraco.Sys.ServerVariables.installApiBaseUrl + "GetPackages"); - }, - - getSteps : function(){ - return $http.get(Umbraco.Sys.ServerVariables.installApiBaseUrl + "GetSetup"); - }, - - gotoStep : function(index){ - var step = service.status.steps[index]; - step.view = resolveView(step.view); - - if(!step.model){ - step.model = {}; - } - - service.status.index = index; - service.status.current = step; - service.retrieveCurrentStep(); - }, - - gotoNamedStep : function(stepName){ - var step = _.find(service.status.steps, function(s, index){ - if (s.view && s.name === stepName) { - service.status.index = index; - return true; - } - return false; - }); - - step.view = resolveView(step.view); - if(!step.model){ - step.model = {}; - } - service.retrieveCurrentStep(); - service.status.current = step; - }, - - /** - Finds the next step containing a view. If one is found it stores it as the current step - and retreives the step information and returns it, otherwise returns null . - */ - findNextStep : function(){ - var step = _.find(service.status.steps, function(s, index){ - if(s.view && index >= service.status.index){ - service.status.index = index; - return true; - } - return false; - }); - - if (step) { - if (step.view.indexOf(".html") < 0) { - step.view = step.view + ".html"; - } - - if (step.view.indexOf("/") < 0) { - step.view = "views/install/" + step.view; - } - - if (!step.model) { - step.model = {}; - } - - service.status.current = step; - service.retrieveCurrentStep(); - - //returns the next found step - return step; - } - else { - //there are no more steps found containing a view so return null - return null; - } - }, - - storeCurrentStep : function(){ - _installerModel.instructions[service.status.current.name] = service.status.current.model; - }, - - retrieveCurrentStep : function(){ - if(_installerModel.instructions[service.status.current.name]){ - service.status.current.model = _installerModel.instructions[service.status.current.name]; - } - }, - - /** Moves the installer forward to the next view, if there are not more views than the installation will commence */ - forward : function(){ - service.storeCurrentStep(); - service.status.index++; - var found = service.findNextStep(); - if (!found) { - //no more steps were found so start the installation process - service.install(); - } - }, - - backwards : function(){ - service.storeCurrentStep(); - service.gotoStep(service.status.index--); - }, - - install : function(){ - service.storeCurrentStep(); - service.switchToFeedback(); - - service.status.feedback = getDescriptionForStepAtIndex(service.status.steps, 0); - service.status.progress = 0; - - function processInstallStep() { - - $http.post(Umbraco.Sys.ServerVariables.installApiBaseUrl + "PostPerformInstall", _installerModel) - .success(function(data, status, headers, config) { - if (!data.complete) { - - //progress feedback - service.status.progress = calculateProgress(service.status.steps, data.nextStep); - - if (data.view) { - //set the current view and model to whatever the process returns, the view is responsible for retriggering install(); - var v = resolveView(data.view); - service.status.current = { view: v, model: data.model }; - - //turn off loading bar and feedback - service.switchToConfiguration(); - } - else { - var desc = getDescriptionForStepName(service.status.steps, data.nextStep); - if (desc) { - service.status.feedback = desc; - } - processInstallStep(); - } - } - else { - service.complete(); - } - }).error(function(data, status, headers, config) { - //need to handle 500's separately, this will happen if something goes wrong outside - // of the installer (like app startup events or something) and these will get returned as text/html - // not as json. If this happens we can't actually load in external views since they will YSOD as well! - // so we need to display this in our own internal way - - if (status >= 500 && status < 600) { - service.status.current = { view: "ysod", model: null }; - var ysod = data; - //we need to manually write the html to the iframe - the html contains full html markup - $timeout(function () { - document.getElementById('ysod').contentDocument.write(ysod); - }, 500); - } - else { - //this is where we handle installer error - var v = data.view ? resolveView(data.view) : resolveView("error"); - var model = data.model ? data.model : data; - service.status.current = { view: v, model: model }; - } - - service.switchToConfiguration(); - - }); - } - processInstallStep(); - }, - - randomFact: function () { - safeApply($rootScope, function() { - service.status.fact = facts[_.random(facts.length - 1)]; - }); - }, - - switchToFeedback : function(){ - service.status.current = undefined; - service.status.loading = true; - service.status.configuring = false; - - //initial fact - service.randomFact(); - - //timed facts - factTimer = window.setInterval(function(){ - service.randomFact(); - },6000); - }, - - switchToConfiguration : function(){ - service.status.loading = false; - service.status.configuring = true; - service.status.feedback = undefined; - service.status.fact = undefined; - - if(factTimer){ - clearInterval(factTimer); - } - }, - - complete : function(){ - - service.status.progress = "100%"; - service.status.done = true; - service.status.feedback = "Redirecting you to Umbraco, please wait"; - service.status.loading = false; - - if(factTimer){ - clearInterval(factTimer); - } - - $timeout(function(){ - window.location.href = Umbraco.Sys.ServerVariables.umbracoBaseUrl; - }, 1500); - } - }; - - return service; -}); +angular.module("umbraco.install").controller("Umbraco.InstallerController", + function ($scope, installerService) { -angular.module("umbraco.install").controller("Umbraco.Installer.DataBaseController", function($scope, $http, installerService){ - - $scope.checking = false; - $scope.dbs = [ - {name: 'Microsoft SQL Server Compact (SQL CE)', id: 0}, - {name: 'Microsoft SQL Server', id: 1}, - { name: 'Microsoft SQL Azure', id: 3 }, - { name: 'MySQL', id: 2 }, - {name: 'Custom connection string', id: -1}]; - - if(installerService.status.current.model.dbType === undefined){ - installerService.status.current.model.dbType = 0; - } - - $scope.validateAndForward = function(){ - if(!$scope.checking && this.myForm.$valid){ - $scope.checking = true; - var model = installerService.status.current.model; - - $http.post(Umbraco.Sys.ServerVariables.installApiBaseUrl + "PostValidateDatabaseConnection", - model).then(function(response){ - - if(response.data === "true"){ - installerService.forward(); - }else{ - $scope.invalidDbDns = true; - } - - $scope.checking = false; - }, function(){ - $scope.invalidDbDns = true; - $scope.checking = false; - }); - } - }; + //TODO: Decouple the service from the controller - the controller should be responsible + // for the model (state) and the service should be responsible for helping the controller, + // the controller should be passing the model into it's methods for manipulation and not hold + // state. We should not be assigning properties from a service to a controller's scope. + // see: https://github.com/umbraco/Umbraco-CMS/commit/b86ef0d7ac83f699aee35d807f7f7ebb6dd0ed2c#commitcomment-5721204 + + $scope.stepIndex = 0; + //comment this out if you just want to see tips + installerService.init(); + + //uncomment this to see tips + //installerService.switchToFeedback(); + + $scope.installer = installerService.status; + + $scope.forward = function () { + installerService.forward(); + }; + + $scope.backward = function () { + installerService.backward(); + }; + + $scope.install = function () { + installerService.install(); + }; + + $scope.gotoStep = function (step) { + installerService.gotoNamedStep(step); + }; + + $scope.restart = function () { + installerService.gotoStep(0); + }; + }); + +//this ensure that we start with a clean slate on every install and upgrade +angular.module("umbraco.install").run(function ($templateCache) { + $templateCache.removeAll(); }); -angular.module("umbraco.install").controller("Umbraco.Installer.PackagesController", function ($scope, installerService) { - - installerService.getPackages().then(function (response) { - $scope.packages = response.data; - }); - - $scope.setPackageAndContinue = function (pckId) { - installerService.status.current.model = pckId; - installerService.forward(); - }; - +angular.module("umbraco.install").factory('installerService', function($rootScope, $q, $timeout, $http, $location, $log){ + + var _status = { + index: 0, + current: undefined, + steps: undefined, + loading: true, + progress: "100%" + }; + + var factTimer = undefined; + var _installerModel = { + installId: undefined, + instructions: { + } + }; + + //add to umbraco installer facts here + var facts = ['Umbraco helped millions of people watch a man jump from the edge of space', + 'Over 370 000 websites are currently powered by Umbraco', + "At least 2 people have named their cat 'Umbraco'", + 'On an average day, more than 1000 people download Umbraco', + 'umbraco.tv is the premier source of Umbraco video tutorials to get you started', + 'You can find the world\'s friendliest CMS community at our.umbraco.org', + 'You can become a certified Umbraco developer by attending one of the official courses', + 'Umbraco works really well on tablets', + 'You have 100% control over your markup and design when crafting a website in Umbraco', + 'Umbraco is the best of both worlds: 100% free and open source, and backed by a professional and profitable company', + "There's a pretty big chance, you've visited a website powered by Umbraco today", + "'Umbraco-spotting' is the game of spotting big brands running Umbraco", + "At least 4 people have the Umbraco logo tattooed on them", + "'Umbraco' is the danish name for an allen key", + "Umbraco has been around since 2005, that's a looong time in IT", + "More than 400 people from all over the world meet each year in Denmark in June for our annual conference CodeGarden", + "While you are installing Umbraco someone else on the other side of the planet is probably doing it too", + "You can extend Umbraco without modifying the source code using either JavaScript or C#", + "Umbraco was installed in more than 165 countries in 2015" + ]; + + /** + Returns the description for the step at a given index based on the order of the serverOrder of steps + Since they don't execute on the server in the order that they are displayed in the UI. + */ + function getDescriptionForStepAtIndex(steps, index) { + var sorted = _.sortBy(steps, "serverOrder"); + if (sorted[index]) { + return sorted[index].description; + } + return null; + } + /* Returns the description for the given step name */ + function getDescriptionForStepName(steps, name) { + var found = _.find(steps, function(i) { + return i.name == name; + }); + return (found) ? found.description : null; + } + + //calculates the offset of the progressbar on the installer + function calculateProgress(steps, next) { + var sorted = _.sortBy(steps, "serverOrder"); + + var pct = "100%"; + for (var i = sorted.length - 1; i >= 0; i--) { + if(sorted[i].name == next){ + pct = Math.floor((i+1) / steps.length * 100) + "%"; + break; + } + } + return pct; + } + + //helpful defaults for the view loading + function resolveView(view){ + + if(view.indexOf(".html") < 0){ + view = view + ".html"; + } + if(view.indexOf("/") < 0){ + view = "views/install/" + view; + } + + return view; + } + + /** Have put this here because we are not referencing our other modules */ + function safeApply (scope, fn) { + if (scope.$$phase || scope.$root.$$phase) { + if (angular.isFunction(fn)) { + fn(); + } + } + else { + if (angular.isFunction(fn)) { + scope.$apply(fn); + } + else { + scope.$apply(); + } + } + } + + var service = { + + status : _status, + //loads the needed steps and sets the intial state + init : function(){ + service.status.loading = true; + if(!_status.all){ + service.getSteps().then(function(response){ + service.status.steps = response.data.steps; + service.status.index = 0; + _installerModel.installId = response.data.installId; + service.findNextStep(); + + $timeout(function(){ + service.status.loading = false; + service.status.configuring = true; + }, 2000); + }); + } + }, + + //loads available packages from our.umbraco.org + getPackages : function(){ + return $http.get(Umbraco.Sys.ServerVariables.installApiBaseUrl + "GetPackages"); + }, + + getSteps : function(){ + return $http.get(Umbraco.Sys.ServerVariables.installApiBaseUrl + "GetSetup"); + }, + + gotoStep : function(index){ + var step = service.status.steps[index]; + step.view = resolveView(step.view); + + if(!step.model){ + step.model = {}; + } + + service.status.index = index; + service.status.current = step; + service.retrieveCurrentStep(); + }, + + gotoNamedStep : function(stepName){ + var step = _.find(service.status.steps, function(s, index){ + if (s.view && s.name === stepName) { + service.status.index = index; + return true; + } + return false; + }); + + step.view = resolveView(step.view); + if(!step.model){ + step.model = {}; + } + service.retrieveCurrentStep(); + service.status.current = step; + }, + + /** + Finds the next step containing a view. If one is found it stores it as the current step + and retreives the step information and returns it, otherwise returns null . + */ + findNextStep : function(){ + var step = _.find(service.status.steps, function(s, index){ + if(s.view && index >= service.status.index){ + service.status.index = index; + return true; + } + return false; + }); + + if (step) { + if (step.view.indexOf(".html") < 0) { + step.view = step.view + ".html"; + } + + if (step.view.indexOf("/") < 0) { + step.view = "views/install/" + step.view; + } + + if (!step.model) { + step.model = {}; + } + + service.status.current = step; + service.retrieveCurrentStep(); + + //returns the next found step + return step; + } + else { + //there are no more steps found containing a view so return null + return null; + } + }, + + storeCurrentStep : function(){ + _installerModel.instructions[service.status.current.name] = service.status.current.model; + }, + + retrieveCurrentStep : function(){ + if(_installerModel.instructions[service.status.current.name]){ + service.status.current.model = _installerModel.instructions[service.status.current.name]; + } + }, + + /** Moves the installer forward to the next view, if there are not more views than the installation will commence */ + forward : function(){ + service.storeCurrentStep(); + service.status.index++; + var found = service.findNextStep(); + if (!found) { + //no more steps were found so start the installation process + service.install(); + } + }, + + backwards : function(){ + service.storeCurrentStep(); + service.gotoStep(service.status.index--); + }, + + install : function(){ + service.storeCurrentStep(); + service.switchToFeedback(); + + service.status.feedback = getDescriptionForStepAtIndex(service.status.steps, 0); + service.status.progress = 0; + + function processInstallStep() { + + $http.post(Umbraco.Sys.ServerVariables.installApiBaseUrl + "PostPerformInstall", _installerModel) + .success(function(data, status, headers, config) { + if (!data.complete) { + + //progress feedback + service.status.progress = calculateProgress(service.status.steps, data.nextStep); + + if (data.view) { + //set the current view and model to whatever the process returns, the view is responsible for retriggering install(); + var v = resolveView(data.view); + service.status.current = { view: v, model: data.model }; + + //turn off loading bar and feedback + service.switchToConfiguration(); + } + else { + var desc = getDescriptionForStepName(service.status.steps, data.nextStep); + if (desc) { + service.status.feedback = desc; + } + processInstallStep(); + } + } + else { + service.complete(); + } + }).error(function(data, status, headers, config) { + //need to handle 500's separately, this will happen if something goes wrong outside + // of the installer (like app startup events or something) and these will get returned as text/html + // not as json. If this happens we can't actually load in external views since they will YSOD as well! + // so we need to display this in our own internal way + + if (status >= 500 && status < 600) { + service.status.current = { view: "ysod", model: null }; + var ysod = data; + //we need to manually write the html to the iframe - the html contains full html markup + $timeout(function () { + document.getElementById('ysod').contentDocument.write(ysod); + }, 500); + } + else { + //this is where we handle installer error + var v = data.view ? resolveView(data.view) : resolveView("error"); + var model = data.model ? data.model : data; + service.status.current = { view: v, model: model }; + } + + service.switchToConfiguration(); + + }); + } + processInstallStep(); + }, + + randomFact: function () { + safeApply($rootScope, function() { + service.status.fact = facts[_.random(facts.length - 1)]; + }); + }, + + switchToFeedback : function(){ + service.status.current = undefined; + service.status.loading = true; + service.status.configuring = false; + + //initial fact + service.randomFact(); + + //timed facts + factTimer = window.setInterval(function(){ + service.randomFact(); + },6000); + }, + + switchToConfiguration : function(){ + service.status.loading = false; + service.status.configuring = true; + service.status.feedback = undefined; + service.status.fact = undefined; + + if(factTimer){ + clearInterval(factTimer); + } + }, + + complete : function(){ + + service.status.progress = "100%"; + service.status.done = true; + service.status.feedback = "Redirecting you to Umbraco, please wait"; + service.status.loading = false; + + if(factTimer){ + clearInterval(factTimer); + } + + $timeout(function(){ + window.location.href = Umbraco.Sys.ServerVariables.umbracoBaseUrl; + }, 1500); + } + }; + + return service; }); -angular.module("umbraco.install").controller("Umbraco.Install.UserController", function($scope, installerService) { - - $scope.passwordPattern = /.*/; - $scope.installer.current.model.subscribeToNewsLetter = true; - - if ($scope.installer.current.model.minNonAlphaNumericLength > 0) { - var exp = ""; - for (var i = 0; i < $scope.installer.current.model.minNonAlphaNumericLength; i++) { - exp += ".*[\\W].*"; - } - //replace duplicates - exp = exp.replace(".*.*", ".*"); - $scope.passwordPattern = new RegExp(exp); - } - - $scope.validateAndInstall = function(){ - installerService.install(); - }; - - $scope.validateAndForward = function(){ - if(this.myForm.$valid){ - installerService.forward(); - } - }; - + +angular.module("umbraco.install").controller("Umbraco.Installer.DataBaseController", function($scope, $http, installerService){ + + $scope.checking = false; + $scope.dbs = [ + {name: 'Microsoft SQL Server Compact (SQL CE)', id: 0}, + {name: 'Microsoft SQL Server', id: 1}, + { name: 'Microsoft SQL Azure', id: 3 }, + { name: 'MySQL', id: 2 }, + {name: 'Custom connection string', id: -1}]; + + if(installerService.status.current.model.dbType === undefined){ + installerService.status.current.model.dbType = 0; + } + + $scope.validateAndForward = function(){ + if(!$scope.checking && this.myForm.$valid){ + $scope.checking = true; + var model = installerService.status.current.model; + + $http.post(Umbraco.Sys.ServerVariables.installApiBaseUrl + "PostValidateDatabaseConnection", + model).then(function(response){ + + if(response.data === "true"){ + installerService.forward(); + }else{ + $scope.invalidDbDns = true; + } + + $scope.checking = false; + }, function(){ + $scope.invalidDbDns = true; + $scope.checking = false; + }); + } + }; +}); +angular.module("umbraco.install").controller("Umbraco.Installer.PackagesController", function ($scope, installerService) { + + installerService.getPackages().then(function (response) { + $scope.packages = response.data; + }); + + $scope.setPackageAndContinue = function (pckId) { + installerService.status.current.model = pckId; + installerService.forward(); + }; + +}); +angular.module("umbraco.install").controller("Umbraco.Install.UserController", function($scope, installerService) { + + $scope.passwordPattern = /.*/; + $scope.installer.current.model.subscribeToNewsLetter = true; + + if ($scope.installer.current.model.minNonAlphaNumericLength > 0) { + var exp = ""; + for (var i = 0; i < $scope.installer.current.model.minNonAlphaNumericLength; i++) { + exp += ".*[\\W].*"; + } + //replace duplicates + exp = exp.replace(".*.*", ".*"); + $scope.passwordPattern = new RegExp(exp); + } + + $scope.validateAndInstall = function(){ + installerService.install(); + }; + + $scope.validateAndForward = function(){ + if(this.myForm.$valid){ + installerService.forward(); + } + }; + }); })(); \ No newline at end of file diff --git a/OurUmbraco.Site/umbraco/js/umbraco.resources.js b/OurUmbraco.Site/umbraco/js/umbraco.resources.js index 437868e7..3b050ea1 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.resources.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.resources.js @@ -1,6 +1,6 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ @@ -1313,139 +1313,139 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { } angular.module('umbraco.resources').factory('contentTypeResource', contentTypeResource); -/** - * @ngdoc service - * @name umbraco.resources.currentUserResource - * @description Used for read/updates for the currently logged in user - * - * - **/ -function currentUserResource($q, $http, umbRequestHelper) { - - //the factory object returned - return { - - /** - * @ngdoc method - * @name umbraco.resources.currentUserResource#changePassword - * @methodOf umbraco.resources.currentUserResource - * - * @description - * Changes the current users password - * - * @returns {Promise} resourcePromise object containing the user array. - * - */ - changePassword: function (changePasswordArgs) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "currentUserApiBaseUrl", - "PostChangePassword"), - changePasswordArgs), - 'Failed to change password'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.currentUserResource#getMembershipProviderConfig - * @methodOf umbraco.resources.currentUserResource - * - * @description - * Gets the configuration of the user membership provider which is used to configure the change password form - */ - getMembershipProviderConfig: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "currentUserApiBaseUrl", - "GetMembershipProviderConfig")), - 'Failed to retrieve membership provider config'); - }, - }; -} - -angular.module('umbraco.resources').factory('currentUserResource', currentUserResource); +/** + * @ngdoc service + * @name umbraco.resources.currentUserResource + * @description Used for read/updates for the currently logged in user + * + * + **/ +function currentUserResource($q, $http, umbRequestHelper) { + + //the factory object returned + return { + + /** + * @ngdoc method + * @name umbraco.resources.currentUserResource#changePassword + * @methodOf umbraco.resources.currentUserResource + * + * @description + * Changes the current users password + * + * @returns {Promise} resourcePromise object containing the user array. + * + */ + changePassword: function (changePasswordArgs) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "currentUserApiBaseUrl", + "PostChangePassword"), + changePasswordArgs), + 'Failed to change password'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.currentUserResource#getMembershipProviderConfig + * @methodOf umbraco.resources.currentUserResource + * + * @description + * Gets the configuration of the user membership provider which is used to configure the change password form + */ + getMembershipProviderConfig: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "currentUserApiBaseUrl", + "GetMembershipProviderConfig")), + 'Failed to retrieve membership provider config'); + }, + }; +} + +angular.module('umbraco.resources').factory('currentUserResource', currentUserResource); + +/** + * @ngdoc service + * @name umbraco.resources.dashboardResource + * @description Handles loading the dashboard manifest + **/ +function dashboardResource($q, $http, umbRequestHelper) { + //the factory object returned + return { + + /** + * @ngdoc method + * @name umbraco.resources.dashboardResource#getDashboard + * @methodOf umbraco.resources.dashboardResource + * + * @description + * Retrieves the dashboard configuration for a given section + * + * @param {string} section Alias of section to retrieve dashboard configuraton for + * @returns {Promise} resourcePromise object containing the user array. + * + */ + getDashboard: function (section) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "dashboardApiBaseUrl", + "GetDashboard", + [{ section: section }])), + 'Failed to get dashboard ' + section); + }, + + /** + * @ngdoc method + * @name umbraco.resources.dashboardResource#getRemoteDashboardContent + * @methodOf umbraco.resources.dashboardResource + * + * @description + * Retrieves dashboard content from a remote source for a given section + * + * @param {string} section Alias of section to retrieve dashboard content for + * @returns {Promise} resourcePromise object containing the user array. + * + */ + getRemoteDashboardContent: function (section, baseurl) { + + //build request values with optional params + var values = [{ section: section }]; + if (baseurl) + { + values.push({ baseurl: baseurl }); + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "dashboardApiBaseUrl", + "GetRemoteDashboardContent", + values)), "Failed to get dashboard content"); + }, + + getRemoteDashboardCssUrl: function (section, baseurl) { + + //build request values with optional params + var values = [{ section: section }]; + if (baseurl) { + values.push({ baseurl: baseurl }); + } + + return umbRequestHelper.getApiUrl( + "dashboardApiBaseUrl", + "GetRemoteDashboardCss", + values); + } + + + + }; +} -/** - * @ngdoc service - * @name umbraco.resources.dashboardResource - * @description Handles loading the dashboard manifest - **/ -function dashboardResource($q, $http, umbRequestHelper) { - //the factory object returned - return { - - /** - * @ngdoc method - * @name umbraco.resources.dashboardResource#getDashboard - * @methodOf umbraco.resources.dashboardResource - * - * @description - * Retrieves the dashboard configuration for a given section - * - * @param {string} section Alias of section to retrieve dashboard configuraton for - * @returns {Promise} resourcePromise object containing the user array. - * - */ - getDashboard: function (section) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "dashboardApiBaseUrl", - "GetDashboard", - [{ section: section }])), - 'Failed to get dashboard ' + section); - }, - - /** - * @ngdoc method - * @name umbraco.resources.dashboardResource#getRemoteDashboardContent - * @methodOf umbraco.resources.dashboardResource - * - * @description - * Retrieves dashboard content from a remote source for a given section - * - * @param {string} section Alias of section to retrieve dashboard content for - * @returns {Promise} resourcePromise object containing the user array. - * - */ - getRemoteDashboardContent: function (section, baseurl) { - - //build request values with optional params - var values = [{ section: section }]; - if (baseurl) - { - values.push({ baseurl: baseurl }); - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "dashboardApiBaseUrl", - "GetRemoteDashboardContent", - values)), "Failed to get dashboard content"); - }, - - getRemoteDashboardCssUrl: function (section, baseurl) { - - //build request values with optional params - var values = [{ section: section }]; - if (baseurl) { - values.push({ baseurl: baseurl }); - } - - return umbRequestHelper.getApiUrl( - "dashboardApiBaseUrl", - "GetRemoteDashboardCss", - values); - } - - - - }; -} - angular.module('umbraco.resources').factory('dashboardResource', dashboardResource); /** * @ngdoc service @@ -2184,82 +2184,82 @@ function entityResource($q, $http, umbRequestHelper) { angular.module('umbraco.resources').factory('entityResource', entityResource); -/** - * @ngdoc service - * @name umbraco.resources.healthCheckResource - * @function - * - * @description - * Used by the health check dashboard to get checks and send requests to fix checks. - */ -(function () { - 'use strict'; - - function healthCheckResource($http, umbRequestHelper) { - - /** - * @ngdoc function - * @name umbraco.resources.healthCheckService#getAllChecks - * @methodOf umbraco.resources.healthCheckResource - * @function - * - * @description - * Called to get all available health checks - */ - function getAllChecks() { - return umbRequestHelper.resourcePromise( - $http.get(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + "GetAllHealthChecks"), - "Failed to retrieve health checks" - ); - } - - /** - * @ngdoc function - * @name umbraco.resources.healthCheckService#getStatus - * @methodOf umbraco.resources.healthCheckResource - * @function - * - * @description - * Called to get execute a health check and return the check status - */ - function getStatus(id) { - return umbRequestHelper.resourcePromise( - $http.get(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + 'GetStatus?id=' + id), - 'Failed to retrieve status for health check with ID ' + id - ); - } - - /** - * @ngdoc function - * @name umbraco.resources.healthCheckService#executeAction - * @methodOf umbraco.resources.healthCheckResource - * @function - * - * @description - * Called to execute a health check action (rectifying an issue) - */ - function executeAction(action) { - return umbRequestHelper.resourcePromise( - $http.post(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + 'ExecuteAction', action), - 'Failed to execute action with alias ' + action.alias + ' and healthCheckId + ' + action.healthCheckId - ); - } - - var resource = { - getAllChecks: getAllChecks, - getStatus: getStatus, - executeAction: executeAction - }; - - return resource; - - } - - - angular.module('umbraco.resources').factory('healthCheckResource', healthCheckResource); - - -})(); +/** + * @ngdoc service + * @name umbraco.resources.healthCheckResource + * @function + * + * @description + * Used by the health check dashboard to get checks and send requests to fix checks. + */ +(function () { + 'use strict'; + + function healthCheckResource($http, umbRequestHelper) { + + /** + * @ngdoc function + * @name umbraco.resources.healthCheckService#getAllChecks + * @methodOf umbraco.resources.healthCheckResource + * @function + * + * @description + * Called to get all available health checks + */ + function getAllChecks() { + return umbRequestHelper.resourcePromise( + $http.get(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + "GetAllHealthChecks"), + "Failed to retrieve health checks" + ); + } + + /** + * @ngdoc function + * @name umbraco.resources.healthCheckService#getStatus + * @methodOf umbraco.resources.healthCheckResource + * @function + * + * @description + * Called to get execute a health check and return the check status + */ + function getStatus(id) { + return umbRequestHelper.resourcePromise( + $http.get(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + 'GetStatus?id=' + id), + 'Failed to retrieve status for health check with ID ' + id + ); + } + + /** + * @ngdoc function + * @name umbraco.resources.healthCheckService#executeAction + * @methodOf umbraco.resources.healthCheckResource + * @function + * + * @description + * Called to execute a health check action (rectifying an issue) + */ + function executeAction(action) { + return umbRequestHelper.resourcePromise( + $http.post(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + 'ExecuteAction', action), + 'Failed to execute action with alias ' + action.alias + ' and healthCheckId + ' + action.healthCheckId + ); + } + + var resource = { + getAllChecks: getAllChecks, + getStatus: getStatus, + executeAction: executeAction + }; + + return resource; + + } + + + angular.module('umbraco.resources').factory('healthCheckResource', healthCheckResource); + + +})(); /** * @ngdoc service @@ -3523,408 +3523,408 @@ function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { } angular.module('umbraco.resources').factory('memberTypeResource', memberTypeResource); -/** - * @ngdoc service - * @name umbraco.resources.ourPackageRepositoryResource - * @description handles data for package installations - **/ -function ourPackageRepositoryResource($q, $http, umbDataFormatter, umbRequestHelper) { - - //var baseurl = "http://localhost:24292/webapi/packages/v1"; - var baseurl = "https://our.umbraco.org/webapi/packages/v1"; - - return { - - getDetails: function (packageId) { - - return umbRequestHelper.resourcePromise( - $http.get(baseurl + "/" + packageId), - 'Failed to get package details'); - }, - - getCategories: function () { - - return umbRequestHelper.resourcePromise( - $http.get(baseurl), - 'Failed to query packages'); - }, - - getPopular: function (maxResults, category) { - - if (maxResults === undefined) { - maxResults = 10; - } - if (category === undefined) { - category = ""; - } - - return umbRequestHelper.resourcePromise( - $http.get(baseurl + "?pageIndex=0&pageSize=" + maxResults + "&category=" + category + "&order=Popular&version=" + Umbraco.Sys.ServerVariables.application.version), - 'Failed to query packages'); - }, - - search: function (pageIndex, pageSize, orderBy, category, query, canceler) { - - var httpConfig = {}; - if (canceler) { - httpConfig["timeout"] = canceler; - } - - if (category === undefined) { - category = ""; - } - if (query === undefined) { - query = ""; - } - - //order by score if there is nothing set - var order = !orderBy ? "&order=Default" : ("&order=" + orderBy); - - return umbRequestHelper.resourcePromise( - $http.get(baseurl + "?pageIndex=" + pageIndex + "&pageSize=" + pageSize + "&category=" + category + "&query=" + query + order + "&version=" + Umbraco.Sys.ServerVariables.application.version), - httpConfig, - 'Failed to query packages'); - } - - - }; -} - -angular.module('umbraco.resources').factory('ourPackageRepositoryResource', ourPackageRepositoryResource); +/** + * @ngdoc service + * @name umbraco.resources.ourPackageRepositoryResource + * @description handles data for package installations + **/ +function ourPackageRepositoryResource($q, $http, umbDataFormatter, umbRequestHelper) { -/** - * @ngdoc service - * @name umbraco.resources.packageInstallResource - * @description handles data for package installations - **/ -function packageResource($q, $http, umbDataFormatter, umbRequestHelper) { - - return { - - /** - * @ngdoc method - * @name umbraco.resources.packageInstallResource#getInstalled - * @methodOf umbraco.resources.packageInstallResource - * - * @description - * Gets a list of installed packages - */ - getInstalled: function() { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "GetInstalled")), - 'Failed to get installed packages'); - }, - - validateInstalled: function (name, version) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "ValidateInstalled", { name: name, version: version })), - 'Failed to validate package ' + name); - }, - - deleteCreatedPackage: function (packageId) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "DeleteCreatedPackage", { packageId: packageId })), - 'Failed to delete package ' + packageId); - }, - - uninstall: function(packageId) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "Uninstall", { packageId: packageId })), - 'Failed to uninstall package'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.packageInstallResource#fetchPackage - * @methodOf umbraco.resources.packageInstallResource - * - * @description - * Downloads a package file from our.umbraco.org to the website server. - * - * ##usage - *
-         * packageResource.download("guid-guid-guid-guid")
-         *    .then(function(path) {
-         *        alert('downloaded');
-         *    });
-         * 
- * - * @param {String} the unique package ID - * @returns {String} path to the downloaded zip file. - * - */ - fetch: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "Fetch", - [{ packageGuid: id }])), - 'Failed to download package with guid ' + id); - }, - - /** - * @ngdoc method - * @name umbraco.resources.packageInstallResource#createmanifest - * @methodOf umbraco.resources.packageInstallResource - * - * @description - * Creates a package manifest for a given folder of files. - * This manifest keeps track of all installed files and data items - * so a package can be uninstalled at a later time. - * After creating a manifest, you can use the ID to install files and data. - * - * ##usage - *
-         * packageResource.createManifest("packages/id-of-install-file")
-         *    .then(function(summary) {
-         *        alert('unzipped');
-         *    });
-         * 
- * - * @param {String} folder the path to the temporary folder containing files - * @returns {Int} the ID assigned to the saved package manifest - * - */ - import: function (package) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "Import"), package), - 'Failed to install package. Error during the step "Import" '); - }, - - installFiles: function (package) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "InstallFiles"), package), - 'Failed to install package. Error during the step "InstallFiles" '); - }, - - installData: function (package) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "InstallData"), package), - 'Failed to install package. Error during the step "InstallData" '); - }, - - cleanUp: function (package) { - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "packageInstallApiBaseUrl", - "CleanUp"), package), - 'Failed to install package. Error during the step "CleanUp" '); - } - }; -} - -angular.module('umbraco.resources').factory('packageResource', packageResource); + //var baseurl = "http://localhost:24292/webapi/packages/v1"; + var baseurl = "https://our.umbraco.org/webapi/packages/v1"; -/** - * @ngdoc service - * @name umbraco.resources.redirectUrlResource - * @function - * - * @description - * Used by the redirect url dashboard to get urls and send requests to remove redirects. - */ -(function() { - 'use strict'; - - function redirectUrlsResource($http, umbRequestHelper) { - - /** - * @ngdoc function - * @name umbraco.resources.redirectUrlResource#searchRedirectUrls - * @methodOf umbraco.resources.redirectUrlResource - * @function - * - * @description - * Called to search redirects - * ##usage - *
-         * redirectUrlsResource.searchRedirectUrls("", 0, 20)
-         *    .then(function(response) {
-         *
-         *    });
-         * 
- * @param {String} searchTerm Searh term - * @param {Int} pageIndex index of the page to retrive items from - * @param {Int} pageSize The number of items on a page - */ - function searchRedirectUrls(searchTerm, pageIndex, pageSize) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "redirectUrlManagementApiBaseUrl", - "SearchRedirectUrls", - { searchTerm: searchTerm, page: pageIndex, pageSize: pageSize })), - 'Failed to retrieve data for searching redirect urls'); - } - - function getEnableState() { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "redirectUrlManagementApiBaseUrl", - "GetEnableState")), - 'Failed to retrieve data to check if the 301 redirect is enabled'); - } - - /** - * @ngdoc function - * @name umbraco.resources.redirectUrlResource#deleteRedirectUrl - * @methodOf umbraco.resources.redirectUrlResource - * @function - * - * @description - * Called to delete a redirect - * ##usage - *
-         * redirectUrlsResource.deleteRedirectUrl(1234)
-         *    .then(function() {
-         *
-         *    });
-         * 
- * @param {Int} id Id of the redirect - */ - function deleteRedirectUrl(id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "redirectUrlManagementApiBaseUrl", - "DeleteRedirectUrl", { id: id })), - 'Failed to remove redirect'); - } - - /** - * @ngdoc function - * @name umbraco.resources.redirectUrlResource#toggleUrlTracker - * @methodOf umbraco.resources.redirectUrlResource - * @function - * - * @description - * Called to enable or disable redirect url tracker - * ##usage - *
-         * redirectUrlsResource.toggleUrlTracker(true)
-         *    .then(function() {
-         *
-         *    });
-         * 
- * @param {Bool} disable true/false to disable/enable the url tracker - */ - function toggleUrlTracker(disable) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "redirectUrlManagementApiBaseUrl", - "ToggleUrlTracker", { disable: disable })), - 'Failed to toggle redirect url tracker'); - } - - var resource = { - searchRedirectUrls: searchRedirectUrls, - deleteRedirectUrl: deleteRedirectUrl, - toggleUrlTracker: toggleUrlTracker, - getEnableState: getEnableState - }; - - return resource; - - } - - angular.module('umbraco.resources').factory('redirectUrlsResource', redirectUrlsResource); - -})(); + return { + + getDetails: function (packageId) { + + return umbRequestHelper.resourcePromise( + $http.get(baseurl + "/" + packageId), + 'Failed to get package details'); + }, + + getCategories: function () { + + return umbRequestHelper.resourcePromise( + $http.get(baseurl), + 'Failed to query packages'); + }, + + getPopular: function (maxResults, category) { + + if (maxResults === undefined) { + maxResults = 10; + } + if (category === undefined) { + category = ""; + } + + return umbRequestHelper.resourcePromise( + $http.get(baseurl + "?pageIndex=0&pageSize=" + maxResults + "&category=" + category + "&order=Popular&version=" + Umbraco.Sys.ServerVariables.application.version), + 'Failed to query packages'); + }, + + search: function (pageIndex, pageSize, orderBy, category, query, canceler) { + + var httpConfig = {}; + if (canceler) { + httpConfig["timeout"] = canceler; + } + + if (category === undefined) { + category = ""; + } + if (query === undefined) { + query = ""; + } + + //order by score if there is nothing set + var order = !orderBy ? "&order=Default" : ("&order=" + orderBy); + + return umbRequestHelper.resourcePromise( + $http.get(baseurl + "?pageIndex=" + pageIndex + "&pageSize=" + pageSize + "&category=" + category + "&query=" + query + order + "&version=" + Umbraco.Sys.ServerVariables.application.version), + httpConfig, + 'Failed to query packages'); + } + + + }; +} + +angular.module('umbraco.resources').factory('ourPackageRepositoryResource', ourPackageRepositoryResource); + +/** + * @ngdoc service + * @name umbraco.resources.packageInstallResource + * @description handles data for package installations + **/ +function packageResource($q, $http, umbDataFormatter, umbRequestHelper) { + + return { + + /** + * @ngdoc method + * @name umbraco.resources.packageInstallResource#getInstalled + * @methodOf umbraco.resources.packageInstallResource + * + * @description + * Gets a list of installed packages + */ + getInstalled: function() { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "packageInstallApiBaseUrl", + "GetInstalled")), + 'Failed to get installed packages'); + }, + + validateInstalled: function (name, version) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "packageInstallApiBaseUrl", + "ValidateInstalled", { name: name, version: version })), + 'Failed to validate package ' + name); + }, + + deleteCreatedPackage: function (packageId) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "packageInstallApiBaseUrl", + "DeleteCreatedPackage", { packageId: packageId })), + 'Failed to delete package ' + packageId); + }, + + uninstall: function(packageId) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "packageInstallApiBaseUrl", + "Uninstall", { packageId: packageId })), + 'Failed to uninstall package'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.packageInstallResource#fetchPackage + * @methodOf umbraco.resources.packageInstallResource + * + * @description + * Downloads a package file from our.umbraco.org to the website server. + * + * ##usage + *
+         * packageResource.download("guid-guid-guid-guid")
+         *    .then(function(path) {
+         *        alert('downloaded');
+         *    });
+         * 
+ * + * @param {String} the unique package ID + * @returns {String} path to the downloaded zip file. + * + */ + fetch: function (id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "packageInstallApiBaseUrl", + "Fetch", + [{ packageGuid: id }])), + 'Failed to download package with guid ' + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.packageInstallResource#createmanifest + * @methodOf umbraco.resources.packageInstallResource + * + * @description + * Creates a package manifest for a given folder of files. + * This manifest keeps track of all installed files and data items + * so a package can be uninstalled at a later time. + * After creating a manifest, you can use the ID to install files and data. + * + * ##usage + *
+         * packageResource.createManifest("packages/id-of-install-file")
+         *    .then(function(summary) {
+         *        alert('unzipped');
+         *    });
+         * 
+ * + * @param {String} folder the path to the temporary folder containing files + * @returns {Int} the ID assigned to the saved package manifest + * + */ + import: function (package) { + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "packageInstallApiBaseUrl", + "Import"), package), + 'Failed to install package. Error during the step "Import" '); + }, + + installFiles: function (package) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "packageInstallApiBaseUrl", + "InstallFiles"), package), + 'Failed to install package. Error during the step "InstallFiles" '); + }, + + installData: function (package) { + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "packageInstallApiBaseUrl", + "InstallData"), package), + 'Failed to install package. Error during the step "InstallData" '); + }, + + cleanUp: function (package) { + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "packageInstallApiBaseUrl", + "CleanUp"), package), + 'Failed to install package. Error during the step "CleanUp" '); + } + }; +} + +angular.module('umbraco.resources').factory('packageResource', packageResource); + +/** + * @ngdoc service + * @name umbraco.resources.redirectUrlResource + * @function + * + * @description + * Used by the redirect url dashboard to get urls and send requests to remove redirects. + */ +(function() { + 'use strict'; + + function redirectUrlsResource($http, umbRequestHelper) { + + /** + * @ngdoc function + * @name umbraco.resources.redirectUrlResource#searchRedirectUrls + * @methodOf umbraco.resources.redirectUrlResource + * @function + * + * @description + * Called to search redirects + * ##usage + *
+         * redirectUrlsResource.searchRedirectUrls("", 0, 20)
+         *    .then(function(response) {
+         *
+         *    });
+         * 
+ * @param {String} searchTerm Searh term + * @param {Int} pageIndex index of the page to retrive items from + * @param {Int} pageSize The number of items on a page + */ + function searchRedirectUrls(searchTerm, pageIndex, pageSize) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "redirectUrlManagementApiBaseUrl", + "SearchRedirectUrls", + { searchTerm: searchTerm, page: pageIndex, pageSize: pageSize })), + 'Failed to retrieve data for searching redirect urls'); + } + + function getEnableState() { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "redirectUrlManagementApiBaseUrl", + "GetEnableState")), + 'Failed to retrieve data to check if the 301 redirect is enabled'); + } + + /** + * @ngdoc function + * @name umbraco.resources.redirectUrlResource#deleteRedirectUrl + * @methodOf umbraco.resources.redirectUrlResource + * @function + * + * @description + * Called to delete a redirect + * ##usage + *
+         * redirectUrlsResource.deleteRedirectUrl(1234)
+         *    .then(function() {
+         *
+         *    });
+         * 
+ * @param {Int} id Id of the redirect + */ + function deleteRedirectUrl(id) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "redirectUrlManagementApiBaseUrl", + "DeleteRedirectUrl", { id: id })), + 'Failed to remove redirect'); + } + + /** + * @ngdoc function + * @name umbraco.resources.redirectUrlResource#toggleUrlTracker + * @methodOf umbraco.resources.redirectUrlResource + * @function + * + * @description + * Called to enable or disable redirect url tracker + * ##usage + *
+         * redirectUrlsResource.toggleUrlTracker(true)
+         *    .then(function() {
+         *
+         *    });
+         * 
+ * @param {Bool} disable true/false to disable/enable the url tracker + */ + function toggleUrlTracker(disable) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "redirectUrlManagementApiBaseUrl", + "ToggleUrlTracker", { disable: disable })), + 'Failed to toggle redirect url tracker'); + } + + var resource = { + searchRedirectUrls: searchRedirectUrls, + deleteRedirectUrl: deleteRedirectUrl, + toggleUrlTracker: toggleUrlTracker, + getEnableState: getEnableState + }; + + return resource; + + } + + angular.module('umbraco.resources').factory('redirectUrlsResource', redirectUrlsResource); + +})(); + +/** + * @ngdoc service + * @name umbraco.resources.relationResource + * @description Handles loading of relation data + **/ +function relationResource($q, $http, umbRequestHelper) { + return { + + /** + * @ngdoc method + * @name umbraco.resources.relationResource#getByChildId + * @methodOf umbraco.resources.relationResource + * + * @description + * Retrieves the relation data for a given child ID + * + * @param {int} id of the child item + * @param {string} alias of the relation type + * @returns {Promise} resourcePromise object containing the relations array. + * + */ + getByChildId: function (id, alias) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "relationApiBaseUrl", + "GetByChildId", + [{ childId: id, relationTypeAlias: alias }])), + "Failed to get relation by child ID " + id + " and type of " + alias); + }, + + /** + * @ngdoc method + * @name umbraco.resources.relationResource#deleteById + * @methodOf umbraco.resources.relationResource + * + * @description + * Deletes a relation item with a given id + * + * ##usage + *
+         * relationResource.deleteById(1234)
+         *    .then(function() {
+         *        alert('its gone!');
+         *    });
+         * 
+ * + * @param {Int} id id of relation item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteById: function (id) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "relationApiBaseUrl", + "DeleteById", + [{ id: id }])), + 'Failed to delete item ' + id); + } + }; +} -/** - * @ngdoc service - * @name umbraco.resources.relationResource - * @description Handles loading of relation data - **/ -function relationResource($q, $http, umbRequestHelper) { - return { - - /** - * @ngdoc method - * @name umbraco.resources.relationResource#getByChildId - * @methodOf umbraco.resources.relationResource - * - * @description - * Retrieves the relation data for a given child ID - * - * @param {int} id of the child item - * @param {string} alias of the relation type - * @returns {Promise} resourcePromise object containing the relations array. - * - */ - getByChildId: function (id, alias) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "relationApiBaseUrl", - "GetByChildId", - [{ childId: id, relationTypeAlias: alias }])), - "Failed to get relation by child ID " + id + " and type of " + alias); - }, - - /** - * @ngdoc method - * @name umbraco.resources.relationResource#deleteById - * @methodOf umbraco.resources.relationResource - * - * @description - * Deletes a relation item with a given id - * - * ##usage - *
-         * relationResource.deleteById(1234)
-         *    .then(function() {
-         *        alert('its gone!');
-         *    });
-         * 
- * - * @param {Int} id id of relation item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteById: function (id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "relationApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete item ' + id); - } - }; -} - angular.module('umbraco.resources').factory('relationResource', relationResource); /** * @ngdoc service @@ -3956,78 +3956,78 @@ function sectionResource($q, $http, umbRequestHelper) { angular.module('umbraco.resources').factory('sectionResource', sectionResource); -/** - * @ngdoc service - * @name umbraco.resources.stylesheetResource - * @description service to retrieve available stylesheets - * - * - **/ -function stylesheetResource($q, $http, umbRequestHelper) { - - //the factory object returned - return { - - /** - * @ngdoc method - * @name umbraco.resources.stylesheetResource#getAll - * @methodOf umbraco.resources.stylesheetResource - * - * @description - * Gets all registered stylesheets - * - * ##usage - *
-         * stylesheetResource.getAll()
-         *    .then(function(stylesheets) {
-         *        alert('its here!');
-         *    });
-         * 
- * - * @returns {Promise} resourcePromise object containing the stylesheets. - * - */ - getAll: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "stylesheetApiBaseUrl", - "GetAll")), - 'Failed to retrieve stylesheets '); - }, - - /** - * @ngdoc method - * @name umbraco.resources.stylesheetResource#getRulesByName - * @methodOf umbraco.resources.stylesheetResource - * - * @description - * Returns all defined child rules for a stylesheet with a given name - * - * ##usage - *
-         * stylesheetResource.getRulesByName("ie7stylesheet")
-         *    .then(function(rules) {
-         *        alert('its here!');
-         *    });
-         * 
- * - * @returns {Promise} resourcePromise object containing the rules. - * - */ - getRulesByName: function (name) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "stylesheetApiBaseUrl", - "GetRulesByName", - [{ name: name }])), - 'Failed to retrieve stylesheets '); - } - }; -} - -angular.module('umbraco.resources').factory('stylesheetResource', stylesheetResource); +/** + * @ngdoc service + * @name umbraco.resources.stylesheetResource + * @description service to retrieve available stylesheets + * + * + **/ +function stylesheetResource($q, $http, umbRequestHelper) { + + //the factory object returned + return { + + /** + * @ngdoc method + * @name umbraco.resources.stylesheetResource#getAll + * @methodOf umbraco.resources.stylesheetResource + * + * @description + * Gets all registered stylesheets + * + * ##usage + *
+         * stylesheetResource.getAll()
+         *    .then(function(stylesheets) {
+         *        alert('its here!');
+         *    });
+         * 
+ * + * @returns {Promise} resourcePromise object containing the stylesheets. + * + */ + getAll: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "stylesheetApiBaseUrl", + "GetAll")), + 'Failed to retrieve stylesheets '); + }, + + /** + * @ngdoc method + * @name umbraco.resources.stylesheetResource#getRulesByName + * @methodOf umbraco.resources.stylesheetResource + * + * @description + * Returns all defined child rules for a stylesheet with a given name + * + * ##usage + *
+         * stylesheetResource.getRulesByName("ie7stylesheet")
+         *    .then(function(rules) {
+         *        alert('its here!');
+         *    });
+         * 
+ * + * @returns {Promise} resourcePromise object containing the rules. + * + */ + getRulesByName: function (name) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "stylesheetApiBaseUrl", + "GetRulesByName", + [{ name: name }])), + 'Failed to retrieve stylesheets '); + } + }; +} + +angular.module('umbraco.resources').factory('stylesheetResource', stylesheetResource); /** * @ngdoc service @@ -4116,31 +4116,31 @@ function treeResource($q, $http, umbRequestHelper) { angular.module('umbraco.resources').factory('treeResource', treeResource); -/** - * @ngdoc service - * @name umbraco.resources.userResource - **/ -function userResource($q, $http, umbDataFormatter, umbRequestHelper) { - - return { - - disableUser: function (userId) { - - if (!userId) { - throw "userId not specified"; - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "userApiBaseUrl", - "PostDisableUser", [{ userId: userId }])), - 'Failed to disable the user ' + userId); - } - }; -} - -angular.module('umbraco.resources').factory('userResource', userResource); +/** + * @ngdoc service + * @name umbraco.resources.userResource + **/ +function userResource($q, $http, umbDataFormatter, umbRequestHelper) { + + return { + + disableUser: function (userId) { + + if (!userId) { + throw "userId not specified"; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostDisableUser", [{ userId: userId }])), + 'Failed to disable the user ' + userId); + } + }; +} + +angular.module('umbraco.resources').factory('userResource', userResource); })(); \ No newline at end of file diff --git a/OurUmbraco.Site/umbraco/js/umbraco.security.js b/OurUmbraco.Site/umbraco/js/umbraco.security.js index 59c4d215..a1cec811 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.security.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.security.js @@ -1,6 +1,6 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ @@ -100,107 +100,107 @@ angular.module('umbraco.security.retryQueue', []) }; return service; }]); -angular.module('umbraco.security.interceptor') - // This http interceptor listens for authentication successes and failures - .factory('securityInterceptor', ['$injector', 'securityRetryQueue', 'notificationsService', 'requestInterceptorFilter', function ($injector, queue, notifications, requestInterceptorFilter) { - return function(promise) { - - return promise.then( - function(originalResponse) { - // Intercept successful requests - - //Here we'll check if our custom header is in the response which indicates how many seconds the user's session has before it - //expires. Then we'll update the user in the user service accordingly. - var headers = originalResponse.headers(); - if (headers["x-umb-user-seconds"]) { - // We must use $injector to get the $http service to prevent circular dependency - var userService = $injector.get('userService'); - userService.setUserTimeout(headers["x-umb-user-seconds"]); - } - - return promise; - }, function(originalResponse) { - // Intercept failed requests - - //Here we'll check if we should ignore the error, this will be based on an original header set - var headers = originalResponse.config ? originalResponse.config.headers : {}; - if (headers["x-umb-ignore-error"] === "ignore") { - //exit/ignore - return promise; - } - var filtered = _.find(requestInterceptorFilter(), function(val) { - return originalResponse.config.url.indexOf(val) > 0; - }); - if (filtered) { - return promise; - } - - //A 401 means that the user is not logged in - if (originalResponse.status === 401) { - - var userService = $injector.get('userService'); // see above - - //Associate the user name with the retry to ensure we retry for the right user - promise = userService.getCurrentUser() - .then(function (user) { - var userName = user ? user.name : null; - //The request bounced because it was not authorized - add a new request to the retry queue - return queue.pushRetryFn('unauthorized-server', userName, function retryRequest() { - // We must use $injector to get the $http service to prevent circular dependency - return $injector.get('$http')(originalResponse.config); - }); - }); - } - else if (originalResponse.status === 404) { - - //a 404 indicates that the request was not found - this could be due to a non existing url, or it could - //be due to accessing a url with a parameter that doesn't exist, either way we should notifiy the user about it - - var errMsg = "The URL returned a 404 (not found):
" + originalResponse.config.url.split('?')[0] + ""; - if (originalResponse.data && originalResponse.data.ExceptionMessage) { - errMsg += "
with error:
" + originalResponse.data.ExceptionMessage + ""; - } - if (originalResponse.config.data) { - errMsg += "
with data:
" + angular.toJson(originalResponse.config.data) + "
Contact your administrator for information."; - } - - notifications.error( - "Request error", - errMsg); - - } - else if (originalResponse.status === 403) { - //if the status was a 403 it means the user didn't have permission to do what the request was trying to do. - //How do we deal with this now, need to tell the user somehow that they don't have permission to do the thing that was - //requested. We can either deal with this globally here, or we can deal with it globally for individual requests on the umbRequestHelper, - // or completely custom for services calling resources. - - //http://issues.umbraco.org/issue/U4-2749 - - //It was decided to just put these messages into the normal status messages. - - var msg = "Unauthorized access to URL:
" + originalResponse.config.url.split('?')[0] + ""; - if (originalResponse.config.data) { - msg += "
with data:
" + angular.toJson(originalResponse.config.data) + "
Contact your administrator for information."; - } - - notifications.error( - "Authorization error", - msg); - } - - return promise; - }); - }; - }]) - - .value('requestInterceptorFilter', function() { - return ["www.gravatar.com"]; - }) - - // We have to add the interceptor to the queue as a string because the interceptor depends upon service instances that are not available in the config block. - .config(['$httpProvider', function ($httpProvider) { - $httpProvider.responseInterceptors.push('securityInterceptor'); +angular.module('umbraco.security.interceptor') + // This http interceptor listens for authentication successes and failures + .factory('securityInterceptor', ['$injector', 'securityRetryQueue', 'notificationsService', 'requestInterceptorFilter', function ($injector, queue, notifications, requestInterceptorFilter) { + return function(promise) { + + return promise.then( + function(originalResponse) { + // Intercept successful requests + + //Here we'll check if our custom header is in the response which indicates how many seconds the user's session has before it + //expires. Then we'll update the user in the user service accordingly. + var headers = originalResponse.headers(); + if (headers["x-umb-user-seconds"]) { + // We must use $injector to get the $http service to prevent circular dependency + var userService = $injector.get('userService'); + userService.setUserTimeout(headers["x-umb-user-seconds"]); + } + + return promise; + }, function(originalResponse) { + // Intercept failed requests + + //Here we'll check if we should ignore the error, this will be based on an original header set + var headers = originalResponse.config ? originalResponse.config.headers : {}; + if (headers["x-umb-ignore-error"] === "ignore") { + //exit/ignore + return promise; + } + var filtered = _.find(requestInterceptorFilter(), function(val) { + return originalResponse.config.url.indexOf(val) > 0; + }); + if (filtered) { + return promise; + } + + //A 401 means that the user is not logged in + if (originalResponse.status === 401) { + + var userService = $injector.get('userService'); // see above + + //Associate the user name with the retry to ensure we retry for the right user + promise = userService.getCurrentUser() + .then(function (user) { + var userName = user ? user.name : null; + //The request bounced because it was not authorized - add a new request to the retry queue + return queue.pushRetryFn('unauthorized-server', userName, function retryRequest() { + // We must use $injector to get the $http service to prevent circular dependency + return $injector.get('$http')(originalResponse.config); + }); + }); + } + else if (originalResponse.status === 404) { + + //a 404 indicates that the request was not found - this could be due to a non existing url, or it could + //be due to accessing a url with a parameter that doesn't exist, either way we should notifiy the user about it + + var errMsg = "The URL returned a 404 (not found):
" + originalResponse.config.url.split('?')[0] + ""; + if (originalResponse.data && originalResponse.data.ExceptionMessage) { + errMsg += "
with error:
" + originalResponse.data.ExceptionMessage + ""; + } + if (originalResponse.config.data) { + errMsg += "
with data:
" + angular.toJson(originalResponse.config.data) + "
Contact your administrator for information."; + } + + notifications.error( + "Request error", + errMsg); + + } + else if (originalResponse.status === 403) { + //if the status was a 403 it means the user didn't have permission to do what the request was trying to do. + //How do we deal with this now, need to tell the user somehow that they don't have permission to do the thing that was + //requested. We can either deal with this globally here, or we can deal with it globally for individual requests on the umbRequestHelper, + // or completely custom for services calling resources. + + //http://issues.umbraco.org/issue/U4-2749 + + //It was decided to just put these messages into the normal status messages. + + var msg = "Unauthorized access to URL:
" + originalResponse.config.url.split('?')[0] + ""; + if (originalResponse.config.data) { + msg += "
with data:
" + angular.toJson(originalResponse.config.data) + "
Contact your administrator for information."; + } + + notifications.error( + "Authorization error", + msg); + } + + return promise; + }); + }; + }]) + + .value('requestInterceptorFilter', function() { + return ["www.gravatar.com"]; + }) + + // We have to add the interceptor to the queue as a string because the interceptor depends upon service instances that are not available in the config block. + .config(['$httpProvider', function ($httpProvider) { + $httpProvider.responseInterceptors.push('securityInterceptor'); }]); })(); \ No newline at end of file diff --git a/OurUmbraco.Site/umbraco/js/umbraco.services.js b/OurUmbraco.Site/umbraco/js/umbraco.services.js index f5410fa5..275c23c7 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.services.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.services.js @@ -1,6 +1,6 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ @@ -164,303 +164,303 @@ function angularHelper($log, $q) { }; } angular.module('umbraco.services').factory('angularHelper', angularHelper); -/** - * @ngdoc service - * @name umbraco.services.appState - * @function - * - * @description - * Tracks the various application state variables when working in the back office, raises events when state changes. - * - * ##Samples - * - * ####Subscribe to global state changes: - * - *
-  *    scope.showTree = appState.getGlobalState("showNavigation");
-  *
-  *    eventsService.on("appState.globalState.changed", function (e, args) {
-  *               if (args.key === "showNavigation") {
-  *                   scope.showTree = args.value;
-  *               }
-  *           });  
-  * 
- * - * ####Subscribe to section-state changes - * - *
- *    scope.currentSection = appState.getSectionState("currentSection");
- *
- *    eventsService.on("appState.sectionState.changed", function (e, args) {
- *               if (args.key === "currentSection") {
- *                   scope.currentSection = args.value;
- *               }
- *           });  
- * 
- */ -function appState(eventsService) { - - //Define all variables here - we are never returning this objects so they cannot be publicly mutable - // changed, we only expose methods to interact with the values. - - var globalState = { - showNavigation: null, - touchDevice: null, - showTray: null, - stickyNavigation: null, - navMode: null, - isReady: null, - isTablet: null - }; - - var sectionState = { - //The currently active section - currentSection: null, - showSearchResults: null - }; - - var treeState = { - //The currently selected node - selectedNode: null, - //The currently loaded root node reference - depending on the section loaded this could be a section root or a normal root. - //We keep this reference so we can lookup nodes to interact with in the UI via the tree service - currentRootNode: null - }; - - var menuState = { - //this list of menu items to display - menuActions: null, - //the title to display in the context menu dialog - dialogTitle: null, - //The tree node that the ctx menu is launched for - currentNode: null, - //Whether the menu's dialog is being shown or not - showMenuDialog: null, - //Whether the context menu is being shown or not - showMenu: null - }; - - /** function to validate and set the state on a state object */ - function setState(stateObj, key, value, stateObjName) { - if (!_.has(stateObj, key)) { - throw "The variable " + key + " does not exist in " + stateObjName; - } - var changed = stateObj[key] !== value; - stateObj[key] = value; - if (changed) { - eventsService.emit("appState." + stateObjName + ".changed", { key: key, value: value }); - } - } - - /** function to validate and set the state on a state object */ - function getState(stateObj, key, stateObjName) { - if (!_.has(stateObj, key)) { - throw "The variable " + key + " does not exist in " + stateObjName; - } - return stateObj[key]; - } - - return { - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#getGlobalState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Returns the current global state value by key - we do not return an object reference here - we do NOT want this - * to be publicly mutable and allow setting arbitrary values - * - */ - getGlobalState: function (key) { - return getState(globalState, key, "globalState"); - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#setGlobalState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Sets a global state value by key - * - */ - setGlobalState: function (key, value) { - setState(globalState, key, value, "globalState"); - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#getSectionState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Returns the current section state value by key - we do not return an object here - we do NOT want this - * to be publicly mutable and allow setting arbitrary values - * - */ - getSectionState: function (key) { - return getState(sectionState, key, "sectionState"); - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#setSectionState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Sets a section state value by key - * - */ - setSectionState: function(key, value) { - setState(sectionState, key, value, "sectionState"); - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#getTreeState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Returns the current tree state value by key - we do not return an object here - we do NOT want this - * to be publicly mutable and allow setting arbitrary values - * - */ - getTreeState: function (key) { - return getState(treeState, key, "treeState"); - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#setTreeState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Sets a section state value by key - * - */ - setTreeState: function (key, value) { - setState(treeState, key, value, "treeState"); - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#getMenuState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Returns the current menu state value by key - we do not return an object here - we do NOT want this - * to be publicly mutable and allow setting arbitrary values - * - */ - getMenuState: function (key) { - return getState(menuState, key, "menuState"); - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#setMenuState - * @methodOf umbraco.services.appState - * @function - * - * @description - * Sets a section state value by key - * - */ - setMenuState: function (key, value) { - setState(menuState, key, value, "menuState"); - }, - - }; -} -angular.module('umbraco.services').factory('appState', appState); - -/** - * @ngdoc service - * @name umbraco.services.editorState - * @function - * - * @description - * Tracks the parent object for complex editors by exposing it as - * an object reference via editorState.current.entity - * - * it is possible to modify this object, so should be used with care - */ -angular.module('umbraco.services').factory("editorState", function() { - - var current = null; - var state = { - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#set - * @methodOf umbraco.services.editorState - * @function - * - * @description - * Sets the current entity object for the currently active editor - * This is only used when implementing an editor with a complex model - * like the content editor, where the model is modified by several - * child controllers. - */ - set: function (entity) { - current = entity; - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#reset - * @methodOf umbraco.services.editorState - * @function - * - * @description - * Since the editorstate entity is read-only, you cannot set it to null - * only through the reset() method - */ - reset: function() { - current = null; - }, - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#getCurrent - * @methodOf umbraco.services.editorState - * @function - * - * @description - * Returns an object reference to the current editor entity. - * the entity is the root object of the editor. - * EditorState is used by property/parameter editors that need - * access to the entire entity being edited, not just the property/parameter - * - * editorState.current can not be overwritten, you should only read values from it - * since modifying individual properties should be handled by the property editors - */ - getCurrent: function() { - return current; - } - }; - - //TODO: This shouldn't be removed! use getCurrent() method instead of a hacked readonly property which is confusing. - - //create a get/set property but don't allow setting - Object.defineProperty(state, "current", { - get: function () { - return current; - }, - set: function (value) { - throw "Use editorState.set to set the value of the current entity"; - }, - }); - - return state; +/** + * @ngdoc service + * @name umbraco.services.appState + * @function + * + * @description + * Tracks the various application state variables when working in the back office, raises events when state changes. + * + * ##Samples + * + * ####Subscribe to global state changes: + * + *
+  *    scope.showTree = appState.getGlobalState("showNavigation");
+  *
+  *    eventsService.on("appState.globalState.changed", function (e, args) {
+  *               if (args.key === "showNavigation") {
+  *                   scope.showTree = args.value;
+  *               }
+  *           });  
+  * 
+ * + * ####Subscribe to section-state changes + * + *
+ *    scope.currentSection = appState.getSectionState("currentSection");
+ *
+ *    eventsService.on("appState.sectionState.changed", function (e, args) {
+ *               if (args.key === "currentSection") {
+ *                   scope.currentSection = args.value;
+ *               }
+ *           });  
+ * 
+ */ +function appState(eventsService) { + + //Define all variables here - we are never returning this objects so they cannot be publicly mutable + // changed, we only expose methods to interact with the values. + + var globalState = { + showNavigation: null, + touchDevice: null, + showTray: null, + stickyNavigation: null, + navMode: null, + isReady: null, + isTablet: null + }; + + var sectionState = { + //The currently active section + currentSection: null, + showSearchResults: null + }; + + var treeState = { + //The currently selected node + selectedNode: null, + //The currently loaded root node reference - depending on the section loaded this could be a section root or a normal root. + //We keep this reference so we can lookup nodes to interact with in the UI via the tree service + currentRootNode: null + }; + + var menuState = { + //this list of menu items to display + menuActions: null, + //the title to display in the context menu dialog + dialogTitle: null, + //The tree node that the ctx menu is launched for + currentNode: null, + //Whether the menu's dialog is being shown or not + showMenuDialog: null, + //Whether the context menu is being shown or not + showMenu: null + }; + + /** function to validate and set the state on a state object */ + function setState(stateObj, key, value, stateObjName) { + if (!_.has(stateObj, key)) { + throw "The variable " + key + " does not exist in " + stateObjName; + } + var changed = stateObj[key] !== value; + stateObj[key] = value; + if (changed) { + eventsService.emit("appState." + stateObjName + ".changed", { key: key, value: value }); + } + } + + /** function to validate and set the state on a state object */ + function getState(stateObj, key, stateObjName) { + if (!_.has(stateObj, key)) { + throw "The variable " + key + " does not exist in " + stateObjName; + } + return stateObj[key]; + } + + return { + + /** + * @ngdoc function + * @name umbraco.services.angularHelper#getGlobalState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Returns the current global state value by key - we do not return an object reference here - we do NOT want this + * to be publicly mutable and allow setting arbitrary values + * + */ + getGlobalState: function (key) { + return getState(globalState, key, "globalState"); + }, + + /** + * @ngdoc function + * @name umbraco.services.angularHelper#setGlobalState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Sets a global state value by key + * + */ + setGlobalState: function (key, value) { + setState(globalState, key, value, "globalState"); + }, + + /** + * @ngdoc function + * @name umbraco.services.angularHelper#getSectionState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Returns the current section state value by key - we do not return an object here - we do NOT want this + * to be publicly mutable and allow setting arbitrary values + * + */ + getSectionState: function (key) { + return getState(sectionState, key, "sectionState"); + }, + + /** + * @ngdoc function + * @name umbraco.services.angularHelper#setSectionState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Sets a section state value by key + * + */ + setSectionState: function(key, value) { + setState(sectionState, key, value, "sectionState"); + }, + + /** + * @ngdoc function + * @name umbraco.services.angularHelper#getTreeState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Returns the current tree state value by key - we do not return an object here - we do NOT want this + * to be publicly mutable and allow setting arbitrary values + * + */ + getTreeState: function (key) { + return getState(treeState, key, "treeState"); + }, + + /** + * @ngdoc function + * @name umbraco.services.angularHelper#setTreeState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Sets a section state value by key + * + */ + setTreeState: function (key, value) { + setState(treeState, key, value, "treeState"); + }, + + /** + * @ngdoc function + * @name umbraco.services.angularHelper#getMenuState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Returns the current menu state value by key - we do not return an object here - we do NOT want this + * to be publicly mutable and allow setting arbitrary values + * + */ + getMenuState: function (key) { + return getState(menuState, key, "menuState"); + }, + + /** + * @ngdoc function + * @name umbraco.services.angularHelper#setMenuState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Sets a section state value by key + * + */ + setMenuState: function (key, value) { + setState(menuState, key, value, "menuState"); + }, + + }; +} +angular.module('umbraco.services').factory('appState', appState); + +/** + * @ngdoc service + * @name umbraco.services.editorState + * @function + * + * @description + * Tracks the parent object for complex editors by exposing it as + * an object reference via editorState.current.entity + * + * it is possible to modify this object, so should be used with care + */ +angular.module('umbraco.services').factory("editorState", function() { + + var current = null; + var state = { + + /** + * @ngdoc function + * @name umbraco.services.angularHelper#set + * @methodOf umbraco.services.editorState + * @function + * + * @description + * Sets the current entity object for the currently active editor + * This is only used when implementing an editor with a complex model + * like the content editor, where the model is modified by several + * child controllers. + */ + set: function (entity) { + current = entity; + }, + + /** + * @ngdoc function + * @name umbraco.services.angularHelper#reset + * @methodOf umbraco.services.editorState + * @function + * + * @description + * Since the editorstate entity is read-only, you cannot set it to null + * only through the reset() method + */ + reset: function() { + current = null; + }, + + /** + * @ngdoc function + * @name umbraco.services.angularHelper#getCurrent + * @methodOf umbraco.services.editorState + * @function + * + * @description + * Returns an object reference to the current editor entity. + * the entity is the root object of the editor. + * EditorState is used by property/parameter editors that need + * access to the entire entity being edited, not just the property/parameter + * + * editorState.current can not be overwritten, you should only read values from it + * since modifying individual properties should be handled by the property editors + */ + getCurrent: function() { + return current; + } + }; + + //TODO: This shouldn't be removed! use getCurrent() method instead of a hacked readonly property which is confusing. + + //create a get/set property but don't allow setting + Object.defineProperty(state, "current", { + get: function () { + return current; + }, + set: function (value) { + throw "Use editorState.set to set the value of the current entity"; + }, + }); + + return state; }); /** * @ngdoc service @@ -1261,583 +1261,583 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } angular.module('umbraco.services').factory('contentEditingHelper', contentEditingHelper); -/** - * @ngdoc service - * @name umbraco.services.contentTypeHelper - * @description A helper service for the content type editor - **/ -function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $injector, $q) { - - var contentTypeHelperService = { - - createIdArray: function(array) { - - var newArray = []; - - angular.forEach(array, function(arrayItem){ - - if(angular.isObject(arrayItem)) { - newArray.push(arrayItem.id); - } else { - newArray.push(arrayItem); - } - - }); - - return newArray; - - }, - - generateModels: function () { - var deferred = $q.defer(); - var modelsResource = $injector.has("modelsBuilderResource") ? $injector.get("modelsBuilderResource") : null; - var modelsBuilderEnabled = Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled; - if (modelsBuilderEnabled && modelsResource) { - modelsResource.buildModels().then(function(result) { - deferred.resolve(result); - - //just calling this to get the servar back to life - modelsResource.getModelsOutOfDateStatus(); - - }, function(e) { - deferred.reject(e); - }); - } - else { - deferred.resolve(false); - } - return deferred.promise; - }, - - checkModelsBuilderStatus: function () { - var deferred = $q.defer(); - var modelsResource = $injector.has("modelsBuilderResource") ? $injector.get("modelsBuilderResource") : null; - var modelsBuilderEnabled = (Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables && Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled === true); - - if (modelsBuilderEnabled && modelsResource) { - modelsResource.getModelsOutOfDateStatus().then(function(result) { - //Generate models buttons should be enabled if it is 0 - deferred.resolve(result.status === 0); - }); - } - else { - deferred.resolve(false); - } - return deferred.promise; - }, - - makeObjectArrayFromId: function (idArray, objectArray) { - var newArray = []; - - for (var idIndex = 0; idArray.length > idIndex; idIndex++) { - var id = idArray[idIndex]; - - for (var objectIndex = 0; objectArray.length > objectIndex; objectIndex++) { - var object = objectArray[objectIndex]; - if (id === object.id) { - newArray.push(object); - } - } - - } - - return newArray; - }, - - validateAddingComposition: function(contentType, compositeContentType) { - - //Validate that by adding this group that we are not adding duplicate property type aliases - - var propertiesAdding = _.flatten(_.map(compositeContentType.groups, function(g) { - return _.map(g.properties, function(p) { - return p.alias; - }); - })); - var propAliasesExisting = _.filter(_.flatten(_.map(contentType.groups, function(g) { - return _.map(g.properties, function(p) { - return p.alias; - }); - })), function(f) { - return f !== null && f !== undefined; - }); - - var intersec = _.intersection(propertiesAdding, propAliasesExisting); - if (intersec.length > 0) { - //return the overlapping property aliases - return intersec; - } - - //no overlapping property aliases - return []; - }, - - mergeCompositeContentType: function(contentType, compositeContentType) { - - //Validate that there are no overlapping aliases - var overlappingAliases = this.validateAddingComposition(contentType, compositeContentType); - if (overlappingAliases.length > 0) { - throw new Error("Cannot add this composition, these properties already exist on the content type: " + overlappingAliases.join()); - } - - angular.forEach(compositeContentType.groups, function(compositionGroup) { - - // order composition groups based on sort order - compositionGroup.properties = $filter('orderBy')(compositionGroup.properties, 'sortOrder'); - - // get data type details - angular.forEach(compositionGroup.properties, function(property) { - dataTypeResource.getById(property.dataTypeId) - .then(function(dataType) { - property.dataTypeIcon = dataType.icon; - property.dataTypeName = dataType.name; - }); - }); - - // set inherited state on tab - compositionGroup.inherited = true; - - // set inherited state on properties - angular.forEach(compositionGroup.properties, function(compositionProperty) { - compositionProperty.inherited = true; - }); - - // set tab state - compositionGroup.tabState = "inActive"; - - // if groups are named the same - merge the groups - angular.forEach(contentType.groups, function(contentTypeGroup) { - - if (contentTypeGroup.name === compositionGroup.name) { - - // set flag to show if properties has been merged into a tab - compositionGroup.groupIsMerged = true; - - // make group inherited - contentTypeGroup.inherited = true; - - // add properties to the top of the array - contentTypeGroup.properties = compositionGroup.properties.concat(contentTypeGroup.properties); - - // update sort order on all properties in merged group - contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties); - - // make parentTabContentTypeNames to an array so we can push values - if (contentTypeGroup.parentTabContentTypeNames === null || contentTypeGroup.parentTabContentTypeNames === undefined) { - contentTypeGroup.parentTabContentTypeNames = []; - } - - // push name to array of merged composite content types - contentTypeGroup.parentTabContentTypeNames.push(compositeContentType.name); - - // make parentTabContentTypes to an array so we can push values - if (contentTypeGroup.parentTabContentTypes === null || contentTypeGroup.parentTabContentTypes === undefined) { - contentTypeGroup.parentTabContentTypes = []; - } - - // push id to array of merged composite content types - contentTypeGroup.parentTabContentTypes.push(compositeContentType.id); - - // get sort order from composition - contentTypeGroup.sortOrder = compositionGroup.sortOrder; - - // splice group to the top of the array - var contentTypeGroupCopy = angular.copy(contentTypeGroup); - var index = contentType.groups.indexOf(contentTypeGroup); - contentType.groups.splice(index, 1); - contentType.groups.unshift(contentTypeGroupCopy); - - } - - }); - - // if group is not merged - push it to the end of the array - before init tab - if (compositionGroup.groupIsMerged === false || compositionGroup.groupIsMerged === undefined) { - - // make parentTabContentTypeNames to an array so we can push values - if (compositionGroup.parentTabContentTypeNames === null || compositionGroup.parentTabContentTypeNames === undefined) { - compositionGroup.parentTabContentTypeNames = []; - } - - // push name to array of merged composite content types - compositionGroup.parentTabContentTypeNames.push(compositeContentType.name); - - // make parentTabContentTypes to an array so we can push values - if (compositionGroup.parentTabContentTypes === null || compositionGroup.parentTabContentTypes === undefined) { - compositionGroup.parentTabContentTypes = []; - } - - // push id to array of merged composite content types - compositionGroup.parentTabContentTypes.push(compositeContentType.id); - - // push group before placeholder tab - contentType.groups.unshift(compositionGroup); - - } - - }); - - // sort all groups by sortOrder property - contentType.groups = $filter('orderBy')(contentType.groups, 'sortOrder'); - - return contentType; - - }, - - splitCompositeContentType: function (contentType, compositeContentType) { - - var groups = []; - - angular.forEach(contentType.groups, function(contentTypeGroup){ - - if( contentTypeGroup.tabState !== "init" ) { - - var idIndex = contentTypeGroup.parentTabContentTypes.indexOf(compositeContentType.id); - var nameIndex = contentTypeGroup.parentTabContentTypeNames.indexOf(compositeContentType.name); - var groupIndex = contentType.groups.indexOf(contentTypeGroup); - - - if( idIndex !== -1 ) { - - var properties = []; - - // remove all properties from composite content type - angular.forEach(contentTypeGroup.properties, function(property){ - if(property.contentTypeId !== compositeContentType.id) { - properties.push(property); - } - }); - - // set new properties array to properties - contentTypeGroup.properties = properties; - - // remove composite content type name and id from inherited arrays - contentTypeGroup.parentTabContentTypes.splice(idIndex, 1); - contentTypeGroup.parentTabContentTypeNames.splice(nameIndex, 1); - - // remove inherited state if there are no inherited properties - if(contentTypeGroup.parentTabContentTypes.length === 0) { - contentTypeGroup.inherited = false; - } - - // remove group if there are no properties left - if(contentTypeGroup.properties.length > 1) { - //contentType.groups.splice(groupIndex, 1); - groups.push(contentTypeGroup); - } - - } else { - groups.push(contentTypeGroup); - } - - } else { - groups.push(contentTypeGroup); - } - - // update sort order on properties - contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties); - - }); - - contentType.groups = groups; - - }, - - updatePropertiesSortOrder: function (properties) { - - var sortOrder = 0; - - angular.forEach(properties, function(property) { - if( !property.inherited && property.propertyState !== "init") { - property.sortOrder = sortOrder; - } - sortOrder++; - }); - - return properties; - - }, - - getTemplatePlaceholder: function() { - - var templatePlaceholder = { - "name": "", - "icon": "icon-layout", - "alias": "templatePlaceholder", - "placeholder": true - }; - - return templatePlaceholder; - - }, - - insertDefaultTemplatePlaceholder: function(defaultTemplate) { - - // get template placeholder - var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder(); - - // add as default template - defaultTemplate = templatePlaceholder; - - return defaultTemplate; - - }, - - insertTemplatePlaceholder: function(array) { - - // get template placeholder - var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder(); - - // add as selected item - array.push(templatePlaceholder); - - return array; - - }, - - insertChildNodePlaceholder: function (array, name, icon, id) { - - var placeholder = { - "name": name, - "icon": icon, - "id": id - }; - - array.push(placeholder); - - } - - }; - - return contentTypeHelperService; -} -angular.module('umbraco.services').factory('contentTypeHelper', contentTypeHelper); +/** + * @ngdoc service + * @name umbraco.services.contentTypeHelper + * @description A helper service for the content type editor + **/ +function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $injector, $q) { -/** -* @ngdoc service -* @name umbraco.services.cropperHelper -* @description A helper object used for dealing with image cropper data -**/ -function cropperHelper(umbRequestHelper, $http) { - var service = { - - /** - * @ngdoc method - * @name umbraco.services.cropperHelper#configuration - * @methodOf umbraco.services.cropperHelper - * - * @description - * Returns a collection of plugins available to the tinyMCE editor - * - */ - configuration: function (mediaTypeAlias) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "imageCropperApiBaseUrl", - "GetConfiguration", - [{ mediaTypeAlias: mediaTypeAlias}])), - 'Failed to retrieve tinymce configuration'); - }, - - - //utill for getting either min/max aspect ratio to scale image after - calculateAspectRatioFit : function(srcWidth, srcHeight, maxWidth, maxHeight, maximize) { - var ratio = [maxWidth / srcWidth, maxHeight / srcHeight ]; - - if(maximize){ - ratio = Math.max(ratio[0], ratio[1]); - }else{ - ratio = Math.min(ratio[0], ratio[1]); - } - - return { width:srcWidth*ratio, height:srcHeight*ratio, ratio: ratio}; - }, - - //utill for scaling width / height given a ratio - calculateSizeToRatio : function(srcWidth, srcHeight, ratio) { - return { width:srcWidth*ratio, height:srcHeight*ratio, ratio: ratio}; - }, - - scaleToMaxSize : function(srcWidth, srcHeight, maxSize) { - - var retVal = {height: srcHeight, width: srcWidth}; - - if(srcWidth > maxSize ||srcHeight > maxSize){ - var ratio = [maxSize / srcWidth, maxSize / srcHeight ]; - ratio = Math.min(ratio[0], ratio[1]); - - retVal.height = srcHeight * ratio; - retVal.width = srcWidth * ratio; - } - - return retVal; - }, - - //returns a ng-style object with top,left,width,height pixel measurements - //expects {left,right,top,bottom} - {width,height}, {width,height}, int - //offset is just to push the image position a number of pixels from top,left - convertToStyle : function(coordinates, originalSize, viewPort, offset){ - - var coordinates_px = service.coordinatesToPixels(coordinates, originalSize, offset); - var _offset = offset || 0; - - var x = 1 - (coordinates.x1 + Math.abs(coordinates.x2)); - var left_of_x = originalSize.width * x; - var ratio = viewPort.width / left_of_x; - - var style = { - position: "absolute", - top: -(coordinates_px.y1*ratio)+ _offset, - left: -(coordinates_px.x1* ratio)+ _offset, - width: Math.floor(originalSize.width * ratio), - height: Math.floor(originalSize.height * ratio), - originalWidth: originalSize.width, - originalHeight: originalSize.height, - ratio: ratio - }; - - return style; - }, - - - coordinatesToPixels : function(coordinates, originalSize, offset){ - - var coordinates_px = { - x1: Math.floor(coordinates.x1 * originalSize.width), - y1: Math.floor(coordinates.y1 * originalSize.height), - x2: Math.floor(coordinates.x2 * originalSize.width), - y2: Math.floor(coordinates.y2 * originalSize.height) - }; - - return coordinates_px; - }, - - pixelsToCoordinates : function(image, width, height, offset){ - - var x1_px = Math.abs(image.left-offset); - var y1_px = Math.abs(image.top-offset); - - var x2_px = image.width - (x1_px + width); - var y2_px = image.height - (y1_px + height); - - - //crop coordinates in % - var crop = {}; - crop.x1 = x1_px / image.width; - crop.y1 = y1_px / image.height; - crop.x2 = x2_px / image.width; - crop.y2 = y2_px / image.height; - - for(var coord in crop){ - if(crop[coord] < 0){ - crop[coord] = 0; - } - } - - return crop; - }, - - centerInsideViewPort : function(img, viewport){ - var left = viewport.width/ 2 - img.width / 2, - top = viewport.height / 2 - img.height / 2; - - return {left: left, top: top}; - }, - - alignToCoordinates : function(image, center, viewport){ - - var min_left = (image.width) - (viewport.width); - var min_top = (image.height) - (viewport.height); - - var c_top = -(center.top * image.height) + (viewport.height / 2); - var c_left = -(center.left * image.width) + (viewport.width / 2); - - if(c_top < -min_top){ - c_top = -min_top; - } - if(c_top > 0){ - c_top = 0; - } - if(c_left < -min_left){ - c_left = -min_left; - } - if(c_left > 0){ - c_left = 0; - } - return {left: c_left, top: c_top}; - }, - - - syncElements : function(source, target){ - target.height(source.height()); - target.width(source.width()); - - target.css({ - "top": source[0].offsetTop, - "left": source[0].offsetLeft - }); - } - }; - - return service; -} - -angular.module('umbraco.services').factory('cropperHelper', cropperHelper); + var contentTypeHelperService = { -/** - * @ngdoc service - * @name umbraco.services.dataTypeHelper - * @description A helper service for data types - **/ -function dataTypeHelper() { - - var dataTypeHelperService = { - - createPreValueProps: function(preVals) { - - var preValues = []; - - for (var i = 0; i < preVals.length; i++) { - preValues.push({ - hideLabel: preVals[i].hideLabel, - alias: preVals[i].key, - description: preVals[i].description, - label: preVals[i].label, - view: preVals[i].view, - value: preVals[i].value - }); - } - - return preValues; - - }, - - rebindChangedProperties: function (origContent, savedContent) { - - //a method to ignore built-in prop changes - var shouldIgnore = function (propName) { - return _.some(["notifications", "ModelState"], function (i) { - return i === propName; - }); - }; - //check for changed built-in properties of the content - for (var o in origContent) { - - //ignore the ones listed in the array - if (shouldIgnore(o)) { - continue; - } - - if (!_.isEqual(origContent[o], savedContent[o])) { - origContent[o] = savedContent[o]; - } - } - } - - }; - - return dataTypeHelperService; -} + createIdArray: function(array) { + + var newArray = []; + + angular.forEach(array, function(arrayItem){ + + if(angular.isObject(arrayItem)) { + newArray.push(arrayItem.id); + } else { + newArray.push(arrayItem); + } + + }); + + return newArray; + + }, + + generateModels: function () { + var deferred = $q.defer(); + var modelsResource = $injector.has("modelsBuilderResource") ? $injector.get("modelsBuilderResource") : null; + var modelsBuilderEnabled = Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled; + if (modelsBuilderEnabled && modelsResource) { + modelsResource.buildModels().then(function(result) { + deferred.resolve(result); + + //just calling this to get the servar back to life + modelsResource.getModelsOutOfDateStatus(); + + }, function(e) { + deferred.reject(e); + }); + } + else { + deferred.resolve(false); + } + return deferred.promise; + }, + + checkModelsBuilderStatus: function () { + var deferred = $q.defer(); + var modelsResource = $injector.has("modelsBuilderResource") ? $injector.get("modelsBuilderResource") : null; + var modelsBuilderEnabled = (Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables && Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled === true); + + if (modelsBuilderEnabled && modelsResource) { + modelsResource.getModelsOutOfDateStatus().then(function(result) { + //Generate models buttons should be enabled if it is 0 + deferred.resolve(result.status === 0); + }); + } + else { + deferred.resolve(false); + } + return deferred.promise; + }, + + makeObjectArrayFromId: function (idArray, objectArray) { + var newArray = []; + + for (var idIndex = 0; idArray.length > idIndex; idIndex++) { + var id = idArray[idIndex]; + + for (var objectIndex = 0; objectArray.length > objectIndex; objectIndex++) { + var object = objectArray[objectIndex]; + if (id === object.id) { + newArray.push(object); + } + } + + } + + return newArray; + }, + + validateAddingComposition: function(contentType, compositeContentType) { + + //Validate that by adding this group that we are not adding duplicate property type aliases + + var propertiesAdding = _.flatten(_.map(compositeContentType.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })); + var propAliasesExisting = _.filter(_.flatten(_.map(contentType.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })), function(f) { + return f !== null && f !== undefined; + }); + + var intersec = _.intersection(propertiesAdding, propAliasesExisting); + if (intersec.length > 0) { + //return the overlapping property aliases + return intersec; + } + + //no overlapping property aliases + return []; + }, + + mergeCompositeContentType: function(contentType, compositeContentType) { + + //Validate that there are no overlapping aliases + var overlappingAliases = this.validateAddingComposition(contentType, compositeContentType); + if (overlappingAliases.length > 0) { + throw new Error("Cannot add this composition, these properties already exist on the content type: " + overlappingAliases.join()); + } + + angular.forEach(compositeContentType.groups, function(compositionGroup) { + + // order composition groups based on sort order + compositionGroup.properties = $filter('orderBy')(compositionGroup.properties, 'sortOrder'); + + // get data type details + angular.forEach(compositionGroup.properties, function(property) { + dataTypeResource.getById(property.dataTypeId) + .then(function(dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; + }); + }); + + // set inherited state on tab + compositionGroup.inherited = true; + + // set inherited state on properties + angular.forEach(compositionGroup.properties, function(compositionProperty) { + compositionProperty.inherited = true; + }); + + // set tab state + compositionGroup.tabState = "inActive"; + + // if groups are named the same - merge the groups + angular.forEach(contentType.groups, function(contentTypeGroup) { + + if (contentTypeGroup.name === compositionGroup.name) { + + // set flag to show if properties has been merged into a tab + compositionGroup.groupIsMerged = true; + + // make group inherited + contentTypeGroup.inherited = true; + + // add properties to the top of the array + contentTypeGroup.properties = compositionGroup.properties.concat(contentTypeGroup.properties); + + // update sort order on all properties in merged group + contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties); + + // make parentTabContentTypeNames to an array so we can push values + if (contentTypeGroup.parentTabContentTypeNames === null || contentTypeGroup.parentTabContentTypeNames === undefined) { + contentTypeGroup.parentTabContentTypeNames = []; + } + + // push name to array of merged composite content types + contentTypeGroup.parentTabContentTypeNames.push(compositeContentType.name); + + // make parentTabContentTypes to an array so we can push values + if (contentTypeGroup.parentTabContentTypes === null || contentTypeGroup.parentTabContentTypes === undefined) { + contentTypeGroup.parentTabContentTypes = []; + } + + // push id to array of merged composite content types + contentTypeGroup.parentTabContentTypes.push(compositeContentType.id); + + // get sort order from composition + contentTypeGroup.sortOrder = compositionGroup.sortOrder; + + // splice group to the top of the array + var contentTypeGroupCopy = angular.copy(contentTypeGroup); + var index = contentType.groups.indexOf(contentTypeGroup); + contentType.groups.splice(index, 1); + contentType.groups.unshift(contentTypeGroupCopy); + + } + + }); + + // if group is not merged - push it to the end of the array - before init tab + if (compositionGroup.groupIsMerged === false || compositionGroup.groupIsMerged === undefined) { + + // make parentTabContentTypeNames to an array so we can push values + if (compositionGroup.parentTabContentTypeNames === null || compositionGroup.parentTabContentTypeNames === undefined) { + compositionGroup.parentTabContentTypeNames = []; + } + + // push name to array of merged composite content types + compositionGroup.parentTabContentTypeNames.push(compositeContentType.name); + + // make parentTabContentTypes to an array so we can push values + if (compositionGroup.parentTabContentTypes === null || compositionGroup.parentTabContentTypes === undefined) { + compositionGroup.parentTabContentTypes = []; + } + + // push id to array of merged composite content types + compositionGroup.parentTabContentTypes.push(compositeContentType.id); + + // push group before placeholder tab + contentType.groups.unshift(compositionGroup); + + } + + }); + + // sort all groups by sortOrder property + contentType.groups = $filter('orderBy')(contentType.groups, 'sortOrder'); + + return contentType; + + }, + + splitCompositeContentType: function (contentType, compositeContentType) { + + var groups = []; + + angular.forEach(contentType.groups, function(contentTypeGroup){ + + if( contentTypeGroup.tabState !== "init" ) { + + var idIndex = contentTypeGroup.parentTabContentTypes.indexOf(compositeContentType.id); + var nameIndex = contentTypeGroup.parentTabContentTypeNames.indexOf(compositeContentType.name); + var groupIndex = contentType.groups.indexOf(contentTypeGroup); + + + if( idIndex !== -1 ) { + + var properties = []; + + // remove all properties from composite content type + angular.forEach(contentTypeGroup.properties, function(property){ + if(property.contentTypeId !== compositeContentType.id) { + properties.push(property); + } + }); + + // set new properties array to properties + contentTypeGroup.properties = properties; + + // remove composite content type name and id from inherited arrays + contentTypeGroup.parentTabContentTypes.splice(idIndex, 1); + contentTypeGroup.parentTabContentTypeNames.splice(nameIndex, 1); + + // remove inherited state if there are no inherited properties + if(contentTypeGroup.parentTabContentTypes.length === 0) { + contentTypeGroup.inherited = false; + } + + // remove group if there are no properties left + if(contentTypeGroup.properties.length > 1) { + //contentType.groups.splice(groupIndex, 1); + groups.push(contentTypeGroup); + } + + } else { + groups.push(contentTypeGroup); + } + + } else { + groups.push(contentTypeGroup); + } + + // update sort order on properties + contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties); + + }); + + contentType.groups = groups; + + }, + + updatePropertiesSortOrder: function (properties) { + + var sortOrder = 0; + + angular.forEach(properties, function(property) { + if( !property.inherited && property.propertyState !== "init") { + property.sortOrder = sortOrder; + } + sortOrder++; + }); + + return properties; + + }, + + getTemplatePlaceholder: function() { + + var templatePlaceholder = { + "name": "", + "icon": "icon-layout", + "alias": "templatePlaceholder", + "placeholder": true + }; + + return templatePlaceholder; + + }, + + insertDefaultTemplatePlaceholder: function(defaultTemplate) { + + // get template placeholder + var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder(); + + // add as default template + defaultTemplate = templatePlaceholder; + + return defaultTemplate; + + }, + + insertTemplatePlaceholder: function(array) { + + // get template placeholder + var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder(); + + // add as selected item + array.push(templatePlaceholder); + + return array; + + }, + + insertChildNodePlaceholder: function (array, name, icon, id) { + + var placeholder = { + "name": name, + "icon": icon, + "id": id + }; + + array.push(placeholder); + + } + + }; + + return contentTypeHelperService; +} +angular.module('umbraco.services').factory('contentTypeHelper', contentTypeHelper); + +/** +* @ngdoc service +* @name umbraco.services.cropperHelper +* @description A helper object used for dealing with image cropper data +**/ +function cropperHelper(umbRequestHelper, $http) { + var service = { + + /** + * @ngdoc method + * @name umbraco.services.cropperHelper#configuration + * @methodOf umbraco.services.cropperHelper + * + * @description + * Returns a collection of plugins available to the tinyMCE editor + * + */ + configuration: function (mediaTypeAlias) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "imageCropperApiBaseUrl", + "GetConfiguration", + [{ mediaTypeAlias: mediaTypeAlias}])), + 'Failed to retrieve tinymce configuration'); + }, + + + //utill for getting either min/max aspect ratio to scale image after + calculateAspectRatioFit : function(srcWidth, srcHeight, maxWidth, maxHeight, maximize) { + var ratio = [maxWidth / srcWidth, maxHeight / srcHeight ]; + + if(maximize){ + ratio = Math.max(ratio[0], ratio[1]); + }else{ + ratio = Math.min(ratio[0], ratio[1]); + } + + return { width:srcWidth*ratio, height:srcHeight*ratio, ratio: ratio}; + }, + + //utill for scaling width / height given a ratio + calculateSizeToRatio : function(srcWidth, srcHeight, ratio) { + return { width:srcWidth*ratio, height:srcHeight*ratio, ratio: ratio}; + }, + + scaleToMaxSize : function(srcWidth, srcHeight, maxSize) { + + var retVal = {height: srcHeight, width: srcWidth}; + + if(srcWidth > maxSize ||srcHeight > maxSize){ + var ratio = [maxSize / srcWidth, maxSize / srcHeight ]; + ratio = Math.min(ratio[0], ratio[1]); + + retVal.height = srcHeight * ratio; + retVal.width = srcWidth * ratio; + } + + return retVal; + }, + + //returns a ng-style object with top,left,width,height pixel measurements + //expects {left,right,top,bottom} - {width,height}, {width,height}, int + //offset is just to push the image position a number of pixels from top,left + convertToStyle : function(coordinates, originalSize, viewPort, offset){ + + var coordinates_px = service.coordinatesToPixels(coordinates, originalSize, offset); + var _offset = offset || 0; + + var x = 1 - (coordinates.x1 + Math.abs(coordinates.x2)); + var left_of_x = originalSize.width * x; + var ratio = viewPort.width / left_of_x; + + var style = { + position: "absolute", + top: -(coordinates_px.y1*ratio)+ _offset, + left: -(coordinates_px.x1* ratio)+ _offset, + width: Math.floor(originalSize.width * ratio), + height: Math.floor(originalSize.height * ratio), + originalWidth: originalSize.width, + originalHeight: originalSize.height, + ratio: ratio + }; + + return style; + }, + + + coordinatesToPixels : function(coordinates, originalSize, offset){ + + var coordinates_px = { + x1: Math.floor(coordinates.x1 * originalSize.width), + y1: Math.floor(coordinates.y1 * originalSize.height), + x2: Math.floor(coordinates.x2 * originalSize.width), + y2: Math.floor(coordinates.y2 * originalSize.height) + }; + + return coordinates_px; + }, + + pixelsToCoordinates : function(image, width, height, offset){ + + var x1_px = Math.abs(image.left-offset); + var y1_px = Math.abs(image.top-offset); + + var x2_px = image.width - (x1_px + width); + var y2_px = image.height - (y1_px + height); + + + //crop coordinates in % + var crop = {}; + crop.x1 = x1_px / image.width; + crop.y1 = y1_px / image.height; + crop.x2 = x2_px / image.width; + crop.y2 = y2_px / image.height; + + for(var coord in crop){ + if(crop[coord] < 0){ + crop[coord] = 0; + } + } + + return crop; + }, + + centerInsideViewPort : function(img, viewport){ + var left = viewport.width/ 2 - img.width / 2, + top = viewport.height / 2 - img.height / 2; + + return {left: left, top: top}; + }, + + alignToCoordinates : function(image, center, viewport){ + + var min_left = (image.width) - (viewport.width); + var min_top = (image.height) - (viewport.height); + + var c_top = -(center.top * image.height) + (viewport.height / 2); + var c_left = -(center.left * image.width) + (viewport.width / 2); + + if(c_top < -min_top){ + c_top = -min_top; + } + if(c_top > 0){ + c_top = 0; + } + if(c_left < -min_left){ + c_left = -min_left; + } + if(c_left > 0){ + c_left = 0; + } + return {left: c_left, top: c_top}; + }, + + + syncElements : function(source, target){ + target.height(source.height()); + target.width(source.width()); + + target.css({ + "top": source[0].offsetTop, + "left": source[0].offsetLeft + }); + } + }; + + return service; +} + +angular.module('umbraco.services').factory('cropperHelper', cropperHelper); + +/** + * @ngdoc service + * @name umbraco.services.dataTypeHelper + * @description A helper service for data types + **/ +function dataTypeHelper() { + + var dataTypeHelperService = { + + createPreValueProps: function(preVals) { + + var preValues = []; + + for (var i = 0; i < preVals.length; i++) { + preValues.push({ + hideLabel: preVals[i].hideLabel, + alias: preVals[i].key, + description: preVals[i].description, + label: preVals[i].label, + view: preVals[i].view, + value: preVals[i].value + }); + } + + return preValues; + + }, + + rebindChangedProperties: function (origContent, savedContent) { + + //a method to ignore built-in prop changes + var shouldIgnore = function (propName) { + return _.some(["notifications", "ModelState"], function (i) { + return i === propName; + }); + }; + //check for changed built-in properties of the content + for (var o in origContent) { + + //ignore the ones listed in the array + if (shouldIgnore(o)) { + continue; + } + + if (!_.isEqual(origContent[o], savedContent[o])) { + origContent[o] = savedContent[o]; + } + } + } + + }; + + return dataTypeHelperService; +} angular.module('umbraco.services').factory('dataTypeHelper', dataTypeHelper); /** * @ngdoc service @@ -2734,89 +2734,89 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati }; } angular.module('umbraco.services').factory('formHelper', formHelper); -angular.module('umbraco.services') - .factory('gridService', function ($http, $q){ - - var configPath = Umbraco.Sys.ServerVariables.umbracoUrls.gridConfig; - var service = { - getGridEditors: function () { - return $http.get(configPath); - } - }; - - return service; - - }); +angular.module('umbraco.services') + .factory('gridService', function ($http, $q){ + + var configPath = Umbraco.Sys.ServerVariables.umbracoUrls.gridConfig; + var service = { + getGridEditors: function () { + return $http.get(configPath); + } + }; + + return service; + + }); + +angular.module('umbraco.services') + .factory('helpService', function ($http, $q){ + var helpTopics = {}; + + var defaultUrl = "http://our.umbraco.org/rss/help"; + var tvUrl = "http://umbraco.tv/feeds/help"; + + function getCachedHelp(url){ + if(helpTopics[url]){ + return helpTopics[cacheKey]; + }else{ + return null; + } + } + + function setCachedHelp(url, data){ + helpTopics[url] = data; + } + + function fetchUrl(url){ + var deferred = $q.defer(); + var found = getCachedHelp(url); + + if(found){ + deferred.resolve(found); + }else{ + + var proxyUrl = "dashboard/feedproxy.aspx?url=" + url; + $http.get(proxyUrl).then(function(data){ + var feed = $(data.data); + var topics = []; + + $('item', feed).each(function (i, item) { + var topic = {}; + topic.thumbnail = $(item).find('thumbnail').attr('url'); + topic.title = $("title", item).text(); + topic.link = $("guid", item).text(); + topic.description = $("description", item).text(); + topics.push(topic); + }); + + setCachedHelp(topics); + deferred.resolve(topics); + }); + } + + return deferred.promise; + } + + + + var service = { + findHelp: function (args) { + var url = service.getUrl(defaultUrl, args); + return fetchUrl(url); + }, + + findVideos: function (args) { + var url = service.getUrl(tvUrl, args); + return fetchUrl(url); + }, + + getUrl: function(url, args){ + return url + "?" + $.param(args); + } + }; + + return service; -angular.module('umbraco.services') - .factory('helpService', function ($http, $q){ - var helpTopics = {}; - - var defaultUrl = "http://our.umbraco.org/rss/help"; - var tvUrl = "http://umbraco.tv/feeds/help"; - - function getCachedHelp(url){ - if(helpTopics[url]){ - return helpTopics[cacheKey]; - }else{ - return null; - } - } - - function setCachedHelp(url, data){ - helpTopics[url] = data; - } - - function fetchUrl(url){ - var deferred = $q.defer(); - var found = getCachedHelp(url); - - if(found){ - deferred.resolve(found); - }else{ - - var proxyUrl = "dashboard/feedproxy.aspx?url=" + url; - $http.get(proxyUrl).then(function(data){ - var feed = $(data.data); - var topics = []; - - $('item', feed).each(function (i, item) { - var topic = {}; - topic.thumbnail = $(item).find('thumbnail').attr('url'); - topic.title = $("title", item).text(); - topic.link = $("guid", item).text(); - topic.description = $("description", item).text(); - topics.push(topic); - }); - - setCachedHelp(topics); - deferred.resolve(topics); - }); - } - - return deferred.promise; - } - - - - var service = { - findHelp: function (args) { - var url = service.getUrl(defaultUrl, args); - return fetchUrl(url); - }, - - findVideos: function (args) { - var url = service.getUrl(tvUrl, args); - return fetchUrl(url); - }, - - getUrl: function(url, args){ - return url + "?" + $.param(args); - } - }; - - return service; - }); /** * @ngdoc service @@ -2938,300 +2938,300 @@ angular.module('umbraco.services') } }; }); -/** -* @ngdoc service -* @name umbraco.services.iconHelper -* @description A helper service for dealing with icons, mostly dealing with legacy tree icons -**/ -function iconHelper($q, $timeout) { - - var converter = [ - { oldIcon: ".sprNew", newIcon: "add" }, - { oldIcon: ".sprDelete", newIcon: "remove" }, - { oldIcon: ".sprMove", newIcon: "enter" }, - { oldIcon: ".sprCopy", newIcon: "documents" }, - { oldIcon: ".sprSort", newIcon: "navigation-vertical" }, - { oldIcon: ".sprPublish", newIcon: "globe" }, - { oldIcon: ".sprRollback", newIcon: "undo" }, - { oldIcon: ".sprProtect", newIcon: "lock" }, - { oldIcon: ".sprAudit", newIcon: "time" }, - { oldIcon: ".sprNotify", newIcon: "envelope" }, - { oldIcon: ".sprDomain", newIcon: "home" }, - { oldIcon: ".sprPermission", newIcon: "lock" }, - { oldIcon: ".sprRefresh", newIcon: "refresh" }, - { oldIcon: ".sprBinEmpty", newIcon: "trash" }, - { oldIcon: ".sprExportDocumentType", newIcon: "download-alt" }, - { oldIcon: ".sprImportDocumentType", newIcon: "page-up" }, - { oldIcon: ".sprLiveEdit", newIcon: "edit" }, - { oldIcon: ".sprCreateFolder", newIcon: "add" }, - { oldIcon: ".sprPackage2", newIcon: "box" }, - { oldIcon: ".sprLogout", newIcon: "logout" }, - { oldIcon: ".sprSave", newIcon: "save" }, - { oldIcon: ".sprSendToTranslate", newIcon: "envelope-alt" }, - { oldIcon: ".sprToPublish", newIcon: "mail-forward" }, - { oldIcon: ".sprTranslate", newIcon: "comments" }, - { oldIcon: ".sprUpdate", newIcon: "save" }, - - { oldIcon: ".sprTreeSettingDomain", newIcon: "icon-home" }, - { oldIcon: ".sprTreeDoc", newIcon: "icon-document" }, - { oldIcon: ".sprTreeDoc2", newIcon: "icon-diploma-alt" }, - { oldIcon: ".sprTreeDoc3", newIcon: "icon-notepad" }, - { oldIcon: ".sprTreeDoc4", newIcon: "icon-newspaper-alt" }, - { oldIcon: ".sprTreeDoc5", newIcon: "icon-notepad-alt" }, - - { oldIcon: ".sprTreeDocPic", newIcon: "icon-picture" }, - { oldIcon: ".sprTreeFolder", newIcon: "icon-folder" }, - { oldIcon: ".sprTreeFolder_o", newIcon: "icon-folder" }, - { oldIcon: ".sprTreeMediaFile", newIcon: "icon-music" }, - { oldIcon: ".sprTreeMediaMovie", newIcon: "icon-movie" }, - { oldIcon: ".sprTreeMediaPhoto", newIcon: "icon-picture" }, - - { oldIcon: ".sprTreeMember", newIcon: "icon-user" }, - { oldIcon: ".sprTreeMemberGroup", newIcon: "icon-users" }, - { oldIcon: ".sprTreeMemberType", newIcon: "icon-users" }, - - { oldIcon: ".sprTreeNewsletter", newIcon: "icon-file-text-alt" }, - { oldIcon: ".sprTreePackage", newIcon: "icon-box" }, - { oldIcon: ".sprTreeRepository", newIcon: "icon-server-alt" }, - - { oldIcon: ".sprTreeSettingDataType", newIcon: "icon-autofill" }, - - //TODO: - /* - { oldIcon: ".sprTreeSettingAgent", newIcon: "" }, - { oldIcon: ".sprTreeSettingCss", newIcon: "" }, - { oldIcon: ".sprTreeSettingCssItem", newIcon: "" }, - - { oldIcon: ".sprTreeSettingDataTypeChild", newIcon: "" }, - { oldIcon: ".sprTreeSettingDomain", newIcon: "" }, - { oldIcon: ".sprTreeSettingLanguage", newIcon: "" }, - { oldIcon: ".sprTreeSettingScript", newIcon: "" }, - { oldIcon: ".sprTreeSettingTemplate", newIcon: "" }, - { oldIcon: ".sprTreeSettingXml", newIcon: "" }, - { oldIcon: ".sprTreeStatistik", newIcon: "" }, - { oldIcon: ".sprTreeUser", newIcon: "" }, - { oldIcon: ".sprTreeUserGroup", newIcon: "" }, - { oldIcon: ".sprTreeUserType", newIcon: "" }, - */ - - { oldIcon: "folder.png", newIcon: "icon-folder" }, - { oldIcon: "mediaphoto.gif", newIcon: "icon-picture" }, - { oldIcon: "mediafile.gif", newIcon: "icon-document" }, - - { oldIcon: ".sprTreeDeveloperCacheItem", newIcon: "icon-box" }, - { oldIcon: ".sprTreeDeveloperCacheTypes", newIcon: "icon-box" }, - { oldIcon: ".sprTreeDeveloperMacro", newIcon: "icon-cogs" }, - { oldIcon: ".sprTreeDeveloperRegistry", newIcon: "icon-windows" }, - { oldIcon: ".sprTreeDeveloperPython", newIcon: "icon-linux" } - ]; - - var imageConverter = [ - {oldImage: "contour.png", newIcon: "icon-umb-contour"} - ]; - - var collectedIcons; - - return { - - /** Used by the create dialogs for content/media types to format the data so that the thumbnails are styled properly */ - formatContentTypeThumbnails: function (contentTypes) { - for (var i = 0; i < contentTypes.length; i++) { - - if (contentTypes[i].thumbnailIsClass === undefined || contentTypes[i].thumbnailIsClass) { - contentTypes[i].cssClass = this.convertFromLegacyIcon(contentTypes[i].thumbnail); - }else { - contentTypes[i].style = "background-image: url('" + contentTypes[i].thumbnailFilePath + "');height:36px; background-position:4px 0px; background-repeat: no-repeat;background-size: 35px 35px;"; - //we need an 'icon-' class in there for certain styles to work so if it is image based we'll add this - contentTypes[i].cssClass = "custom-file"; - } - } - return contentTypes; - }, - formatContentTypeIcons: function (contentTypes) { - for (var i = 0; i < contentTypes.length; i++) { - if (!contentTypes[i].icon) { - //just to be safe (e.g. when focus was on close link and hitting save) - contentTypes[i].icon = "icon-document"; // default icon - } else { - contentTypes[i].icon = this.convertFromLegacyIcon(contentTypes[i].icon); - } - - //couldnt find replacement - if(contentTypes[i].icon.indexOf(".") > 0){ - contentTypes[i].icon = "icon-document-dashed-line"; - } - } - return contentTypes; - }, - /** If the icon is file based (i.e. it has a file path) */ - isFileBasedIcon: function (icon) { - //if it doesn't start with a '.' but contains one then we'll assume it's file based - if (icon.startsWith('..') || (!icon.startsWith('.') && icon.indexOf('.') > 1)) { - return true; - } - return false; - }, - /** If the icon is legacy */ - isLegacyIcon: function (icon) { - if(!icon) { - return false; - } - - if(icon.startsWith('..')){ - return false; - } - - if (icon.startsWith('.')) { - return true; - } - return false; - }, - /** If the tree node has a legacy icon */ - isLegacyTreeNodeIcon: function(treeNode){ - if (treeNode.iconIsClass) { - return this.isLegacyIcon(treeNode.icon); - } - return false; - }, - - /** Return a list of icons, optionally filter them */ - /** It fetches them directly from the active stylesheets in the browser */ - getIcons: function(){ - var deferred = $q.defer(); - $timeout(function(){ - if(collectedIcons){ - deferred.resolve(collectedIcons); - }else{ - collectedIcons = []; - var c = ".icon-"; - - for (var i = document.styleSheets.length - 1; i >= 0; i--) { - var classes = document.styleSheets[i].rules || document.styleSheets[i].cssRules; - - if (classes !== null) { - for(var x=0;x0){ - s = s.substring(0, hasSpace); - } - var hasPseudo = s.indexOf(":"); - if(hasPseudo>0){ - s = s.substring(0, hasPseudo); - } - - if(collectedIcons.indexOf(s) < 0){ - collectedIcons.push(s); - } - } - } - } - } - deferred.resolve(collectedIcons); - } - }, 100); - - return deferred.promise; - }, - - /** Converts the icon from legacy to a new one if an old one is detected */ - convertFromLegacyIcon: function (icon) { - if (this.isLegacyIcon(icon)) { - //its legacy so convert it if we can - var found = _.find(converter, function (item) { - return item.oldIcon.toLowerCase() === icon.toLowerCase(); - }); - return (found ? found.newIcon : icon); - } - return icon; - }, - - convertFromLegacyImage: function (icon) { - var found = _.find(imageConverter, function (item) { - return item.oldImage.toLowerCase() === icon.toLowerCase(); - }); - return (found ? found.newIcon : undefined); - }, - - /** If we detect that the tree node has legacy icons that can be converted, this will convert them */ - convertFromLegacyTreeNodeIcon: function (treeNode) { - if (this.isLegacyTreeNodeIcon(treeNode)) { - return this.convertFromLegacyIcon(treeNode.icon); - } - return treeNode.icon; - } - }; -} +/** +* @ngdoc service +* @name umbraco.services.iconHelper +* @description A helper service for dealing with icons, mostly dealing with legacy tree icons +**/ +function iconHelper($q, $timeout) { + + var converter = [ + { oldIcon: ".sprNew", newIcon: "add" }, + { oldIcon: ".sprDelete", newIcon: "remove" }, + { oldIcon: ".sprMove", newIcon: "enter" }, + { oldIcon: ".sprCopy", newIcon: "documents" }, + { oldIcon: ".sprSort", newIcon: "navigation-vertical" }, + { oldIcon: ".sprPublish", newIcon: "globe" }, + { oldIcon: ".sprRollback", newIcon: "undo" }, + { oldIcon: ".sprProtect", newIcon: "lock" }, + { oldIcon: ".sprAudit", newIcon: "time" }, + { oldIcon: ".sprNotify", newIcon: "envelope" }, + { oldIcon: ".sprDomain", newIcon: "home" }, + { oldIcon: ".sprPermission", newIcon: "lock" }, + { oldIcon: ".sprRefresh", newIcon: "refresh" }, + { oldIcon: ".sprBinEmpty", newIcon: "trash" }, + { oldIcon: ".sprExportDocumentType", newIcon: "download-alt" }, + { oldIcon: ".sprImportDocumentType", newIcon: "page-up" }, + { oldIcon: ".sprLiveEdit", newIcon: "edit" }, + { oldIcon: ".sprCreateFolder", newIcon: "add" }, + { oldIcon: ".sprPackage2", newIcon: "box" }, + { oldIcon: ".sprLogout", newIcon: "logout" }, + { oldIcon: ".sprSave", newIcon: "save" }, + { oldIcon: ".sprSendToTranslate", newIcon: "envelope-alt" }, + { oldIcon: ".sprToPublish", newIcon: "mail-forward" }, + { oldIcon: ".sprTranslate", newIcon: "comments" }, + { oldIcon: ".sprUpdate", newIcon: "save" }, + + { oldIcon: ".sprTreeSettingDomain", newIcon: "icon-home" }, + { oldIcon: ".sprTreeDoc", newIcon: "icon-document" }, + { oldIcon: ".sprTreeDoc2", newIcon: "icon-diploma-alt" }, + { oldIcon: ".sprTreeDoc3", newIcon: "icon-notepad" }, + { oldIcon: ".sprTreeDoc4", newIcon: "icon-newspaper-alt" }, + { oldIcon: ".sprTreeDoc5", newIcon: "icon-notepad-alt" }, + + { oldIcon: ".sprTreeDocPic", newIcon: "icon-picture" }, + { oldIcon: ".sprTreeFolder", newIcon: "icon-folder" }, + { oldIcon: ".sprTreeFolder_o", newIcon: "icon-folder" }, + { oldIcon: ".sprTreeMediaFile", newIcon: "icon-music" }, + { oldIcon: ".sprTreeMediaMovie", newIcon: "icon-movie" }, + { oldIcon: ".sprTreeMediaPhoto", newIcon: "icon-picture" }, + + { oldIcon: ".sprTreeMember", newIcon: "icon-user" }, + { oldIcon: ".sprTreeMemberGroup", newIcon: "icon-users" }, + { oldIcon: ".sprTreeMemberType", newIcon: "icon-users" }, + + { oldIcon: ".sprTreeNewsletter", newIcon: "icon-file-text-alt" }, + { oldIcon: ".sprTreePackage", newIcon: "icon-box" }, + { oldIcon: ".sprTreeRepository", newIcon: "icon-server-alt" }, + + { oldIcon: ".sprTreeSettingDataType", newIcon: "icon-autofill" }, + + //TODO: + /* + { oldIcon: ".sprTreeSettingAgent", newIcon: "" }, + { oldIcon: ".sprTreeSettingCss", newIcon: "" }, + { oldIcon: ".sprTreeSettingCssItem", newIcon: "" }, + + { oldIcon: ".sprTreeSettingDataTypeChild", newIcon: "" }, + { oldIcon: ".sprTreeSettingDomain", newIcon: "" }, + { oldIcon: ".sprTreeSettingLanguage", newIcon: "" }, + { oldIcon: ".sprTreeSettingScript", newIcon: "" }, + { oldIcon: ".sprTreeSettingTemplate", newIcon: "" }, + { oldIcon: ".sprTreeSettingXml", newIcon: "" }, + { oldIcon: ".sprTreeStatistik", newIcon: "" }, + { oldIcon: ".sprTreeUser", newIcon: "" }, + { oldIcon: ".sprTreeUserGroup", newIcon: "" }, + { oldIcon: ".sprTreeUserType", newIcon: "" }, + */ + + { oldIcon: "folder.png", newIcon: "icon-folder" }, + { oldIcon: "mediaphoto.gif", newIcon: "icon-picture" }, + { oldIcon: "mediafile.gif", newIcon: "icon-document" }, + + { oldIcon: ".sprTreeDeveloperCacheItem", newIcon: "icon-box" }, + { oldIcon: ".sprTreeDeveloperCacheTypes", newIcon: "icon-box" }, + { oldIcon: ".sprTreeDeveloperMacro", newIcon: "icon-cogs" }, + { oldIcon: ".sprTreeDeveloperRegistry", newIcon: "icon-windows" }, + { oldIcon: ".sprTreeDeveloperPython", newIcon: "icon-linux" } + ]; + + var imageConverter = [ + {oldImage: "contour.png", newIcon: "icon-umb-contour"} + ]; + + var collectedIcons; + + return { + + /** Used by the create dialogs for content/media types to format the data so that the thumbnails are styled properly */ + formatContentTypeThumbnails: function (contentTypes) { + for (var i = 0; i < contentTypes.length; i++) { + + if (contentTypes[i].thumbnailIsClass === undefined || contentTypes[i].thumbnailIsClass) { + contentTypes[i].cssClass = this.convertFromLegacyIcon(contentTypes[i].thumbnail); + }else { + contentTypes[i].style = "background-image: url('" + contentTypes[i].thumbnailFilePath + "');height:36px; background-position:4px 0px; background-repeat: no-repeat;background-size: 35px 35px;"; + //we need an 'icon-' class in there for certain styles to work so if it is image based we'll add this + contentTypes[i].cssClass = "custom-file"; + } + } + return contentTypes; + }, + formatContentTypeIcons: function (contentTypes) { + for (var i = 0; i < contentTypes.length; i++) { + if (!contentTypes[i].icon) { + //just to be safe (e.g. when focus was on close link and hitting save) + contentTypes[i].icon = "icon-document"; // default icon + } else { + contentTypes[i].icon = this.convertFromLegacyIcon(contentTypes[i].icon); + } + + //couldnt find replacement + if(contentTypes[i].icon.indexOf(".") > 0){ + contentTypes[i].icon = "icon-document-dashed-line"; + } + } + return contentTypes; + }, + /** If the icon is file based (i.e. it has a file path) */ + isFileBasedIcon: function (icon) { + //if it doesn't start with a '.' but contains one then we'll assume it's file based + if (icon.startsWith('..') || (!icon.startsWith('.') && icon.indexOf('.') > 1)) { + return true; + } + return false; + }, + /** If the icon is legacy */ + isLegacyIcon: function (icon) { + if(!icon) { + return false; + } + + if(icon.startsWith('..')){ + return false; + } + + if (icon.startsWith('.')) { + return true; + } + return false; + }, + /** If the tree node has a legacy icon */ + isLegacyTreeNodeIcon: function(treeNode){ + if (treeNode.iconIsClass) { + return this.isLegacyIcon(treeNode.icon); + } + return false; + }, + + /** Return a list of icons, optionally filter them */ + /** It fetches them directly from the active stylesheets in the browser */ + getIcons: function(){ + var deferred = $q.defer(); + $timeout(function(){ + if(collectedIcons){ + deferred.resolve(collectedIcons); + }else{ + collectedIcons = []; + var c = ".icon-"; + + for (var i = document.styleSheets.length - 1; i >= 0; i--) { + var classes = document.styleSheets[i].rules || document.styleSheets[i].cssRules; + + if (classes !== null) { + for(var x=0;x0){ + s = s.substring(0, hasSpace); + } + var hasPseudo = s.indexOf(":"); + if(hasPseudo>0){ + s = s.substring(0, hasPseudo); + } + + if(collectedIcons.indexOf(s) < 0){ + collectedIcons.push(s); + } + } + } + } + } + deferred.resolve(collectedIcons); + } + }, 100); + + return deferred.promise; + }, + + /** Converts the icon from legacy to a new one if an old one is detected */ + convertFromLegacyIcon: function (icon) { + if (this.isLegacyIcon(icon)) { + //its legacy so convert it if we can + var found = _.find(converter, function (item) { + return item.oldIcon.toLowerCase() === icon.toLowerCase(); + }); + return (found ? found.newIcon : icon); + } + return icon; + }, + + convertFromLegacyImage: function (icon) { + var found = _.find(imageConverter, function (item) { + return item.oldImage.toLowerCase() === icon.toLowerCase(); + }); + return (found ? found.newIcon : undefined); + }, + + /** If we detect that the tree node has legacy icons that can be converted, this will convert them */ + convertFromLegacyTreeNodeIcon: function (treeNode) { + if (this.isLegacyTreeNodeIcon(treeNode)) { + return this.convertFromLegacyIcon(treeNode.icon); + } + return treeNode.icon; + } + }; +} angular.module('umbraco.services').factory('iconHelper', iconHelper); -/** -* @ngdoc service -* @name umbraco.services.imageHelper -* @deprecated -**/ -function imageHelper(umbRequestHelper, mediaHelper) { - return { - /** - * @ngdoc function - * @name umbraco.services.imageHelper#getImagePropertyValue - * @methodOf umbraco.services.imageHelper - * @function - * - * @deprecated - */ - getImagePropertyValue: function (options) { - return mediaHelper.getImagePropertyValue(options); - }, - /** - * @ngdoc function - * @name umbraco.services.imageHelper#getThumbnail - * @methodOf umbraco.services.imageHelper - * @function - * - * @deprecated - */ - getThumbnail: function (options) { - return mediaHelper.getThumbnail(options); - }, - - /** - * @ngdoc function - * @name umbraco.services.imageHelper#scaleToMaxSize - * @methodOf umbraco.services.imageHelper - * @function - * - * @deprecated - */ - scaleToMaxSize: function (maxSize, width, height) { - return mediaHelper.scaleToMaxSize(maxSize, width, height); - }, - - /** - * @ngdoc function - * @name umbraco.services.imageHelper#getThumbnailFromPath - * @methodOf umbraco.services.imageHelper - * @function - * - * @deprecated - */ - getThumbnailFromPath: function (imagePath) { - return mediaHelper.getThumbnailFromPath(imagePath); - }, - - /** - * @ngdoc function - * @name umbraco.services.imageHelper#detectIfImageByExtension - * @methodOf umbraco.services.imageHelper - * @function - * - * @deprecated - */ - detectIfImageByExtension: function (imagePath) { - return mediaHelper.detectIfImageByExtension(imagePath); - } - }; -} +/** +* @ngdoc service +* @name umbraco.services.imageHelper +* @deprecated +**/ +function imageHelper(umbRequestHelper, mediaHelper) { + return { + /** + * @ngdoc function + * @name umbraco.services.imageHelper#getImagePropertyValue + * @methodOf umbraco.services.imageHelper + * @function + * + * @deprecated + */ + getImagePropertyValue: function (options) { + return mediaHelper.getImagePropertyValue(options); + }, + /** + * @ngdoc function + * @name umbraco.services.imageHelper#getThumbnail + * @methodOf umbraco.services.imageHelper + * @function + * + * @deprecated + */ + getThumbnail: function (options) { + return mediaHelper.getThumbnail(options); + }, + + /** + * @ngdoc function + * @name umbraco.services.imageHelper#scaleToMaxSize + * @methodOf umbraco.services.imageHelper + * @function + * + * @deprecated + */ + scaleToMaxSize: function (maxSize, width, height) { + return mediaHelper.scaleToMaxSize(maxSize, width, height); + }, + + /** + * @ngdoc function + * @name umbraco.services.imageHelper#getThumbnailFromPath + * @methodOf umbraco.services.imageHelper + * @function + * + * @deprecated + */ + getThumbnailFromPath: function (imagePath) { + return mediaHelper.getThumbnailFromPath(imagePath); + }, + + /** + * @ngdoc function + * @name umbraco.services.imageHelper#detectIfImageByExtension + * @methodOf umbraco.services.imageHelper + * @function + * + * @deprecated + */ + detectIfImageByExtension: function (imagePath) { + return mediaHelper.detectIfImageByExtension(imagePath); + } + }; +} angular.module('umbraco.services').factory('imageHelper', imageHelper); // This service was based on OpenJS library available in BSD License // http://www.openjs.com/scripts/events/keyboard_shortcuts/index.php @@ -3556,589 +3556,589 @@ function keyboardService($window, $timeout) { return keyboardManagerService; } angular.module('umbraco.services').factory('keyboardService', ['$window', '$timeout', keyboardService]); -/** - @ngdoc service - * @name umbraco.services.listViewHelper - * - * - * @description - * Service for performing operations against items in the list view UI. Used by the built-in internal listviews - * as well as custom listview. - * - * A custom listview is always used inside a wrapper listview, so there are a number of inherited values on its - * scope by default: - * - * **$scope.selection**: Array containing all items currently selected in the listview - * - * **$scope.items**: Array containing all items currently displayed in the listview - * - * **$scope.folders**: Array containing all folders in the current listview (only for media) - * - * **$scope.options**: configuration object containing information such as pagesize, permissions, order direction etc. - * - * **$scope.model.config.layouts**: array of available layouts to apply to the listview (grid, list or custom layout) - * - * ##Usage## - * To use, inject listViewHelper into custom listview controller, listviewhelper expects you - * to pass in the full collection of items in the listview in several of its methods - * this collection is inherited from the parent controller and is available on $scope.selection - * - *
- *      angular.module("umbraco").controller("my.listVieweditor". function($scope, listViewHelper){
- *
- *          //current items in the listview
- *          var items = $scope.items;
- *
- *          //current selection
- *          var selection = $scope.selection;
- *
- *          //deselect an item , $scope.selection is inherited, item is picked from inherited $scope.items
- *          listViewHelper.deselectItem(item, $scope.selection);
- *
- *          //test if all items are selected, $scope.items + $scope.selection are inherited
- *          listViewhelper.isSelectedAll($scope.items, $scope.selection);
- *      });
- * 
- */ -(function () { - 'use strict'; - - function listViewHelper(localStorageService) { - - var firstSelectedIndex = 0; - var localStorageKey = "umblistViewLayout"; - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#getLayout - * @methodOf umbraco.services.listViewHelper - * - * @description - * Method for internal use, based on the collection of layouts passed, the method selects either - * any previous layout from local storage, or picks the first allowed layout - * - * @param {Number} nodeId The id of the current node displayed in the content editor - * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts - */ - - function getLayout(nodeId, availableLayouts) { - - var storedLayouts = []; - - if (localStorageService.get(localStorageKey)) { - storedLayouts = localStorageService.get(localStorageKey); - } - - if (storedLayouts && storedLayouts.length > 0) { - for (var i = 0; storedLayouts.length > i; i++) { - var layout = storedLayouts[i]; - if (layout.nodeId === nodeId) { - return setLayout(nodeId, layout, availableLayouts); - } - } - - } - - return getFirstAllowedLayout(availableLayouts); - - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#setLayout - * @methodOf umbraco.services.listViewHelper - * - * @description - * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage - * - * @param {Number} nodeID Id of the current node displayed in the content editor - * @param {Object} selectedLayout Layout selected as the layout to set as the current layout - * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts - */ - - function setLayout(nodeId, selectedLayout, availableLayouts) { - - var activeLayout = {}; - var layoutFound = false; - - for (var i = 0; availableLayouts.length > i; i++) { - var layout = availableLayouts[i]; - if (layout.path === selectedLayout.path) { - activeLayout = layout; - layout.active = true; - layoutFound = true; - } else { - layout.active = false; - } - } - - if (!layoutFound) { - activeLayout = getFirstAllowedLayout(availableLayouts); - } - - saveLayoutInLocalStorage(nodeId, activeLayout); - - return activeLayout; - - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#saveLayoutInLocalStorage - * @methodOf umbraco.services.listViewHelper - * - * @description - * Stores a given layout as the current default selection in local storage - * - * @param {Number} nodeId Id of the current node displayed in the content editor - * @param {Object} selectedLayout Layout selected as the layout to set as the current layout - */ - - function saveLayoutInLocalStorage(nodeId, selectedLayout) { - var layoutFound = false; - var storedLayouts = []; - - if (localStorageService.get(localStorageKey)) { - storedLayouts = localStorageService.get(localStorageKey); - } - - if (storedLayouts.length > 0) { - for (var i = 0; storedLayouts.length > i; i++) { - var layout = storedLayouts[i]; - if (layout.nodeId === nodeId) { - layout.path = selectedLayout.path; - layoutFound = true; - } - } - } - - if (!layoutFound) { - var storageObject = { - "nodeId": nodeId, - "path": selectedLayout.path - }; - storedLayouts.push(storageObject); - } - - localStorageService.set(localStorageKey, storedLayouts); - - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#getFirstAllowedLayout - * @methodOf umbraco.services.listViewHelper - * - * @description - * Returns currently selected layout, or alternatively the first layout in the available layouts collection - * - * @param {Array} layouts Array of all allowed layouts, available from $scope.model.config.layouts - */ - - function getFirstAllowedLayout(layouts) { - - var firstAllowedLayout = {}; - - for (var i = 0; layouts.length > i; i++) { - var layout = layouts[i]; - if (layout.selected === true) { - firstAllowedLayout = layout; - break; - } - } - - return firstAllowedLayout; - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#selectHandler - * @methodOf umbraco.services.listViewHelper - * - * @description - * Helper method for working with item selection via a checkbox, internally it uses selectItem and deselectItem. - * Working with this method, requires its triggered via a checkbox which can then pass in its triggered $event - * When the checkbox is clicked, this method will toggle selection of the associated item so it matches the state of the checkbox - * - * @param {Object} selectedItem Item being selected or deselected by the checkbox - * @param {Number} selectedIndex Index of item being selected/deselected, usually passed as $index - * @param {Array} items All items in the current listview, available as $scope.items - * @param {Array} selection All selected items in the current listview, available as $scope.selection - * @param {Event} $event Event triggered by the checkbox being checked to select / deselect an item - */ - - function selectHandler(selectedItem, selectedIndex, items, selection, $event) { - - var start = 0; - var end = 0; - var item = null; - - if ($event.shiftKey === true) { - - if (selectedIndex > firstSelectedIndex) { - - start = firstSelectedIndex; - end = selectedIndex; - - for (; end >= start; start++) { - item = items[start]; - selectItem(item, selection); - } - - } else { - - start = firstSelectedIndex; - end = selectedIndex; - - for (; end <= start; start--) { - item = items[start]; - selectItem(item, selection); - } - - } - - } else { - - if (selectedItem.selected) { - deselectItem(selectedItem, selection); - } else { - selectItem(selectedItem, selection); - } - - firstSelectedIndex = selectedIndex; - - } - - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#selectItem - * @methodOf umbraco.services.listViewHelper - * - * @description - * Selects a given item to the listview selection array, requires you pass in the inherited $scope.selection collection - * - * @param {Object} item Item to select - * @param {Array} selection Listview selection, available as $scope.selection - */ - - function selectItem(item, selection) { - var isSelected = false; - for (var i = 0; selection.length > i; i++) { - var selectedItem = selection[i]; - if (item.id === selectedItem.id) { - isSelected = true; - } - } - if (!isSelected) { - selection.push({ id: item.id }); - item.selected = true; - } - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#deselectItem - * @methodOf umbraco.services.listViewHelper - * - * @description - * Deselects a given item from the listviews selection array, requires you pass in the inherited $scope.selection collection - * - * @param {Object} item Item to deselect - * @param {Array} selection Listview selection, available as $scope.selection - */ - - function deselectItem(item, selection) { - for (var i = 0; selection.length > i; i++) { - var selectedItem = selection[i]; - if (item.id === selectedItem.id) { - selection.splice(i, 1); - item.selected = false; - } - } - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#clearSelection - * @methodOf umbraco.services.listViewHelper - * - * @description - * Removes a given number of items and folders from the listviews selection array - * Folders can only be passed in if the listview is used in the media section which has a concept of folders. - * - * @param {Array} items Items to remove, can be null - * @param {Array} folders Folders to remove, can be null - * @param {Array} selection Listview selection, available as $scope.selection - */ - - function clearSelection(items, folders, selection) { - - var i = 0; - - selection.length = 0; - - if (angular.isArray(items)) { - for (i = 0; items.length > i; i++) { - var item = items[i]; - item.selected = false; - } - } - - if(angular.isArray(folders)) { - for (i = 0; folders.length > i; i++) { - var folder = folders[i]; - folder.selected = false; - } - } - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#selectAllItems - * @methodOf umbraco.services.listViewHelper - * - * @description - * Helper method for toggling the select state on all items in the active listview - * Can only be used from a checkbox as a checkbox $event is required to pass in. - * - * @param {Array} items Items to toggle selection on, should be $scope.items - * @param {Array} selection Listview selection, available as $scope.selection - * @param {$event} $event Event passed from the checkbox being toggled - */ - - function selectAllItems(items, selection, $event) { - - var checkbox = $event.target; - var clearSelection = false; - - if (!angular.isArray(items)) { - return; - } - - selection.length = 0; - - for (var i = 0; i < items.length; i++) { - - var item = items[i]; - - if (checkbox.checked) { - selection.push({ id: item.id }); - } else { - clearSelection = true; - } - - item.selected = checkbox.checked; - - } - - if (clearSelection) { - selection.length = 0; - } - - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#isSelectedAll - * @methodOf umbraco.services.listViewHelper - * - * @description - * Method to determine if all items on the current page in the list has been selected - * Given the current items in the view, and the current selection, it will return true/false - * - * @param {Array} items Items to test if all are selected, should be $scope.items - * @param {Array} selection Listview selection, available as $scope.selection - * @returns {Boolean} boolean indicate if all items in the listview have been selected - */ - - function isSelectedAll(items, selection) { - - var numberOfSelectedItem = 0; - - for (var itemIndex = 0; items.length > itemIndex; itemIndex++) { - var item = items[itemIndex]; - - for (var selectedIndex = 0; selection.length > selectedIndex; selectedIndex++) { - var selectedItem = selection[selectedIndex]; - - if (item.id === selectedItem.id) { - numberOfSelectedItem++; - } - } - - } - - if (numberOfSelectedItem === items.length) { - return true; - } - - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#setSortingDirection - * @methodOf umbraco.services.listViewHelper - * - * @description - * *Internal* method for changing sort order icon - * @param {String} col Column alias to order after - * @param {String} direction Order direction `asc` or `desc` - * @param {Object} options object passed from the parent listview available as $scope.options - */ - - function setSortingDirection(col, direction, options) { - return options.orderBy.toUpperCase() === col.toUpperCase() && options.orderDirection === direction; - } - - /** - * @ngdoc method - * @name umbraco.services.listViewHelper#setSorting - * @methodOf umbraco.services.listViewHelper - * - * @description - * Method for setting the field on which the listview will order its items after. - * - * @param {String} field Field alias to order after - * @param {Boolean} allow Determines if the user is allowed to set this field, normally true - * @param {Object} options Options object passed from the parent listview available as $scope.options - */ - - function setSorting(field, allow, options) { - if (allow) { - if (options.orderBy === field && options.orderDirection === 'asc') { - options.orderDirection = "desc"; - } else { - options.orderDirection = "asc"; - } - options.orderBy = field; - } - } - - //This takes in a dictionary of Ids with Permissions and determines - // the intersect of all permissions to return an object representing the - // listview button permissions - function getButtonPermissions(unmergedPermissions, currentIdsWithPermissions) { - - if (currentIdsWithPermissions == null) { - currentIdsWithPermissions = {}; - } - - //merge the newly retrieved permissions to the main dictionary - _.each(unmergedPermissions, function (value, key, list) { - currentIdsWithPermissions[key] = value; - }); - - //get the intersect permissions - var arr = []; - _.each(currentIdsWithPermissions, function (value, key, list) { - arr.push(value); - }); - - //we need to use 'apply' to call intersection with an array of arrays, - //see: http://stackoverflow.com/a/16229480/694494 - var intersectPermissions = _.intersection.apply(_, arr); - - return { - canCopy: _.contains(intersectPermissions, 'O'), //Magic Char = O - canCreate: _.contains(intersectPermissions, 'C'), //Magic Char = C - canDelete: _.contains(intersectPermissions, 'D'), //Magic Char = D - canMove: _.contains(intersectPermissions, 'M'), //Magic Char = M - canPublish: _.contains(intersectPermissions, 'U'), //Magic Char = U - canUnpublish: _.contains(intersectPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) - }; - } - - var service = { - - getLayout: getLayout, - getFirstAllowedLayout: getFirstAllowedLayout, - setLayout: setLayout, - saveLayoutInLocalStorage: saveLayoutInLocalStorage, - selectHandler: selectHandler, - selectItem: selectItem, - deselectItem: deselectItem, - clearSelection: clearSelection, - selectAllItems: selectAllItems, - isSelectedAll: isSelectedAll, - setSortingDirection: setSortingDirection, - setSorting: setSorting, - getButtonPermissions: getButtonPermissions - - }; - - return service; - - } - - - angular.module('umbraco.services').factory('listViewHelper', listViewHelper); - - -})(); +/** + @ngdoc service + * @name umbraco.services.listViewHelper + * + * + * @description + * Service for performing operations against items in the list view UI. Used by the built-in internal listviews + * as well as custom listview. + * + * A custom listview is always used inside a wrapper listview, so there are a number of inherited values on its + * scope by default: + * + * **$scope.selection**: Array containing all items currently selected in the listview + * + * **$scope.items**: Array containing all items currently displayed in the listview + * + * **$scope.folders**: Array containing all folders in the current listview (only for media) + * + * **$scope.options**: configuration object containing information such as pagesize, permissions, order direction etc. + * + * **$scope.model.config.layouts**: array of available layouts to apply to the listview (grid, list or custom layout) + * + * ##Usage## + * To use, inject listViewHelper into custom listview controller, listviewhelper expects you + * to pass in the full collection of items in the listview in several of its methods + * this collection is inherited from the parent controller and is available on $scope.selection + * + *
+ *      angular.module("umbraco").controller("my.listVieweditor". function($scope, listViewHelper){
+ *
+ *          //current items in the listview
+ *          var items = $scope.items;
+ *
+ *          //current selection
+ *          var selection = $scope.selection;
+ *
+ *          //deselect an item , $scope.selection is inherited, item is picked from inherited $scope.items
+ *          listViewHelper.deselectItem(item, $scope.selection);
+ *
+ *          //test if all items are selected, $scope.items + $scope.selection are inherited
+ *          listViewhelper.isSelectedAll($scope.items, $scope.selection);
+ *      });
+ * 
+ */ +(function () { + 'use strict'; -/** - @ngdoc service - * @name umbraco.services.listViewPrevalueHelper - * - * - * @description - * Service for accessing the prevalues of a list view being edited in the inline list view editor in the doctype editor - */ -(function () { - 'use strict'; - - function listViewPrevalueHelper() { - - var prevalues = []; - - /** - * @ngdoc method - * @name umbraco.services.listViewPrevalueHelper#getPrevalues - * @methodOf umbraco.services.listViewPrevalueHelper - * - * @description - * Set the collection of prevalues - */ - - function getPrevalues() { - return prevalues; - } - - /** - * @ngdoc method - * @name umbraco.services.listViewPrevalueHelper#setPrevalues - * @methodOf umbraco.services.listViewPrevalueHelper - * - * @description - * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage - * - * @param {Array} values Array of prevalues - */ - - function setPrevalues(values) { - prevalues = values; - } - - - - var service = { - - getPrevalues: getPrevalues, - setPrevalues: setPrevalues - - }; - - return service; - - } - - - angular.module('umbraco.services').factory('listViewPrevalueHelper', listViewPrevalueHelper); - - -})(); + function listViewHelper(localStorageService) { + + var firstSelectedIndex = 0; + var localStorageKey = "umblistViewLayout"; + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#getLayout + * @methodOf umbraco.services.listViewHelper + * + * @description + * Method for internal use, based on the collection of layouts passed, the method selects either + * any previous layout from local storage, or picks the first allowed layout + * + * @param {Number} nodeId The id of the current node displayed in the content editor + * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts + */ + + function getLayout(nodeId, availableLayouts) { + + var storedLayouts = []; + + if (localStorageService.get(localStorageKey)) { + storedLayouts = localStorageService.get(localStorageKey); + } + + if (storedLayouts && storedLayouts.length > 0) { + for (var i = 0; storedLayouts.length > i; i++) { + var layout = storedLayouts[i]; + if (layout.nodeId === nodeId) { + return setLayout(nodeId, layout, availableLayouts); + } + } + + } + + return getFirstAllowedLayout(availableLayouts); + + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#setLayout + * @methodOf umbraco.services.listViewHelper + * + * @description + * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage + * + * @param {Number} nodeID Id of the current node displayed in the content editor + * @param {Object} selectedLayout Layout selected as the layout to set as the current layout + * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts + */ + + function setLayout(nodeId, selectedLayout, availableLayouts) { + + var activeLayout = {}; + var layoutFound = false; + + for (var i = 0; availableLayouts.length > i; i++) { + var layout = availableLayouts[i]; + if (layout.path === selectedLayout.path) { + activeLayout = layout; + layout.active = true; + layoutFound = true; + } else { + layout.active = false; + } + } + + if (!layoutFound) { + activeLayout = getFirstAllowedLayout(availableLayouts); + } + + saveLayoutInLocalStorage(nodeId, activeLayout); + + return activeLayout; + + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#saveLayoutInLocalStorage + * @methodOf umbraco.services.listViewHelper + * + * @description + * Stores a given layout as the current default selection in local storage + * + * @param {Number} nodeId Id of the current node displayed in the content editor + * @param {Object} selectedLayout Layout selected as the layout to set as the current layout + */ + + function saveLayoutInLocalStorage(nodeId, selectedLayout) { + var layoutFound = false; + var storedLayouts = []; + + if (localStorageService.get(localStorageKey)) { + storedLayouts = localStorageService.get(localStorageKey); + } + + if (storedLayouts.length > 0) { + for (var i = 0; storedLayouts.length > i; i++) { + var layout = storedLayouts[i]; + if (layout.nodeId === nodeId) { + layout.path = selectedLayout.path; + layoutFound = true; + } + } + } + + if (!layoutFound) { + var storageObject = { + "nodeId": nodeId, + "path": selectedLayout.path + }; + storedLayouts.push(storageObject); + } + + localStorageService.set(localStorageKey, storedLayouts); + + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#getFirstAllowedLayout + * @methodOf umbraco.services.listViewHelper + * + * @description + * Returns currently selected layout, or alternatively the first layout in the available layouts collection + * + * @param {Array} layouts Array of all allowed layouts, available from $scope.model.config.layouts + */ + + function getFirstAllowedLayout(layouts) { + + var firstAllowedLayout = {}; + + for (var i = 0; layouts.length > i; i++) { + var layout = layouts[i]; + if (layout.selected === true) { + firstAllowedLayout = layout; + break; + } + } + + return firstAllowedLayout; + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#selectHandler + * @methodOf umbraco.services.listViewHelper + * + * @description + * Helper method for working with item selection via a checkbox, internally it uses selectItem and deselectItem. + * Working with this method, requires its triggered via a checkbox which can then pass in its triggered $event + * When the checkbox is clicked, this method will toggle selection of the associated item so it matches the state of the checkbox + * + * @param {Object} selectedItem Item being selected or deselected by the checkbox + * @param {Number} selectedIndex Index of item being selected/deselected, usually passed as $index + * @param {Array} items All items in the current listview, available as $scope.items + * @param {Array} selection All selected items in the current listview, available as $scope.selection + * @param {Event} $event Event triggered by the checkbox being checked to select / deselect an item + */ + + function selectHandler(selectedItem, selectedIndex, items, selection, $event) { + + var start = 0; + var end = 0; + var item = null; + + if ($event.shiftKey === true) { + + if (selectedIndex > firstSelectedIndex) { + + start = firstSelectedIndex; + end = selectedIndex; + + for (; end >= start; start++) { + item = items[start]; + selectItem(item, selection); + } + + } else { + + start = firstSelectedIndex; + end = selectedIndex; + + for (; end <= start; start--) { + item = items[start]; + selectItem(item, selection); + } + + } + + } else { + + if (selectedItem.selected) { + deselectItem(selectedItem, selection); + } else { + selectItem(selectedItem, selection); + } + + firstSelectedIndex = selectedIndex; + + } + + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#selectItem + * @methodOf umbraco.services.listViewHelper + * + * @description + * Selects a given item to the listview selection array, requires you pass in the inherited $scope.selection collection + * + * @param {Object} item Item to select + * @param {Array} selection Listview selection, available as $scope.selection + */ + + function selectItem(item, selection) { + var isSelected = false; + for (var i = 0; selection.length > i; i++) { + var selectedItem = selection[i]; + if (item.id === selectedItem.id) { + isSelected = true; + } + } + if (!isSelected) { + selection.push({ id: item.id }); + item.selected = true; + } + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#deselectItem + * @methodOf umbraco.services.listViewHelper + * + * @description + * Deselects a given item from the listviews selection array, requires you pass in the inherited $scope.selection collection + * + * @param {Object} item Item to deselect + * @param {Array} selection Listview selection, available as $scope.selection + */ + + function deselectItem(item, selection) { + for (var i = 0; selection.length > i; i++) { + var selectedItem = selection[i]; + if (item.id === selectedItem.id) { + selection.splice(i, 1); + item.selected = false; + } + } + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#clearSelection + * @methodOf umbraco.services.listViewHelper + * + * @description + * Removes a given number of items and folders from the listviews selection array + * Folders can only be passed in if the listview is used in the media section which has a concept of folders. + * + * @param {Array} items Items to remove, can be null + * @param {Array} folders Folders to remove, can be null + * @param {Array} selection Listview selection, available as $scope.selection + */ + + function clearSelection(items, folders, selection) { + + var i = 0; + + selection.length = 0; + + if (angular.isArray(items)) { + for (i = 0; items.length > i; i++) { + var item = items[i]; + item.selected = false; + } + } + + if(angular.isArray(folders)) { + for (i = 0; folders.length > i; i++) { + var folder = folders[i]; + folder.selected = false; + } + } + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#selectAllItems + * @methodOf umbraco.services.listViewHelper + * + * @description + * Helper method for toggling the select state on all items in the active listview + * Can only be used from a checkbox as a checkbox $event is required to pass in. + * + * @param {Array} items Items to toggle selection on, should be $scope.items + * @param {Array} selection Listview selection, available as $scope.selection + * @param {$event} $event Event passed from the checkbox being toggled + */ + + function selectAllItems(items, selection, $event) { + + var checkbox = $event.target; + var clearSelection = false; + + if (!angular.isArray(items)) { + return; + } + + selection.length = 0; + + for (var i = 0; i < items.length; i++) { + + var item = items[i]; + + if (checkbox.checked) { + selection.push({ id: item.id }); + } else { + clearSelection = true; + } + + item.selected = checkbox.checked; + + } + + if (clearSelection) { + selection.length = 0; + } + + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#isSelectedAll + * @methodOf umbraco.services.listViewHelper + * + * @description + * Method to determine if all items on the current page in the list has been selected + * Given the current items in the view, and the current selection, it will return true/false + * + * @param {Array} items Items to test if all are selected, should be $scope.items + * @param {Array} selection Listview selection, available as $scope.selection + * @returns {Boolean} boolean indicate if all items in the listview have been selected + */ + + function isSelectedAll(items, selection) { + + var numberOfSelectedItem = 0; + + for (var itemIndex = 0; items.length > itemIndex; itemIndex++) { + var item = items[itemIndex]; + + for (var selectedIndex = 0; selection.length > selectedIndex; selectedIndex++) { + var selectedItem = selection[selectedIndex]; + + if (item.id === selectedItem.id) { + numberOfSelectedItem++; + } + } + + } + + if (numberOfSelectedItem === items.length) { + return true; + } + + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#setSortingDirection + * @methodOf umbraco.services.listViewHelper + * + * @description + * *Internal* method for changing sort order icon + * @param {String} col Column alias to order after + * @param {String} direction Order direction `asc` or `desc` + * @param {Object} options object passed from the parent listview available as $scope.options + */ + + function setSortingDirection(col, direction, options) { + return options.orderBy.toUpperCase() === col.toUpperCase() && options.orderDirection === direction; + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#setSorting + * @methodOf umbraco.services.listViewHelper + * + * @description + * Method for setting the field on which the listview will order its items after. + * + * @param {String} field Field alias to order after + * @param {Boolean} allow Determines if the user is allowed to set this field, normally true + * @param {Object} options Options object passed from the parent listview available as $scope.options + */ + + function setSorting(field, allow, options) { + if (allow) { + if (options.orderBy === field && options.orderDirection === 'asc') { + options.orderDirection = "desc"; + } else { + options.orderDirection = "asc"; + } + options.orderBy = field; + } + } + + //This takes in a dictionary of Ids with Permissions and determines + // the intersect of all permissions to return an object representing the + // listview button permissions + function getButtonPermissions(unmergedPermissions, currentIdsWithPermissions) { + + if (currentIdsWithPermissions == null) { + currentIdsWithPermissions = {}; + } + + //merge the newly retrieved permissions to the main dictionary + _.each(unmergedPermissions, function (value, key, list) { + currentIdsWithPermissions[key] = value; + }); + + //get the intersect permissions + var arr = []; + _.each(currentIdsWithPermissions, function (value, key, list) { + arr.push(value); + }); + + //we need to use 'apply' to call intersection with an array of arrays, + //see: http://stackoverflow.com/a/16229480/694494 + var intersectPermissions = _.intersection.apply(_, arr); + + return { + canCopy: _.contains(intersectPermissions, 'O'), //Magic Char = O + canCreate: _.contains(intersectPermissions, 'C'), //Magic Char = C + canDelete: _.contains(intersectPermissions, 'D'), //Magic Char = D + canMove: _.contains(intersectPermissions, 'M'), //Magic Char = M + canPublish: _.contains(intersectPermissions, 'U'), //Magic Char = U + canUnpublish: _.contains(intersectPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) + }; + } + + var service = { + + getLayout: getLayout, + getFirstAllowedLayout: getFirstAllowedLayout, + setLayout: setLayout, + saveLayoutInLocalStorage: saveLayoutInLocalStorage, + selectHandler: selectHandler, + selectItem: selectItem, + deselectItem: deselectItem, + clearSelection: clearSelection, + selectAllItems: selectAllItems, + isSelectedAll: isSelectedAll, + setSortingDirection: setSortingDirection, + setSorting: setSorting, + getButtonPermissions: getButtonPermissions + + }; + + return service; + + } + + + angular.module('umbraco.services').factory('listViewHelper', listViewHelper); + + +})(); + +/** + @ngdoc service + * @name umbraco.services.listViewPrevalueHelper + * + * + * @description + * Service for accessing the prevalues of a list view being edited in the inline list view editor in the doctype editor + */ +(function () { + 'use strict'; + + function listViewPrevalueHelper() { + + var prevalues = []; + + /** + * @ngdoc method + * @name umbraco.services.listViewPrevalueHelper#getPrevalues + * @methodOf umbraco.services.listViewPrevalueHelper + * + * @description + * Set the collection of prevalues + */ + + function getPrevalues() { + return prevalues; + } + + /** + * @ngdoc method + * @name umbraco.services.listViewPrevalueHelper#setPrevalues + * @methodOf umbraco.services.listViewPrevalueHelper + * + * @description + * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage + * + * @param {Array} values Array of prevalues + */ + + function setPrevalues(values) { + prevalues = values; + } + + + + var service = { + + getPrevalues: getPrevalues, + setPrevalues: setPrevalues + + }; + + return service; + + } + + + angular.module('umbraco.services').factory('listViewPrevalueHelper', listViewPrevalueHelper); + + +})(); /** * @ngdoc service @@ -4508,436 +4508,436 @@ function macroService() { angular.module('umbraco.services').factory('macroService', macroService); -/** -* @ngdoc service -* @name umbraco.services.mediaHelper -* @description A helper object used for dealing with media items -**/ -function mediaHelper(umbRequestHelper) { - - //container of fileresolvers - var _mediaFileResolvers = {}; - - return { - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#getImagePropertyValue - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns the file path associated with the media property if there is one - * - * @param {object} options Options object - * @param {object} options.mediaModel The media object to retrieve the image path from - * @param {object} options.imageOnly Optional, if true then will only return a path if the media item is an image - */ - getMediaPropertyValue: function (options) { - if (!options || !options.mediaModel) { - throw "The options objet does not contain the required parameters: mediaModel"; - } - - //combine all props, TODO: we really need a better way then this - var props = []; - if (options.mediaModel.properties) { - props = options.mediaModel.properties; - } else { - $(options.mediaModel.tabs).each(function (i, tab) { - props = props.concat(tab.properties); - }); - } - - var mediaRoot = Umbraco.Sys.ServerVariables.umbracoSettings.mediaPath; - var imageProp = _.find(props, function (item) { - if (item.alias === "umbracoFile") { - return true; - } - - //this performs a simple check to see if we have a media file as value - //it doesnt catch everything, but better then nothing - if (angular.isString(item.value) && item.value.indexOf(mediaRoot) === 0) { - return true; - } - - return false; - }); - - if (!imageProp) { - return ""; - } - - var mediaVal; - - //our default images might store one or many images (as csv) - var split = imageProp.value.split(','); - var self = this; - mediaVal = _.map(split, function (item) { - return { file: item, isImage: self.detectIfImageByExtension(item) }; - }); - - //for now we'll just return the first image in the collection. - //TODO: we should enable returning many to be displayed in the picker if the uploader supports many. - if (mediaVal.length && mediaVal.length > 0) { - if (!options.imageOnly || (options.imageOnly === true && mediaVal[0].isImage)) { - return mediaVal[0].file; - } - } - - return ""; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#getImagePropertyValue - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns the actual image path associated with the image property if there is one - * - * @param {object} options Options object - * @param {object} options.imageModel The media object to retrieve the image path from - */ - getImagePropertyValue: function (options) { - if (!options || (!options.imageModel && !options.mediaModel)) { - throw "The options objet does not contain the required parameters: imageModel"; - } - - //required to support backwards compatibility. - options.mediaModel = options.imageModel ? options.imageModel : options.mediaModel; - - options.imageOnly = true; - - return this.getMediaPropertyValue(options); - }, - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#getThumbnail - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * formats the display model used to display the content to the model used to save the content - * - * @param {object} options Options object - * @param {object} options.imageModel The media object to retrieve the image path from - */ - getThumbnail: function (options) { - - if (!options || !options.imageModel) { - throw "The options objet does not contain the required parameters: imageModel"; - } - - var imagePropVal = this.getImagePropertyValue(options); - if (imagePropVal !== "") { - return this.getThumbnailFromPath(imagePropVal); - } - return ""; - }, - - registerFileResolver: function(propertyEditorAlias, func){ - _mediaFileResolvers[propertyEditorAlias] = func; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#resolveFileFromEntity - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Gets the media file url for a media entity returned with the entityResource - * - * @param {object} mediaEntity A media Entity returned from the entityResource - * @param {boolean} thumbnail Whether to return the thumbnail url or normal url - */ - resolveFileFromEntity : function(mediaEntity, thumbnail) { - - if (!angular.isObject(mediaEntity.metaData)) { - throw "Cannot resolve the file url from the mediaEntity, it does not contain the required metaData"; - } - - var values = _.values(mediaEntity.metaData); - for (var i = 0; i < values.length; i++) { - var val = values[i]; - if (angular.isObject(val) && val.PropertyEditorAlias) { - for (var resolver in _mediaFileResolvers) { - if (val.PropertyEditorAlias === resolver) { - //we need to format a property variable that coincides with how the property would be structured - // if it came from the mediaResource just to keep things slightly easier for the file resolvers. - var property = { value: val.Value }; - - return _mediaFileResolvers[resolver](property, mediaEntity, thumbnail); - } - } - } - } - - return ""; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#resolveFile - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Gets the media file url for a media object returned with the mediaResource - * - * @param {object} mediaEntity A media Entity returned from the entityResource - * @param {boolean} thumbnail Whether to return the thumbnail url or normal url - */ - /*jshint loopfunc: true */ - resolveFile : function(mediaItem, thumbnail){ - - function iterateProps(props){ - var res = null; - for(var resolver in _mediaFileResolvers) { - var property = _.find(props, function(prop){ return prop.editor === resolver; }); - if(property){ - res = _mediaFileResolvers[resolver](property, mediaItem, thumbnail); - break; - } - } - - return res; - } - - //we either have properties raw on the object, or spread out on tabs - var result = ""; - if(mediaItem.properties){ - result = iterateProps(mediaItem.properties); - }else if(mediaItem.tabs){ - for(var tab in mediaItem.tabs) { - if(mediaItem.tabs[tab].properties){ - result = iterateProps(mediaItem.tabs[tab].properties); - if(result){ - break; - } - } - } - } - return result; - }, - - /*jshint loopfunc: true */ - hasFilePropertyType : function(mediaItem){ - function iterateProps(props){ - var res = false; - for(var resolver in _mediaFileResolvers) { - var property = _.find(props, function(prop){ return prop.editor === resolver; }); - if(property){ - res = true; - break; - } - } - return res; - } - - //we either have properties raw on the object, or spread out on tabs - var result = false; - if(mediaItem.properties){ - result = iterateProps(mediaItem.properties); - }else if(mediaItem.tabs){ - for(var tab in mediaItem.tabs) { - if(mediaItem.tabs[tab].properties){ - result = iterateProps(mediaItem.tabs[tab].properties); - if(result){ - break; - } - } - } - } - return result; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#scaleToMaxSize - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Finds the corrct max width and max height, given maximum dimensions and keeping aspect ratios - * - * @param {number} maxSize Maximum width & height - * @param {number} width Current width - * @param {number} height Current height - */ - scaleToMaxSize: function (maxSize, width, height) { - var retval = { width: width, height: height }; - - var maxWidth = maxSize; // Max width for the image - var maxHeight = maxSize; // Max height for the image - var ratio = 0; // Used for aspect ratio - - // Check if the current width is larger than the max - if (width > maxWidth) { - ratio = maxWidth / width; // get ratio for scaling image - - retval.width = maxWidth; - retval.height = height * ratio; - - height = height * ratio; // Reset height to match scaled image - width = width * ratio; // Reset width to match scaled image - } - - // Check if current height is larger than max - if (height > maxHeight) { - ratio = maxHeight / height; // get ratio for scaling image - - retval.height = maxHeight; - retval.width = width * ratio; - width = width * ratio; // Reset width to match scaled image - } - - return retval; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#getThumbnailFromPath - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns the path to the thumbnail version of a given media library image path - * - * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg - */ - getThumbnailFromPath: function (imagePath) { - - //If the path is not an image we cannot get a thumb - if (!this.detectIfImageByExtension(imagePath)) { - return null; - } - - //get the proxy url for big thumbnails (this ensures one is always generated) - var thumbnailUrl = umbRequestHelper.getApiUrl( - "imagesApiBaseUrl", - "GetBigThumbnail", - [{ originalImagePath: imagePath }]); - - //var ext = imagePath.substr(imagePath.lastIndexOf('.')); - //return imagePath.substr(0, imagePath.lastIndexOf('.')) + "_big-thumb" + ".jpg"; - - return thumbnailUrl; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#detectIfImageByExtension - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns true/false, indicating if the given path has an allowed image extension - * - * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg - */ - detectIfImageByExtension: function (imagePath) { - - if (!imagePath) { - return false; - } - - var lowered = imagePath.toLowerCase(); - var ext = lowered.substr(lowered.lastIndexOf(".") + 1); - return ("," + Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes + ",").indexOf("," + ext + ",") !== -1; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#formatFileTypes - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns a string with correctly formated file types for ng-file-upload - * - * @param {string} file types, ex: jpg,png,tiff - */ - formatFileTypes: function(fileTypes) { - - var fileTypesArray = fileTypes.split(','); - var newFileTypesArray = []; - - for (var i = 0; i < fileTypesArray.length; i++) { - var fileType = fileTypesArray[i]; - - if (fileType.indexOf(".") !== 0) { - fileType = ".".concat(fileType); - } - - newFileTypesArray.push(fileType); - } - - return newFileTypesArray.join(","); - - } - - }; -}angular.module('umbraco.services').factory('mediaHelper', mediaHelper); +/** +* @ngdoc service +* @name umbraco.services.mediaHelper +* @description A helper object used for dealing with media items +**/ +function mediaHelper(umbRequestHelper) { + + //container of fileresolvers + var _mediaFileResolvers = {}; -/** - * @ngdoc service - * @name umbraco.services.mediaTypeHelper - * @description A helper service for the media types - **/ -function mediaTypeHelper(mediaTypeResource, $q) { - - var mediaTypeHelperService = { - - getAllowedImagetypes: function (mediaId){ - - // Get All allowedTypes - return mediaTypeResource.getAllowedTypes(mediaId) - .then(function(types){ - - var allowedQ = types.map(function(type){ - return mediaTypeResource.getById(type.id); - }); - - // Get full list - return $q.all(allowedQ).then(function(fullTypes){ - - // Find all the media types with an Image Cropper property editor - var filteredTypes = mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper']); - - // If there is only one media type with an Image Cropper we will return this one - if(filteredTypes.length === 1) { - return filteredTypes; - // If there is more than one Image cropper, custom media types have been added, and we return all media types with and Image cropper or UploadField - } else { - return mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper', 'Umbraco.UploadField']); - } - - }); - }); - }, - - getTypeWithEditor: function (types, editors) { - - return types.filter(function (mediatype) { - for (var i = 0; i < mediatype.groups.length; i++) { - var group = mediatype.groups[i]; - for (var j = 0; j < group.properties.length; j++) { - var property = group.properties[j]; - if( editors.indexOf(property.editor) !== -1 ) { - return mediatype; - } - } - } - }); - - } - - }; - - return mediaTypeHelperService; -} -angular.module('umbraco.services').factory('mediaTypeHelper', mediaTypeHelper); + return { + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#getImagePropertyValue + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Returns the file path associated with the media property if there is one + * + * @param {object} options Options object + * @param {object} options.mediaModel The media object to retrieve the image path from + * @param {object} options.imageOnly Optional, if true then will only return a path if the media item is an image + */ + getMediaPropertyValue: function (options) { + if (!options || !options.mediaModel) { + throw "The options objet does not contain the required parameters: mediaModel"; + } + + //combine all props, TODO: we really need a better way then this + var props = []; + if (options.mediaModel.properties) { + props = options.mediaModel.properties; + } else { + $(options.mediaModel.tabs).each(function (i, tab) { + props = props.concat(tab.properties); + }); + } + + var mediaRoot = Umbraco.Sys.ServerVariables.umbracoSettings.mediaPath; + var imageProp = _.find(props, function (item) { + if (item.alias === "umbracoFile") { + return true; + } + + //this performs a simple check to see if we have a media file as value + //it doesnt catch everything, but better then nothing + if (angular.isString(item.value) && item.value.indexOf(mediaRoot) === 0) { + return true; + } + + return false; + }); + + if (!imageProp) { + return ""; + } + + var mediaVal; + + //our default images might store one or many images (as csv) + var split = imageProp.value.split(','); + var self = this; + mediaVal = _.map(split, function (item) { + return { file: item, isImage: self.detectIfImageByExtension(item) }; + }); + + //for now we'll just return the first image in the collection. + //TODO: we should enable returning many to be displayed in the picker if the uploader supports many. + if (mediaVal.length && mediaVal.length > 0) { + if (!options.imageOnly || (options.imageOnly === true && mediaVal[0].isImage)) { + return mediaVal[0].file; + } + } + + return ""; + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#getImagePropertyValue + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Returns the actual image path associated with the image property if there is one + * + * @param {object} options Options object + * @param {object} options.imageModel The media object to retrieve the image path from + */ + getImagePropertyValue: function (options) { + if (!options || (!options.imageModel && !options.mediaModel)) { + throw "The options objet does not contain the required parameters: imageModel"; + } + + //required to support backwards compatibility. + options.mediaModel = options.imageModel ? options.imageModel : options.mediaModel; + + options.imageOnly = true; + + return this.getMediaPropertyValue(options); + }, + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#getThumbnail + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * formats the display model used to display the content to the model used to save the content + * + * @param {object} options Options object + * @param {object} options.imageModel The media object to retrieve the image path from + */ + getThumbnail: function (options) { + + if (!options || !options.imageModel) { + throw "The options objet does not contain the required parameters: imageModel"; + } + + var imagePropVal = this.getImagePropertyValue(options); + if (imagePropVal !== "") { + return this.getThumbnailFromPath(imagePropVal); + } + return ""; + }, + + registerFileResolver: function(propertyEditorAlias, func){ + _mediaFileResolvers[propertyEditorAlias] = func; + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#resolveFileFromEntity + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Gets the media file url for a media entity returned with the entityResource + * + * @param {object} mediaEntity A media Entity returned from the entityResource + * @param {boolean} thumbnail Whether to return the thumbnail url or normal url + */ + resolveFileFromEntity : function(mediaEntity, thumbnail) { + + if (!angular.isObject(mediaEntity.metaData)) { + throw "Cannot resolve the file url from the mediaEntity, it does not contain the required metaData"; + } + + var values = _.values(mediaEntity.metaData); + for (var i = 0; i < values.length; i++) { + var val = values[i]; + if (angular.isObject(val) && val.PropertyEditorAlias) { + for (var resolver in _mediaFileResolvers) { + if (val.PropertyEditorAlias === resolver) { + //we need to format a property variable that coincides with how the property would be structured + // if it came from the mediaResource just to keep things slightly easier for the file resolvers. + var property = { value: val.Value }; + + return _mediaFileResolvers[resolver](property, mediaEntity, thumbnail); + } + } + } + } + + return ""; + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#resolveFile + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Gets the media file url for a media object returned with the mediaResource + * + * @param {object} mediaEntity A media Entity returned from the entityResource + * @param {boolean} thumbnail Whether to return the thumbnail url or normal url + */ + /*jshint loopfunc: true */ + resolveFile : function(mediaItem, thumbnail){ + + function iterateProps(props){ + var res = null; + for(var resolver in _mediaFileResolvers) { + var property = _.find(props, function(prop){ return prop.editor === resolver; }); + if(property){ + res = _mediaFileResolvers[resolver](property, mediaItem, thumbnail); + break; + } + } + + return res; + } + + //we either have properties raw on the object, or spread out on tabs + var result = ""; + if(mediaItem.properties){ + result = iterateProps(mediaItem.properties); + }else if(mediaItem.tabs){ + for(var tab in mediaItem.tabs) { + if(mediaItem.tabs[tab].properties){ + result = iterateProps(mediaItem.tabs[tab].properties); + if(result){ + break; + } + } + } + } + return result; + }, + + /*jshint loopfunc: true */ + hasFilePropertyType : function(mediaItem){ + function iterateProps(props){ + var res = false; + for(var resolver in _mediaFileResolvers) { + var property = _.find(props, function(prop){ return prop.editor === resolver; }); + if(property){ + res = true; + break; + } + } + return res; + } + + //we either have properties raw on the object, or spread out on tabs + var result = false; + if(mediaItem.properties){ + result = iterateProps(mediaItem.properties); + }else if(mediaItem.tabs){ + for(var tab in mediaItem.tabs) { + if(mediaItem.tabs[tab].properties){ + result = iterateProps(mediaItem.tabs[tab].properties); + if(result){ + break; + } + } + } + } + return result; + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#scaleToMaxSize + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Finds the corrct max width and max height, given maximum dimensions and keeping aspect ratios + * + * @param {number} maxSize Maximum width & height + * @param {number} width Current width + * @param {number} height Current height + */ + scaleToMaxSize: function (maxSize, width, height) { + var retval = { width: width, height: height }; + + var maxWidth = maxSize; // Max width for the image + var maxHeight = maxSize; // Max height for the image + var ratio = 0; // Used for aspect ratio + + // Check if the current width is larger than the max + if (width > maxWidth) { + ratio = maxWidth / width; // get ratio for scaling image + + retval.width = maxWidth; + retval.height = height * ratio; + + height = height * ratio; // Reset height to match scaled image + width = width * ratio; // Reset width to match scaled image + } + + // Check if current height is larger than max + if (height > maxHeight) { + ratio = maxHeight / height; // get ratio for scaling image + + retval.height = maxHeight; + retval.width = width * ratio; + width = width * ratio; // Reset width to match scaled image + } + + return retval; + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#getThumbnailFromPath + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Returns the path to the thumbnail version of a given media library image path + * + * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg + */ + getThumbnailFromPath: function (imagePath) { + + //If the path is not an image we cannot get a thumb + if (!this.detectIfImageByExtension(imagePath)) { + return null; + } + + //get the proxy url for big thumbnails (this ensures one is always generated) + var thumbnailUrl = umbRequestHelper.getApiUrl( + "imagesApiBaseUrl", + "GetBigThumbnail", + [{ originalImagePath: imagePath }]); + + //var ext = imagePath.substr(imagePath.lastIndexOf('.')); + //return imagePath.substr(0, imagePath.lastIndexOf('.')) + "_big-thumb" + ".jpg"; + + return thumbnailUrl; + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#detectIfImageByExtension + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Returns true/false, indicating if the given path has an allowed image extension + * + * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg + */ + detectIfImageByExtension: function (imagePath) { + + if (!imagePath) { + return false; + } + + var lowered = imagePath.toLowerCase(); + var ext = lowered.substr(lowered.lastIndexOf(".") + 1); + return ("," + Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes + ",").indexOf("," + ext + ",") !== -1; + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#formatFileTypes + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Returns a string with correctly formated file types for ng-file-upload + * + * @param {string} file types, ex: jpg,png,tiff + */ + formatFileTypes: function(fileTypes) { + + var fileTypesArray = fileTypes.split(','); + var newFileTypesArray = []; + + for (var i = 0; i < fileTypesArray.length; i++) { + var fileType = fileTypesArray[i]; + + if (fileType.indexOf(".") !== 0) { + fileType = ".".concat(fileType); + } + + newFileTypesArray.push(fileType); + } + + return newFileTypesArray.join(","); + + } + + }; +}angular.module('umbraco.services').factory('mediaHelper', mediaHelper); + +/** + * @ngdoc service + * @name umbraco.services.mediaTypeHelper + * @description A helper service for the media types + **/ +function mediaTypeHelper(mediaTypeResource, $q) { + + var mediaTypeHelperService = { + + getAllowedImagetypes: function (mediaId){ + + // Get All allowedTypes + return mediaTypeResource.getAllowedTypes(mediaId) + .then(function(types){ + + var allowedQ = types.map(function(type){ + return mediaTypeResource.getById(type.id); + }); + + // Get full list + return $q.all(allowedQ).then(function(fullTypes){ + + // Find all the media types with an Image Cropper property editor + var filteredTypes = mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper']); + + // If there is only one media type with an Image Cropper we will return this one + if(filteredTypes.length === 1) { + return filteredTypes; + // If there is more than one Image cropper, custom media types have been added, and we return all media types with and Image cropper or UploadField + } else { + return mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper', 'Umbraco.UploadField']); + } + + }); + }); + }, + + getTypeWithEditor: function (types, editors) { + + return types.filter(function (mediatype) { + for (var i = 0; i < mediatype.groups.length; i++) { + var group = mediatype.groups[i]; + for (var j = 0; j < group.properties.length; j++) { + var property = group.properties[j]; + if( editors.indexOf(property.editor) !== -1 ) { + return mediatype; + } + } + } + }); + + } + + }; + + return mediaTypeHelperService; +} +angular.module('umbraco.services').factory('mediaTypeHelper', mediaTypeHelper); /** * @ngdoc service @@ -5016,741 +5016,741 @@ function umbracoMenuActions($q, treeService, $location, navigationService, appSt } angular.module('umbraco.services').factory('umbracoMenuActions', umbracoMenuActions); -/** - * @ngdoc service - * @name umbraco.services.navigationService - * - * @requires $rootScope - * @requires $routeParams - * @requires $log - * @requires $location - * @requires dialogService - * @requires treeService - * @requires sectionResource - * - * @description - * Service to handle the main application navigation. Responsible for invoking the tree - * Section navigation and search, and maintain their state for the entire application lifetime - * - */ -function navigationService($rootScope, $routeParams, $log, $location, $q, $timeout, $injector, dialogService, umbModelMapper, treeService, notificationsService, historyService, appState, angularHelper) { - - - //used to track the current dialog object - var currentDialog = null; - - //the main tree event handler, which gets assigned via the setupTreeEvents method - var mainTreeEventHandler = null; - //tracks the user profile dialog - var userDialog = null; - - function setMode(mode) { - switch (mode) { - case 'tree': - appState.setGlobalState("navMode", "tree"); - appState.setGlobalState("showNavigation", true); - appState.setMenuState("showMenu", false); - appState.setMenuState("showMenuDialog", false); - appState.setGlobalState("stickyNavigation", false); - appState.setGlobalState("showTray", false); - - //$("#search-form input").focus(); - break; - case 'menu': - appState.setGlobalState("navMode", "menu"); - appState.setGlobalState("showNavigation", true); - appState.setMenuState("showMenu", true); - appState.setMenuState("showMenuDialog", false); - appState.setGlobalState("stickyNavigation", true); - break; - case 'dialog': - appState.setGlobalState("navMode", "dialog"); - appState.setGlobalState("stickyNavigation", true); - appState.setGlobalState("showNavigation", true); - appState.setMenuState("showMenu", false); - appState.setMenuState("showMenuDialog", true); - break; - case 'search': - appState.setGlobalState("navMode", "search"); - appState.setGlobalState("stickyNavigation", false); - appState.setGlobalState("showNavigation", true); - appState.setMenuState("showMenu", false); - appState.setSectionState("showSearchResults", true); - appState.setMenuState("showMenuDialog", false); - - //TODO: This would be much better off in the search field controller listening to appState changes - $timeout(function() { - $("#search-field").focus(); - }); - - break; - default: - appState.setGlobalState("navMode", "default"); - appState.setMenuState("showMenu", false); - appState.setMenuState("showMenuDialog", false); - appState.setSectionState("showSearchResults", false); - appState.setGlobalState("stickyNavigation", false); - appState.setGlobalState("showTray", false); - - if (appState.getGlobalState("isTablet") === true) { - appState.setGlobalState("showNavigation", false); - } - - break; - } - } - - var service = { - - /** initializes the navigation service */ - init: function() { - - //keep track of the current section - initially this will always be undefined so - // no point in setting it now until it changes. - $rootScope.$watch(function () { - return $routeParams.section; - }, function (newVal, oldVal) { - appState.setSectionState("currentSection", newVal); - }); - - - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#load - * @methodOf umbraco.services.navigationService - * - * @description - * Shows the legacy iframe and loads in the content based on the source url - * @param {String} source The URL to load into the iframe - */ - loadLegacyIFrame: function (source) { - $location.path("/" + appState.getSectionState("currentSection") + "/framed/" + encodeURIComponent(source)); - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#changeSection - * @methodOf umbraco.services.navigationService - * - * @description - * Changes the active section to a given section alias - * If the navigation is 'sticky' this will load the associated tree - * and load the dashboard related to the section - * @param {string} sectionAlias The alias of the section - */ - changeSection: function(sectionAlias, force) { - setMode("default-opensection"); - - if (force && appState.getSectionState("currentSection") === sectionAlias) { - appState.setSectionState("currentSection", ""); - } - - appState.setSectionState("currentSection", sectionAlias); - this.showTree(sectionAlias); - - $location.path(sectionAlias); - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#showTree - * @methodOf umbraco.services.navigationService - * - * @description - * Displays the tree for a given section alias but turning on the containing dom element - * only changes if the section is different from the current one - * @param {string} sectionAlias The alias of the section to load - * @param {Object} syncArgs Optional object of arguments for syncing the tree for the section being shown - */ - showTree: function (sectionAlias, syncArgs) { - if (sectionAlias !== appState.getSectionState("currentSection")) { - appState.setSectionState("currentSection", sectionAlias); - - if (syncArgs) { - this.syncTree(syncArgs); - } - } - setMode("tree"); - }, - - showTray: function () { - appState.setGlobalState("showTray", true); - }, - - hideTray: function () { - appState.setGlobalState("showTray", false); - }, - - /** - Called to assign the main tree event handler - this is called by the navigation controller. - TODO: Potentially another dev could call this which would kind of mung the whole app so potentially there's a better way. - */ - setupTreeEvents: function(treeEventHandler) { - mainTreeEventHandler = treeEventHandler; - - //when a tree is loaded into a section, we need to put it into appState - mainTreeEventHandler.bind("treeLoaded", function(ev, args) { - appState.setTreeState("currentRootNode", args.tree); - }); - - //when a tree node is synced this event will fire, this allows us to set the currentNode - mainTreeEventHandler.bind("treeSynced", function (ev, args) { - - if (args.activate === undefined || args.activate === true) { - //set the current selected node - appState.setTreeState("selectedNode", args.node); - //when a node is activated, this is the same as clicking it and we need to set the - //current menu item to be this node as well. - appState.setMenuState("currentNode", args.node); - } - }); - - //this reacts to the options item in the tree - mainTreeEventHandler.bind("treeOptionsClick", function(ev, args) { - ev.stopPropagation(); - ev.preventDefault(); - - //Set the current action node (this is not the same as the current selected node!) - appState.setMenuState("currentNode", args.node); - - if (args.event && args.event.altKey) { - args.skipDefault = true; - } - - service.showMenu(ev, args); - }); - - mainTreeEventHandler.bind("treeNodeAltSelect", function(ev, args) { - ev.stopPropagation(); - ev.preventDefault(); - - args.skipDefault = true; - service.showMenu(ev, args); - }); - - //this reacts to tree items themselves being clicked - //the tree directive should not contain any handling, simply just bubble events - mainTreeEventHandler.bind("treeNodeSelect", function (ev, args) { - var n = args.node; - ev.stopPropagation(); - ev.preventDefault(); - - if (n.metaData && n.metaData["jsClickCallback"] && angular.isString(n.metaData["jsClickCallback"]) && n.metaData["jsClickCallback"] !== "") { - //this is a legacy tree node! - var jsPrefix = "javascript:"; - var js; - if (n.metaData["jsClickCallback"].startsWith(jsPrefix)) { - js = n.metaData["jsClickCallback"].substr(jsPrefix.length); - } - else { - js = n.metaData["jsClickCallback"]; - } - try { - var func = eval(js); - //this is normally not necessary since the eval above should execute the method and will return nothing. - if (func != null && (typeof func === "function")) { - func.call(); - } - } - catch(ex) { - $log.error("Error evaluating js callback from legacy tree node: " + ex); - } - } - else if (n.routePath) { - //add action to the history service - historyService.add({ name: n.name, link: n.routePath, icon: n.icon }); - - //put this node into the tree state - appState.setTreeState("selectedNode", args.node); - //when a node is clicked we also need to set the active menu node to this node - appState.setMenuState("currentNode", args.node); - - //not legacy, lets just set the route value and clear the query string if there is one. - $location.path(n.routePath).search(""); - } - else if (args.element.section) { - $location.path(args.element.section).search(""); - } - - service.hideNavigation(); - }); - }, - /** - * @ngdoc method - * @name umbraco.services.navigationService#syncTree - * @methodOf umbraco.services.navigationService - * - * @description - * Syncs a tree with a given path, returns a promise - * The path format is: ["itemId","itemId"], and so on - * so to sync to a specific document type node do: - *
-         * navigationService.syncTree({tree: 'content', path: ["-1","123d"], forceReload: true});
-         * 
- * @param {Object} args arguments passed to the function - * @param {String} args.tree the tree alias to sync to - * @param {Array} args.path the path to sync the tree to - * @param {Boolean} args.forceReload optional, specifies whether to force reload the node data from the server even if it already exists in the tree currently - * @param {Boolean} args.activate optional, specifies whether to set the synced node to be the active node, this will default to true if not specified - */ - syncTree: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.path) { - throw "args.path cannot be null"; - } - if (!args.tree) { - throw "args.tree cannot be null"; - } - - if (mainTreeEventHandler) { - //returns a promise - return mainTreeEventHandler.syncTree(args); - } - - //couldn't sync - return angularHelper.rejectedPromise(); - }, - - /** - Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to - have to set an active tree and then sync, the new API does this in one method by using syncTree - */ - _syncPath: function(path, forceReload) { - if (mainTreeEventHandler) { - mainTreeEventHandler.syncTree({ path: path, forceReload: forceReload }); - } - }, - - //TODO: This should return a promise - reloadNode: function(node) { - if (mainTreeEventHandler) { - mainTreeEventHandler.reloadNode(node); - } - }, - - //TODO: This should return a promise - reloadSection: function(sectionAlias) { - if (mainTreeEventHandler) { - mainTreeEventHandler.clearCache({ section: sectionAlias }); - mainTreeEventHandler.load(sectionAlias); - } - }, - - /** - Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to - have to set an active tree and then sync, the new API does this in one method by using syncTreePath - */ - _setActiveTreeType: function (treeAlias, loadChildren) { - if (mainTreeEventHandler) { - mainTreeEventHandler._setActiveTreeType(treeAlias, loadChildren); - } - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#hideTree - * @methodOf umbraco.services.navigationService - * - * @description - * Hides the tree by hiding the containing dom element - */ - hideTree: function() { - - if (appState.getGlobalState("isTablet") === true && !appState.getGlobalState("stickyNavigation")) { - //reset it to whatever is in the url - appState.setSectionState("currentSection", $routeParams.section); - setMode("default-hidesectiontree"); - } - - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#showMenu - * @methodOf umbraco.services.navigationService - * - * @description - * Hides the tree by hiding the containing dom element. - * This always returns a promise! - * - * @param {Event} event the click event triggering the method, passed from the DOM element - */ - showMenu: function(event, args) { - - var deferred = $q.defer(); - var self = this; - - treeService.getMenu({ treeNode: args.node }) - .then(function(data) { - - //check for a default - //NOTE: event will be undefined when a call to hideDialog is made so it won't re-load the default again. - // but perhaps there's a better way to deal with with an additional parameter in the args ? it works though. - if (data.defaultAlias && !args.skipDefault) { - - var found = _.find(data.menuItems, function(item) { - return item.alias = data.defaultAlias; - }); - - if (found) { - - //NOTE: This is assigning the current action node - this is not the same as the currently selected node! - appState.setMenuState("currentNode", args.node); - - //ensure the current dialog is cleared before creating another! - if (currentDialog) { - dialogService.close(currentDialog); - } - - var dialog = self.showDialog({ - node: args.node, - action: found, - section: appState.getSectionState("currentSection") - }); - - //return the dialog this is opening. - deferred.resolve(dialog); - return; - } - } - - //there is no default or we couldn't find one so just continue showing the menu - - setMode("menu"); - - appState.setMenuState("currentNode", args.node); - appState.setMenuState("menuActions", data.menuItems); - appState.setMenuState("dialogTitle", args.node.name); - - //we're not opening a dialog, return null. - deferred.resolve(null); - }); - - return deferred.promise; - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#hideMenu - * @methodOf umbraco.services.navigationService - * - * @description - * Hides the menu by hiding the containing dom element - */ - hideMenu: function() { - //SD: Would we ever want to access the last action'd node instead of clearing it here? - appState.setMenuState("currentNode", null); - appState.setMenuState("menuActions", []); - setMode("tree"); - }, - - /** Executes a given menu action */ - executeMenuAction: function (action, node, section) { - - if (!action) { - throw "action cannot be null"; - } - if (!node) { - throw "node cannot be null"; - } - if (!section) { - throw "section cannot be null"; - } - - if (action.metaData && action.metaData["actionRoute"] && angular.isString(action.metaData["actionRoute"])) { - //first check if the menu item simply navigates to a route - var parts = action.metaData["actionRoute"].split("?"); - $location.path(parts[0]).search(parts.length > 1 ? parts[1] : ""); - this.hideNavigation(); - return; - } - else if (action.metaData && action.metaData["jsAction"] && angular.isString(action.metaData["jsAction"])) { - - //we'll try to get the jsAction from the injector - var menuAction = action.metaData["jsAction"].split('.'); - if (menuAction.length !== 2) { - - //if it is not two parts long then this most likely means that it's a legacy action - var js = action.metaData["jsAction"].replace("javascript:", ""); - //there's not really a different way to acheive this except for eval - eval(js); - } - else { - var menuActionService = $injector.get(menuAction[0]); - if (!menuActionService) { - throw "The angular service " + menuAction[0] + " could not be found"; - } - - var method = menuActionService[menuAction[1]]; - - if (!method) { - throw "The method " + menuAction[1] + " on the angular service " + menuAction[0] + " could not be found"; - } - - method.apply(this, [{ - //map our content object to a basic entity to pass in to the menu handlers, - //this is required for consistency since a menu item needs to be decoupled from a tree node since the menu can - //exist standalone in the editor for which it can only pass in an entity (not tree node). - entity: umbModelMapper.convertToEntityBasic(node), - action: action, - section: section, - treeAlias: treeService.getTreeAlias(node) - }]); - } - } - else { - service.showDialog({ - node: node, - action: action, - section: section - }); - } - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#showUserDialog - * @methodOf umbraco.services.navigationService - * - * @description - * Opens the user dialog, next to the sections navigation - * template is located in views/common/dialogs/user.html - */ - showUserDialog: function () { - // hide tray and close help dialog - if (service.helpDialog) { - service.helpDialog.close(); - } - service.hideTray(); - - if (service.userDialog) { - service.userDialog.close(); - service.userDialog = undefined; - } - - service.userDialog = dialogService.open( - { - template: "views/common/dialogs/user.html", - modalClass: "umb-modal-left", - show: true - }); - - return service.userDialog; - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#showUserDialog - * @methodOf umbraco.services.navigationService - * - * @description - * Opens the user dialog, next to the sections navigation - * template is located in views/common/dialogs/user.html - */ - showHelpDialog: function () { - // hide tray and close user dialog - service.hideTray(); - if (service.userDialog) { - service.userDialog.close(); - } - - if(service.helpDialog){ - service.helpDialog.close(); - service.helpDialog = undefined; - } - - service.helpDialog = dialogService.open( - { - template: "views/common/dialogs/help.html", - modalClass: "umb-modal-left", - show: true - }); - - return service.helpDialog; - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#showDialog - * @methodOf umbraco.services.navigationService - * - * @description - * Opens a dialog, for a given action on a given tree node - * uses the dialogService to inject the selected action dialog - * into #dialog div.umb-panel-body - * the path to the dialog view is determined by: - * "views/" + current tree + "/" + action alias + ".html" - * The dialog controller will get passed a scope object that is created here with the properties: - * scope.currentNode = the selected tree node - * scope.currentAction = the selected menu item - * so that the dialog controllers can use these properties - * - * @param {Object} args arguments passed to the function - * @param {Scope} args.scope current scope passed to the dialog - * @param {Object} args.action the clicked action containing `name` and `alias` - */ - showDialog: function(args) { - - if (!args) { - throw "showDialog is missing the args parameter"; - } - if (!args.action) { - throw "The args parameter must have an 'action' property as the clicked menu action object"; - } - if (!args.node) { - throw "The args parameter must have a 'node' as the active tree node"; - } - - //ensure the current dialog is cleared before creating another! - if (currentDialog) { - dialogService.close(currentDialog); - currentDialog = null; - } - - setMode("dialog"); - - //NOTE: Set up the scope object and assign properties, this is legacy functionality but we have to live with it now. - // we should be passing in currentNode and currentAction using 'dialogData' for the dialog, not attaching it to a scope. - // This scope instance will be destroyed by the dialog so it cannot be a scope that exists outside of the dialog. - // If a scope instance has been passed in, we'll have to create a child scope of it, otherwise a new root scope. - var dialogScope = args.scope ? args.scope.$new() : $rootScope.$new(); - dialogScope.currentNode = args.node; - dialogScope.currentAction = args.action; - - //the title might be in the meta data, check there first - if (args.action.metaData["dialogTitle"]) { - appState.setMenuState("dialogTitle", args.action.metaData["dialogTitle"]); - } - else { - appState.setMenuState("dialogTitle", args.action.name); - } - - var templateUrl; - var iframe; - - if (args.action.metaData["actionUrl"]) { - templateUrl = args.action.metaData["actionUrl"]; - iframe = true; - } - else if (args.action.metaData["actionView"]) { - templateUrl = args.action.metaData["actionView"]; - iframe = false; - } - else { - - //by convention we will look into the /views/{treetype}/{action}.html - // for example: /views/content/create.html - - //we will also check for a 'packageName' for the current tree, if it exists then the convention will be: - // for example: /App_Plugins/{mypackage}/backoffice/{treetype}/create.html - - var treeAlias = treeService.getTreeAlias(args.node); - var packageTreeFolder = treeService.getTreePackageFolder(treeAlias); - - if (!treeAlias) { - throw "Could not get tree alias for node " + args.node.id; - } - - if (packageTreeFolder) { - templateUrl = Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + - "/" + packageTreeFolder + - "/backoffice/" + treeAlias + "/" + args.action.alias + ".html"; - } - else { - templateUrl = "views/" + treeAlias + "/" + args.action.alias + ".html"; - } - - iframe = false; - } - - //TODO: some action's want to launch a new window like live editing, we support this in the menu item's metadata with - // a key called: "actionUrlMethod" which can be set to either: Dialog, BlankWindow. Normally this is always set to Dialog - // if a URL is specified in the "actionUrl" metadata. For now I'm not going to implement launching in a blank window, - // though would be v-easy, just not sure we want to ever support that? - - var dialog = dialogService.open( - { - container: $("#dialog div.umb-modalcolumn-body"), - //The ONLY reason we're passing in scope to the dialogService (which is legacy functionality) is - // for backwards compatibility since many dialogs require $scope.currentNode or $scope.currentAction - // to exist - scope: dialogScope, - inline: true, - show: true, - iframe: iframe, - modalClass: "umb-dialog", - template: templateUrl, - - //These will show up on the dialog controller's $scope under dialogOptions - currentNode: args.node, - currentAction: args.action, - }); - - //save the currently assigned dialog so it can be removed before a new one is created - currentDialog = dialog; - return dialog; - }, - - /** - * @ngdoc method - * @name umbraco.services.navigationService#hideDialog - * @methodOf umbraco.services.navigationService - * - * @description - * hides the currently open dialog - */ - hideDialog: function (showMenu) { - - setMode("default"); - - if(showMenu){ - this.showMenu(undefined, { skipDefault: true, node: appState.getMenuState("currentNode") }); - } - }, - /** - * @ngdoc method - * @name umbraco.services.navigationService#showSearch - * @methodOf umbraco.services.navigationService - * - * @description - * shows the search pane - */ - showSearch: function() { - setMode("search"); - }, - /** - * @ngdoc method - * @name umbraco.services.navigationService#hideSearch - * @methodOf umbraco.services.navigationService - * - * @description - * hides the search pane - */ - hideSearch: function() { - setMode("default-hidesearch"); - }, - /** - * @ngdoc method - * @name umbraco.services.navigationService#hideNavigation - * @methodOf umbraco.services.navigationService - * - * @description - * hides any open navigation panes and resets the tree, actions and the currently selected node - */ - hideNavigation: function() { - appState.setMenuState("menuActions", []); - setMode("default"); - } - }; - - return service; -} - -angular.module('umbraco.services').factory('navigationService', navigationService); +/** + * @ngdoc service + * @name umbraco.services.navigationService + * + * @requires $rootScope + * @requires $routeParams + * @requires $log + * @requires $location + * @requires dialogService + * @requires treeService + * @requires sectionResource + * + * @description + * Service to handle the main application navigation. Responsible for invoking the tree + * Section navigation and search, and maintain their state for the entire application lifetime + * + */ +function navigationService($rootScope, $routeParams, $log, $location, $q, $timeout, $injector, dialogService, umbModelMapper, treeService, notificationsService, historyService, appState, angularHelper) { + + + //used to track the current dialog object + var currentDialog = null; + + //the main tree event handler, which gets assigned via the setupTreeEvents method + var mainTreeEventHandler = null; + //tracks the user profile dialog + var userDialog = null; + + function setMode(mode) { + switch (mode) { + case 'tree': + appState.setGlobalState("navMode", "tree"); + appState.setGlobalState("showNavigation", true); + appState.setMenuState("showMenu", false); + appState.setMenuState("showMenuDialog", false); + appState.setGlobalState("stickyNavigation", false); + appState.setGlobalState("showTray", false); + + //$("#search-form input").focus(); + break; + case 'menu': + appState.setGlobalState("navMode", "menu"); + appState.setGlobalState("showNavigation", true); + appState.setMenuState("showMenu", true); + appState.setMenuState("showMenuDialog", false); + appState.setGlobalState("stickyNavigation", true); + break; + case 'dialog': + appState.setGlobalState("navMode", "dialog"); + appState.setGlobalState("stickyNavigation", true); + appState.setGlobalState("showNavigation", true); + appState.setMenuState("showMenu", false); + appState.setMenuState("showMenuDialog", true); + break; + case 'search': + appState.setGlobalState("navMode", "search"); + appState.setGlobalState("stickyNavigation", false); + appState.setGlobalState("showNavigation", true); + appState.setMenuState("showMenu", false); + appState.setSectionState("showSearchResults", true); + appState.setMenuState("showMenuDialog", false); + + //TODO: This would be much better off in the search field controller listening to appState changes + $timeout(function() { + $("#search-field").focus(); + }); + + break; + default: + appState.setGlobalState("navMode", "default"); + appState.setMenuState("showMenu", false); + appState.setMenuState("showMenuDialog", false); + appState.setSectionState("showSearchResults", false); + appState.setGlobalState("stickyNavigation", false); + appState.setGlobalState("showTray", false); + + if (appState.getGlobalState("isTablet") === true) { + appState.setGlobalState("showNavigation", false); + } + + break; + } + } + + var service = { + + /** initializes the navigation service */ + init: function() { + + //keep track of the current section - initially this will always be undefined so + // no point in setting it now until it changes. + $rootScope.$watch(function () { + return $routeParams.section; + }, function (newVal, oldVal) { + appState.setSectionState("currentSection", newVal); + }); + + + }, + + /** + * @ngdoc method + * @name umbraco.services.navigationService#load + * @methodOf umbraco.services.navigationService + * + * @description + * Shows the legacy iframe and loads in the content based on the source url + * @param {String} source The URL to load into the iframe + */ + loadLegacyIFrame: function (source) { + $location.path("/" + appState.getSectionState("currentSection") + "/framed/" + encodeURIComponent(source)); + }, + + /** + * @ngdoc method + * @name umbraco.services.navigationService#changeSection + * @methodOf umbraco.services.navigationService + * + * @description + * Changes the active section to a given section alias + * If the navigation is 'sticky' this will load the associated tree + * and load the dashboard related to the section + * @param {string} sectionAlias The alias of the section + */ + changeSection: function(sectionAlias, force) { + setMode("default-opensection"); + + if (force && appState.getSectionState("currentSection") === sectionAlias) { + appState.setSectionState("currentSection", ""); + } + + appState.setSectionState("currentSection", sectionAlias); + this.showTree(sectionAlias); + + $location.path(sectionAlias); + }, + + /** + * @ngdoc method + * @name umbraco.services.navigationService#showTree + * @methodOf umbraco.services.navigationService + * + * @description + * Displays the tree for a given section alias but turning on the containing dom element + * only changes if the section is different from the current one + * @param {string} sectionAlias The alias of the section to load + * @param {Object} syncArgs Optional object of arguments for syncing the tree for the section being shown + */ + showTree: function (sectionAlias, syncArgs) { + if (sectionAlias !== appState.getSectionState("currentSection")) { + appState.setSectionState("currentSection", sectionAlias); + + if (syncArgs) { + this.syncTree(syncArgs); + } + } + setMode("tree"); + }, + + showTray: function () { + appState.setGlobalState("showTray", true); + }, + + hideTray: function () { + appState.setGlobalState("showTray", false); + }, + + /** + Called to assign the main tree event handler - this is called by the navigation controller. + TODO: Potentially another dev could call this which would kind of mung the whole app so potentially there's a better way. + */ + setupTreeEvents: function(treeEventHandler) { + mainTreeEventHandler = treeEventHandler; + + //when a tree is loaded into a section, we need to put it into appState + mainTreeEventHandler.bind("treeLoaded", function(ev, args) { + appState.setTreeState("currentRootNode", args.tree); + }); + + //when a tree node is synced this event will fire, this allows us to set the currentNode + mainTreeEventHandler.bind("treeSynced", function (ev, args) { + + if (args.activate === undefined || args.activate === true) { + //set the current selected node + appState.setTreeState("selectedNode", args.node); + //when a node is activated, this is the same as clicking it and we need to set the + //current menu item to be this node as well. + appState.setMenuState("currentNode", args.node); + } + }); + + //this reacts to the options item in the tree + mainTreeEventHandler.bind("treeOptionsClick", function(ev, args) { + ev.stopPropagation(); + ev.preventDefault(); + + //Set the current action node (this is not the same as the current selected node!) + appState.setMenuState("currentNode", args.node); + + if (args.event && args.event.altKey) { + args.skipDefault = true; + } + + service.showMenu(ev, args); + }); + + mainTreeEventHandler.bind("treeNodeAltSelect", function(ev, args) { + ev.stopPropagation(); + ev.preventDefault(); + + args.skipDefault = true; + service.showMenu(ev, args); + }); + + //this reacts to tree items themselves being clicked + //the tree directive should not contain any handling, simply just bubble events + mainTreeEventHandler.bind("treeNodeSelect", function (ev, args) { + var n = args.node; + ev.stopPropagation(); + ev.preventDefault(); + + if (n.metaData && n.metaData["jsClickCallback"] && angular.isString(n.metaData["jsClickCallback"]) && n.metaData["jsClickCallback"] !== "") { + //this is a legacy tree node! + var jsPrefix = "javascript:"; + var js; + if (n.metaData["jsClickCallback"].startsWith(jsPrefix)) { + js = n.metaData["jsClickCallback"].substr(jsPrefix.length); + } + else { + js = n.metaData["jsClickCallback"]; + } + try { + var func = eval(js); + //this is normally not necessary since the eval above should execute the method and will return nothing. + if (func != null && (typeof func === "function")) { + func.call(); + } + } + catch(ex) { + $log.error("Error evaluating js callback from legacy tree node: " + ex); + } + } + else if (n.routePath) { + //add action to the history service + historyService.add({ name: n.name, link: n.routePath, icon: n.icon }); + + //put this node into the tree state + appState.setTreeState("selectedNode", args.node); + //when a node is clicked we also need to set the active menu node to this node + appState.setMenuState("currentNode", args.node); + + //not legacy, lets just set the route value and clear the query string if there is one. + $location.path(n.routePath).search(""); + } + else if (args.element.section) { + $location.path(args.element.section).search(""); + } + + service.hideNavigation(); + }); + }, + /** + * @ngdoc method + * @name umbraco.services.navigationService#syncTree + * @methodOf umbraco.services.navigationService + * + * @description + * Syncs a tree with a given path, returns a promise + * The path format is: ["itemId","itemId"], and so on + * so to sync to a specific document type node do: + *
+         * navigationService.syncTree({tree: 'content', path: ["-1","123d"], forceReload: true});
+         * 
+ * @param {Object} args arguments passed to the function + * @param {String} args.tree the tree alias to sync to + * @param {Array} args.path the path to sync the tree to + * @param {Boolean} args.forceReload optional, specifies whether to force reload the node data from the server even if it already exists in the tree currently + * @param {Boolean} args.activate optional, specifies whether to set the synced node to be the active node, this will default to true if not specified + */ + syncTree: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.path) { + throw "args.path cannot be null"; + } + if (!args.tree) { + throw "args.tree cannot be null"; + } + + if (mainTreeEventHandler) { + //returns a promise + return mainTreeEventHandler.syncTree(args); + } + + //couldn't sync + return angularHelper.rejectedPromise(); + }, + + /** + Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to + have to set an active tree and then sync, the new API does this in one method by using syncTree + */ + _syncPath: function(path, forceReload) { + if (mainTreeEventHandler) { + mainTreeEventHandler.syncTree({ path: path, forceReload: forceReload }); + } + }, + + //TODO: This should return a promise + reloadNode: function(node) { + if (mainTreeEventHandler) { + mainTreeEventHandler.reloadNode(node); + } + }, + + //TODO: This should return a promise + reloadSection: function(sectionAlias) { + if (mainTreeEventHandler) { + mainTreeEventHandler.clearCache({ section: sectionAlias }); + mainTreeEventHandler.load(sectionAlias); + } + }, + + /** + Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to + have to set an active tree and then sync, the new API does this in one method by using syncTreePath + */ + _setActiveTreeType: function (treeAlias, loadChildren) { + if (mainTreeEventHandler) { + mainTreeEventHandler._setActiveTreeType(treeAlias, loadChildren); + } + }, + + /** + * @ngdoc method + * @name umbraco.services.navigationService#hideTree + * @methodOf umbraco.services.navigationService + * + * @description + * Hides the tree by hiding the containing dom element + */ + hideTree: function() { + + if (appState.getGlobalState("isTablet") === true && !appState.getGlobalState("stickyNavigation")) { + //reset it to whatever is in the url + appState.setSectionState("currentSection", $routeParams.section); + setMode("default-hidesectiontree"); + } + + }, + + /** + * @ngdoc method + * @name umbraco.services.navigationService#showMenu + * @methodOf umbraco.services.navigationService + * + * @description + * Hides the tree by hiding the containing dom element. + * This always returns a promise! + * + * @param {Event} event the click event triggering the method, passed from the DOM element + */ + showMenu: function(event, args) { + + var deferred = $q.defer(); + var self = this; + + treeService.getMenu({ treeNode: args.node }) + .then(function(data) { + + //check for a default + //NOTE: event will be undefined when a call to hideDialog is made so it won't re-load the default again. + // but perhaps there's a better way to deal with with an additional parameter in the args ? it works though. + if (data.defaultAlias && !args.skipDefault) { + + var found = _.find(data.menuItems, function(item) { + return item.alias = data.defaultAlias; + }); + + if (found) { + + //NOTE: This is assigning the current action node - this is not the same as the currently selected node! + appState.setMenuState("currentNode", args.node); + + //ensure the current dialog is cleared before creating another! + if (currentDialog) { + dialogService.close(currentDialog); + } + + var dialog = self.showDialog({ + node: args.node, + action: found, + section: appState.getSectionState("currentSection") + }); + + //return the dialog this is opening. + deferred.resolve(dialog); + return; + } + } + + //there is no default or we couldn't find one so just continue showing the menu + + setMode("menu"); + + appState.setMenuState("currentNode", args.node); + appState.setMenuState("menuActions", data.menuItems); + appState.setMenuState("dialogTitle", args.node.name); + + //we're not opening a dialog, return null. + deferred.resolve(null); + }); + + return deferred.promise; + }, + + /** + * @ngdoc method + * @name umbraco.services.navigationService#hideMenu + * @methodOf umbraco.services.navigationService + * + * @description + * Hides the menu by hiding the containing dom element + */ + hideMenu: function() { + //SD: Would we ever want to access the last action'd node instead of clearing it here? + appState.setMenuState("currentNode", null); + appState.setMenuState("menuActions", []); + setMode("tree"); + }, + + /** Executes a given menu action */ + executeMenuAction: function (action, node, section) { + + if (!action) { + throw "action cannot be null"; + } + if (!node) { + throw "node cannot be null"; + } + if (!section) { + throw "section cannot be null"; + } + + if (action.metaData && action.metaData["actionRoute"] && angular.isString(action.metaData["actionRoute"])) { + //first check if the menu item simply navigates to a route + var parts = action.metaData["actionRoute"].split("?"); + $location.path(parts[0]).search(parts.length > 1 ? parts[1] : ""); + this.hideNavigation(); + return; + } + else if (action.metaData && action.metaData["jsAction"] && angular.isString(action.metaData["jsAction"])) { + + //we'll try to get the jsAction from the injector + var menuAction = action.metaData["jsAction"].split('.'); + if (menuAction.length !== 2) { + + //if it is not two parts long then this most likely means that it's a legacy action + var js = action.metaData["jsAction"].replace("javascript:", ""); + //there's not really a different way to acheive this except for eval + eval(js); + } + else { + var menuActionService = $injector.get(menuAction[0]); + if (!menuActionService) { + throw "The angular service " + menuAction[0] + " could not be found"; + } + + var method = menuActionService[menuAction[1]]; + + if (!method) { + throw "The method " + menuAction[1] + " on the angular service " + menuAction[0] + " could not be found"; + } + + method.apply(this, [{ + //map our content object to a basic entity to pass in to the menu handlers, + //this is required for consistency since a menu item needs to be decoupled from a tree node since the menu can + //exist standalone in the editor for which it can only pass in an entity (not tree node). + entity: umbModelMapper.convertToEntityBasic(node), + action: action, + section: section, + treeAlias: treeService.getTreeAlias(node) + }]); + } + } + else { + service.showDialog({ + node: node, + action: action, + section: section + }); + } + }, + + /** + * @ngdoc method + * @name umbraco.services.navigationService#showUserDialog + * @methodOf umbraco.services.navigationService + * + * @description + * Opens the user dialog, next to the sections navigation + * template is located in views/common/dialogs/user.html + */ + showUserDialog: function () { + // hide tray and close help dialog + if (service.helpDialog) { + service.helpDialog.close(); + } + service.hideTray(); + + if (service.userDialog) { + service.userDialog.close(); + service.userDialog = undefined; + } + + service.userDialog = dialogService.open( + { + template: "views/common/dialogs/user.html", + modalClass: "umb-modal-left", + show: true + }); + + return service.userDialog; + }, + + /** + * @ngdoc method + * @name umbraco.services.navigationService#showUserDialog + * @methodOf umbraco.services.navigationService + * + * @description + * Opens the user dialog, next to the sections navigation + * template is located in views/common/dialogs/user.html + */ + showHelpDialog: function () { + // hide tray and close user dialog + service.hideTray(); + if (service.userDialog) { + service.userDialog.close(); + } + + if(service.helpDialog){ + service.helpDialog.close(); + service.helpDialog = undefined; + } + + service.helpDialog = dialogService.open( + { + template: "views/common/dialogs/help.html", + modalClass: "umb-modal-left", + show: true + }); + + return service.helpDialog; + }, + + /** + * @ngdoc method + * @name umbraco.services.navigationService#showDialog + * @methodOf umbraco.services.navigationService + * + * @description + * Opens a dialog, for a given action on a given tree node + * uses the dialogService to inject the selected action dialog + * into #dialog div.umb-panel-body + * the path to the dialog view is determined by: + * "views/" + current tree + "/" + action alias + ".html" + * The dialog controller will get passed a scope object that is created here with the properties: + * scope.currentNode = the selected tree node + * scope.currentAction = the selected menu item + * so that the dialog controllers can use these properties + * + * @param {Object} args arguments passed to the function + * @param {Scope} args.scope current scope passed to the dialog + * @param {Object} args.action the clicked action containing `name` and `alias` + */ + showDialog: function(args) { + + if (!args) { + throw "showDialog is missing the args parameter"; + } + if (!args.action) { + throw "The args parameter must have an 'action' property as the clicked menu action object"; + } + if (!args.node) { + throw "The args parameter must have a 'node' as the active tree node"; + } + + //ensure the current dialog is cleared before creating another! + if (currentDialog) { + dialogService.close(currentDialog); + currentDialog = null; + } + + setMode("dialog"); + + //NOTE: Set up the scope object and assign properties, this is legacy functionality but we have to live with it now. + // we should be passing in currentNode and currentAction using 'dialogData' for the dialog, not attaching it to a scope. + // This scope instance will be destroyed by the dialog so it cannot be a scope that exists outside of the dialog. + // If a scope instance has been passed in, we'll have to create a child scope of it, otherwise a new root scope. + var dialogScope = args.scope ? args.scope.$new() : $rootScope.$new(); + dialogScope.currentNode = args.node; + dialogScope.currentAction = args.action; + + //the title might be in the meta data, check there first + if (args.action.metaData["dialogTitle"]) { + appState.setMenuState("dialogTitle", args.action.metaData["dialogTitle"]); + } + else { + appState.setMenuState("dialogTitle", args.action.name); + } + + var templateUrl; + var iframe; + + if (args.action.metaData["actionUrl"]) { + templateUrl = args.action.metaData["actionUrl"]; + iframe = true; + } + else if (args.action.metaData["actionView"]) { + templateUrl = args.action.metaData["actionView"]; + iframe = false; + } + else { + + //by convention we will look into the /views/{treetype}/{action}.html + // for example: /views/content/create.html + + //we will also check for a 'packageName' for the current tree, if it exists then the convention will be: + // for example: /App_Plugins/{mypackage}/backoffice/{treetype}/create.html + + var treeAlias = treeService.getTreeAlias(args.node); + var packageTreeFolder = treeService.getTreePackageFolder(treeAlias); + + if (!treeAlias) { + throw "Could not get tree alias for node " + args.node.id; + } + + if (packageTreeFolder) { + templateUrl = Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + + "/" + packageTreeFolder + + "/backoffice/" + treeAlias + "/" + args.action.alias + ".html"; + } + else { + templateUrl = "views/" + treeAlias + "/" + args.action.alias + ".html"; + } + + iframe = false; + } + + //TODO: some action's want to launch a new window like live editing, we support this in the menu item's metadata with + // a key called: "actionUrlMethod" which can be set to either: Dialog, BlankWindow. Normally this is always set to Dialog + // if a URL is specified in the "actionUrl" metadata. For now I'm not going to implement launching in a blank window, + // though would be v-easy, just not sure we want to ever support that? + + var dialog = dialogService.open( + { + container: $("#dialog div.umb-modalcolumn-body"), + //The ONLY reason we're passing in scope to the dialogService (which is legacy functionality) is + // for backwards compatibility since many dialogs require $scope.currentNode or $scope.currentAction + // to exist + scope: dialogScope, + inline: true, + show: true, + iframe: iframe, + modalClass: "umb-dialog", + template: templateUrl, + + //These will show up on the dialog controller's $scope under dialogOptions + currentNode: args.node, + currentAction: args.action, + }); + + //save the currently assigned dialog so it can be removed before a new one is created + currentDialog = dialog; + return dialog; + }, + + /** + * @ngdoc method + * @name umbraco.services.navigationService#hideDialog + * @methodOf umbraco.services.navigationService + * + * @description + * hides the currently open dialog + */ + hideDialog: function (showMenu) { + + setMode("default"); + + if(showMenu){ + this.showMenu(undefined, { skipDefault: true, node: appState.getMenuState("currentNode") }); + } + }, + /** + * @ngdoc method + * @name umbraco.services.navigationService#showSearch + * @methodOf umbraco.services.navigationService + * + * @description + * shows the search pane + */ + showSearch: function() { + setMode("search"); + }, + /** + * @ngdoc method + * @name umbraco.services.navigationService#hideSearch + * @methodOf umbraco.services.navigationService + * + * @description + * hides the search pane + */ + hideSearch: function() { + setMode("default-hidesearch"); + }, + /** + * @ngdoc method + * @name umbraco.services.navigationService#hideNavigation + * @methodOf umbraco.services.navigationService + * + * @description + * hides any open navigation panes and resets the tree, actions and the currently selected node + */ + hideNavigation: function() { + appState.setMenuState("menuActions", []); + setMode("default"); + } + }; + + return service; +} + +angular.module('umbraco.services').factory('navigationService', navigationService); /** * @ngdoc service @@ -6052,43 +6052,43 @@ angular.module('umbraco.services') return service; }); -(function() { - 'use strict'; - - function overlayHelper() { - - var numberOfOverlays = 0; - - function registerOverlay() { - numberOfOverlays++; - return numberOfOverlays; - } - - function unregisterOverlay() { - numberOfOverlays--; - return numberOfOverlays; - } - - function getNumberOfOverlays() { - return numberOfOverlays; - } - - var service = { - numberOfOverlays: numberOfOverlays, - registerOverlay: registerOverlay, - unregisterOverlay: unregisterOverlay, - getNumberOfOverlays: getNumberOfOverlays - }; - - return service; - - } - - - angular.module('umbraco.services').factory('overlayHelper', overlayHelper); - - -})(); +(function() { + 'use strict'; + + function overlayHelper() { + + var numberOfOverlays = 0; + + function registerOverlay() { + numberOfOverlays++; + return numberOfOverlays; + } + + function unregisterOverlay() { + numberOfOverlays--; + return numberOfOverlays; + } + + function getNumberOfOverlays() { + return numberOfOverlays; + } + + var service = { + numberOfOverlays: numberOfOverlays, + registerOverlay: registerOverlay, + unregisterOverlay: unregisterOverlay, + getNumberOfOverlays: getNumberOfOverlays + }; + + return service; + + } + + + angular.module('umbraco.services').factory('overlayHelper', overlayHelper); + + +})(); /** * @ngdoc service @@ -8305,15 +8305,15 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ * @description * This returns a promise with an underlying http call, it is a helper method to reduce * the amount of duplicate code needed to query http resources and automatically handle any - * Http errors. See /docs/source/using-promises-resources.md + * 500 Http server errors. * - * @param {object} opts A mixed object which can either be a string representing the error message to be - * returned OR an object containing either: + * @param {object} opts A mixed object which can either be a `string` representing the error message to be + * returned OR an `object` containing either: * { success: successCallback, errorMsg: errorMessage } * OR * { success: successCallback, error: errorCallback } - * In both of the above, the successCallback must accept these parameters: data, status, headers, config - * If using the errorCallback it must accept these parameters: data, status, headers, config + * In both of the above, the successCallback must accept these parameters: `data`, `status`, `headers`, `config` + * If using the errorCallback it must accept these parameters: `data`, `status`, `headers`, `config` * The success callback must return the data which will be resolved by the deferred object. * The error callback must return an object containing: {errorMsg: errorMessage, data: originalData, status: status } */ @@ -9594,450 +9594,450 @@ angular.module('umbraco.services').factory('umbDataFormatter', umbDataFormatter) -/** - * @ngdoc service - * @name umbraco.services.windowResizeListener - * @function - * - * @description - * A single window resize listener... we don't want to have more than one in theory to ensure that - * there aren't too many events raised. This will debounce the event with 100 ms intervals and force - * a $rootScope.$apply when changed and notify all listeners - * - */ -function windowResizeListener($rootScope) { - - var WinReszier = (function () { - var registered = []; - var inited = false; - var resize = _.debounce(function(ev) { - notify(); - }, 100); - var notify = function () { - var h = $(window).height(); - var w = $(window).width(); - //execute all registrations inside of a digest - $rootScope.$apply(function() { - for (var i = 0, cnt = registered.length; i < cnt; i++) { - registered[i].apply($(window), [{ width: w, height: h }]); - } - }); - }; - return { - register: function (fn) { - registered.push(fn); - if (inited === false) { - $(window).bind('resize', resize); - inited = true; - } - }, - unregister: function (fn) { - var index = registered.indexOf(fn); - if (index > -1) { - registered.splice(index, 1); - } - } - }; - }()); - - return { - - /** - * Register a callback for resizing - * @param {Function} cb - */ - register: function (cb) { - WinReszier.register(cb); - }, - - /** - * Removes a registered callback - * @param {Function} cb - */ - unregister: function(cb) { - WinReszier.unregister(cb); - } - - }; -} +/** + * @ngdoc service + * @name umbraco.services.windowResizeListener + * @function + * + * @description + * A single window resize listener... we don't want to have more than one in theory to ensure that + * there aren't too many events raised. This will debounce the event with 100 ms intervals and force + * a $rootScope.$apply when changed and notify all listeners + * + */ +function windowResizeListener($rootScope) { + + var WinReszier = (function () { + var registered = []; + var inited = false; + var resize = _.debounce(function(ev) { + notify(); + }, 100); + var notify = function () { + var h = $(window).height(); + var w = $(window).width(); + //execute all registrations inside of a digest + $rootScope.$apply(function() { + for (var i = 0, cnt = registered.length; i < cnt; i++) { + registered[i].apply($(window), [{ width: w, height: h }]); + } + }); + }; + return { + register: function (fn) { + registered.push(fn); + if (inited === false) { + $(window).bind('resize', resize); + inited = true; + } + }, + unregister: function (fn) { + var index = registered.indexOf(fn); + if (index > -1) { + registered.splice(index, 1); + } + } + }; + }()); + + return { + + /** + * Register a callback for resizing + * @param {Function} cb + */ + register: function (cb) { + WinReszier.register(cb); + }, + + /** + * Removes a registered callback + * @param {Function} cb + */ + unregister: function(cb) { + WinReszier.unregister(cb); + } + + }; +} angular.module('umbraco.services').factory('windowResizeListener', windowResizeListener); -/** - * @ngdoc service - * @name umbraco.services.xmlhelper - * @function - * - * @description - * Used to convert legacy xml data to json and back again - */ -function xmlhelper($http) { - /* - Copyright 2011 Abdulla Abdurakhmanov - Original sources are available at https://code.google.com/p/x2js/ - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - - function X2JS() { - var VERSION = "1.0.11"; - var escapeMode = false; - - var DOMNodeTypes = { - ELEMENT_NODE: 1, - TEXT_NODE: 3, - CDATA_SECTION_NODE: 4, - DOCUMENT_NODE: 9 - }; - - function getNodeLocalName(node) { - var nodeLocalName = node.localName; - if (nodeLocalName == null) { - nodeLocalName = node.baseName; - } // Yeah, this is IE!! - - if (nodeLocalName === null || nodeLocalName === "") { - nodeLocalName = node.nodeName; - } // =="" is IE too - - return nodeLocalName; - } - - function getNodePrefix(node) { - return node.prefix; - } - - function escapeXmlChars(str) { - if (typeof (str) === "string") { - return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/'); - } else { - return str; - } - } - - function unescapeXmlChars(str) { - return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(///g, '\/'); - } - - function parseDOMChildren(node) { - var result, child, childName; - - if (node.nodeType === DOMNodeTypes.DOCUMENT_NODE) { - result = {}; - child = node.firstChild; - childName = getNodeLocalName(child); - result[childName] = parseDOMChildren(child); - return result; - } - else { - - if (node.nodeType === DOMNodeTypes.ELEMENT_NODE) { - result = {}; - result.__cnt = 0; - var nodeChildren = node.childNodes; - - // Children nodes - for (var cidx = 0; cidx < nodeChildren.length; cidx++) { - child = nodeChildren.item(cidx); // nodeChildren[cidx]; - childName = getNodeLocalName(child); - - result.__cnt++; - if (result[childName] === null) { - result[childName] = parseDOMChildren(child); - result[childName + "_asArray"] = new Array(1); - result[childName + "_asArray"][0] = result[childName]; - } - else { - if (result[childName] !== null) { - if (!(result[childName] instanceof Array)) { - var tmpObj = result[childName]; - result[childName] = []; - result[childName][0] = tmpObj; - - result[childName + "_asArray"] = result[childName]; - } - } - var aridx = 0; - while (result[childName][aridx] !== null) { - aridx++; - } - - (result[childName])[aridx] = parseDOMChildren(child); - } - } - - // Attributes - for (var aidx = 0; aidx < node.attributes.length; aidx++) { - var attr = node.attributes.item(aidx); // [aidx]; - result.__cnt++; - result["_" + attr.name] = attr.value; - } - - // Node namespace prefix - var nodePrefix = getNodePrefix(node); - if (nodePrefix !== null && nodePrefix !== "") { - result.__cnt++; - result.__prefix = nodePrefix; - } - - if (result.__cnt === 1 && result["#text"] !== null) { - result = result["#text"]; - } - - if (result["#text"] !== null) { - result.__text = result["#text"]; - if (escapeMode) { - result.__text = unescapeXmlChars(result.__text); - } - - delete result["#text"]; - delete result["#text_asArray"]; - } - if (result["#cdata-section"] != null) { - result.__cdata = result["#cdata-section"]; - delete result["#cdata-section"]; - delete result["#cdata-section_asArray"]; - } - - if (result.__text != null || result.__cdata != null) { - result.toString = function () { - return (this.__text != null ? this.__text : '') + (this.__cdata != null ? this.__cdata : ''); - }; - } - return result; - } - else { - if (node.nodeType === DOMNodeTypes.TEXT_NODE || node.nodeType === DOMNodeTypes.CDATA_SECTION_NODE) { - return node.nodeValue; - } - } - } - } - - function startTag(jsonObj, element, attrList, closed) { - var resultStr = "<" + ((jsonObj != null && jsonObj.__prefix != null) ? (jsonObj.__prefix + ":") : "") + element; - if (attrList != null) { - for (var aidx = 0; aidx < attrList.length; aidx++) { - var attrName = attrList[aidx]; - var attrVal = jsonObj[attrName]; - resultStr += " " + attrName.substr(1) + "='" + attrVal + "'"; - } - } - if (!closed) { - resultStr += ">"; - } else { - resultStr += "/>"; - } - - return resultStr; - } - - function endTag(jsonObj, elementName) { - return ""; - } - - function endsWith(str, suffix) { - return str.indexOf(suffix, str.length - suffix.length) !== -1; - } - - function jsonXmlSpecialElem(jsonObj, jsonObjField) { - if (endsWith(jsonObjField.toString(), ("_asArray")) || jsonObjField.toString().indexOf("_") === 0 || (jsonObj[jsonObjField] instanceof Function)) { - return true; - } else { - return false; - } - } - - function jsonXmlElemCount(jsonObj) { - var elementsCnt = 0; - if (jsonObj instanceof Object) { - for (var it in jsonObj) { - if (jsonXmlSpecialElem(jsonObj, it)) { - continue; - } - elementsCnt++; - } - } - return elementsCnt; - } - - function parseJSONAttributes(jsonObj) { - var attrList = []; - if (jsonObj instanceof Object) { - for (var ait in jsonObj) { - if (ait.toString().indexOf("__") === -1 && ait.toString().indexOf("_") === 0) { - attrList.push(ait); - } - } - } - - return attrList; - } - - function parseJSONTextAttrs(jsonTxtObj) { - var result = ""; - - if (jsonTxtObj.__cdata != null) { - result += ""; - } - - if (jsonTxtObj.__text != null) { - if (escapeMode) { - result += escapeXmlChars(jsonTxtObj.__text); - } else { - result += jsonTxtObj.__text; - } - } - return result; - } - - function parseJSONTextObject(jsonTxtObj) { - var result = ""; - - if (jsonTxtObj instanceof Object) { - result += parseJSONTextAttrs(jsonTxtObj); - } - else { - if (jsonTxtObj != null) { - if (escapeMode) { - result += escapeXmlChars(jsonTxtObj); - } else { - result += jsonTxtObj; - } - } - } - - - return result; - } - - function parseJSONArray(jsonArrRoot, jsonArrObj, attrList) { - var result = ""; - if (jsonArrRoot.length === 0) { - result += startTag(jsonArrRoot, jsonArrObj, attrList, true); - } - else { - for (var arIdx = 0; arIdx < jsonArrRoot.length; arIdx++) { - result += startTag(jsonArrRoot[arIdx], jsonArrObj, parseJSONAttributes(jsonArrRoot[arIdx]), false); - result += parseJSONObject(jsonArrRoot[arIdx]); - result += endTag(jsonArrRoot[arIdx], jsonArrObj); - } - } - return result; - } - - function parseJSONObject(jsonObj) { - var result = ""; - - var elementsCnt = jsonXmlElemCount(jsonObj); - - if (elementsCnt > 0) { - for (var it in jsonObj) { - if (jsonXmlSpecialElem(jsonObj, it)) { - continue; - } - - var subObj = jsonObj[it]; - var attrList = parseJSONAttributes(subObj); - - if (subObj === null || subObj === undefined) { - result += startTag(subObj, it, attrList, true); - } else { - if (subObj instanceof Object) { - - if (subObj instanceof Array) { - result += parseJSONArray(subObj, it, attrList); - } else { - var subObjElementsCnt = jsonXmlElemCount(subObj); - if (subObjElementsCnt > 0 || subObj.__text !== null || subObj.__cdata !== null) { - result += startTag(subObj, it, attrList, false); - result += parseJSONObject(subObj); - result += endTag(subObj, it); - } else { - result += startTag(subObj, it, attrList, true); - } - } - - } else { - result += startTag(subObj, it, attrList, false); - result += parseJSONTextObject(subObj); - result += endTag(subObj, it); - } - } - } - } - result += parseJSONTextObject(jsonObj); - - return result; - } - - this.parseXmlString = function (xmlDocStr) { - var xmlDoc; - if (window.DOMParser) { - var parser = new window.DOMParser(); - xmlDoc = parser.parseFromString(xmlDocStr, "text/xml"); - } - else { - // IE :( - if (xmlDocStr.indexOf("") + 2); - } - xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); - xmlDoc.async = "false"; - xmlDoc.loadXML(xmlDocStr); - } - return xmlDoc; - }; - - this.xml2json = function (xmlDoc) { - return parseDOMChildren(xmlDoc); - }; - - this.xml_str2json = function (xmlDocStr) { - var xmlDoc = this.parseXmlString(xmlDocStr); - return this.xml2json(xmlDoc); - }; - - this.json2xml_str = function (jsonObj) { - return parseJSONObject(jsonObj); - }; - - this.json2xml = function (jsonObj) { - var xmlDocStr = this.json2xml_str(jsonObj); - return this.parseXmlString(xmlDocStr); - }; - - this.getVersion = function () { - return VERSION; - }; - - this.escapeMode = function (enabled) { - escapeMode = enabled; - }; - } - - var x2js = new X2JS(); - return { - /** Called to load in the legacy tree js which is required on startup if a user is logged in or - after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. */ - toJson: function (xml) { - var json = x2js.xml_str2json(xml); - return json; - }, - fromJson: function (json) { - var xml = x2js.json2xml_str(json); - return xml; - } - }; -} +/** + * @ngdoc service + * @name umbraco.services.xmlhelper + * @function + * + * @description + * Used to convert legacy xml data to json and back again + */ +function xmlhelper($http) { + /* + Copyright 2011 Abdulla Abdurakhmanov + Original sources are available at https://code.google.com/p/x2js/ + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + + function X2JS() { + var VERSION = "1.0.11"; + var escapeMode = false; + + var DOMNodeTypes = { + ELEMENT_NODE: 1, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + DOCUMENT_NODE: 9 + }; + + function getNodeLocalName(node) { + var nodeLocalName = node.localName; + if (nodeLocalName == null) { + nodeLocalName = node.baseName; + } // Yeah, this is IE!! + + if (nodeLocalName === null || nodeLocalName === "") { + nodeLocalName = node.nodeName; + } // =="" is IE too + + return nodeLocalName; + } + + function getNodePrefix(node) { + return node.prefix; + } + + function escapeXmlChars(str) { + if (typeof (str) === "string") { + return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/'); + } else { + return str; + } + } + + function unescapeXmlChars(str) { + return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(///g, '\/'); + } + + function parseDOMChildren(node) { + var result, child, childName; + + if (node.nodeType === DOMNodeTypes.DOCUMENT_NODE) { + result = {}; + child = node.firstChild; + childName = getNodeLocalName(child); + result[childName] = parseDOMChildren(child); + return result; + } + else { + + if (node.nodeType === DOMNodeTypes.ELEMENT_NODE) { + result = {}; + result.__cnt = 0; + var nodeChildren = node.childNodes; + + // Children nodes + for (var cidx = 0; cidx < nodeChildren.length; cidx++) { + child = nodeChildren.item(cidx); // nodeChildren[cidx]; + childName = getNodeLocalName(child); + + result.__cnt++; + if (result[childName] === null) { + result[childName] = parseDOMChildren(child); + result[childName + "_asArray"] = new Array(1); + result[childName + "_asArray"][0] = result[childName]; + } + else { + if (result[childName] !== null) { + if (!(result[childName] instanceof Array)) { + var tmpObj = result[childName]; + result[childName] = []; + result[childName][0] = tmpObj; + + result[childName + "_asArray"] = result[childName]; + } + } + var aridx = 0; + while (result[childName][aridx] !== null) { + aridx++; + } + + (result[childName])[aridx] = parseDOMChildren(child); + } + } + + // Attributes + for (var aidx = 0; aidx < node.attributes.length; aidx++) { + var attr = node.attributes.item(aidx); // [aidx]; + result.__cnt++; + result["_" + attr.name] = attr.value; + } + + // Node namespace prefix + var nodePrefix = getNodePrefix(node); + if (nodePrefix !== null && nodePrefix !== "") { + result.__cnt++; + result.__prefix = nodePrefix; + } + + if (result.__cnt === 1 && result["#text"] !== null) { + result = result["#text"]; + } + + if (result["#text"] !== null) { + result.__text = result["#text"]; + if (escapeMode) { + result.__text = unescapeXmlChars(result.__text); + } + + delete result["#text"]; + delete result["#text_asArray"]; + } + if (result["#cdata-section"] != null) { + result.__cdata = result["#cdata-section"]; + delete result["#cdata-section"]; + delete result["#cdata-section_asArray"]; + } + + if (result.__text != null || result.__cdata != null) { + result.toString = function () { + return (this.__text != null ? this.__text : '') + (this.__cdata != null ? this.__cdata : ''); + }; + } + return result; + } + else { + if (node.nodeType === DOMNodeTypes.TEXT_NODE || node.nodeType === DOMNodeTypes.CDATA_SECTION_NODE) { + return node.nodeValue; + } + } + } + } + + function startTag(jsonObj, element, attrList, closed) { + var resultStr = "<" + ((jsonObj != null && jsonObj.__prefix != null) ? (jsonObj.__prefix + ":") : "") + element; + if (attrList != null) { + for (var aidx = 0; aidx < attrList.length; aidx++) { + var attrName = attrList[aidx]; + var attrVal = jsonObj[attrName]; + resultStr += " " + attrName.substr(1) + "='" + attrVal + "'"; + } + } + if (!closed) { + resultStr += ">"; + } else { + resultStr += "/>"; + } + + return resultStr; + } + + function endTag(jsonObj, elementName) { + return ""; + } + + function endsWith(str, suffix) { + return str.indexOf(suffix, str.length - suffix.length) !== -1; + } + + function jsonXmlSpecialElem(jsonObj, jsonObjField) { + if (endsWith(jsonObjField.toString(), ("_asArray")) || jsonObjField.toString().indexOf("_") === 0 || (jsonObj[jsonObjField] instanceof Function)) { + return true; + } else { + return false; + } + } + + function jsonXmlElemCount(jsonObj) { + var elementsCnt = 0; + if (jsonObj instanceof Object) { + for (var it in jsonObj) { + if (jsonXmlSpecialElem(jsonObj, it)) { + continue; + } + elementsCnt++; + } + } + return elementsCnt; + } + + function parseJSONAttributes(jsonObj) { + var attrList = []; + if (jsonObj instanceof Object) { + for (var ait in jsonObj) { + if (ait.toString().indexOf("__") === -1 && ait.toString().indexOf("_") === 0) { + attrList.push(ait); + } + } + } + + return attrList; + } + + function parseJSONTextAttrs(jsonTxtObj) { + var result = ""; + + if (jsonTxtObj.__cdata != null) { + result += ""; + } + + if (jsonTxtObj.__text != null) { + if (escapeMode) { + result += escapeXmlChars(jsonTxtObj.__text); + } else { + result += jsonTxtObj.__text; + } + } + return result; + } + + function parseJSONTextObject(jsonTxtObj) { + var result = ""; + + if (jsonTxtObj instanceof Object) { + result += parseJSONTextAttrs(jsonTxtObj); + } + else { + if (jsonTxtObj != null) { + if (escapeMode) { + result += escapeXmlChars(jsonTxtObj); + } else { + result += jsonTxtObj; + } + } + } + + + return result; + } + + function parseJSONArray(jsonArrRoot, jsonArrObj, attrList) { + var result = ""; + if (jsonArrRoot.length === 0) { + result += startTag(jsonArrRoot, jsonArrObj, attrList, true); + } + else { + for (var arIdx = 0; arIdx < jsonArrRoot.length; arIdx++) { + result += startTag(jsonArrRoot[arIdx], jsonArrObj, parseJSONAttributes(jsonArrRoot[arIdx]), false); + result += parseJSONObject(jsonArrRoot[arIdx]); + result += endTag(jsonArrRoot[arIdx], jsonArrObj); + } + } + return result; + } + + function parseJSONObject(jsonObj) { + var result = ""; + + var elementsCnt = jsonXmlElemCount(jsonObj); + + if (elementsCnt > 0) { + for (var it in jsonObj) { + if (jsonXmlSpecialElem(jsonObj, it)) { + continue; + } + + var subObj = jsonObj[it]; + var attrList = parseJSONAttributes(subObj); + + if (subObj === null || subObj === undefined) { + result += startTag(subObj, it, attrList, true); + } else { + if (subObj instanceof Object) { + + if (subObj instanceof Array) { + result += parseJSONArray(subObj, it, attrList); + } else { + var subObjElementsCnt = jsonXmlElemCount(subObj); + if (subObjElementsCnt > 0 || subObj.__text !== null || subObj.__cdata !== null) { + result += startTag(subObj, it, attrList, false); + result += parseJSONObject(subObj); + result += endTag(subObj, it); + } else { + result += startTag(subObj, it, attrList, true); + } + } + + } else { + result += startTag(subObj, it, attrList, false); + result += parseJSONTextObject(subObj); + result += endTag(subObj, it); + } + } + } + } + result += parseJSONTextObject(jsonObj); + + return result; + } + + this.parseXmlString = function (xmlDocStr) { + var xmlDoc; + if (window.DOMParser) { + var parser = new window.DOMParser(); + xmlDoc = parser.parseFromString(xmlDocStr, "text/xml"); + } + else { + // IE :( + if (xmlDocStr.indexOf("") + 2); + } + xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); + xmlDoc.async = "false"; + xmlDoc.loadXML(xmlDocStr); + } + return xmlDoc; + }; + + this.xml2json = function (xmlDoc) { + return parseDOMChildren(xmlDoc); + }; + + this.xml_str2json = function (xmlDocStr) { + var xmlDoc = this.parseXmlString(xmlDocStr); + return this.xml2json(xmlDoc); + }; + + this.json2xml_str = function (jsonObj) { + return parseJSONObject(jsonObj); + }; + + this.json2xml = function (jsonObj) { + var xmlDocStr = this.json2xml_str(jsonObj); + return this.parseXmlString(xmlDocStr); + }; + + this.getVersion = function () { + return VERSION; + }; + + this.escapeMode = function (enabled) { + escapeMode = enabled; + }; + } + + var x2js = new X2JS(); + return { + /** Called to load in the legacy tree js which is required on startup if a user is logged in or + after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. */ + toJson: function (xml) { + var json = x2js.xml_str2json(xml); + return json; + }, + fromJson: function (json) { + var xml = x2js.json2xml_str(json); + return xml; + } + }; +} angular.module('umbraco.services').factory('xmlhelper', xmlhelper); })(); \ No newline at end of file diff --git a/OurUmbraco.Site/umbraco/js/umbraco.testing.js b/OurUmbraco.Site/umbraco/js/umbraco.testing.js index eed826e8..812904e3 100644 --- a/OurUmbraco.Site/umbraco/js/umbraco.testing.js +++ b/OurUmbraco.Site/umbraco/js/umbraco.testing.js @@ -1,12 +1,12 @@ /*! umbraco * https://github.com/umbraco/umbraco-cms/ - * Copyright (c) 2016 Umbraco HQ; + * Copyright (c) 2017 Umbraco HQ; * Licensed */ (function() { -angular.module("umbraco.mocks", ['ngCookies']); +angular.module("umbraco.mocks", ['ngCookies']); angular.module("umbraco.mocks.services", []); angular.module('umbraco.mocks'). factory('prevaluesMocks', ['$httpBackend', 'mocksUtils', function ($httpBackend, mocksUtils) { @@ -1318,37 +1318,37 @@ angular.module('umbraco.mocks'). } }; }]); -angular.module('umbraco.mocks.services') -.factory('assetsService', function ($q) { - - return { - loadCss : function(path, scope, attributes, timeout){ - var deferred = $q.defer(); - deferred.resolve(); - return deferred.promise; - }, - loadJs : function(path, scope, attributes, timeout){ - var deferred = $q.defer(); - - if(path[0] !== "/"){ - path = "/" + path; - } - - $.getScript( "base" + path, function( data, textStatus, jqxhr ) { - deferred.resolve(); - }); - - return deferred.promise; - } - }; +angular.module('umbraco.mocks.services') +.factory('assetsService', function ($q) { + + return { + loadCss : function(path, scope, attributes, timeout){ + var deferred = $q.defer(); + deferred.resolve(); + return deferred.promise; + }, + loadJs : function(path, scope, attributes, timeout){ + var deferred = $q.defer(); + + if(path[0] !== "/"){ + path = "/" + path; + } + + $.getScript( "base" + path, function( data, textStatus, jqxhr ) { + deferred.resolve(); + }); + + return deferred.promise; + } + }; }); -angular.module("umbraco.mocks").factory('externalLoginInfo', - function () { - return { - errors: [], - providers: [] - }; - } +angular.module("umbraco.mocks").factory('externalLoginInfo', + function () { + return { + errors: [], + providers: [] + }; + } ); angular.module('umbraco.mocks'). factory('localizationMocks', ['$httpBackend', 'mocksUtils', function ($httpBackend, mocksUtils) { @@ -2118,146 +2118,146 @@ angular.module('umbraco.mocks'). } }; }]); -/** -* @ngdoc service -* @name umbraco.mocks.mediaHelperService -* @description A helper object used for dealing with media items -**/ -function mediaHelper(umbRequestHelper) { - return { - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#getImagePropertyValue - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns the file path associated with the media property if there is one - * - * @param {object} options Options object - * @param {object} options.mediaModel The media object to retrieve the image path from - * @param {object} options.imageOnly Optional, if true then will only return a path if the media item is an image - */ - getMediaPropertyValue: function (options) { - return "assets/img/mocks/big-image.jpg"; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#getImagePropertyValue - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns the actual image path associated with the image property if there is one - * - * @param {object} options Options object - * @param {object} options.imageModel The media object to retrieve the image path from - */ - getImagePropertyValue: function (options) { - return "assets/img/mocks/big-image.jpg"; - }, - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#getThumbnail - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * formats the display model used to display the content to the model used to save the content - * - * @param {object} options Options object - * @param {object} options.imageModel The media object to retrieve the image path from - */ - getThumbnail: function (options) { - - if (!options || !options.imageModel) { - throw "The options objet does not contain the required parameters: imageModel"; - } - - var imagePropVal = this.getImagePropertyValue(options); - if (imagePropVal !== "") { - return this.getThumbnailFromPath(imagePropVal); - } - return ""; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#scaleToMaxSize - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Finds the corrct max width and max height, given maximum dimensions and keeping aspect ratios - * - * @param {number} maxSize Maximum width & height - * @param {number} width Current width - * @param {number} height Current height - */ - scaleToMaxSize: function (maxSize, width, height) { - var retval = { width: width, height: height }; - - var maxWidth = maxSize; // Max width for the image - var maxHeight = maxSize; // Max height for the image - var ratio = 0; // Used for aspect ratio - - // Check if the current width is larger than the max - if (width > maxWidth) { - ratio = maxWidth / width; // get ratio for scaling image - - retval.width = maxWidth; - retval.height = height * ratio; - - height = height * ratio; // Reset height to match scaled image - width = width * ratio; // Reset width to match scaled image - } - - // Check if current height is larger than max - if (height > maxHeight) { - ratio = maxHeight / height; // get ratio for scaling image - - retval.height = maxHeight; - retval.width = width * ratio; - width = width * ratio; // Reset width to match scaled image - } - - return retval; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#getThumbnailFromPath - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns the path to the thumbnail version of a given media library image path - * - * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg - */ - getThumbnailFromPath: function (imagePath) { - return "assets/img/mocks/big-thumb.jpg"; - }, - - /** - * @ngdoc function - * @name umbraco.services.mediaHelper#detectIfImageByExtension - * @methodOf umbraco.services.mediaHelper - * @function - * - * @description - * Returns true/false, indicating if the given path has an allowed image extension - * - * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg - */ - detectIfImageByExtension: function (imagePath) { - var lowered = imagePath.toLowerCase(); - var ext = lowered.substr(lowered.lastIndexOf(".") + 1); - return ("," + Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes + ",").indexOf("," + ext + ",") !== -1; - } - }; -} +/** +* @ngdoc service +* @name umbraco.mocks.mediaHelperService +* @description A helper object used for dealing with media items +**/ +function mediaHelper(umbRequestHelper) { + return { + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#getImagePropertyValue + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Returns the file path associated with the media property if there is one + * + * @param {object} options Options object + * @param {object} options.mediaModel The media object to retrieve the image path from + * @param {object} options.imageOnly Optional, if true then will only return a path if the media item is an image + */ + getMediaPropertyValue: function (options) { + return "assets/img/mocks/big-image.jpg"; + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#getImagePropertyValue + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Returns the actual image path associated with the image property if there is one + * + * @param {object} options Options object + * @param {object} options.imageModel The media object to retrieve the image path from + */ + getImagePropertyValue: function (options) { + return "assets/img/mocks/big-image.jpg"; + }, + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#getThumbnail + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * formats the display model used to display the content to the model used to save the content + * + * @param {object} options Options object + * @param {object} options.imageModel The media object to retrieve the image path from + */ + getThumbnail: function (options) { + + if (!options || !options.imageModel) { + throw "The options objet does not contain the required parameters: imageModel"; + } + + var imagePropVal = this.getImagePropertyValue(options); + if (imagePropVal !== "") { + return this.getThumbnailFromPath(imagePropVal); + } + return ""; + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#scaleToMaxSize + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Finds the corrct max width and max height, given maximum dimensions and keeping aspect ratios + * + * @param {number} maxSize Maximum width & height + * @param {number} width Current width + * @param {number} height Current height + */ + scaleToMaxSize: function (maxSize, width, height) { + var retval = { width: width, height: height }; + + var maxWidth = maxSize; // Max width for the image + var maxHeight = maxSize; // Max height for the image + var ratio = 0; // Used for aspect ratio + + // Check if the current width is larger than the max + if (width > maxWidth) { + ratio = maxWidth / width; // get ratio for scaling image + + retval.width = maxWidth; + retval.height = height * ratio; + + height = height * ratio; // Reset height to match scaled image + width = width * ratio; // Reset width to match scaled image + } + + // Check if current height is larger than max + if (height > maxHeight) { + ratio = maxHeight / height; // get ratio for scaling image + + retval.height = maxHeight; + retval.width = width * ratio; + width = width * ratio; // Reset width to match scaled image + } + + return retval; + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#getThumbnailFromPath + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Returns the path to the thumbnail version of a given media library image path + * + * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg + */ + getThumbnailFromPath: function (imagePath) { + return "assets/img/mocks/big-thumb.jpg"; + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#detectIfImageByExtension + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Returns true/false, indicating if the given path has an allowed image extension + * + * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg + */ + detectIfImageByExtension: function (imagePath) { + var lowered = imagePath.toLowerCase(); + var ext = lowered.substr(lowered.lastIndexOf(".") + 1); + return ("," + Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes + ",").indexOf("," + ext + ",") !== -1; + } + }; +} angular.module('umbraco.mocks').factory('mediaHelper', mediaHelper); angular.module('umbraco.mocks'). factory('utilMocks', ['$httpBackend', 'mocksUtils', function ($httpBackend, mocksUtils) { diff --git a/OurUmbraco.Site/umbraco/lib/angular/angular.js b/OurUmbraco.Site/umbraco/lib/angular/angular.js index 400c7740..0f3f3858 100644 --- a/OurUmbraco.Site/umbraco/lib/angular/angular.js +++ b/OurUmbraco.Site/umbraco/lib/angular/angular.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.5.9 + * @license AngularJS v1.6.1 * (c) 2010-2016 Google, Inc. http://angularjs.org * License: MIT */ @@ -57,7 +57,7 @@ function minErr(module, ErrorConstructor) { return match; }); - message += '\nhttp://errors.angularjs.org/1.5.9/' + + message += '\nhttp://errors.angularjs.org/1.6.1/' + (module ? module + '/' : '') + code; for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { @@ -157,6 +157,7 @@ function minErr(module, ErrorConstructor) { getBlockNodes, hasOwnProperty, createMap, + stringify, NODE_TYPE_ELEMENT, NODE_TYPE_ATTRIBUTE, @@ -190,9 +191,41 @@ var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; // This is used so that it's possible for internal tests to create mock ValidityStates. var VALIDITY_STATE_PROPERTY = 'validity'; + var hasOwnProperty = Object.prototype.hasOwnProperty; +/** + * @ngdoc function + * @name angular.lowercase + * @module ng + * @kind function + * + * @deprecated + * sinceVersion="1.5.0" + * removeVersion="1.7.0" + * Use [String.prototype.toLowerCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase) instead. + * + * @description Converts the specified string to lowercase. + * @param {string} string String to be converted to lowercase. + * @returns {string} Lowercased string. + */ var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;}; + +/** + * @ngdoc function + * @name angular.uppercase + * @module ng + * @kind function + * + * @deprecated + * sinceVersion="1.5.0" + * removeVersion="1.7.0" + * Use [String.prototype.toUpperCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase) instead. + * + * @description Converts the specified string to uppercase. + * @param {string} string String to be converted to uppercase. + * @returns {string} Uppercased string. + */ var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;}; @@ -237,6 +270,7 @@ var angularModule, uid = 0; +// Support: IE 9-11 only /** * documentMode is an IE-only property * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx @@ -312,9 +346,7 @@ function forEach(obj, iterator, context) { if (obj) { if (isFunction(obj)) { for (key in obj) { - // Need to check if hasOwnProperty exists, - // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function - if (key !== 'prototype' && key !== 'length' && key !== 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) { + if (key !== 'prototype' && key !== 'length' && key !== 'name' && obj.hasOwnProperty(key)) { iterator.call(context, obj[key], key, obj); } } @@ -671,7 +703,7 @@ function isDate(value) { * @kind function * * @description - * Determines if a reference is an `Array`. + * Determines if a reference is an `Array`. Alias of Array.isArray. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. @@ -747,7 +779,7 @@ function isPromiseLike(obj) { } -var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/; +var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array]$/; function isTypedArray(value) { return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value)); } @@ -766,7 +798,7 @@ var trim = function(value) { // Prereq: s is a string. var escapeForRegexp = function(s) { return s - .replace(/([-()\[\]{}+?*.$\^|,:#]+>)/)[1]. - replace(/^<([\w\-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);}); + replace(/^<([\w-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);}); } catch (e) { return lowercase(elemHtml); } @@ -1484,7 +1517,7 @@ function encodeUriSegment(val) { * This method is intended for encoding *key* or *value* parts of query component. We need a custom * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be * encoded per http://tools.ietf.org/html/rfc3986: - * query = *( pchar / "/" / "?" ) + * query = *( pchar / "/" / "?" ) * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * pct-encoded = "%" HEXDIG HEXDIG @@ -1521,12 +1554,14 @@ function allowAutoBootstrap(document) { var src = document.currentScript.getAttribute('src'); var link = document.createElement('a'); link.href = src; - var scriptProtocol = link.protocol; - var docLoadProtocol = document.location.protocol; - if (docLoadProtocol === scriptProtocol) { + if (document.location.origin === link.origin) { + // Same-origin resources are always allowed, even for non-whitelisted schemes. return true; } - switch (scriptProtocol) { + // Disabled bootstrapping unless angular.js was loaded from a known scheme used on the web. + // This is to prevent angular.js bundled with browser extensions from being used to bypass the + // content security policy in web pages and other browser extensions. + switch (link.protocol) { case 'http:': case 'https:': case 'ftp:': @@ -2026,6 +2061,27 @@ function createMap() { return Object.create(null); } +function stringify(value) { + if (value == null) { // null || undefined + return ''; + } + switch (typeof value) { + case 'string': + break; + case 'number': + value = '' + value; + break; + default: + if (hasCustomToString(value) && !isArray(value) && !isDate(value)) { + value = value.toString(); + } else { + value = toJson(value); + } + } + + return value; +} + var NODE_TYPE_ELEMENT = 1; var NODE_TYPE_ATTRIBUTE = 2; var NODE_TYPE_TEXT = 3; @@ -2236,7 +2292,7 @@ function setupModuleLoader(window) { * @description * See {@link auto.$provide#decorator $provide.decorator()}. */ - decorator: invokeLaterAndSetModuleName('$provide', 'decorator'), + decorator: invokeLaterAndSetModuleName('$provide', 'decorator', configBlocks), /** * @ngdoc method @@ -2382,10 +2438,11 @@ function setupModuleLoader(window) { * @param {string} method * @returns {angular.Module} */ - function invokeLaterAndSetModuleName(provider, method) { + function invokeLaterAndSetModuleName(provider, method, queue) { + if (!queue) queue = invokeQueue; return function(recipeName, factoryFunction) { if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name; - invokeQueue.push([provider, method, arguments]); + queue.push([provider, method, arguments]); return moduleInstance; }; } @@ -2514,6 +2571,7 @@ function toDebugString(obj) { $ControllerProvider, $DateProvider, $DocumentProvider, + $$IsDocumentHiddenProvider, $ExceptionHandlerProvider, $FilterProvider, $$ForceReflowProvider, @@ -2565,11 +2623,11 @@ function toDebugString(obj) { var version = { // These placeholder strings will be replaced by grunt's `build` task. // They need to be double- or single-quoted. - full: '1.5.9', + full: '1.6.1', major: 1, - minor: 5, - dot: 9, - codeName: 'timeturning-lockdown' + minor: 6, + dot: 1, + codeName: 'promise-rectification' }; @@ -2602,9 +2660,12 @@ function publishExternalAPI(angular) { 'uppercase': uppercase, 'callbacks': {$$counter: 0}, 'getTestability': getTestability, + 'reloadWithDebugInfo': reloadWithDebugInfo, '$$minErr': minErr, '$$csp': csp, - 'reloadWithDebugInfo': reloadWithDebugInfo + '$$encodeUriSegment': encodeUriSegment, + '$$encodeUriQuery': encodeUriQuery, + '$$stringify': stringify }); angularModule = setupModuleLoader(window); @@ -2678,6 +2739,7 @@ function publishExternalAPI(angular) { $cacheFactory: $CacheFactoryProvider, $controller: $ControllerProvider, $document: $DocumentProvider, + $$isDocumentHidden: $$IsDocumentHiddenProvider, $exceptionHandler: $ExceptionHandlerProvider, $filter: $FilterProvider, $$forceReflow: $$ForceReflowProvider, @@ -2723,9 +2785,8 @@ function publishExternalAPI(angular) { * Or gives undesired access to variables likes document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -/* global JQLitePrototype: true, - addEventListenerFn: true, - removeEventListenerFn: true, +/* global + JQLitePrototype: true, BOOLEAN_ATTR: true, ALIASED_ATTR: true */ @@ -2769,7 +2830,7 @@ function publishExternalAPI(angular) { * - [`after()`](http://api.jquery.com/after/) * - [`append()`](http://api.jquery.com/append/) * - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters - * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData + * - [`bind()`](http://api.jquery.com/bind/) (_deprecated_, use [`on()`](http://api.jquery.com/on/)) - Does not support namespaces, selectors or eventData * - [`children()`](http://api.jquery.com/children/) - Does not support selectors * - [`clone()`](http://api.jquery.com/clone/) * - [`contents()`](http://api.jquery.com/contents/) @@ -2789,16 +2850,16 @@ function publishExternalAPI(angular) { * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors * - [`prepend()`](http://api.jquery.com/prepend/) * - [`prop()`](http://api.jquery.com/prop/) - * - [`ready()`](http://api.jquery.com/ready/) + * - [`ready()`](http://api.jquery.com/ready/) (_deprecated_, use `angular.element(callback)` instead of `angular.element(document).ready(callback)`) * - [`remove()`](http://api.jquery.com/remove/) - * - [`removeAttr()`](http://api.jquery.com/removeAttr/) + * - [`removeAttr()`](http://api.jquery.com/removeAttr/) - Does not support multiple attributes * - [`removeClass()`](http://api.jquery.com/removeClass/) - Does not support a function as first argument * - [`removeData()`](http://api.jquery.com/removeData/) * - [`replaceWith()`](http://api.jquery.com/replaceWith/) * - [`text()`](http://api.jquery.com/text/) * - [`toggleClass()`](http://api.jquery.com/toggleClass/) - Does not support a function as first argument * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers - * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces or event object as parameter + * - [`unbind()`](http://api.jquery.com/unbind/) (_deprecated_, use [`off()`](http://api.jquery.com/off/)) - Does not support namespaces or event object as parameter * - [`val()`](http://api.jquery.com/val/) * - [`wrap()`](http://api.jquery.com/wrap/) * @@ -2836,13 +2897,7 @@ function publishExternalAPI(angular) { JQLite.expando = 'ng339'; var jqCache = JQLite.cache = {}, - jqId = 1, - addEventListenerFn = function(element, type, fn) { - element.addEventListener(type, fn, false); - }, - removeEventListenerFn = function(element, type, fn) { - element.removeEventListener(type, fn, false); - }; + jqId = 1; /* * !!! This is an undocumented "private" function !!! @@ -2855,22 +2910,31 @@ JQLite._data = function(node) { function jqNextId() { return ++jqId; } -var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g; -var MOZ_HACK_REGEXP = /^moz([A-Z])/; +var DASH_LOWERCASE_REGEXP = /-([a-z])/g; +var MS_HACK_REGEXP = /^-ms-/; var MOUSE_EVENT_MAP = { mouseleave: 'mouseout', mouseenter: 'mouseover' }; var jqLiteMinErr = minErr('jqLite'); /** - * Converts snake_case to camelCase. - * Also there is special case for Moz prefix starting with upper case letter. + * Converts kebab-case to camelCase. + * There is also a special case for the ms prefix starting with a lowercase letter. * @param name Name to normalize */ -function camelCase(name) { - return name. - replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { - return offset ? letter.toUpperCase() : letter; - }). - replace(MOZ_HACK_REGEXP, 'Moz$1'); +function cssKebabToCamel(name) { + return kebabToCamel(name.replace(MS_HACK_REGEXP, 'ms-')); +} + +function fnCamelCaseReplace(all, letter) { + return letter.toUpperCase(); +} + +/** + * Converts kebab-case to camelCase. + * @param name Name to normalize + */ +function kebabToCamel(name) { + return name + .replace(DASH_LOWERCASE_REGEXP, fnCamelCaseReplace); } var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/; @@ -3007,6 +3071,8 @@ function JQLite(element) { if (argIsString) { jqLiteAddNodes(this, jqLiteParseHTML(element)); + } else if (isFunction(element)) { + jqLiteReady(element); } else { jqLiteAddNodes(this, element); } @@ -3039,7 +3105,7 @@ function jqLiteOff(element, type, fn, unsupported) { if (!type) { for (type in events) { if (type !== '$destroy') { - removeEventListenerFn(element, type, handle); + element.removeEventListener(type, handle); } delete events[type]; } @@ -3051,7 +3117,7 @@ function jqLiteOff(element, type, fn, unsupported) { arrayRemove(listenerFns || [], fn); } if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) { - removeEventListenerFn(element, type, handle); + element.removeEventListener(type, handle); delete events[type]; } }; @@ -3102,6 +3168,7 @@ function jqLiteExpandoStore(element, createIfNecessary) { function jqLiteData(element, key, value) { if (jqLiteAcceptsData(element)) { + var prop; var isSimpleSetter = isDefined(value); var isSimpleGetter = !isSimpleSetter && key && !isObject(key); @@ -3110,16 +3177,18 @@ function jqLiteData(element, key, value) { var data = expandoStore && expandoStore.data; if (isSimpleSetter) { // data('key', value) - data[key] = value; + data[kebabToCamel(key)] = value; } else { if (massGetter) { // data() return data; } else { if (isSimpleGetter) { // data('key') // don't force creation of expandoStore if it doesn't exist yet - return data && data[key]; + return data && data[kebabToCamel(key)]; } else { // mass-setter: data({key1: val1, key2: val2}) - extend(data, key); + for (prop in key) { + data[kebabToCamel(prop)] = key[prop]; + } } } } @@ -3238,29 +3307,32 @@ function jqLiteDocumentLoaded(action, win) { } } +function jqLiteReady(fn) { + function trigger() { + window.document.removeEventListener('DOMContentLoaded', trigger); + window.removeEventListener('load', trigger); + fn(); + } + + // check if document is already loaded + if (window.document.readyState === 'complete') { + window.setTimeout(fn); + } else { + // We can not use jqLite since we are not done loading and jQuery could be loaded later. + + // Works for modern browsers and IE9 + window.document.addEventListener('DOMContentLoaded', trigger); + + // Fallback to window.onload for others + window.addEventListener('load', trigger); + } +} + ////////////////////////////////////////// // Functions which are declared directly. ////////////////////////////////////////// var JQLitePrototype = JQLite.prototype = { - ready: function(fn) { - var fired = false; - - function trigger() { - if (fired) return; - fired = true; - fn(); - } - - // check if document is already loaded - if (window.document.readyState === 'complete') { - window.setTimeout(trigger); - } else { - this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 - // we can not use jqLite since we are not done loading and jQuery could be loaded later. - // eslint-disable-next-line new-cap - JQLite(window).on('load', trigger); // fallback to window.onload for others - } - }, + ready: jqLiteReady, toString: function() { var value = []; forEach(this, function(e) { value.push('' + e);}); @@ -3295,7 +3367,8 @@ var ALIASED_ATTR = { 'ngMaxlength': 'maxlength', 'ngMin': 'min', 'ngMax': 'max', - 'ngPattern': 'pattern' + 'ngPattern': 'pattern', + 'ngStep': 'step' }; function getBooleanAttrName(element, name) { @@ -3346,7 +3419,7 @@ forEach({ hasClass: jqLiteHasClass, css: function(element, name, value) { - name = camelCase(name); + name = cssKebabToCamel(name); if (isDefined(value)) { element.style[name] = value; @@ -3356,33 +3429,33 @@ forEach({ }, attr: function(element, name, value) { + var ret; var nodeType = element.nodeType; - if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT) { + if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT || + !element.getAttribute) { return; } + var lowercasedName = lowercase(name); - if (BOOLEAN_ATTR[lowercasedName]) { - if (isDefined(value)) { - if (value) { - element[name] = true; - element.setAttribute(name, lowercasedName); - } else { - element[name] = false; - element.removeAttribute(lowercasedName); - } + var isBooleanAttr = BOOLEAN_ATTR[lowercasedName]; + + if (isDefined(value)) { + // setter + + if (value === null || (value === false && isBooleanAttr)) { + element.removeAttribute(name); } else { - return (element[name] || - (element.attributes.getNamedItem(name) || noop).specified) - ? lowercasedName - : undefined; + element.setAttribute(name, isBooleanAttr ? lowercasedName : value); } - } else if (isDefined(value)) { - element.setAttribute(name, value); - } else if (element.getAttribute) { - // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code - // some elements (e.g. Document) don't have get attribute, so return undefined - var ret = element.getAttribute(name, 2); - // normalize non-existing attributes to undefined (as jQuery) + } else { + // getter + + ret = element.getAttribute(name); + + if (isBooleanAttr && ret !== null) { + ret = lowercasedName; + } + // Normalize non-existing attributes to undefined (as jQuery). return ret === null ? undefined : ret; } }, @@ -3417,7 +3490,7 @@ forEach({ result.push(option.value || option.text); } }); - return result.length === 0 ? null : result; + return result; } return element.value; } @@ -3587,7 +3660,7 @@ forEach({ eventFns = events[type] = []; eventFns.specialHandlerWrapper = specialHandlerWrapper; if (type !== '$destroy' && !noEventListener) { - addEventListenerFn(element, type, handle); + element.addEventListener(type, handle); } } @@ -3680,12 +3753,15 @@ forEach({ after: function(element, newElement) { var index = element, parent = element.parentNode; - newElement = new JQLite(newElement); - for (var i = 0, ii = newElement.length; i < ii; i++) { - var node = newElement[i]; - parent.insertBefore(node, index.nextSibling); - index = node; + if (parent) { + newElement = new JQLite(newElement); + + for (var i = 0, ii = newElement.length; i < ii; i++) { + var node = newElement[i]; + parent.insertBefore(node, index.nextSibling); + index = node; + } } }, @@ -3779,12 +3855,12 @@ forEach({ } return isDefined(value) ? value : this; }; - - // bind legacy bind/unbind to on/off - JQLite.prototype.bind = JQLite.prototype.on; - JQLite.prototype.unbind = JQLite.prototype.off; }); +// bind legacy bind/unbind to on/off +JQLite.prototype.bind = JQLite.prototype.on; +JQLite.prototype.unbind = JQLite.prototype.off; + // Provider for private $$jqLite service /** @this */ @@ -3949,8 +4025,8 @@ var $$HashMapProvider = [/** @this */function() { * Implicit module which gets automatically added to each {@link auto.$injector $injector}. */ -var ARROW_ARG = /^([^\(]+?)=>/; -var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m; +var ARROW_ARG = /^([^(]+?)=>/; +var FN_ARGS = /^[^(]*\(\s*([^)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; @@ -4735,14 +4811,18 @@ function createInjector(modulesToLoad, strictDi) { } function isClass(func) { + // Support: IE 9-11 only // IE 9-11 do not support classes and IE9 leaks with the code below. - if (msie <= 11) { + if (msie || typeof func !== 'function') { return false; } - // Support: Edge 12-13 only - // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6156135/ - return typeof func === 'function' - && /^(?:class\b|constructor\()/.test(stringifyFn(func)); + var result = func.$$ngIsClass; + if (!isBoolean(result)) { + // Support: Edge 12-13 only + // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6156135/ + result = func.$$ngIsClass = /^(?:class\b|constructor\()/.test(stringifyFn(func)); + } + return result; } function invoke(fn, self, locals, serviceName) { @@ -5030,7 +5110,8 @@ function $AnchorScrollProvider() { } function scroll(hash) { - hash = isString(hash) ? hash : $location.hash(); + // Allow numeric hashes + hash = isString(hash) ? hash : isNumber(hash) ? hash.toString() : $location.hash(); var elm; // empty hash, scroll to the top of the page @@ -5316,7 +5397,6 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { var reservedRegex = new RegExp('(\\s+|\\/)' + NG_ANIMATE_CLASSNAME + '(\\s+|\\/)'); if (reservedRegex.test(this.$$classNameFilter.toString())) { throw $animateMinErr('nongcls','$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME); - } } } @@ -5674,7 +5754,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) { * * @description Performs an inline animation on the element which applies the provided to and from CSS styles to the element. * If any detected CSS transition, keyframe or JavaScript matches the provided className value, then the animation will take - * on the provided styles. For example, if a transition animation is set for the given classNamem, then the provided `from` and + * on the provided styles. For example, if a transition animation is set for the given className, then the provided `from` and * `to` styles will be applied alongside the given transition. If the CSS style provided in `from` does not have a corresponding * style in `to`, the style in `from` is applied immediately, and no animation is run. * If a JavaScript animation is detected then the provided styles will be given in as function parameters into the `animate` @@ -5752,8 +5832,8 @@ var $$AnimateAsyncRunFactoryProvider = /** @this */ function() { }; var $$AnimateRunnerFactoryProvider = /** @this */ function() { - this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$document', '$timeout', - function($q, $sniffer, $$animateAsyncRun, $document, $timeout) { + this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$$isDocumentHidden', '$timeout', + function($q, $sniffer, $$animateAsyncRun, $$isDocumentHidden, $timeout) { var INITIAL_STATE = 0; var DONE_PENDING_STATE = 1; @@ -5805,11 +5885,7 @@ var $$AnimateRunnerFactoryProvider = /** @this */ function() { this._doneCallbacks = []; this._tick = function(fn) { - var doc = $document[0]; - - // the document may not be ready or attached - // to the module for some internal tests - if (doc && doc.hidden) { + if ($$isDocumentHidden()) { timeoutTick(fn); } else { rafTick(fn); @@ -6289,7 +6365,7 @@ function Browser(window, document, $log, $sniffer) { */ self.baseHref = function() { var href = baseElement.attr('href'); - return href ? href.replace(/^(https?:)?\/\/[^\/]*/, '') : ''; + return href ? href.replace(/^(https?:)?\/\/[^/]*/, '') : ''; }; /** @@ -6733,12 +6809,14 @@ function $CacheFactoryProvider() { * }); * ``` * - * To retrieve the template later, simply use it in your HTML: - * ```html - *
+ * To retrieve the template later, simply use it in your component: + * ```js + * myApp.component('myComponent', { + * templateUrl: 'templateId.html' + * }); * ``` * - * or get it via Javascript: + * or get it via the `$templateCache` service: * ```js * $templateCache.get('templateId.html') * ``` @@ -6760,7 +6838,7 @@ function $TemplateCacheProvider() { * * * Does the change somehow allow for arbitrary javascript to be executed? * * Or allows for someone to change the prototype of built-in objects? * - * Or gives undesired access to variables likes document or window? * + * Or gives undesired access to variables like document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE! @@ -6819,33 +6897,34 @@ function $TemplateCacheProvider() { * * myModule.directive('directiveName', function factory(injectables) { * var directiveDefinitionObject = { - * priority: 0, - * template: '
', // or // function(tElement, tAttrs) { ... }, + * {@link $compile#-priority- priority}: 0, + * {@link $compile#-template- template}: '
', // or // function(tElement, tAttrs) { ... }, * // or - * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... }, - * transclude: false, - * restrict: 'A', - * templateNamespace: 'html', - * scope: false, - * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, - * controllerAs: 'stringIdentifier', - * bindToController: false, - * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], - * compile: function compile(tElement, tAttrs, transclude) { + * // {@link $compile#-templateurl- templateUrl}: 'directive.html', // or // function(tElement, tAttrs) { ... }, + * {@link $compile#-transclude- transclude}: false, + * {@link $compile#-restrict- restrict}: 'A', + * {@link $compile#-templatenamespace- templateNamespace}: 'html', + * {@link $compile#-scope- scope}: false, + * {@link $compile#-controller- controller}: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, + * {@link $compile#-controlleras- controllerAs}: 'stringIdentifier', + * {@link $compile#-bindtocontroller- bindToController}: false, + * {@link $compile#-require- require}: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], + * {@link $compile#-multielement- multiElement}: false, + * {@link $compile#-compile- compile}: function compile(tElement, tAttrs, transclude) { * return { - * pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * post: function postLink(scope, iElement, iAttrs, controller) { ... } + * {@link $compile#pre-linking-function pre}: function preLink(scope, iElement, iAttrs, controller) { ... }, + * {@link $compile#post-linking-function post}: function postLink(scope, iElement, iAttrs, controller) { ... } * } * // or * // return function postLink( ... ) { ... } * }, * // or - * // link: { - * // pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * // post: function postLink(scope, iElement, iAttrs, controller) { ... } + * // {@link $compile#-link- link}: { + * // {@link $compile#pre-linking-function pre}: function preLink(scope, iElement, iAttrs, controller) { ... }, + * // {@link $compile#post-linking-function post}: function postLink(scope, iElement, iAttrs, controller) { ... } * // } * // or - * // link: function postLink( ... ) { ... } + * // {@link $compile#-link- link}: function postLink( ... ) { ... } * }; * return directiveDefinitionObject; * }); @@ -6997,7 +7076,7 @@ function $TemplateCacheProvider() { * compiler}. The attributes are: * * #### `multiElement` - * When this property is set to true, the HTML compiler will collect DOM nodes between + * When this property is set to true (default is `false`), the HTML compiler will collect DOM nodes between * nodes with the attributes `directive-name-start` and `directive-name-end`, and group them * together as the directive elements. It is recommended that this feature be used on directives * which are not strictly behavioral (such as {@link ngClick}), and which @@ -7018,14 +7097,14 @@ function $TemplateCacheProvider() { * and other directives used in the directive's template will also be excluded from execution. * * #### `scope` - * The scope property can be `true`, an object or a falsy value: + * The scope property can be `false`, `true`, or an object: * - * * **falsy:** No scope will be created for the directive. The directive will use its parent's scope. + * * **`false` (default):** No scope will be created for the directive. The directive will use its + * parent's scope. * * * **`true`:** A new child scope that prototypically inherits from its parent will be created for * the directive's element. If multiple directives on the same element request a new scope, - * only one new scope is created. The new scope rule does not apply for the root of the template - * since the root of the template always gets a new scope. + * only one new scope is created. * * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's element. The * 'isolate' scope differs from normal scope in that it does not prototypically inherit from its parent @@ -7112,9 +7191,7 @@ function $TemplateCacheProvider() { * * #### `bindToController` * This property is used to bind scope properties directly to the controller. It can be either - * `true` or an object hash with the same format as the `scope` property. Additionally, a controller - * alias must be set, either by using `controllerAs: 'myAlias'` or by specifying the alias in the controller - * definition: `controller: 'myCtrl as myAlias'`. + * `true` or an object hash with the same format as the `scope` property. * * When an isolate scope is used for a directive (see above), `bindToController: true` will * allow a component to have its properties bound to the controller, rather than to scope. @@ -7155,12 +7232,12 @@ function $TemplateCacheProvider() { * * defines the parent to which the `cloneLinkingFn` will add the cloned elements. * * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`. * * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements) - * and when the `cloneLinkinFn` is passed, + * and when the `cloneLinkingFn` is passed, * as those elements need to created and cloned in a special way when they are defined outside their * usual containers (e.g. like ``). * * See also the `directive.templateNamespace` property. * * `slotName`: (optional) the name of the slot to transclude. If falsy (e.g. `null`, `undefined` or `''`) - * then the default translusion is provided. + * then the default transclusion is provided. * The `$transclude` function also has a method on it, `$transclude.isSlotFilled(slotName)`, which returns * `true` if the specified slot contains content (i.e. one or more DOM nodes). * @@ -7449,7 +7526,7 @@ function $TemplateCacheProvider() { * * When you call a transclusion function you can pass in a **clone attach function**. This function accepts * two parameters, `function(clone, scope) { ... }`, where the `clone` is a fresh compiled copy of your transcluded - * content and the `scope` is the newly created transclusion scope, to which the clone is bound. + * content and the `scope` is the newly created transclusion scope, which the clone will be linked to. * *
* **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a transclude function @@ -7696,6 +7773,16 @@ function $TemplateCacheProvider() { * * For information on how the compiler works, see the * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. + * + * @knownIssue + * + * ### Double Compilation + * + Double compilation occurs when an already compiled part of the DOM gets + compiled again. This is an undesired effect and can lead to misbehaving directives, performance issues, + and memory leaks. Refer to the Compiler Guide {@link guide/compiler#double-compilation-and-how-to-avoid-it + section on double compilation} for an in-depth explanation and ways to avoid it. + * */ var $compileMinErr = minErr('$compile'); @@ -7714,8 +7801,8 @@ $CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; function $CompileProvider($provide, $$sanitizeUriProvider) { var hasDirectives = {}, Suffix = 'Directive', - COMMENT_DIRECTIVE_REGEXP = /^\s*directive:\s*([\w\-]+)\s+(.*)$/, - CLASS_DIRECTIVE_REGEXP = /(([\w\-]+)(?::([^;]+))?;?)/, + COMMENT_DIRECTIVE_REGEXP = /^\s*directive:\s*([\w-]+)\s+(.*)$/, + CLASS_DIRECTIVE_REGEXP = /(([\w-]+)(?::([^;]+))?;?)/, ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'), REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/; @@ -7779,20 +7866,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { bindings.bindToController = parseIsolateBindings(directive.bindToController, directiveName, true); } - if (isObject(bindings.bindToController)) { - var controller = directive.controller; - var controllerAs = directive.controllerAs; - if (!controller) { - // There is no controller, there may or may not be a controllerAs property - throw $compileMinErr('noctrl', - 'Cannot bind to controller without directive \'{0}\'s controller.', - directiveName); - } else if (!identifierForController(controller, controllerAs)) { - // There is a controller, but no identifier or controllerAs property - throw $compileMinErr('noident', - 'Cannot bind to controller without identifier for directive \'{0}\'.', - directiveName); - } + if (bindings.bindToController && !directive.controller) { + // There is no controller + throw $compileMinErr('noctrl', + 'Cannot bind to controller without directive \'{0}\'s controller.', + directiveName); } return bindings; } @@ -7823,6 +7901,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return require; } + function getDirectiveRestrict(restrict, name) { + if (restrict && !(isString(restrict) && /[EACM]/.test(restrict))) { + throw $compileMinErr('badrestrict', + 'Restrict property \'{0}\' of directive \'{1}\' is invalid', + restrict, + name); + } + + return restrict || 'EA'; + } + /** * @ngdoc method * @name $compileProvider#directive @@ -7839,6 +7928,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @returns {ng.$compileProvider} Self for chaining. */ this.directive = function registerDirective(name, directiveFactory) { + assertArg(name, 'name'); assertNotHasOwnProperty(name, 'directive'); if (isString(name)) { assertValidDirectiveName(name); @@ -7860,7 +7950,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directive.index = index; directive.name = directive.name || name; directive.require = getDirectiveRequire(directive); - directive.restrict = directive.restrict || 'EA'; + directive.restrict = getDirectiveRestrict(directive.restrict, name); directive.$$moduleName = directiveFactory.$$moduleName; directives.push(directive); } catch (e) { @@ -8109,6 +8199,35 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return debugInfoEnabled; }; + /** + * @ngdoc method + * @name $compileProvider#preAssignBindingsEnabled + * + * @param {boolean=} enabled update the preAssignBindingsEnabled state if provided, otherwise just return the + * current preAssignBindingsEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable whether directive controllers are assigned bindings before + * calling the controller's constructor. + * If enabled (true), the compiler assigns the value of each of the bindings to the + * properties of the controller object before the constructor of this object is called. + * + * If disabled (false), the compiler calls the constructor first before assigning bindings. + * + * The default value is true in Angular 1.5.x but will switch to false in Angular 1.6.x. + */ + var preAssignBindingsEnabled = false; + this.preAssignBindingsEnabled = function(enabled) { + if (isDefined(enabled)) { + preAssignBindingsEnabled = enabled; + return this; + } + return preAssignBindingsEnabled; + }; + var TTL = 10; /** @@ -8562,25 +8681,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // modify it. $compileNodes = jqLite($compileNodes); } - - var NOT_EMPTY = /\S+/; - - // We can not compile top level text elements since text nodes can be merged and we will - // not be able to attach scope data to them, so we will wrap them in - for (var i = 0, len = $compileNodes.length; i < len; i++) { - var domNode = $compileNodes[i]; - - if (domNode.nodeType === NODE_TYPE_TEXT && domNode.nodeValue.match(NOT_EMPTY) /* non-empty */) { - jqLiteWrapNode(domNode, $compileNodes[i] = window.document.createElement('span')); - } - } - var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); compile.$$addScopeClass($compileNodes); var namespace = null; return function publicLinkFn(scope, cloneConnectFn, options) { + if (!$compileNodes) { + throw $compileMinErr('multilink', 'This element has already been linked.'); + } assertArg(scope, 'scope'); if (previousCompileContext && previousCompileContext.needsNewScope) { @@ -8635,6 +8744,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (cloneConnectFn) cloneConnectFn($linkNode, scope); if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); + + if (!cloneConnectFn) { + $compileNodes = compositeLinkFn = null; + } return $linkNode; }; } @@ -8667,12 +8780,23 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) { var linkFns = [], + // `nodeList` can be either an element's `.childNodes` (live NodeList) + // or a jqLite/jQuery collection or an array + notLiveList = isArray(nodeList) || (nodeList instanceof jqLite), attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound; + for (var i = 0; i < nodeList.length; i++) { attrs = new Attributes(); - // we must always refer to nodeList[i] since the nodes can be replaced underneath us. + // Support: IE 11 only + // Workaround for #11781 and #14924 + if (msie === 11) { + mergeConsecutiveTextNodes(nodeList, i, notLiveList); + } + + // We must always refer to `nodeList[i]` hereafter, + // since the nodes can be replaced underneath us. directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined, ignoreDirective); @@ -8763,6 +8887,32 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } + function mergeConsecutiveTextNodes(nodeList, idx, notLiveList) { + var node = nodeList[idx]; + var parent = node.parentNode; + var sibling; + + if (node.nodeType !== NODE_TYPE_TEXT) { + return; + } + + while (true) { + sibling = parent ? node.nextSibling : nodeList[idx + 1]; + if (!sibling || sibling.nodeType !== NODE_TYPE_TEXT) { + break; + } + + node.nodeValue = node.nodeValue + sibling.nodeValue; + + if (sibling.parentNode) { + sibling.parentNode.removeChild(sibling); + } + if (notLiveList && sibling === nodeList[idx + 1]) { + nodeList.splice(idx + 1, 1); + } + } + } + function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { @@ -8826,7 +8976,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { attr = nAttrs[j]; name = attr.name; - value = trim(attr.value); + value = attr.value; // support ngAttr attribute binding ngAttrName = directiveNormalize(name); @@ -8882,13 +9032,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } break; case NODE_TYPE_TEXT: /* Text Node */ - if (msie === 11) { - // Workaround for #11781 - while (node.parentNode && node.nextSibling && node.nextSibling.nodeType === NODE_TYPE_TEXT) { - node.nodeValue = node.nodeValue + node.nextSibling.nodeValue; - node.parentNode.removeChild(node.nextSibling); - } - } addTextInterpolateDirective(directives, node.nodeValue); break; case NODE_TYPE_COMMENT: /* Comment */ @@ -8920,7 +9063,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } /** - * Given a node with an directive-start it collects all of the siblings until it finds + * Given a node with a directive-start it collects all of the siblings until it finds * directive-end. * @param node * @param attrStart @@ -9107,7 +9250,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (!directive.templateUrl && directive.controller) { - directiveValue = directive.controller; controllerDirectives = controllerDirectives || createMap(); assertNoDuplicate('\'' + directiveName + '\' controller', controllerDirectives[directiveName], directive, $compileNode); @@ -9162,9 +9304,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var slots = createMap(); - $template = jqLite(jqLiteClone(compileNode)).contents(); - - if (isObject(directiveValue)) { + if (!isObject(directiveValue)) { + $template = jqLite(jqLiteClone(compileNode)).contents(); + } else { // We have transclusion slots, // collect them up, compile them and store their transclusion functions @@ -9406,22 +9548,29 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var controller = elementControllers[name]; var bindings = controllerDirective.$$bindings.bindToController; - if (controller.identifier && bindings) { - controller.bindingInfo = - initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); - } else { - controller.bindingInfo = {}; - } - - var controllerResult = controller(); - if (controllerResult !== controller.instance) { - // If the controller constructor has a return value, overwrite the instance - // from setupControllers - controller.instance = controllerResult; - $element.data('$' + controllerDirective.name + 'Controller', controllerResult); - if (controller.bindingInfo.removeWatches) { - controller.bindingInfo.removeWatches(); + if (preAssignBindingsEnabled) { + if (bindings) { + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + } else { + controller.bindingInfo = {}; } + + var controllerResult = controller(); + if (controllerResult !== controller.instance) { + // If the controller constructor has a return value, overwrite the instance + // from setupControllers + controller.instance = controllerResult; + $element.data('$' + controllerDirective.name + 'Controller', controllerResult); + if (controller.bindingInfo.removeWatches) { + controller.bindingInfo.removeWatches(); + } + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + } + } else { + controller.instance = controller(); + $element.data('$' + controllerDirective.name + 'Controller', controller.instance); controller.bindingInfo = initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); } @@ -9650,24 +9799,22 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (hasDirectives.hasOwnProperty(name)) { for (var directive, directives = $injector.get(name + Suffix), i = 0, ii = directives.length; i < ii; i++) { - try { - directive = directives[i]; - if ((isUndefined(maxPriority) || maxPriority > directive.priority) && - directive.restrict.indexOf(location) !== -1) { - if (startAttrName) { - directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); - } - if (!directive.$$bindings) { - var bindings = directive.$$bindings = - parseDirectiveBindings(directive, directive.name); - if (isObject(bindings.isolateScope)) { - directive.$$isolateBindings = bindings.isolateScope; - } - } - tDirectives.push(directive); - match = directive; + directive = directives[i]; + if ((isUndefined(maxPriority) || maxPriority > directive.priority) && + directive.restrict.indexOf(location) !== -1) { + if (startAttrName) { + directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); } - } catch (e) { $exceptionHandler(e); } + if (!directive.$$bindings) { + var bindings = directive.$$bindings = + parseDirectiveBindings(directive, directive.name); + if (isObject(bindings.isolateScope)) { + directive.$$isolateBindings = bindings.isolateScope; + } + } + tDirectives.push(directive); + match = directive; + } } } return match; @@ -9711,7 +9858,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { forEach(dst, function(value, key) { if (key.charAt(0) !== '$') { if (src[key] && src[key] !== value) { - value += (key === 'style' ? ';' : ' ') + src[key]; + if (value.length) { + value += (key === 'style' ? ';' : ' ') + src[key]; + } else { + value = src[key]; + } } dst.$set(key, value, true, srcAttr[key]); } @@ -9830,7 +9981,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childBoundTranscludeFn); } linkQueue = null; - }); + }).catch(function(error) { + if (error instanceof Error) { + $exceptionHandler(error); + } + }).catch(noop); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { var childBoundTranscludeFn = boundTranscludeFn; @@ -9930,29 +10085,37 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } // maction[xlink:href] can source SVG. It's not limited to . } else if (attrNormalizedName === 'xlinkHref' || - (tag === 'form' && attrNormalizedName === 'action') + (tag === 'form' && attrNormalizedName === 'action') || + // links can be stylesheets or imports, which can run script in the current origin + (tag === 'link' && attrNormalizedName === 'href') ) { return $sce.RESOURCE_URL; } } - function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) { + function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) { var trustedContext = getTrustedContext(node, name); - allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing; + var mustHaveExpression = !isNgAttr; + var allOrNothing = ALL_OR_NOTHING_ATTRS[name] || isNgAttr; - var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing); + var interpolateFn = $interpolate(value, mustHaveExpression, trustedContext, allOrNothing); // no interpolation found -> ignore if (!interpolateFn) return; - if (name === 'multiple' && nodeName_(node) === 'select') { throw $compileMinErr('selmulti', 'Binding to the \'multiple\' attribute is not supported. Element: {0}', startingTag(node)); } + if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { + throw $compileMinErr('nodomevents', + 'Interpolations for HTML DOM event attributes are disallowed. Please use the ' + + 'ng- versions (such as ng-click instead of onclick) instead.'); + } + directives.push({ priority: 100, compile: function() { @@ -9960,12 +10123,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { pre: function attrInterpolatePreLinkFn(scope, element, attr) { var $$observers = (attr.$$observers || (attr.$$observers = createMap())); - if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { - throw $compileMinErr('nodomevents', - 'Interpolations for HTML DOM event attributes are disallowed. Please use the ' + - 'ng- versions (such as ng-click instead of onclick) instead.'); - } - // If the attribute has changed since last $interpolate()ed var newValue = attr[name]; if (newValue !== value) { @@ -10099,8 +10256,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } - // Set up $watches for isolate scope and controller bindings. This process - // only occurs for isolate scopes and new scopes with controllerAs. + // Set up $watches for isolate scope and controller bindings. function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) { var removeWatchCollection = []; var initialChanges = {}; @@ -10118,7 +10274,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (!optional && !hasOwnProperty.call(attrs, attrName)) { destination[scopeName] = attrs[attrName] = undefined; } - attrs.$observe(attrName, function(value) { + removeWatch = attrs.$observe(attrName, function(value) { if (isString(value) || isBoolean(value)) { var oldValue = destination[scopeName]; recordChanges(scopeName, value, oldValue); @@ -10137,6 +10293,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { destination[scopeName] = lastValue; } initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); + removeWatchCollection.push(removeWatch); break; case '=': @@ -10192,18 +10349,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (optional && !attrs[attrName]) break; parentGet = $parse(attrs[attrName]); + var deepWatch = parentGet.literal; var initialValue = destination[scopeName] = parentGet(scope); initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) { if (oldValue === newValue) { - if (oldValue === initialValue) return; + if (oldValue === initialValue || (deepWatch && equals(oldValue, initialValue))) { + return; + } oldValue = initialValue; } recordChanges(scopeName, newValue, oldValue); destination[scopeName] = newValue; - }, parentGet.literal); + }, deepWatch); removeWatchCollection.push(removeWatch); break; @@ -10223,7 +10383,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }); function recordChanges(key, currentValue, previousValue) { - if (isFunction(destination.$onChanges) && currentValue !== previousValue) { + if (isFunction(destination.$onChanges) && currentValue !== previousValue && + // eslint-disable-next-line no-self-compare + (currentValue === currentValue || previousValue === previousValue)) { // If we have not already scheduled the top level onChangesQueue handler then do so now if (!onChangesQueue) { scope.$$postDigest(flushOnChangesQueue); @@ -10269,12 +10431,16 @@ SimpleChange.prototype.isFirstChange = function() { return this.previousValue == var PREFIX_REGEXP = /^((?:x|data)[:\-_])/i; +var SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g; + /** * Converts all accepted directives format into proper directive name. * @param name Name to normalize */ function directiveNormalize(name) { - return camelCase(name.replace(PREFIX_REGEXP, '')); + return name + .replace(PREFIX_REGEXP, '') + .replace(SPECIAL_CHARS_REGEXP, fnCamelCaseReplace); } /** @@ -10363,8 +10529,9 @@ function removeComments(jqNodes) { while (i--) { var node = jqNodes[i]; - if (node.nodeType === NODE_TYPE_COMMENT) { - splice.call(jqNodes, i, 1); + if (node.nodeType === NODE_TYPE_COMMENT || + (node.nodeType === NODE_TYPE_TEXT && node.nodeValue.trim() === '')) { + splice.call(jqNodes, i, 1); } } return jqNodes; @@ -10429,6 +10596,11 @@ function $ControllerProvider() { * @ngdoc method * @name $controllerProvider#allowGlobals * @description If called, allows `$controller` to find controller constructors on `window` + * + * @deprecated + * sinceVersion="v1.3.0" + * removeVersion="v1.7.0" + * This method of finding controllers has been deprecated. */ this.allowGlobals = function() { globals = true; @@ -10449,7 +10621,7 @@ function $ControllerProvider() { * * check if a controller with given name is registered via `$controllerProvider` * * check if evaluating the string on the current scope returns a constructor * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global - * `window` object (not recommended) + * `window` object (deprecated, not recommended) * * The string can use the `controller as property` syntax, where the controller instance is published * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this @@ -10492,6 +10664,11 @@ function $ControllerProvider() { : getter(locals.$scope, constructor, true) || (globals ? getter($window, constructor, true) : undefined); + if (!expression) { + throw $controllerMinErr('ctrlreg', + 'The controller with the name \'{0}\' is not registered.', constructor); + } + assertArgFn(expression, constructor, true); } @@ -10583,6 +10760,33 @@ function $DocumentProvider() { }]; } + +/** + * @private + * @this + * Listens for document visibility change and makes the current status accessible. + */ +function $$IsDocumentHiddenProvider() { + this.$get = ['$document', '$rootScope', function($document, $rootScope) { + var doc = $document[0]; + var hidden = doc && doc.hidden; + + $document.on('visibilitychange', changeListener); + + $rootScope.$on('$destroy', function() { + $document.off('visibilitychange', changeListener); + }); + + function changeListener() { + hidden = doc.hidden; + } + + return function() { + return hidden; + }; + }]; +} + /** * @ngdoc service * @name $exceptionHandler @@ -10665,13 +10869,8 @@ var JSON_ENDS = { '[': /]$/, '{': /}$/ }; -var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/; +var JSON_PROTECTION_PREFIX = /^\)]\}',?\n/; var $httpMinErr = minErr('$http'); -var $httpMinErrLegacyFn = function(method) { - return function() { - throw $httpMinErr('legacy', 'The method `{0}` on the promise returned from `$http` has been disabled.', method); - }; -}; function serializeValue(v) { if (isObject(v)) { @@ -10853,7 +11052,7 @@ function parseHeaders(headers) { * @param {(string|Object)} headers Headers to provide access to. * @returns {function(string=)} Returns a getter function which if called with: * - * - if called with single an argument returns a single header value or null + * - if called with an argument returns a single header value or null * - if called with no arguments returns an object containing all headers. */ function headersGetter(headers) { @@ -10944,6 +11143,10 @@ function $HttpProvider() { * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}. * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}. * + * - **`defaults.jsonpCallbackParam`** - `{string}` - the name of the query parameter that passes the name of the + * callback in a JSONP request. The value of this parameter will be replaced with the expression generated by the + * {@link $jsonpCallbacks} service. Defaults to `'callback'`. + * **/ var defaults = this.defaults = { // transform incoming response data @@ -10967,7 +11170,9 @@ function $HttpProvider() { xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', - paramSerializer: '$httpParamSerializer' + paramSerializer: '$httpParamSerializer', + + jsonpCallbackParam: 'callback' }; var useApplyAsync = false; @@ -10998,30 +11203,6 @@ function $HttpProvider() { return useApplyAsync; }; - var useLegacyPromise = true; - /** - * @ngdoc method - * @name $httpProvider#useLegacyPromiseExtensions - * @description - * - * Configure `$http` service to return promises without the shorthand methods `success` and `error`. - * This should be used to make sure that applications work without these methods. - * - * Defaults to true. If no value is specified, returns the current configured value. - * - * @param {boolean=} value If true, `$http` will return a promise with the deprecated legacy `success` and `error` methods. - * - * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. - * otherwise, returns the current configured value. - **/ - this.useLegacyPromiseExtensions = function(value) { - if (isDefined(value)) { - useLegacyPromise = !!value; - return this; - } - return useLegacyPromise; - }; - /** * @ngdoc property * @name $httpProvider#interceptors @@ -11037,8 +11218,8 @@ function $HttpProvider() { **/ var interceptorFactories = this.interceptors = []; - this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) { + this.$get = ['$browser', '$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', '$sce', + function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector, $sce) { var defaultCache = $cacheFactory('$http'); @@ -11155,14 +11336,6 @@ function $HttpProvider() { * $httpBackend.flush(); * ``` * - * ## Deprecation Notice - *
- * The `$http` legacy promise methods `success` and `error` have been deprecated. - * Use the standard `then` method instead. - * If {@link $httpProvider#useLegacyPromiseExtensions `$httpProvider.useLegacyPromiseExtensions`} is set to - * `false` then these methods will throw {@link $http:legacy `$http/legacy`} error. - *
- * * ## Setting HTTP Headers * * The $http service will automatically add certain HTTP headers to all requests. These defaults @@ -11170,7 +11343,7 @@ function $HttpProvider() { * object, which currently contains this default configuration: * * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): - * - `Accept: application/json, text/plain, * / *` + * - Accept: application/json, text/plain, \*/\* * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) * - `Content-Type: application/json` * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) @@ -11460,7 +11633,8 @@ function $HttpProvider() { * processed. The object has following properties: * * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) - * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. + * - **url** – `{string|TrustedObject}` – Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * - **params** – `{Object.}` – Map of strings or objects which will be serialized * with the `paramSerializer` and appended as GET parameters. * - **data** – `{string|Object}` – Data to be sent as the request message data. @@ -11526,11 +11700,11 @@ function $HttpProvider() {
http status code: {{status}}
@@ -11539,6 +11713,13 @@ function $HttpProvider() { angular.module('httpExample', []) + .config(['$sceDelegateProvider', function($sceDelegateProvider) { + // We must whitelist the JSONP endpoint that we are using to show that we trust it + $sceDelegateProvider.resourceUrlWhitelist([ + 'self', + 'https://angularjs.org/**' + ]); + }]) .controller('FetchController', ['$scope', '$http', '$templateCache', function($scope, $http, $templateCache) { $scope.method = 'GET'; @@ -11606,15 +11787,16 @@ function $HttpProvider() { throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig); } - if (!isString(requestConfig.url)) { - throw minErr('$http')('badreq', 'Http request configuration url must be a string. Received: {0}', requestConfig.url); + if (!isString($sce.valueOf(requestConfig.url))) { + throw minErr('$http')('badreq', 'Http request configuration url must be a string or a $sce trusted object. Received: {0}', requestConfig.url); } var config = extend({ method: 'get', transformRequest: defaults.transformRequest, transformResponse: defaults.transformResponse, - paramSerializer: defaults.paramSerializer + paramSerializer: defaults.paramSerializer, + jsonpCallbackParam: defaults.jsonpCallbackParam }, requestConfig); config.headers = mergeHeaders(requestConfig); @@ -11622,9 +11804,11 @@ function $HttpProvider() { config.paramSerializer = isString(config.paramSerializer) ? $injector.get(config.paramSerializer) : config.paramSerializer; + $browser.$$incOutstandingRequestCount(); + var requestInterceptors = []; var responseInterceptors = []; - var promise = $q.when(config); + var promise = $q.resolve(config); // apply interceptors forEach(reversedInterceptors, function(interceptor) { @@ -11639,29 +11823,7 @@ function $HttpProvider() { promise = chainInterceptors(promise, requestInterceptors); promise = promise.then(serverRequest); promise = chainInterceptors(promise, responseInterceptors); - - if (useLegacyPromise) { - promise.success = function(fn) { - assertArgFn(fn, 'fn'); - - promise.then(function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; - - promise.error = function(fn) { - assertArgFn(fn, 'fn'); - - promise.then(null, function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; - } else { - promise.success = $httpMinErrLegacyFn('success'); - promise.error = $httpMinErrLegacyFn('error'); - } + promise = promise.finally(completeOutstandingRequest); return promise; @@ -11679,6 +11841,10 @@ function $HttpProvider() { return promise; } + function completeOutstandingRequest() { + $browser.$$completeOutstandingRequest(noop); + } + function executeHeaderFns(headers, config) { var headerContent, processedHeaders = {}; @@ -11762,7 +11928,8 @@ function $HttpProvider() { * @description * Shortcut method to perform `GET` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -11774,7 +11941,8 @@ function $HttpProvider() { * @description * Shortcut method to perform `DELETE` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -11786,7 +11954,8 @@ function $HttpProvider() { * @description * Shortcut method to perform `HEAD` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -11797,11 +11966,34 @@ function $HttpProvider() { * * @description * Shortcut method to perform `JSONP` request. + * + * Note that, since JSONP requests are sensitive because the response is given full access to the browser, + * the url must be declared, via {@link $sce} as a trusted resource URL. + * You can trust a URL by adding it to the whitelist via + * {@link $sceDelegateProvider#resourceUrlWhitelist `$sceDelegateProvider.resourceUrlWhitelist`} or + * by explicitly trusting the URL via {@link $sce#trustAsResourceUrl `$sce.trustAsResourceUrl(url)`}. + * + * JSONP requests must specify a callback to be used in the response from the server. This callback + * is passed as a query parameter in the request. You must specify the name of this parameter by + * setting the `jsonpCallbackParam` property on the request config object. + * + * ``` + * $http.jsonp('some/trusted/url', {jsonpCallbackParam: 'callback'}) + * ``` + * + * You can also specify a default callback parameter name in `$http.defaults.jsonpCallbackParam`. + * Initially this is set to `'callback'`. + * + *
+ * You can no longer use the `JSON_CALLBACK` string as a placeholder for specifying where the callback + * parameter value should go. + *
+ * * If you would like to customise where and how the callbacks are stored then try overriding * or decorating the {@link $jsonpCallbacks} service. * - * @param {string} url Relative or absolute URL specifying the destination of the request. - * The name of the callback should be the string `JSON_CALLBACK`. + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -11900,12 +12092,28 @@ function $HttpProvider() { cache, cachedResp, reqHeaders = config.headers, - url = buildUrl(config.url, config.paramSerializer(config.params)); + isJsonp = lowercase(config.method) === 'jsonp', + url = config.url; + + if (isJsonp) { + // JSONP is a pretty sensitive operation where we're allowing a script to have full access to + // our DOM and JS space. So we require that the URL satisfies SCE.RESOURCE_URL. + url = $sce.getTrustedResourceUrl(url); + } else if (!isString(url)) { + // If it is not a string then the URL must be a $sce trusted object + url = $sce.valueOf(url); + } + + url = buildUrl(url, config.paramSerializer(config.params)); + + if (isJsonp) { + // Check the url and add the JSONP callback placeholder + url = sanitizeJsonpCallbackParam(url, config.jsonpCallbackParam); + } $http.pendingRequests.push(config); promise.then(removePendingReq, removePendingReq); - if ((config.cache || defaults.cache) && config.cache !== false && (config.method === 'GET' || config.method === 'JSONP')) { cache = isObject(config.cache) ? config.cache @@ -12037,6 +12245,24 @@ function $HttpProvider() { } return url; } + + function sanitizeJsonpCallbackParam(url, key) { + if (/[&?][^=]+=JSON_CALLBACK/.test(url)) { + // Throw if the url already contains a reference to JSON_CALLBACK + throw $httpMinErr('badjsonp', 'Illegal use of JSON_CALLBACK in url, "{0}"', url); + } + + var callbackParamRegex = new RegExp('[&?]' + key + '='); + if (callbackParamRegex.test(url)) { + // Throw if the callback param was already provided + throw $httpMinErr('badjsonp', 'Illegal use of callback param, "{0}", in url, "{1}"', key, url); + } + + // Add in the JSON_CALLBACK callback param value + url += ((url.indexOf('?') === -1) ? '?' : '&') + key + '=JSON_CALLBACK'; + + return url; + } }]; } @@ -12097,7 +12323,6 @@ function $HttpBackendProvider() { function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { // TODO(vojta): fix the signature return function(method, url, post, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) { - $browser.$$incOutstandingRequestCount(); url = url || $browser.url(); if (lowercase(method) === 'jsonp') { @@ -12209,7 +12434,6 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc jsonpDone = xhr = null; callback(status, response, headersString, statusText); - $browser.$$completeOutstandingRequest(noop); } }; @@ -12224,8 +12448,8 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc script.async = true; callback = function(event) { - removeEventListenerFn(script, 'load', callback); - removeEventListenerFn(script, 'error', callback); + script.removeEventListener('load', callback); + script.removeEventListener('error', callback); rawDocument.body.removeChild(script); script = null; var status = -1; @@ -12244,8 +12468,8 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc } }; - addEventListenerFn(script, 'load', callback); - addEventListenerFn(script, 'error', callback); + script.addEventListener('load', callback); + script.addEventListener('error', callback); rawDocument.body.appendChild(script); return callback; } @@ -12363,23 +12587,6 @@ function $InterpolateProvider() { replace(escapedEndRegexp, endSymbol); } - function stringify(value) { - if (value == null) { // null || undefined - return ''; - } - switch (typeof value) { - case 'string': - break; - case 'number': - value = '' + value; - break; - default: - value = toJson(value); - } - - return value; - } - // TODO: this is the same as the constantWatchDelegate in parse.js function constantWatchDelegate(scope, listener, objectEquality, constantInterp) { var unwatch = scope.$watch(function constantInterpolateWatch(scope) { @@ -12689,7 +12896,8 @@ function $IntervalProvider() { * appropriate moment. See the example below for more details on how and when to do this. *
* - * @param {function()} fn A function that should be called repeatedly. + * @param {function()} fn A function that should be called repeatedly. If no additional arguments + * are passed (see below), the function is called with the current iteration count. * @param {number} delay Number of milliseconds between each function call. * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat * indefinitely. @@ -12844,6 +13052,8 @@ function $IntervalProvider() { */ interval.cancel = function(promise) { if (promise && promise.$$intervalId in intervals) { + // Interval cancels should not report as unhandled promise. + intervals[promise.$$intervalId].promise.catch(noop); intervals[promise.$$intervalId].reject('canceled'); $window.clearInterval(promise.$$intervalId); delete intervals[promise.$$intervalId]; @@ -12948,7 +13158,7 @@ var $jsonpCallbacksProvider = /** @this */ function() { * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) */ -var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, +var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; var $locationMinErr = minErr('$location'); @@ -13001,8 +13211,8 @@ function parseAppUrl(url, locationObj) { } } -function startsWith(haystack, needle) { - return haystack.lastIndexOf(needle, 0) === 0; +function startsWith(str, search) { + return str.slice(0, search.length) === search; } /** @@ -13040,13 +13250,13 @@ function serverBase(url) { /** - * LocationHtml5Url represents an url + * LocationHtml5Url represents a URL * This object is exposed as $location service when HTML5 mode is enabled and supported * * @constructor * @param {string} appBase application base URL * @param {string} appBaseNoFile application base URL stripped of any filename - * @param {string} basePrefix url path prefix + * @param {string} basePrefix URL path prefix */ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { this.$$html5 = true; @@ -13055,8 +13265,8 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { /** - * Parse given html5 (regular) url string into properties - * @param {string} url HTML5 url + * Parse given HTML5 (regular) URL string into properties + * @param {string} url HTML5 URL * @private */ this.$$parse = function(url) { @@ -13119,7 +13329,7 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { /** - * LocationHashbangUrl represents url + * LocationHashbangUrl represents URL * This object is exposed as $location service when developer doesn't opt into html5 mode. * It also serves as the base class for html5 mode fallback on legacy browsers. * @@ -13134,8 +13344,8 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { /** - * Parse given hashbang url into properties - * @param {string} url Hashbang url + * Parse given hashbang URL into properties + * @param {string} url Hashbang URL * @private */ this.$$parse = function(url) { @@ -13144,7 +13354,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') { - // The rest of the url starts with a hash so we have + // The rest of the URL starts with a hash so we have // got either a hashbang path or a plain hash fragment withoutHashUrl = stripBaseUrl(hashPrefix, withoutBaseUrl); if (isUndefined(withoutHashUrl)) { @@ -13209,7 +13419,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { }; /** - * Compose hashbang url and update `absUrl` property + * Compose hashbang URL and update `absUrl` property * @private */ this.$$compose = function() { @@ -13231,7 +13441,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { /** - * LocationHashbangUrl represents url + * LocationHashbangUrl represents URL * This object is exposed as $location service when html5 history api is enabled but the browser * does not support it. * @@ -13283,7 +13493,7 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { var locationPrototype = { /** - * Ensure absolute url is initialized. + * Ensure absolute URL is initialized. * @private */ $$absUrl:'', @@ -13307,17 +13517,17 @@ var locationPrototype = { * @description * This method is getter only. * - * Return full url representation with all segments encoded according to rules specified in + * Return full URL representation with all segments encoded according to rules specified in * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var absUrl = $location.absUrl(); * // => "http://example.com/#/some/path?foo=bar&baz=xoxo" * ``` * - * @return {string} full url + * @return {string} full URL */ absUrl: locationGetter('$$absUrl'), @@ -13328,18 +13538,18 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return url (e.g. `/path?a=b#hash`) when called without any parameter. + * Return URL (e.g. `/path?a=b#hash`) when called without any parameter. * * Change path, search and hash, when called with parameter and return `$location`. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var url = $location.url(); * // => "/some/path?foo=bar&baz=xoxo" * ``` * - * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) + * @param {string=} url New URL without base prefix (e.g. `/path?a=b#hash`) * @return {string} url */ url: function(url) { @@ -13362,16 +13572,16 @@ var locationPrototype = { * @description * This method is getter only. * - * Return protocol of current url. + * Return protocol of current URL. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var protocol = $location.protocol(); * // => "http" * ``` * - * @return {string} protocol of current url + * @return {string} protocol of current URL */ protocol: locationGetter('$$protocol'), @@ -13382,24 +13592,24 @@ var locationPrototype = { * @description * This method is getter only. * - * Return host of current url. + * Return host of current URL. * * Note: compared to the non-angular version `location.host` which returns `hostname:port`, this returns the `hostname` portion only. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var host = $location.host(); * // => "example.com" * - * // given url http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo + * // given URL http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo * host = $location.host(); * // => "example.com" * host = location.host; * // => "example.com:8080" * ``` * - * @return {string} host of current url. + * @return {string} host of current URL. */ host: locationGetter('$$host'), @@ -13410,11 +13620,11 @@ var locationPrototype = { * @description * This method is getter only. * - * Return port of current url. + * Return port of current URL. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var port = $location.port(); * // => 80 * ``` @@ -13430,7 +13640,7 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return path of current url when called without any parameter. + * Return path of current URL when called without any parameter. * * Change path when called with parameter and return `$location`. * @@ -13439,7 +13649,7 @@ var locationPrototype = { * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var path = $location.path(); * // => "/some/path" * ``` @@ -13459,13 +13669,13 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return search part (as object) of current url when called without any parameter. + * Return search part (as object) of current URL when called without any parameter. * * Change search part when called with parameter and return `$location`. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var searchObject = $location.search(); * // => {foo: 'bar', baz: 'xoxo'} * @@ -13481,7 +13691,7 @@ var locationPrototype = { * of `$location` to the specified value. * * If the argument is a hash object containing an array of values, these values will be encoded - * as duplicate search parameters in the url. + * as duplicate search parameters in the URL. * * @param {(string|Number|Array|boolean)=} paramValue If `search` is a string or number, then `paramValue` * will override only a single search property. @@ -13543,7 +13753,7 @@ var locationPrototype = { * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue * var hash = $location.hash(); * // => "hashValue" * ``` @@ -13666,7 +13876,7 @@ function locationGetterSetter(property, preprocess) { * Use the `$locationProvider` to configure how the application deep linking paths are stored. */ function $LocationProvider() { - var hashPrefix = '', + var hashPrefix = '!', html5Mode = { enabled: false, requireBase: true, @@ -13677,6 +13887,7 @@ function $LocationProvider() { * @ngdoc method * @name $locationProvider#hashPrefix * @description + * The default value for the prefix is `'!'`. * @param {string=} prefix Prefix for hash part (containing path and search) * @returns {*} current value if used as getter or itself (chaining) if used as setter */ @@ -13703,8 +13914,12 @@ function $LocationProvider() { * whether or not a tag is required to be present. If `enabled` and `requireBase` are * true, and a base tag is not present, an error will be thrown when `$location` is injected. * See the {@link guide/$location $location guide for more information} - * - **rewriteLinks** - `{boolean}` - (default: `true`) When html5Mode is enabled, - * enables/disables url rewriting for relative links. + * - **rewriteLinks** - `{boolean|string}` - (default: `true`) When html5Mode is enabled, + * enables/disables URL rewriting for relative links. If set to a string, URL rewriting will + * only happen on links with an attribute that matches the given string. For example, if set + * to `'internal-link'`, then the URL will only be rewritten for `` links. + * Note that [attribute name normalization](guide/directive#normalization) does not apply + * here, so `'internalLink'` will **not** match `'internal-link'`. * * @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter */ @@ -13722,7 +13937,7 @@ function $LocationProvider() { html5Mode.requireBase = mode.requireBase; } - if (isBoolean(mode.rewriteLinks)) { + if (isBoolean(mode.rewriteLinks) || isString(mode.rewriteLinks)) { html5Mode.rewriteLinks = mode.rewriteLinks; } @@ -13819,10 +14034,11 @@ function $LocationProvider() { } $rootElement.on('click', function(event) { + var rewriteLinks = html5Mode.rewriteLinks; // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) // currently we open nice url link and redirect then - if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which === 2 || event.button === 2) return; + if (!rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which === 2 || event.button === 2) return; var elm = jqLite(event.target); @@ -13832,6 +14048,8 @@ function $LocationProvider() { if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; } + if (isString(rewriteLinks) && isUndefined(elm.attr(rewriteLinks))) return; + var absHref = elm.prop('href'); // get the actual href attribute - see // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx @@ -13873,7 +14091,7 @@ function $LocationProvider() { // update $location when $browser url changes $browser.onUrlChange(function(newUrl, newState) { - if (isUndefined(stripBaseUrl(appBaseNoFile, newUrl))) { + if (!startsWith(newUrl, appBaseNoFile)) { // If we are navigating outside of the app then force a reload $window.location.href = newUrl; return; @@ -14133,60 +14351,23 @@ function $LogProvider() { var $parseMinErr = minErr('$parse'); -var ARRAY_CTOR = [].constructor; -var BOOLEAN_CTOR = (false).constructor; -var FUNCTION_CTOR = Function.constructor; -var NUMBER_CTOR = (0).constructor; -var OBJECT_CTOR = {}.constructor; -var STRING_CTOR = ''.constructor; -var ARRAY_CTOR_PROTO = ARRAY_CTOR.prototype; -var BOOLEAN_CTOR_PROTO = BOOLEAN_CTOR.prototype; -var FUNCTION_CTOR_PROTO = FUNCTION_CTOR.prototype; -var NUMBER_CTOR_PROTO = NUMBER_CTOR.prototype; -var OBJECT_CTOR_PROTO = OBJECT_CTOR.prototype; -var STRING_CTOR_PROTO = STRING_CTOR.prototype; - -var CALL = FUNCTION_CTOR_PROTO.call; -var APPLY = FUNCTION_CTOR_PROTO.apply; -var BIND = FUNCTION_CTOR_PROTO.bind; - -var objectValueOf = OBJECT_CTOR_PROTO.valueOf; +var objectValueOf = {}.constructor.prototype.valueOf; // Sandboxing Angular Expressions // ------------------------------ -// Angular expressions are generally considered safe because these expressions only have direct -// access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by -// obtaining a reference to native JS functions such as the Function constructor. +// Angular expressions are no longer sandboxed. So it is now even easier to access arbitrary JS code by +// various means such as obtaining a reference to native JS functions like the Function constructor. // // As an example, consider the following Angular expression: // // {}.toString.constructor('alert("evil JS code")') // -// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits -// against the expression language, but not to prevent exploits that were enabled by exposing -// sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good -// practice and therefore we are not even trying to protect against interaction with an object -// explicitly exposed in this way. -// -// In general, it is not possible to access a Window object from an angular expression unless a -// window or some DOM object that has a reference to window is published onto a Scope. -// Similarly we prevent invocations of function known to be dangerous, as well as assignments to -// native objects. +// It is important to realize that if you create an expression from a string that contains user provided +// content then it is possible that your application contains a security vulnerability to an XSS style attack. // // See https://docs.angularjs.org/guide/security -function ensureSafeMemberName(name, fullExpression) { - if (name === '__defineGetter__' || name === '__defineSetter__' - || name === '__lookupGetter__' || name === '__lookupSetter__' - || name === '__proto__') { - throw $parseMinErr('isecfld', - 'Attempting to access a disallowed field in Angular expressions! ' - + 'Expression: {0}', fullExpression); - } - return name; -} - function getStringValue(name) { // Property names must be strings. This means that non-string objects cannot be used // as keys in an object. Any non-string object, including a number, is typecasted @@ -14205,67 +14386,6 @@ function getStringValue(name) { return name + ''; } -function ensureSafeObject(obj, fullExpression) { - // nifty check if obj is Function that is fast and works across iframes and other contexts - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isWindow(obj) - obj.window === obj) { - throw $parseMinErr('isecwindow', - 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isElement(obj) - obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { - throw $parseMinErr('isecdom', - 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// block Object so that we can't get hold of dangerous Object.* methods - obj === Object) { - throw $parseMinErr('isecobj', - 'Referencing Object in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } - return obj; -} - -function ensureSafeFunction(obj, fullExpression) { - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (obj === CALL || obj === APPLY || obj === BIND) { - throw $parseMinErr('isecff', - 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } -} - -function ensureSafeAssignContext(obj, fullExpression) { - if (obj) { - if (obj === ARRAY_CTOR || - obj === BOOLEAN_CTOR || - obj === FUNCTION_CTOR || - obj === NUMBER_CTOR || - obj === OBJECT_CTOR || - obj === STRING_CTOR || - obj === ARRAY_CTOR_PROTO || - obj === BOOLEAN_CTOR_PROTO || - obj === FUNCTION_CTOR_PROTO || - obj === NUMBER_CTOR_PROTO || - obj === OBJECT_CTOR_PROTO || - obj === STRING_CTOR_PROTO) { - throw $parseMinErr('isecaf', - 'Assigning to a constructor or its prototype is disallowed! Expression: {0}', - fullExpression); - } - } -} var OPERATORS = createMap(); forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; }); @@ -14556,6 +14676,10 @@ AST.prototype = { assignment: function() { var result = this.ternary(); if (this.expect('=')) { + if (!isAssignable(result)) { + throw $parseMinErr('lval', 'Trying to assign a value to a non l-value'); + } + result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='}; } return result; @@ -14982,13 +15106,12 @@ function ASTCompiler(astBuilder, $filter) { } ASTCompiler.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(expression) { var self = this; var ast = this.astBuilder.ast(expression); this.state = { nextId: 0, filters: {}, - expensiveChecks: expensiveChecks, fn: {vars: [], body: [], own: {}}, assign: {vars: [], body: [], own: {}}, inputs: [] @@ -15031,24 +15154,14 @@ ASTCompiler.prototype = { // eslint-disable-next-line no-new-func var fn = (new Function('$filter', - 'ensureSafeMemberName', - 'ensureSafeObject', - 'ensureSafeFunction', 'getStringValue', - 'ensureSafeAssignContext', 'ifDefined', 'plus', - 'text', fnString))( this.$filter, - ensureSafeMemberName, - ensureSafeObject, - ensureSafeFunction, getStringValue, - ensureSafeAssignContext, ifDefined, - plusFn, - expression); + plusFn); this.state = this.stage = undefined; fn.literal = isLiteral(ast); fn.constant = isConstant(ast); @@ -15122,7 +15235,7 @@ ASTCompiler.prototype = { case AST.Literal: expression = this.escape(ast.value); this.assign(intoId, expression); - recursionFn(expression); + recursionFn(intoId || expression); break; case AST.UnaryExpression: this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; }); @@ -15162,22 +15275,18 @@ ASTCompiler.prototype = { nameId.computed = false; nameId.name = ast.name; } - ensureSafeMemberName(ast.name); self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)), function() { self.if_(self.stage === 'inputs' || 's', function() { if (create && create !== 1) { self.if_( - self.not(self.nonComputedMember('s', ast.name)), + self.isNull(self.nonComputedMember('s', ast.name)), self.lazyAssign(self.nonComputedMember('s', ast.name), '{}')); } self.assign(intoId, self.nonComputedMember('s', ast.name)); }); }, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name)) ); - if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) { - self.addEnsureSafeObject(intoId); - } recursionFn(intoId); break; case AST.MemberExpression: @@ -15185,32 +15294,24 @@ ASTCompiler.prototype = { intoId = intoId || this.nextId(); self.recurse(ast.object, left, undefined, function() { self.if_(self.notNull(left), function() { - if (create && create !== 1) { - self.addEnsureSafeAssignContext(left); - } if (ast.computed) { right = self.nextId(); self.recurse(ast.property, right); self.getStringValue(right); - self.addEnsureSafeMemberName(right); if (create && create !== 1) { self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}')); } - expression = self.ensureSafeObject(self.computedMember(left, right)); + expression = self.computedMember(left, right); self.assign(intoId, expression); if (nameId) { nameId.computed = true; nameId.name = right; } } else { - ensureSafeMemberName(ast.property.name); if (create && create !== 1) { - self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); + self.if_(self.isNull(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); } expression = self.nonComputedMember(left, ast.property.name); - if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) { - expression = self.ensureSafeObject(expression); - } self.assign(intoId, expression); if (nameId) { nameId.computed = false; @@ -15242,21 +15343,16 @@ ASTCompiler.prototype = { args = []; self.recurse(ast.callee, right, left, function() { self.if_(self.notNull(right), function() { - self.addEnsureSafeFunction(right); forEach(ast.arguments, function(expr) { - self.recurse(expr, self.nextId(), undefined, function(argument) { - args.push(self.ensureSafeObject(argument)); + self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { + args.push(argument); }); }); if (left.name) { - if (!self.state.expensiveChecks) { - self.addEnsureSafeObject(left.context); - } expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')'; } else { expression = right + '(' + args.join(',') + ')'; } - expression = self.ensureSafeObject(expression); self.assign(intoId, expression); }, function() { self.assign(intoId, 'undefined'); @@ -15268,14 +15364,9 @@ ASTCompiler.prototype = { case AST.AssignmentExpression: right = this.nextId(); left = {}; - if (!isAssignable(ast.left)) { - throw $parseMinErr('lval', 'Trying to assign a value to a non l-value'); - } this.recurse(ast.left, undefined, left, function() { self.if_(self.notNull(left.context), function() { self.recurse(ast.right, right); - self.addEnsureSafeObject(self.member(left.context, left.name, left.computed)); - self.addEnsureSafeAssignContext(left.context); expression = self.member(left.context, left.name, left.computed) + ast.operator + right; self.assign(intoId, expression); recursionFn(intoId || expression); @@ -15285,13 +15376,13 @@ ASTCompiler.prototype = { case AST.ArrayExpression: args = []; forEach(ast.elements, function(expr) { - self.recurse(expr, self.nextId(), undefined, function(argument) { + self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { args.push(argument); }); }); expression = '[' + args.join(',') + ']'; this.assign(intoId, expression); - recursionFn(expression); + recursionFn(intoId || expression); break; case AST.ObjectExpression: args = []; @@ -15333,15 +15424,15 @@ ASTCompiler.prototype = { break; case AST.ThisExpression: this.assign(intoId, 's'); - recursionFn('s'); + recursionFn(intoId || 's'); break; case AST.LocalsExpression: this.assign(intoId, 'l'); - recursionFn('l'); + recursionFn(intoId || 'l'); break; case AST.NGValueParameter: this.assign(intoId, 'v'); - recursionFn('v'); + recursionFn(intoId || 'v'); break; } }, @@ -15400,6 +15491,10 @@ ASTCompiler.prototype = { return '!(' + expression + ')'; }, + isNull: function(expression) { + return expression + '==null'; + }, + notNull: function(expression) { return expression + '!=null'; }, @@ -15423,42 +15518,10 @@ ASTCompiler.prototype = { return this.nonComputedMember(left, right); }, - addEnsureSafeObject: function(item) { - this.current().body.push(this.ensureSafeObject(item), ';'); - }, - - addEnsureSafeMemberName: function(item) { - this.current().body.push(this.ensureSafeMemberName(item), ';'); - }, - - addEnsureSafeFunction: function(item) { - this.current().body.push(this.ensureSafeFunction(item), ';'); - }, - - addEnsureSafeAssignContext: function(item) { - this.current().body.push(this.ensureSafeAssignContext(item), ';'); - }, - - ensureSafeObject: function(item) { - return 'ensureSafeObject(' + item + ',text)'; - }, - - ensureSafeMemberName: function(item) { - return 'ensureSafeMemberName(' + item + ',text)'; - }, - - ensureSafeFunction: function(item) { - return 'ensureSafeFunction(' + item + ',text)'; - }, - getStringValue: function(item) { this.assign(item, 'getStringValue(' + item + ')'); }, - ensureSafeAssignContext: function(item) { - return 'ensureSafeAssignContext(' + item + ',text)'; - }, - lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { var self = this; return function() { @@ -15510,11 +15573,9 @@ function ASTInterpreter(astBuilder, $filter) { } ASTInterpreter.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(expression) { var self = this; var ast = this.astBuilder.ast(expression); - this.expression = expression; - this.expensiveChecks = expensiveChecks; findConstantAndWatchExpressions(ast, self.$filter); var assignable; var assign; @@ -15585,20 +15646,16 @@ ASTInterpreter.prototype = { context ); case AST.Identifier: - ensureSafeMemberName(ast.name, self.expression); - return self.identifier(ast.name, - self.expensiveChecks || isPossiblyDangerousMemberName(ast.name), - context, create, self.expression); + return self.identifier(ast.name, context, create); case AST.MemberExpression: left = this.recurse(ast.object, false, !!create); if (!ast.computed) { - ensureSafeMemberName(ast.property.name, self.expression); right = ast.property.name; } if (ast.computed) right = this.recurse(ast.property); return ast.computed ? - this.computedMember(left, right, context, create, self.expression) : - this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression); + this.computedMember(left, right, context, create) : + this.nonComputedMember(left, right, context, create); case AST.CallExpression: args = []; forEach(ast.arguments, function(expr) { @@ -15619,13 +15676,11 @@ ASTInterpreter.prototype = { var rhs = right(scope, locals, assign, inputs); var value; if (rhs.value != null) { - ensureSafeObject(rhs.context, self.expression); - ensureSafeFunction(rhs.value, self.expression); var values = []; for (var i = 0; i < args.length; ++i) { - values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression)); + values.push(args[i](scope, locals, assign, inputs)); } - value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression); + value = rhs.value.apply(rhs.context, values); } return context ? {value: value} : value; }; @@ -15635,8 +15690,6 @@ ASTInterpreter.prototype = { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); var rhs = right(scope, locals, assign, inputs); - ensureSafeObject(lhs.value, self.expression); - ensureSafeAssignContext(lhs.context); lhs.context[lhs.name] = rhs; return context ? {value: rhs} : rhs; }; @@ -15712,7 +15765,7 @@ ASTInterpreter.prototype = { if (isDefined(arg)) { arg = -arg; } else { - arg = 0; + arg = -0; } return context ? {value: arg} : arg; }; @@ -15828,16 +15881,13 @@ ASTInterpreter.prototype = { value: function(value, context) { return function() { return context ? {context: undefined, name: undefined, value: value} : value; }; }, - identifier: function(name, expensiveChecks, context, create, expression) { + identifier: function(name, context, create) { return function(scope, locals, assign, inputs) { var base = locals && (name in locals) ? locals : scope; - if (create && create !== 1 && base && !(base[name])) { + if (create && create !== 1 && base && base[name] == null) { base[name] = {}; } var value = base ? base[name] : undefined; - if (expensiveChecks) { - ensureSafeObject(value, expression); - } if (context) { return {context: base, name: name, value: value}; } else { @@ -15845,7 +15895,7 @@ ASTInterpreter.prototype = { } }; }, - computedMember: function(left, right, context, create, expression) { + computedMember: function(left, right, context, create) { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); var rhs; @@ -15853,15 +15903,12 @@ ASTInterpreter.prototype = { if (lhs != null) { rhs = right(scope, locals, assign, inputs); rhs = getStringValue(rhs); - ensureSafeMemberName(rhs, expression); if (create && create !== 1) { - ensureSafeAssignContext(lhs); if (lhs && !(lhs[rhs])) { lhs[rhs] = {}; } } value = lhs[rhs]; - ensureSafeObject(value, expression); } if (context) { return {context: lhs, name: rhs, value: value}; @@ -15870,19 +15917,15 @@ ASTInterpreter.prototype = { } }; }, - nonComputedMember: function(left, right, expensiveChecks, context, create, expression) { + nonComputedMember: function(left, right, context, create) { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); if (create && create !== 1) { - ensureSafeAssignContext(lhs); - if (lhs && !(lhs[right])) { + if (lhs && lhs[right] == null) { lhs[right] = {}; } } var value = lhs != null ? lhs[right] : undefined; - if (expensiveChecks || isPossiblyDangerousMemberName(right)) { - ensureSafeObject(value, expression); - } if (context) { return {context: lhs, name: right, value: value}; } else { @@ -15914,14 +15957,10 @@ Parser.prototype = { constructor: Parser, parse: function(text) { - return this.astCompiler.compile(text, this.options.expensiveChecks); + return this.astCompiler.compile(text); } }; -function isPossiblyDangerousMemberName(name) { - return name === 'constructor'; -} - function getValueOf(value) { return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); } @@ -15979,8 +16018,7 @@ function getValueOf(value) { * service. */ function $ParseProvider() { - var cacheDefault = createMap(); - var cacheExpensive = createMap(); + var cache = createMap(); var literals = { 'true': true, 'false': false, @@ -16020,7 +16058,7 @@ function $ParseProvider() { * representation. It is expected for the function to return `true` or `false`, whether that * character is allowed or not. * - * Since this function will be called extensivelly, keep the implementation of these functions fast, + * Since this function will be called extensively, keep the implementation of these functions fast, * as the performance of these functions have a direct impact on the expressions parsing speed. * * @param {function=} identifierStart The function that will decide whether the given character is @@ -16038,37 +16076,20 @@ function $ParseProvider() { var noUnsafeEval = csp().noUnsafeEval; var $parseOptions = { csp: noUnsafeEval, - expensiveChecks: false, - literals: copy(literals), - isIdentifierStart: isFunction(identStart) && identStart, - isIdentifierContinue: isFunction(identContinue) && identContinue - }, - $parseOptionsExpensive = { - csp: noUnsafeEval, - expensiveChecks: true, literals: copy(literals), isIdentifierStart: isFunction(identStart) && identStart, isIdentifierContinue: isFunction(identContinue) && identContinue }; - var runningChecksEnabled = false; - - $parse.$$runningExpensiveChecks = function() { - return runningChecksEnabled; - }; - return $parse; - function $parse(exp, interceptorFn, expensiveChecks) { + function $parse(exp, interceptorFn) { var parsedExpression, oneTime, cacheKey; - expensiveChecks = expensiveChecks || runningChecksEnabled; - switch (typeof exp) { case 'string': exp = exp.trim(); cacheKey = exp; - var cache = (expensiveChecks ? cacheExpensive : cacheDefault); parsedExpression = cache[cacheKey]; if (!parsedExpression) { @@ -16076,9 +16097,8 @@ function $ParseProvider() { oneTime = true; exp = exp.substring(2); } - var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions; - var lexer = new Lexer(parseOptions); - var parser = new Parser(lexer, $filter, parseOptions); + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); parsedExpression = parser.parse(exp); if (parsedExpression.constant) { parsedExpression.$$watchDelegate = constantWatchDelegate; @@ -16088,9 +16108,6 @@ function $ParseProvider() { } else if (parsedExpression.inputs) { parsedExpression.$$watchDelegate = inputsWatchDelegate; } - if (expensiveChecks) { - parsedExpression = expensiveChecksInterceptor(parsedExpression); - } cache[cacheKey] = parsedExpression; } return addInterceptor(parsedExpression, interceptorFn); @@ -16103,30 +16120,6 @@ function $ParseProvider() { } } - function expensiveChecksInterceptor(fn) { - if (!fn) return fn; - expensiveCheckFn.$$watchDelegate = fn.$$watchDelegate; - expensiveCheckFn.assign = expensiveChecksInterceptor(fn.assign); - expensiveCheckFn.constant = fn.constant; - expensiveCheckFn.literal = fn.literal; - for (var i = 0; fn.inputs && i < fn.inputs.length; ++i) { - fn.inputs[i] = expensiveChecksInterceptor(fn.inputs[i]); - } - expensiveCheckFn.inputs = fn.inputs; - - return expensiveCheckFn; - - function expensiveCheckFn(scope, locals, assign, inputs) { - var expensiveCheckOldValue = runningChecksEnabled; - runningChecksEnabled = true; - try { - return fn(scope, locals, assign, inputs); - } finally { - runningChecksEnabled = expensiveCheckOldValue; - } - } - } - function expressionInputDirtyCheck(newValue, oldValueOfValue) { if (newValue == null || oldValueOfValue == null) { // null/undefined @@ -16196,14 +16189,22 @@ function $ParseProvider() { }, listener, objectEquality, prettyPrintExpression); } - function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { + function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) { var unwatch, lastValue; - unwatch = scope.$watch(function oneTimeWatch(scope) { + if (parsedExpression.inputs) { + unwatch = inputsWatchDelegate(scope, oneTimeListener, objectEquality, parsedExpression, prettyPrintExpression); + } else { + unwatch = scope.$watch(oneTimeWatch, oneTimeListener, objectEquality); + } + return unwatch; + + function oneTimeWatch(scope) { return parsedExpression(scope); - }, /** @this */ function oneTimeListener(value, old, scope) { + } + function oneTimeListener(value, old, scope) { lastValue = value; if (isFunction(listener)) { - listener.apply(this, arguments); + listener(value, old, scope); } if (isDefined(value)) { scope.$$postDigest(function() { @@ -16212,18 +16213,17 @@ function $ParseProvider() { } }); } - }, objectEquality); - return unwatch; + } } function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { var unwatch, lastValue; unwatch = scope.$watch(function oneTimeWatch(scope) { return parsedExpression(scope); - }, /** @this */ function oneTimeListener(value, old, scope) { + }, function oneTimeListener(value, old, scope) { lastValue = value; if (isFunction(listener)) { - listener.call(this, value, old, scope); + listener(value, old, scope); } if (isAllDefined(value)) { scope.$$postDigest(function() { @@ -16272,14 +16272,15 @@ function $ParseProvider() { }; // Propagate $$watchDelegates other then inputsWatchDelegate + useInputs = !parsedExpression.inputs; if (parsedExpression.$$watchDelegate && parsedExpression.$$watchDelegate !== inputsWatchDelegate) { fn.$$watchDelegate = parsedExpression.$$watchDelegate; + fn.inputs = parsedExpression.inputs; } else if (!interceptorFn.$stateful) { // If there is an interceptor, but no watchDelegate then treat the interceptor like // we treat filters - it is assumed to be a pure function unless flagged with $stateful fn.$$watchDelegate = inputsWatchDelegate; - useInputs = !parsedExpression.inputs; fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]; } @@ -16292,14 +16293,13 @@ function $ParseProvider() { * @ngdoc service * @name $q * @requires $rootScope - * @this * * @description * A service that helps you run functions asynchronously, and use their return values (or exceptions) * when they are done processing. * - * This is an implementation of promises/deferred objects inspired by - * [Kris Kowal's Q](https://github.com/kriskowal/q). + * This is a [Promises/A+](https://promisesaplus.com/)-compliant implementation of promises/deferred + * objects inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). * * $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred * implementations, and the other which resembles ES6 (ES2015) promises to some degree. @@ -16506,22 +16506,61 @@ function $ParseProvider() { * * @returns {Promise} The newly created promise. */ +/** + * @ngdoc provider + * @name $qProvider + * @this + * + * @description + */ function $QProvider() { - + var errorOnUnhandledRejections = true; this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { return qFactory(function(callback) { $rootScope.$evalAsync(callback); - }, $exceptionHandler); + }, $exceptionHandler, errorOnUnhandledRejections); }]; + + /** + * @ngdoc method + * @name $qProvider#errorOnUnhandledRejections + * @kind function + * + * @description + * Retrieves or overrides whether to generate an error when a rejected promise is not handled. + * This feature is enabled by default. + * + * @param {boolean=} value Whether to generate an error when a rejected promise is not handled. + * @returns {boolean|ng.$qProvider} Current value when called without a new value or self for + * chaining otherwise. + */ + this.errorOnUnhandledRejections = function(value) { + if (isDefined(value)) { + errorOnUnhandledRejections = value; + return this; + } else { + return errorOnUnhandledRejections; + } + }; } /** @this */ function $$QProvider() { + var errorOnUnhandledRejections = true; this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { return qFactory(function(callback) { $browser.defer(callback); - }, $exceptionHandler); + }, $exceptionHandler, errorOnUnhandledRejections); }]; + + this.errorOnUnhandledRejections = function(value) { + if (isDefined(value)) { + errorOnUnhandledRejections = value; + return this; + } else { + return errorOnUnhandledRejections; + } + }; } /** @@ -16530,10 +16569,14 @@ function $$QProvider() { * @param {function(function)} nextTick Function for executing functions in the next turn. * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for * debugging purposes. + @ param {=boolean} errorOnUnhandledRejections Whether an error should be generated on unhandled + * promises rejections. * @returns {object} Promise manager. */ -function qFactory(nextTick, exceptionHandler) { +function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { var $qMinErr = minErr('$q', TypeError); + var queueSize = 0; + var checkQueue = []; /** * @ngdoc method @@ -16545,14 +16588,18 @@ function qFactory(nextTick, exceptionHandler) { * * @returns {Deferred} Returns a new instance of deferred. */ - var defer = function() { - var d = new Deferred(); - //Necessary to support unbound execution :/ - d.resolve = simpleBind(d, d.resolve); - d.reject = simpleBind(d, d.reject); - d.notify = simpleBind(d, d.notify); - return d; - }; + function defer() { + return new Deferred(); + } + + function Deferred() { + var promise = this.promise = new Promise(); + //Non prototype methods necessary to support unbound execution :/ + this.resolve = function(val) { resolvePromise(promise, val); }; + this.reject = function(reason) { rejectPromise(promise, reason); }; + this.notify = function(progress) { notifyPromise(promise, progress); }; + } + function Promise() { this.$$state = { status: 0 }; @@ -16563,13 +16610,13 @@ function qFactory(nextTick, exceptionHandler) { if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) { return this; } - var result = new Deferred(); + var result = new Promise(); this.$$state.pending = this.$$state.pending || []; this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]); if (this.$$state.status > 0) scheduleProcessQueue(this.$$state); - return result.promise; + return result; }, 'catch': function(callback) { @@ -16578,129 +16625,147 @@ function qFactory(nextTick, exceptionHandler) { 'finally': function(callback, progressBack) { return this.then(function(value) { - return handleCallback(value, true, callback); + return handleCallback(value, resolve, callback); }, function(error) { - return handleCallback(error, false, callback); + return handleCallback(error, reject, callback); }, progressBack); } }); - //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native - function simpleBind(context, fn) { - return function(value) { - fn.call(context, value); - }; - } - function processQueue(state) { - var fn, deferred, pending; + var fn, promise, pending; pending = state.pending; state.processScheduled = false; state.pending = undefined; - for (var i = 0, ii = pending.length; i < ii; ++i) { - deferred = pending[i][0]; - fn = pending[i][state.status]; - try { - if (isFunction(fn)) { - deferred.resolve(fn(state.value)); - } else if (state.status === 1) { - deferred.resolve(state.value); - } else { - deferred.reject(state.value); + try { + for (var i = 0, ii = pending.length; i < ii; ++i) { + state.pur = true; + promise = pending[i][0]; + fn = pending[i][state.status]; + try { + if (isFunction(fn)) { + resolvePromise(promise, fn(state.value)); + } else if (state.status === 1) { + resolvePromise(promise, state.value); + } else { + rejectPromise(promise, state.value); + } + } catch (e) { + rejectPromise(promise, e); + } + } + } finally { + --queueSize; + if (errorOnUnhandledRejections && queueSize === 0) { + nextTick(processChecks); + } + } + } + + function processChecks() { + // eslint-disable-next-line no-unmodified-loop-condition + while (!queueSize && checkQueue.length) { + var toCheck = checkQueue.shift(); + if (!toCheck.pur) { + toCheck.pur = true; + var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value); + if (toCheck.value instanceof Error) { + exceptionHandler(toCheck.value, errorMessage); + } else { + exceptionHandler(errorMessage); } - } catch (e) { - deferred.reject(e); - exceptionHandler(e); } } } function scheduleProcessQueue(state) { + if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !state.pur) { + if (queueSize === 0 && checkQueue.length === 0) { + nextTick(processChecks); + } + checkQueue.push(state); + } if (state.processScheduled || !state.pending) return; state.processScheduled = true; + ++queueSize; nextTick(function() { processQueue(state); }); } - function Deferred() { - this.promise = new Promise(); + function resolvePromise(promise, val) { + if (promise.$$state.status) return; + if (val === promise) { + $$reject(promise, $qMinErr( + 'qcycle', + 'Expected promise to be resolved with value other than itself \'{0}\'', + val)); + } else { + $$resolve(promise, val); + } + } - extend(Deferred.prototype, { - resolve: function(val) { - if (this.promise.$$state.status) return; - if (val === this.promise) { - this.$$reject($qMinErr( - 'qcycle', - 'Expected promise to be resolved with value other than itself \'{0}\'', - val)); + function $$resolve(promise, val) { + var then; + var done = false; + try { + if (isObject(val) || isFunction(val)) then = val.then; + if (isFunction(then)) { + promise.$$state.status = -1; + then.call(val, doResolve, doReject, doNotify); } else { - this.$$resolve(val); - } - - }, - - $$resolve: function(val) { - var then; - var that = this; - var done = false; - try { - if ((isObject(val) || isFunction(val))) then = val && val.then; - if (isFunction(then)) { - this.promise.$$state.status = -1; - then.call(val, resolvePromise, rejectPromise, simpleBind(this, this.notify)); - } else { - this.promise.$$state.value = val; - this.promise.$$state.status = 1; - scheduleProcessQueue(this.promise.$$state); - } - } catch (e) { - rejectPromise(e); - exceptionHandler(e); - } - - function resolvePromise(val) { - if (done) return; - done = true; - that.$$resolve(val); - } - function rejectPromise(val) { - if (done) return; - done = true; - that.$$reject(val); - } - }, - - reject: function(reason) { - if (this.promise.$$state.status) return; - this.$$reject(reason); - }, - - $$reject: function(reason) { - this.promise.$$state.value = reason; - this.promise.$$state.status = 2; - scheduleProcessQueue(this.promise.$$state); - }, - - notify: function(progress) { - var callbacks = this.promise.$$state.pending; - - if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) { - nextTick(function() { - var callback, result; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - result = callbacks[i][0]; - callback = callbacks[i][3]; - try { - result.notify(isFunction(callback) ? callback(progress) : progress); - } catch (e) { - exceptionHandler(e); - } - } - }); + promise.$$state.value = val; + promise.$$state.status = 1; + scheduleProcessQueue(promise.$$state); } + } catch (e) { + doReject(e); } - }); + + function doResolve(val) { + if (done) return; + done = true; + $$resolve(promise, val); + } + function doReject(val) { + if (done) return; + done = true; + $$reject(promise, val); + } + function doNotify(progress) { + notifyPromise(promise, progress); + } + } + + function rejectPromise(promise, reason) { + if (promise.$$state.status) return; + $$reject(promise, reason); + } + + function $$reject(promise, reason) { + promise.$$state.value = reason; + promise.$$state.status = 2; + scheduleProcessQueue(promise.$$state); + } + + function notifyPromise(promise, progress) { + var callbacks = promise.$$state.pending; + + if ((promise.$$state.status <= 0) && callbacks && callbacks.length) { + nextTick(function() { + var callback, result; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + result = callbacks[i][0]; + callback = callbacks[i][3]; + try { + notifyPromise(result, isFunction(callback) ? callback(progress) : progress); + } catch (e) { + exceptionHandler(e); + } + } + }); + } + } /** * @ngdoc method @@ -16738,39 +16803,27 @@ function qFactory(nextTick, exceptionHandler) { * @param {*} reason Constant, message, exception or an object representing the rejection reason. * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. */ - var reject = function(reason) { - var result = new Deferred(); - result.reject(reason); - return result.promise; - }; + function reject(reason) { + var result = new Promise(); + rejectPromise(result, reason); + return result; + } - var makePromise = function makePromise(value, resolved) { - var result = new Deferred(); - if (resolved) { - result.resolve(value); - } else { - result.reject(value); - } - return result.promise; - }; - - var handleCallback = function handleCallback(value, isResolved, callback) { + function handleCallback(value, resolver, callback) { var callbackOutput = null; try { if (isFunction(callback)) callbackOutput = callback(); } catch (e) { - return makePromise(e, false); + return reject(e); } if (isPromiseLike(callbackOutput)) { return callbackOutput.then(function() { - return makePromise(value, isResolved); - }, function(error) { - return makePromise(error, false); - }); + return resolver(value); + }, reject); } else { - return makePromise(value, isResolved); + return resolver(value); } - }; + } /** * @ngdoc method @@ -16790,11 +16843,11 @@ function qFactory(nextTick, exceptionHandler) { */ - var when = function(value, callback, errback, progressBack) { - var result = new Deferred(); - result.resolve(value); - return result.promise.then(callback, errback, progressBack); - }; + function when(value, callback, errback, progressBack) { + var result = new Promise(); + resolvePromise(result, value); + return result.then(callback, errback, progressBack); + } /** * @ngdoc method @@ -16829,27 +16882,25 @@ function qFactory(nextTick, exceptionHandler) { */ function all(promises) { - var deferred = new Deferred(), + var result = new Promise(), counter = 0, results = isArray(promises) ? [] : {}; forEach(promises, function(promise, key) { counter++; when(promise).then(function(value) { - if (results.hasOwnProperty(key)) return; results[key] = value; - if (!(--counter)) deferred.resolve(results); + if (!(--counter)) resolvePromise(result, results); }, function(reason) { - if (results.hasOwnProperty(key)) return; - deferred.reject(reason); + rejectPromise(result, reason); }); }); if (counter === 0) { - deferred.resolve(results); + resolvePromise(result, results); } - return deferred.promise; + return result; } /** @@ -16876,25 +16927,25 @@ function qFactory(nextTick, exceptionHandler) { return deferred.promise; } - var $Q = function Q(resolver) { + function $Q(resolver) { if (!isFunction(resolver)) { throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver); } - var deferred = new Deferred(); + var promise = new Promise(); function resolveFn(value) { - deferred.resolve(value); + resolvePromise(promise, value); } function rejectFn(reason) { - deferred.reject(reason); + rejectPromise(promise, reason); } resolver(resolveFn, rejectFn); - return deferred.promise; - }; + return promise; + } // Let's make the instanceof operator work for promises, so that // `new $q(fn) instanceof $q` would evaluate to true. @@ -17046,6 +17097,7 @@ function $RootScopeProvider() { function cleanUpScope($scope) { + // Support: IE 9 only if (msie === 9) { // There is a memory leak in IE9 if all child scopes are not disconnected // completely when a scope is destroyed. So this code will recurse up through @@ -17226,7 +17278,7 @@ function $RootScopeProvider() { * $digest()} and should return the value that will be watched. (`watchExpression` should not change * its value when executed multiple times with the same input because it may be executed multiple * times by {@link ng.$rootScope.Scope#$digest $digest()}. That is, `watchExpression` should be - * [idempotent](http://en.wikipedia.org/wiki/Idempotence). + * [idempotent](http://en.wikipedia.org/wiki/Idempotence).) * - The `listener` is called only when the value from the current `watchExpression` and the * previous call to `watchExpression` are not equal (with the exception of the initial run, * see below). Inequality is determined according to reference inequality, @@ -17237,6 +17289,8 @@ function $RootScopeProvider() { * according to the {@link angular.equals} function. To save the value of the object for * later comparison, the {@link angular.copy} function is used. This therefore means that * watching complex objects will have adverse memory and performance implications. + * - This should not be used to watch for changes in objects that are + * or contain [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link angular.copy `angular.copy`}. * - The watch `listener` may change the model, which may trigger other `listener`s to fire. * This is achieved by rerunning the watchers until no changes are detected. The rerun * iteration limit is 10 to prevent an infinite loop deadlock. @@ -17354,15 +17408,21 @@ function $RootScopeProvider() { if (!array) { array = scope.$$watchers = []; + array.$$digestWatchIndex = -1; } // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. array.unshift(watcher); + array.$$digestWatchIndex++; incrementWatchersCount(this, 1); return function deregisterWatch() { - if (arrayRemove(array, watcher) >= 0) { + var index = arrayRemove(array, watcher); + if (index >= 0) { incrementWatchersCount(scope, -1); + if (index < array.$$digestWatchIndex) { + array.$$digestWatchIndex--; + } } lastDirtyWatch = null; }; @@ -17377,8 +17437,8 @@ function $RootScopeProvider() { * A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`. * If any one expression in the collection changes the `listener` is executed. * - * - The items in the `watchExpressions` array are observed via standard $watch operation and are examined on every - * call to $digest() to see if any items changes. + * - The items in the `watchExpressions` array are observed via the standard `$watch` operation. Their return + * values are examined for changes on every call to `$digest`. * - The `listener` is called whenever any expression in the `watchExpressions` array changes. * * @param {Array.} watchExpressions Array of expressions that will be individually @@ -17695,7 +17755,6 @@ function $RootScopeProvider() { $digest: function() { var watch, value, last, fn, get, watchers, - length, dirty, ttl = TTL, next, current, target = this, watchLog = [], @@ -17736,10 +17795,10 @@ function $RootScopeProvider() { do { // "traverse the scopes" loop if ((watchers = current.$$watchers)) { // process our watches - length = watchers.length; - while (length--) { + watchers.$$digestWatchIndex = watchers.length; + while (watchers.$$digestWatchIndex--) { try { - watch = watchers[length]; + watch = watchers[watchers.$$digestWatchIndex]; // Most common watches are on primitives, in which case we can short // circuit it with === operator, only when === fails do we use .equals if (watch) { @@ -18439,6 +18498,13 @@ var SCE_CONTEXTS = { // Helper functions follow. +var UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g; + +function snakeToCamel(name) { + return name + .replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace); +} + function adjustMatcher(matcher) { if (matcher === 'self') { return matcher; @@ -18452,8 +18518,8 @@ function adjustMatcher(matcher) { 'Illegal sequence *** in string matcher. String: {0}', matcher); } matcher = escapeForRegexp(matcher). - replace('\\*\\*', '.*'). - replace('\\*', '[^:/.?&;]*'); + replace(/\\\*\\\*/g, '.*'). + replace(/\\\*/g, '[^:/.?&;]*'); return new RegExp('^' + matcher + '$'); } else if (isRegExp(matcher)) { // The only other type of matcher allowed is a Regexp. @@ -19024,8 +19090,8 @@ function $SceDelegateProvider() { * .controller('AppController', ['$http', '$templateCache', '$sce', * function AppController($http, $templateCache, $sce) { * var self = this; - * $http.get('test_data.json', {cache: $templateCache}).success(function(userComments) { - * self.userComments = userComments; + * $http.get('test_data.json', {cache: $templateCache}).then(function(response) { + * self.userComments = response.data; * }); * self.explicitlyTrustedHtml = $sce.trustAsHtml( * '
default currency symbol ($): {{amount | currency}}
- custom currency identifier (USD$): {{amount | currency:"USD$"}} + custom currency identifier (USD$): {{amount | currency:"USD$"}}
no fractions (0): {{amount | currency:"USD$":0}}
@@ -21004,7 +21071,7 @@ var DATE_FORMATS = { }; var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, - NUMBER_STRING = /^\-?\d+$/; + NUMBER_STRING = /^-?\d+$/; /** * @ngdoc filter @@ -21091,7 +21158,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+ expect(element(by.binding("1288323623006 | date:'medium'")).getText()). toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()). - toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); + toMatch(/2010-10-2\d \d{2}:\d{2}:\d{2} (-|\+)?\d{4}/); expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()). @@ -21387,7 +21454,7 @@ function sliceFn(input, begin, end) { * String, etc). * * The `expression` can be a single predicate, or a list of predicates each serving as a tie-breaker - * for the preceeding one. The `expression` is evaluated against each item and the output is used + * for the preceding one. The `expression` is evaluated against each item and the output is used * for comparing with other items. * * You can change the sorting order by setting `reverse` to `true`. By default, items are sorted in @@ -21457,6 +21524,9 @@ function sliceFn(input, begin, end) { * * **Note:** If you notice numbers not being sorted as expected, make sure they are actually being * saved as numbers and not strings. + * **Note:** For the purpose of sorting, `null` values are treated as the string `'null'` (i.e. + * `type: 'string'`, `value: 'null'`). This may cause unexpected sort order relative to + * other values. * * @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort. * @param {(Function|string|Array.)=} expression - A predicate (or list of @@ -22078,12 +22148,10 @@ function ngDirective(directive) { * @restrict E * * @description - * Modifies the default behavior of the html A tag so that the default action is prevented when + * Modifies the default behavior of the html a tag so that the default action is prevented when * the href attribute is empty. * - * This change permits the easy creation of action links with the `ngClick` directive - * without changing the location or causing page reloads, e.g.: - * `Add Item` + * For dynamically creating `href` attributes for a tags, see the {@link ng.ngHref `ngHref`} directive. */ var htmlAnchorDirective = valueFn({ restrict: 'E', @@ -22533,10 +22601,11 @@ forEach(['src', 'srcset', 'href'], function(attrName) { attr.$set(name, value); - // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist + // Support: IE 9-11 only + // On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need // to set the property as well to achieve the desired effect. - // we use attr[attrName] value since $set can sanitize the url. + // We use attr[attrName] value since $set can sanitize the url. if (msie && propName) element.prop(propName, attr[name]); }); } @@ -22544,7 +22613,7 @@ forEach(['src', 'srcset', 'href'], function(attrName) { }; }); -/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true +/* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS */ var nullFormCtrl = { $addControl: noop, @@ -22555,6 +22624,7 @@ var nullFormCtrl = { $setPristine: noop, $setSubmitted: noop }, +PENDING_CLASS = 'ng-pending', SUBMITTED_CLASS = 'ng-submitted'; function nullFormRenameControl(control, name) { @@ -22605,22 +22675,28 @@ function nullFormRenameControl(control, name) { */ //asks for $scope to fool the BC controller module FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; -function FormController(element, attrs, $scope, $animate, $interpolate) { - var form = this, - controls = []; +function FormController($element, $attrs, $scope, $animate, $interpolate) { + this.$$controls = []; // init state - form.$error = {}; - form.$$success = {}; - form.$pending = undefined; - form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope); - form.$dirty = false; - form.$pristine = true; - form.$valid = true; - form.$invalid = false; - form.$submitted = false; - form.$$parentForm = nullFormCtrl; + this.$error = {}; + this.$$success = {}; + this.$pending = undefined; + this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope); + this.$dirty = false; + this.$pristine = true; + this.$valid = true; + this.$invalid = false; + this.$submitted = false; + this.$$parentForm = nullFormCtrl; + this.$$element = $element; + this.$$animate = $animate; + + setupValidity(this); +} + +FormController.prototype = { /** * @ngdoc method * @name form.FormController#$rollbackViewValue @@ -22632,11 +22708,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * event defined in `ng-model-options`. This method is typically needed by the reset button of * a form that uses `ng-model-options` to pend updates. */ - form.$rollbackViewValue = function() { - forEach(controls, function(control) { + $rollbackViewValue: function() { + forEach(this.$$controls, function(control) { control.$rollbackViewValue(); }); - }; + }, /** * @ngdoc method @@ -22649,11 +22725,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * event defined in `ng-model-options`. This method is rarely needed as `NgModelController` * usually handles calling this in response to input events. */ - form.$commitViewValue = function() { - forEach(controls, function(control) { + $commitViewValue: function() { + forEach(this.$$controls, function(control) { control.$commitViewValue(); }); - }; + }, /** * @ngdoc method @@ -22676,29 +22752,29 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * For example, if an input control is added that is already `$dirty` and has `$error` properties, * calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form. */ - form.$addControl = function(control) { + $addControl: function(control) { // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored // and not added to the scope. Now we throw an error. assertNotHasOwnProperty(control.$name, 'input'); - controls.push(control); + this.$$controls.push(control); if (control.$name) { - form[control.$name] = control; + this[control.$name] = control; } - control.$$parentForm = form; - }; + control.$$parentForm = this; + }, // Private API: rename a form control - form.$$renameControl = function(control, newName) { + $$renameControl: function(control, newName) { var oldName = control.$name; - if (form[oldName] === control) { - delete form[oldName]; + if (this[oldName] === control) { + delete this[oldName]; } - form[newName] = control; + this[newName] = control; control.$name = newName; - }; + }, /** * @ngdoc method @@ -22716,60 +22792,26 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * different from case to case. For example, removing the only `$dirty` control from a form may or * may not mean that the form is still `$dirty`. */ - form.$removeControl = function(control) { - if (control.$name && form[control.$name] === control) { - delete form[control.$name]; + $removeControl: function(control) { + if (control.$name && this[control.$name] === control) { + delete this[control.$name]; } - forEach(form.$pending, function(value, name) { - form.$setValidity(name, null, control); - }); - forEach(form.$error, function(value, name) { - form.$setValidity(name, null, control); - }); - forEach(form.$$success, function(value, name) { - form.$setValidity(name, null, control); - }); + forEach(this.$pending, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$error, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$$success, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); - arrayRemove(controls, control); + arrayRemove(this.$$controls, control); control.$$parentForm = nullFormCtrl; - }; - - - /** - * @ngdoc method - * @name form.FormController#$setValidity - * - * @description - * Sets the validity of a form control. - * - * This method will also propagate to parent forms. - */ - addSetValidityMethod({ - ctrl: this, - $element: element, - set: function(object, property, controller) { - var list = object[property]; - if (!list) { - object[property] = [controller]; - } else { - var index = list.indexOf(controller); - if (index === -1) { - list.push(controller); - } - } - }, - unset: function(object, property, controller) { - var list = object[property]; - if (!list) { - return; - } - arrayRemove(list, controller); - if (list.length === 0) { - delete object[property]; - } - }, - $animate: $animate - }); + }, /** * @ngdoc method @@ -22781,13 +22823,13 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * This method can be called to add the 'ng-dirty' class and set the form to a dirty * state (ng-dirty class). This method will also propagate to parent forms. */ - form.$setDirty = function() { - $animate.removeClass(element, PRISTINE_CLASS); - $animate.addClass(element, DIRTY_CLASS); - form.$dirty = true; - form.$pristine = false; - form.$$parentForm.$setDirty(); - }; + $setDirty: function() { + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$dirty = true; + this.$pristine = false; + this.$$parentForm.$setDirty(); + }, /** * @ngdoc method @@ -22805,15 +22847,15 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after * saving or resetting it. */ - form.$setPristine = function() { - $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); - form.$dirty = false; - form.$pristine = true; - form.$submitted = false; - forEach(controls, function(control) { + $setPristine: function() { + this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); + this.$dirty = false; + this.$pristine = true; + this.$submitted = false; + forEach(this.$$controls, function(control) { control.$setPristine(); }); - }; + }, /** * @ngdoc method @@ -22828,11 +22870,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * Setting a form controls back to their untouched state is often useful when setting the form * back to its pristine state. */ - form.$setUntouched = function() { - forEach(controls, function(control) { + $setUntouched: function() { + forEach(this.$$controls, function(control) { control.$setUntouched(); }); - }; + }, /** * @ngdoc method @@ -22841,12 +22883,46 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * @description * Sets the form to its submitted state. */ - form.$setSubmitted = function() { - $animate.addClass(element, SUBMITTED_CLASS); - form.$submitted = true; - form.$$parentForm.$setSubmitted(); - }; -} + $setSubmitted: function() { + this.$$animate.addClass(this.$$element, SUBMITTED_CLASS); + this.$submitted = true; + this.$$parentForm.$setSubmitted(); + } +}; + +/** + * @ngdoc method + * @name form.FormController#$setValidity + * + * @description + * Sets the validity of a form control. + * + * This method will also propagate to parent forms. + */ +addSetValidityMethod({ + clazz: FormController, + set: function(object, property, controller) { + var list = object[property]; + if (!list) { + object[property] = [controller]; + } else { + var index = list.indexOf(controller); + if (index === -1) { + list.push(controller); + } + } + }, + unset: function(object, property, controller) { + var list = object[property]; + if (!list) { + return; + } + arrayRemove(list, controller); + if (list.length === 0) { + delete object[property]; + } + } +}); /** * @ngdoc directive @@ -23043,13 +23119,13 @@ var formDirectiveFactory = function(isNgForm) { event.preventDefault(); }; - addEventListenerFn(formElement[0], 'submit', handleFormSubmission); + formElement[0].addEventListener('submit', handleFormSubmission); // unregister the preventDefault listener so that we don't not leak memory but in a // way that will achieve the prevention of the default action. formElement.on('$destroy', function() { $timeout(function() { - removeEventListenerFn(formElement[0], 'submit', handleFormSubmission); + formElement[0].removeEventListener('submit', handleFormSubmission); }, 0, false); }); } @@ -23094,6 +23170,111 @@ var formDirectiveFactory = function(isNgForm) { var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); + + +// helper methods +function setupValidity(instance) { + instance.$$classCache = {}; + instance.$$classCache[INVALID_CLASS] = !(instance.$$classCache[VALID_CLASS] = instance.$$element.hasClass(VALID_CLASS)); +} +function addSetValidityMethod(context) { + var clazz = context.clazz, + set = context.set, + unset = context.unset; + + clazz.prototype.$setValidity = function(validationErrorKey, state, controller) { + if (isUndefined(state)) { + createAndSet(this, '$pending', validationErrorKey, controller); + } else { + unsetAndCleanup(this, '$pending', validationErrorKey, controller); + } + if (!isBoolean(state)) { + unset(this.$error, validationErrorKey, controller); + unset(this.$$success, validationErrorKey, controller); + } else { + if (state) { + unset(this.$error, validationErrorKey, controller); + set(this.$$success, validationErrorKey, controller); + } else { + set(this.$error, validationErrorKey, controller); + unset(this.$$success, validationErrorKey, controller); + } + } + if (this.$pending) { + cachedToggleClass(this, PENDING_CLASS, true); + this.$valid = this.$invalid = undefined; + toggleValidationCss(this, '', null); + } else { + cachedToggleClass(this, PENDING_CLASS, false); + this.$valid = isObjectEmpty(this.$error); + this.$invalid = !this.$valid; + toggleValidationCss(this, '', this.$valid); + } + + // re-read the state as the set/unset methods could have + // combined state in this.$error[validationError] (used for forms), + // where setting/unsetting only increments/decrements the value, + // and does not replace it. + var combinedState; + if (this.$pending && this.$pending[validationErrorKey]) { + combinedState = undefined; + } else if (this.$error[validationErrorKey]) { + combinedState = false; + } else if (this.$$success[validationErrorKey]) { + combinedState = true; + } else { + combinedState = null; + } + + toggleValidationCss(this, validationErrorKey, combinedState); + this.$$parentForm.$setValidity(validationErrorKey, combinedState, this); + }; + + function createAndSet(ctrl, name, value, controller) { + if (!ctrl[name]) { + ctrl[name] = {}; + } + set(ctrl[name], value, controller); + } + + function unsetAndCleanup(ctrl, name, value, controller) { + if (ctrl[name]) { + unset(ctrl[name], value, controller); + } + if (isObjectEmpty(ctrl[name])) { + ctrl[name] = undefined; + } + } + + function cachedToggleClass(ctrl, className, switchValue) { + if (switchValue && !ctrl.$$classCache[className]) { + ctrl.$$animate.addClass(ctrl.$$element, className); + ctrl.$$classCache[className] = true; + } else if (!switchValue && ctrl.$$classCache[className]) { + ctrl.$$animate.removeClass(ctrl.$$element, className); + ctrl.$$classCache[className] = false; + } + } + + function toggleValidationCss(ctrl, validationErrorKey, isValid) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + + cachedToggleClass(ctrl, VALID_CLASS + validationErrorKey, isValid === true); + cachedToggleClass(ctrl, INVALID_CLASS + validationErrorKey, isValid === false); + } +} + +function isObjectEmpty(obj) { + if (obj) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + return false; + } + } + } + return true; +} + /* global VALID_CLASS: false, INVALID_CLASS: false, @@ -23115,11 +23296,11 @@ var ISO_DATE_REGEXP = /^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+- // 7. Path // 8. Query // 9. Fragment -// 1111111111111111 222 333333 44444 555555555555555555555555 666 77777777 8888888 999 -var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+\])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i; +// 1111111111111111 222 333333 44444 55555555555555555555555 666 77777777 8888888 999 +var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i; // eslint-disable-next-line max-len -var EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/; -var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/; +var EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/; +var NUMBER_REGEXP = /^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/; var DATE_REGEXP = /^(\d{4,})-(\d{2})-(\d{2})$/; var DATETIMELOCAL_REGEXP = /^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; var WEEK_REGEXP = /^(\d{4,})-W(\d\d)$/; @@ -23773,7 +23954,17 @@ var inputType = { * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * Can be interpolated. * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * Can be interpolated. + * @param {string=} ngMin Like `min`, sets the `min` validation error key if the value entered is less than `ngMin`, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} ngMax Like `max`, sets the `max` validation error key if the value entered is greater than `ngMax`, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} step Sets the `step` validation error key if the value entered does not fit the `step` constraint. + * Can be interpolated. + * @param {string=} ngStep Like `step`, sets the `step` validation error key if the value entered does not fit the `ngStep` constraint, + * but does not trigger HTML5 native validation. Takes an expression. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of @@ -24128,28 +24319,6 @@ var inputType = { * @description * Native range input with validation and transformation. * - *
- *

- * In v1.5.9+, in order to avoid interfering with already existing, custom directives for - * `input[range]`, you need to let Angular know that you want to enable its built-in support. - * You can do this by adding the `ng-input-range` attribute to the input element. E.g.: - * `` - *


- *

- * Input elements without the `ng-input-range` attibute will continue to be treated the same - * as in previous versions (e.g. their model value will be a string not a number and Angular - * will not take `min`/`max`/`step` attributes and properties into account). - *


- *

- * **Note:** From v1.6.x onwards, the support for `input[range]` will be always enabled and - * the `ng-input-range` attribute will have no effect. - *


- *

- * This documentation page refers to elements which have the built-in support enabled; i.e. - * elements _with_ the `ng-input-range` attribute. - *

- *
- * * The model for the range input must always be a `Number`. * * IE9 and other browsers that do not support the `range` type fall back @@ -24171,7 +24340,7 @@ var inputType = { * * Since the element value should always reflect the current model value, a range input * will set the bound ngModel expression to the value that the browser has set for the - * input element. For example, in the following input ``, + * input element. For example, in the following input ``, * if the application sets `model.value = null`, the browser will set the input to `'50'`. * Angular will then set the model to `50`, to prevent input and model value being out of sync. * @@ -24190,12 +24359,10 @@ var inputType = { * instead may set the `stepMismatch` error. If that's the case, the Angular will set the `step` * error on the input, and set the model to `undefined`. * - * Note that `input[range]` is not compatible with `ngMax`, `ngMin`, and `ngStep`, because they do + * Note that `input[range]` is not compatible with`ngMax`, `ngMin`, and `ngStep`, because they do * not set the `min` and `max` attributes, which means that the browser won't automatically adjust * the input value based on their values, and will always assume min = 0, max = 100, and step = 1. * - * @param ngInputRange The presense of this attribute enables the built-in support for - * `input[range]`. * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation to ensure that the value entered is greater @@ -24206,6 +24373,9 @@ var inputType = { * Can be interpolated. * @param {string=} ngChange Angular expression to be executed when the ngModel value changes due * to user interaction with the input element. + * @param {expression=} ngChecked If the expression is truthy, then the `checked` attribute will be set on the + * element. **Note** : `ngChecked` should not be used alongside `ngModel`. + * Checkout {@link ng.directive:ngChecked ngChecked} for usage. * * @example @@ -24220,7 +24390,7 @@ var inputType = {
- Model as range: + Model as range:
Model as number:
Min:
@@ -24246,7 +24416,7 @@ var inputType = { }]); - Model as range: + Model as range:
Model as number:
Min:
@@ -24339,7 +24509,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { var type = lowercase(element[0].type); - // In composition mode, users are still inputing intermediate text buffer, + // In composition mode, users are still inputting intermediate text buffer, // hold the listener until composition is done. // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent if (!$sniffer.android) { @@ -24532,7 +24702,7 @@ function createDateInputType(type, regexp, parseDate, format) { return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { badInputChecker(scope, element, attr, ctrl); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - var timezone = ctrl && ctrl.$options && ctrl.$options.timezone; + var timezone = ctrl && ctrl.$options.getOption('timezone'); var previousDate; ctrl.$$parserName = type; @@ -24686,8 +24856,8 @@ function isValidForStep(viewValue, stepBase, step) { function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { badInputChecker(scope, element, attr, ctrl); - baseInputType(scope, element, attr, ctrl, $sniffer, $browser); numberFormatterParser(ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); var minVal; var maxVal; @@ -24715,6 +24885,20 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { ctrl.$validate(); }); } + + if (isDefined(attr.step) || attr.ngStep) { + var stepVal; + ctrl.$validators.step = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || + isValidForStep(viewValue, minVal || 0, stepVal); + }; + + attr.$observe('step', function(val) { + stepVal = parseNumberAttrVal(val); + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); + } } function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) { @@ -24878,14 +25062,20 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { } function radioInputType(scope, element, attr, ctrl) { + var doTrim = !attr.ngTrim || trim(attr.ngTrim) !== 'false'; // make the name unique, if not defined if (isUndefined(attr.name)) { element.attr('name', nextUid()); } var listener = function(ev) { + var value; if (element[0].checked) { - ctrl.$setViewValue(attr.value, ev && ev.type); + value = attr.value; + if (doTrim) { + value = trim(value); + } + ctrl.$setViewValue(value, ev && ev.type); } }; @@ -24893,6 +25083,9 @@ function radioInputType(scope, element, attr, ctrl) { ctrl.$render = function() { var value = attr.value; + if (doTrim) { + value = trim(value); + } element[0].checked = (value === ctrl.$viewValue); }; @@ -25138,11 +25331,7 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', link: { pre: function(scope, element, attr, ctrls) { if (ctrls[0]) { - var type = lowercase(attr.type); - if ((type === 'range') && !attr.hasOwnProperty('ngInputRange')) { - type = 'text'; - } - (inputType[type] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, + (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, $browser, $filter, $parse); } } @@ -25158,21 +25347,19 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; * @name ngValue * * @description - * Binds the given expression to the value of `