diff --git a/src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js b/src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js index b283a1fec8..550c227613 100644 --- a/src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js +++ b/src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js @@ -1,10 +1,10 @@ angular.module('umbraco.security.interceptor') // This http interceptor listens for authentication successes and failures .factory('securityInterceptor', ['$injector', 'securityRetryQueue', 'notificationsService', 'eventsService', 'requestInterceptorFilter', function ($injector, queue, notifications, eventsService, requestInterceptorFilter) { - return function(promise) { + return function (promise) { return promise.then( - function(originalResponse) { + 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 @@ -23,12 +23,12 @@ angular.module('umbraco.security.interceptor') } return promise; - }, function(originalResponse) { + }, function (originalResponse) { // Intercept failed requests - + // Make sure we have the configuration of the request (don't we always?) var config = originalResponse.config ? originalResponse.config : {}; - + // Make sure we have an object for the headers of the request var headers = config.headers ? config.headers : {}; @@ -37,7 +37,7 @@ angular.module('umbraco.security.interceptor') //exit/ignore return promise; } - var filtered = _.find(requestInterceptorFilter(), function(val) { + var filtered = _.find(requestInterceptorFilter(), function (val) { return config.url.indexOf(val) > 0; }); if (filtered) { @@ -47,18 +47,18 @@ angular.module('umbraco.security.interceptor') //A 401 means that the user is not logged in if (originalResponse.status === 401 && !originalResponse.config.url.endsWith("umbraco/backoffice/UmbracoApi/Authentication/GetCurrentUser")) { - var userService = $injector.get('userService'); // see above + 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); - }); - }); + //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) { @@ -103,18 +103,19 @@ angular.module('umbraco.security.interceptor') }; }]) //used to set headers on all requests where necessary - .factory('umbracoRequestInterceptor', function ($q, queryStrings) { - return { - //dealing with requests: - 'request': function(config) { - if (queryStrings.getParams().umbDebug === "true" || queryStrings.getParams().umbdebug === "true") { - config.headers["X-UMB-DEBUG"] = "true"; - } - return config; - } - }; + .factory('umbracoRequestInterceptor', function ($q, urlHelper) { + return { + //dealing with requests: + 'request': function (config) { + var queryStrings = urlHelper.getQueryStringParams(); + if (queryStrings.umbDebug === "true" || queryStrings.umbdebug === "true") { + config.headers["X-UMB-DEBUG"] = "true"; + } + return config; + } + }; }) - .value('requestInterceptorFilter', function() { + .value('requestInterceptorFilter', function () { return ["www.gravatar.com"]; }) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index 27da6be9d3..94781a6529 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -15,7 +15,7 @@ * Section navigation and search, and maintain their state for the entire application lifetime * */ -function navigationService($rootScope, $routeParams, $log, $location, $q, $timeout, $injector, eventsService, dialogService, umbModelMapper, treeService, notificationsService, historyService, appState, angularHelper) { +function navigationService($rootScope, $route, $routeParams, $log, $location, $q, $timeout, $injector, urlHelper, eventsService, dialogService, umbModelMapper, treeService, notificationsService, historyService, appState, angularHelper) { //the promise that will be resolved when the navigation is ready var navReadyPromise = $q.defer(); @@ -28,6 +28,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo navReadyPromise.resolve(mainTreeApi); }); + //used to track the current dialog object var currentDialog = null; @@ -104,25 +105,6 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo return navReadyPromise.promise; }, - /** - * @ngdoc method - * @name umbraco.services.navigationService#setMainCulture - * @methodOf umbraco.services.navigationService - * - * @description - * Utility to set the mculture query string without changing the route. This is a hack to work around angular's limitations - */ - setMainCulture: function (culture) { - - $location.search("mculture", culture); - - //fixme: This can work but interferes with our other $locationChangeStart - //var un = $rootScope.$on('$locationChangeStart', function (event) { - // event.preventDefault(); - // un(); - //}); - }, - /** * @ngdoc method * @name umbraco.services.navigationService#clearSearch @@ -132,10 +114,10 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo * utility to clear the querystring/search params while maintaining a known list of parameters that should be maintained throughout the app */ clearSearch: function () { - var retainKeys = ["mculture"]; + var toRetain = ["mculture"]; var currentSearch = $location.search(); $location.search(''); - _.each(retainKeys, function (k) { + _.each(toRetain, function (k) { if (currentSearch[k]) { $location.search(k, currentSearch[k]); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/urlhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/urlhelper.service.js new file mode 100644 index 0000000000..a6a22bab03 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/urlhelper.service.js @@ -0,0 +1,108 @@ +/** +* @ngdoc service +* @name umbraco.services.urlHelper +* @description A helper used to work with URLs +**/ + +(function () { + "use strict"; + + function urlHelper($window) { + + var pl = /\+/g; // Regex for replacing addition symbol with a space + var search = /([^&=]+)=?([^&]*)/g; + var decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); }; + + //Used for browsers that don't support $window.URL + function polyFillUrl(url) { + var parser = document.createElement('a'); + // Let the browser do the work + parser.href = url; + + return { + protocol: parser.protocol, + host: parser.host, + hostname: parser.hostname, + port: parser.port, + pathname: parser.pathname, + search: parser.search, + hash: parser.hash + }; + } + + return { + + /** + * @ngdoc function + * @name parseUrl + * @methodOf umbraco.services.urlHelper + * @function + * + * @description + * Returns an object representing each part of the url + * + * @param {string} url the url string to parse + */ + parseUrl: function (url) { + + //create a URL object based on either the native URL method or the polyfill method + var urlObj = $window.URL ? new $window.URL(url) : polyFillUrl(url); + //append the searchObject + urlObj.searchObject = this.getQueryStringParams(urlObj.search); + return urlObj; + }, + + /** + * @ngdoc function + * @name parseHashIntoUrl + * @methodOf umbraco.services.urlHelper + * @function + * + * @description + * If the hash of a URL contains a path + query strings, this will parse the hash into a url object + * + * @param {string} url the url string to parse + */ + parseHashIntoUrl: function (url) { + var urlObj = this.parseUrl(url); + if (!urlObj.hash) { + throw new "No hash found in url: " + url; + } + if (!urlObj.hash.startsWith("#/")) { + throw new "The hash in url does not contain a path to parse: " + url; + } + //now create a fake full URL with the hash + var fakeUrl = "http://fakeurl.com" + urlObj.hash.trimStart("#"); + var fakeUrlObj = this.parseUrl(fakeUrl); + return fakeUrlObj; + }, + + /** + * @ngdoc function + * @name getQueryStringParams + * @methodOf umbraco.services.urlHelper + * @function + * + * @description + * Returns a dictionary of query string key/vals + * + * @param {string} location optional URL to parse, the default will use $window.location + */ + getQueryStringParams: function (location) { + var match; + + //use the current location if none specified + var query = location ? location.substring(1) : $window.location.search.substring(1); + + var urlParams = {}; + while (match = search.exec(query)) { + urlParams[decode(match[1])] = decode(match[2]); + } + + return urlParams; + } + }; + } + angular.module('umbraco.services').factory('urlHelper', urlHelper); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index 11444ff65b..79a42b92d4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -141,307 +141,6 @@ function packageHelper(assetsService, treeService, eventsService, $templateCache } angular.module('umbraco.services').factory('packageHelper', packageHelper); -//TODO: I believe this is obsolete -function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, mediaHelper, umbRequestHelper) { - return { - /** sets the image's url, thumbnail and if its a folder */ - setImageData: function (img) { - - img.isFolder = !mediaHelper.hasFilePropertyType(img); - - if (!img.isFolder) { - img.thumbnail = mediaHelper.resolveFile(img, true); - img.image = mediaHelper.resolveFile(img, false); - } - }, - - /** sets the images original size properties - will check if it is a folder and if so will just make it square */ - setOriginalSize: function (img, maxHeight) { - //set to a square by default - img.originalWidth = maxHeight; - img.originalHeight = maxHeight; - - var widthProp = _.find(img.properties, function (v) { return (v.alias === "umbracoWidth"); }); - if (widthProp && widthProp.value) { - img.originalWidth = parseInt(widthProp.value, 10); - if (isNaN(img.originalWidth)) { - img.originalWidth = maxHeight; - } - } - var heightProp = _.find(img.properties, function (v) { return (v.alias === "umbracoHeight"); }); - if (heightProp && heightProp.value) { - img.originalHeight = parseInt(heightProp.value, 10); - if (isNaN(img.originalHeight)) { - img.originalHeight = maxHeight; - } - } - }, - - /** sets the image style which get's used in the angular markup */ - setImageStyle: function (img, width, height, rightMargin, bottomMargin) { - img.style = { width: width + "px", height: height + "px", "margin-right": rightMargin + "px", "margin-bottom": bottomMargin + "px" }; - img.thumbStyle = { - "background-image": "url('" + img.thumbnail + "')", - "background-repeat": "no-repeat", - "background-position": "center", - "background-size": Math.min(width, img.originalWidth) + "px " + Math.min(height, img.originalHeight) + "px" - }; - }, - - /** gets the image's scaled wdith based on the max row height */ - getScaledWidth: function (img, maxHeight) { - var scaled = img.originalWidth * maxHeight / img.originalHeight; - return scaled; - //round down, we don't want it too big even by half a pixel otherwise it'll drop to the next row - //return Math.floor(scaled); - }, - - /** returns the target row width taking into account how many images will be in the row and removing what the margin is */ - getTargetWidth: function (imgsPerRow, maxRowWidth, margin) { - //take into account the margin, we will have 1 less margin item than we have total images - return (maxRowWidth - ((imgsPerRow - 1) * margin)); - }, - - /** - This will determine the row/image height for the next collection of images which takes into account the - ideal image count per row. It will check if a row can be filled with this ideal count and if not - if there - are additional images available to fill the row it will keep calculating until they fit. - - It will return the calculated height and the number of images for the row. - - targetHeight = optional; - */ - getRowHeightForImages: function (imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, targetHeight) { - - var idealImages = imgs.slice(0, idealImgPerRow); - //get the target row width without margin - var targetRowWidth = this.getTargetWidth(idealImages.length, maxRowWidth, margin); - //this gets the image with the smallest height which equals the maximum we can scale up for this image block - var maxScaleableHeight = this.getMaxScaleableHeight(idealImages, maxRowHeight); - //if the max scale height is smaller than the min display height, we'll use the min display height - targetHeight = targetHeight !== undefined ? targetHeight : Math.max(maxScaleableHeight, minDisplayHeight); - - var attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); - - if (attemptedRowHeight != null) { - - //if this is smaller than the min display then we need to use the min display, - // which means we'll need to remove one from the row so we can scale up to fill the row - if (attemptedRowHeight < minDisplayHeight) { - - if (idealImages.length > 1) { - - //we'll generate a new targetHeight that is halfway between the max and the current and recurse, passing in a new targetHeight - targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); - return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin, targetHeight); - } - else { - //this will occur when we only have one image remaining in the row but it's still going to be too wide even when - // using the minimum display height specified. In this case we're going to have to just crop the image in it's center - // using the minimum display height and the full row width - return { height: minDisplayHeight, imgCount: 1 }; - } - } - else { - //success! - return { height: attemptedRowHeight, imgCount: idealImages.length }; - } - } - - //we know the width will fit in a row, but we now need to figure out if we can fill - // the entire row in the case that we have more images remaining than the idealImgPerRow. - - if (idealImages.length === imgs.length) { - //we have no more remaining images to fill the space, so we'll just use the calc height - return { height: targetHeight, imgCount: idealImages.length }; - } - else if (idealImages.length === 1) { - //this will occur when we only have one image remaining in the row to process but it's not really going to fit ideally - // in the row. - return { height: minDisplayHeight, imgCount: 1 }; - } - else if (idealImages.length === idealImgPerRow && targetHeight < maxRowHeight) { - - //if we're already dealing with the ideal images per row and it's not quite wide enough, we can scale up a little bit so - // long as the targetHeight is currently less than the maxRowHeight. The scale up will be half-way between our current - // target height and the maxRowHeight (we won't loop forever though - if there's a difference of 5 px we'll just quit) - - while (targetHeight < maxRowHeight && (maxRowHeight - targetHeight) > 5) { - targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); - attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); - if (attemptedRowHeight != null) { - //success! - return { height: attemptedRowHeight, imgCount: idealImages.length }; - } - } - - //Ok, we couldn't actually scale it up with the ideal row count we'll just recurse with a lesser image count. - return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin); - } - else if (targetHeight === maxRowHeight) { - - //This is going to happen when: - // * We can fit a list of images in a row, but they come up too short (based on minDisplayHeight) - // * Then we'll try to remove an image, but when we try to scale to fit, the width comes up too narrow but the images are already at their - // maximum height (maxRowHeight) - // * So we're stuck, we cannot precicely fit the current list of images, so we'll render a row that will be max height but won't be wide enough - // which is better than rendering a row that is shorter than the minimum since that could be quite small. - - return { height: targetHeight, imgCount: idealImages.length }; - } - else { - - //we have additional images so we'll recurse and add 1 to the idealImgPerRow until it fits - return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow + 1, margin); - } - - }, - - performGetRowHeight: function (idealImages, targetRowWidth, minDisplayHeight, targetHeight) { - - var currRowWidth = 0; - - for (var i = 0; i < idealImages.length; i++) { - var scaledW = this.getScaledWidth(idealImages[i], targetHeight); - currRowWidth += scaledW; - } - - if (currRowWidth > targetRowWidth) { - //get the new scaled height to fit - var newHeight = targetRowWidth * targetHeight / currRowWidth; - - return newHeight; - } - else if (idealImages.length === 1 && (currRowWidth <= targetRowWidth) && !idealImages[0].isFolder) { - //if there is only one image, then return the target height - return targetHeight; - } - else if (currRowWidth / targetRowWidth > 0.90) { - //it's close enough, it's at least 90% of the width so we'll accept it with the target height - return targetHeight; - } - else { - //if it's not successful, return null - return null; - } - }, - - /** builds an image grid row */ - buildRow: function (imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, totalRemaining) { - var currRowWidth = 0; - var row = { images: [] }; - - var imageRowHeight = this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin); - var targetWidth = this.getTargetWidth(imageRowHeight.imgCount, maxRowWidth, margin); - - var sizes = []; - //loop through the images we know fit into the height - for (var i = 0; i < imageRowHeight.imgCount; i++) { - //get the lower width to ensure it always fits - var scaledWidth = Math.floor(this.getScaledWidth(imgs[i], imageRowHeight.height)); - - if (currRowWidth + scaledWidth <= targetWidth) { - currRowWidth += scaledWidth; - sizes.push({ - width: scaledWidth, - //ensure that the height is rounded - height: Math.round(imageRowHeight.height) - }); - row.images.push(imgs[i]); - } - else if (imageRowHeight.imgCount === 1 && row.images.length === 0) { - //the image is simply too wide, we'll crop/center it - sizes.push({ - width: maxRowWidth, - //ensure that the height is rounded - height: Math.round(imageRowHeight.height) - }); - row.images.push(imgs[i]); - } - else { - //the max width has been reached - break; - } - } - - //loop through the images for the row and apply the styles - for (var j = 0; j < row.images.length; j++) { - var bottomMargin = margin; - //make the margin 0 for the last one - if (j === (row.images.length - 1)) { - margin = 0; - } - this.setImageStyle(row.images[j], sizes[j].width, sizes[j].height, margin, bottomMargin); - } - - if (row.images.length === 1 && totalRemaining > 1) { - //if there's only one image on the row and there are more images remaining, set the container to max width - row.images[0].style.width = maxRowWidth + "px"; - } - - - return row; - }, - - /** Returns the maximum image scaling height for the current image collection */ - getMaxScaleableHeight: function (imgs, maxRowHeight) { - - var smallestHeight = _.min(imgs, function (item) { return item.originalHeight; }).originalHeight; - - //adjust the smallestHeight if it is larger than the static max row height - if (smallestHeight > maxRowHeight) { - smallestHeight = maxRowHeight; - } - return smallestHeight; - }, - - /** Creates the image grid with calculated widths/heights for images to fill the grid nicely */ - buildGrid: function (images, maxRowWidth, maxRowHeight, startingIndex, minDisplayHeight, idealImgPerRow, margin, imagesOnly) { - - var rows = []; - var imagesProcessed = 0; - - //first fill in all of the original image sizes and URLs - for (var i = startingIndex; i < images.length; i++) { - var item = images[i]; - - this.setImageData(item); - this.setOriginalSize(item, maxRowHeight); - - if (imagesOnly && !item.isFolder && !item.thumbnail) { - images.splice(i, 1); - i--; - } - } - - while ((imagesProcessed + startingIndex) < images.length) { - //get the maxHeight for the current un-processed images - var currImgs = images.slice(imagesProcessed); - - //build the row - var remaining = images.length - imagesProcessed; - var row = this.buildRow(currImgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, remaining); - if (row.images.length > 0) { - rows.push(row); - imagesProcessed += row.images.length; - } - else { - - if (currImgs.length > 0) { - throw "Could not fill grid with all images, images remaining: " + currImgs.length; - } - - //if there was nothing processed, exit - break; - } - } - - return rows; - } - }; -} -angular.module("umbraco.services").factory("umbPhotoFolderHelper", umbPhotoFolderHelper); - /** * @ngdoc function * @name umbraco.services.umbModelMapper @@ -596,34 +295,4 @@ function umbPropEditorHelper() { } }; } -angular.module('umbraco.services').factory('umbPropEditorHelper', umbPropEditorHelper); - -/** -* @ngdoc service -* @name umbraco.services.queryStrings -* @description A helper used to get query strings in the real URL (not the hash URL) -**/ -function queryStrings($window) { - - var pl = /\+/g; // Regex for replacing addition symbol with a space - var search = /([^&=]+)=?([^&]*)/g; - var decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); }; - - return { - - getParams: function () { - var match; - var query = $window.location.search.substring(1); - - var urlParams = {}; - while (match = search.exec(query)) { - urlParams[decode(match[1])] = decode(match[2]); - } - - return urlParams; - } - }; -} -angular.module('umbraco.services').factory('queryStrings', queryStrings); - - +angular.module('umbraco.services').factory('umbPropEditorHelper', umbPropEditorHelper); diff --git a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js index 2c92d1d10c..758471061d 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js @@ -334,7 +334,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar }); } if (!currCulture) { - navigationService.setMainCulture(defaultLang ? defaultLang.culture : null); + $location.search("mculture", defaultLang ? defaultLang.culture : null); } } @@ -369,10 +369,8 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar $scope.selectLanguage = function (language) { - navigationService.setMainCulture(language.culture); - - //$scope.selectedLanguage = language; - + $location.search("mculture", language.culture); + // close the language selector $scope.page.languageSelectorIsOpen = false; diff --git a/src/Umbraco.Web.UI.Client/src/init.js b/src/Umbraco.Web.UI.Client/src/init.js index 20b80b26d9..891a9a4753 100644 --- a/src/Umbraco.Web.UI.Client/src/init.js +++ b/src/Umbraco.Web.UI.Client/src/init.js @@ -1,10 +1,6 @@ /** Executed when the application starts, binds to events and set global state */ -app.run(['userService', '$q', '$log', '$rootScope', '$location', 'queryStrings', 'navigationService', 'appState', 'editorState', 'fileManager', 'assetsService', 'eventsService', '$cookies', '$templateCache', 'localStorageService', 'tourService', 'dashboardResource', - function (userService, $q, $log, $rootScope, $location, queryStrings, navigationService, appState, editorState, fileManager, assetsService, eventsService, $cookies, $templateCache, localStorageService, tourService, dashboardResource) { - - $rootScope.$on('$locationChangeStart', function (event, next, current, newState, oldState) { - $log.info("location changing to:" + next); - }); +app.run(['userService', '$q', '$log', '$rootScope', '$route', '$location', 'urlHelper', 'navigationService', 'appState', 'editorState', 'fileManager', 'assetsService', 'eventsService', '$cookies', '$templateCache', 'localStorageService', 'tourService', 'dashboardResource', + function (userService, $q, $log, $rootScope, $route, $location, urlHelper, navigationService, appState, editorState, fileManager, assetsService, eventsService, $cookies, $templateCache, localStorageService, tourService, dashboardResource) { //This sets the default jquery ajax headers to include our csrf token, we // need to user the beforeSend method because our token changes per user/login so @@ -12,7 +8,8 @@ app.run(['userService', '$q', '$log', '$rootScope', '$location', 'queryStrings', $.ajaxSetup({ beforeSend: function (xhr) { xhr.setRequestHeader("X-UMB-XSRF-TOKEN", $cookies["UMB-XSRF-TOKEN"]); - if (queryStrings.getParams().umbDebug === "true" || queryStrings.getParams().umbdebug === "true") { + var queryStrings = urlHelper.getQueryStringParams(); + if (queryStrings.umbDebug === "true" || queryStrings.umbdebug === "true") { xhr.setRequestHeader("X-UMB-DEBUG", "true"); } } @@ -45,9 +42,14 @@ app.run(['userService', '$q', '$log', '$rootScope', '$location', 'queryStrings', returnToPath = null, returnToSearch = null; } + var currentRoute = null; + var globalQueryStrings = ["mculture"]; + /** execute code on each successful route */ $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { + currentRoute = current; //store this so we can reference it in $routeUpdate + var deployConfig = Umbraco.Sys.ServerVariables.deploy; var deployEnv, deployEnvTitle; if (deployConfig) { @@ -92,19 +94,61 @@ app.run(['userService', '$q', '$log', '$rootScope', '$location', 'queryStrings', /** When the route change is rejected - based on checkAuth - we'll prevent the rejected route from executing including wiring up it's controller, etc... and then redirect to the rejected URL. */ $rootScope.$on('$routeChangeError', function (event, current, previous, rejection) { - event.preventDefault(); - var returnPath = null; - if (rejection.path == "/login" || rejection.path.startsWith("/login/")) { - //Set the current path before redirecting so we know where to redirect back to - returnPath = encodeURIComponent($location.url()); + if (rejection.path) { + event.preventDefault(); + + var returnPath = null; + if (rejection.path == "/login" || rejection.path.startsWith("/login/")) { + //Set the current path before redirecting so we know where to redirect back to + returnPath = encodeURIComponent($location.url()); + } + + $location.path(rejection.path) + if (returnPath) { + $location.search("returnPath", returnPath); + } } - $location.path(rejection.path) - if (returnPath) { - $location.search("returnPath", returnPath); - } + }); + //Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered. + //This is the case when a route uses reloadOnSearch: false which is the case for many or our routes so that we are able to maintain + //global state query strings without force re-loading views. + //We can then detect if it's a location change that should force a route or not programatically. + $rootScope.$on('$routeUpdate', function (event, next) { + + if (!currentRoute) { + //if there is no current route then always route which is done with reload + $route.reload(); + } + else { + //check if the location being changed is only the mculture query string, if so, cancel the routing since this is just + //used as a global persistent query string that does not change routes. + + var currUrlParts = currentRoute.params; + var nextUrlParts = next.params; + + var allowRoute = false; + + if (_.keys(currUrlParts).length == _.keys(nextUrlParts).length) { //the number of parts are the same + + //check if any route parameter is not the same (excluding our special global query strings), in that case + //we should allow the route. + _.each(currUrlParts, function (value, key) { + if (globalQueryStrings.indexOf(key) === -1) { + if (value.toLowerCase() !== nextUrlParts[key].toLowerCase()) { + allowRoute = true; + } + } + }); + } + + if (allowRoute) { + //continue the route + $route.reload(); + } + } }); //check for touch device, add to global appState diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js index 84a1db5aeb..94521eaa0e 100644 --- a/src/Umbraco.Web.UI.Client/src/routes.js +++ b/src/Umbraco.Web.UI.Client/src/routes.js @@ -1,5 +1,5 @@ app.config(function ($routeProvider) { - + /** * This determines if the route can continue depending on authentication and initialization requirements * @param {boolean} authRequired If true, it checks if the user is authenticated and will resolve successfully @@ -126,7 +126,8 @@ app.config(function ($routeProvider) { $scope.templateUrl = 'views/common/dashboard.html'; } }); - }, + }, + reloadOnSearch: false, resolve: canRoute(true) }) .when('/:section/framed/:url', { @@ -136,7 +137,8 @@ app.config(function ($routeProvider) { throw "A framed resource must have a url route parameter"; return 'views/common/legacy.html'; - }, + }, + reloadOnSearch: false, resolve: canRoute(true) }) .when('/:section/:tree/:method', { @@ -147,7 +149,8 @@ app.config(function ($routeProvider) { return "views/common/dashboard.html"; return ('views/' + rp.tree + '/' + rp.method + '.html'); - }, + }, + reloadOnSearch: false, resolve: canRoute(true) }) .when('/:section/:tree/:method/:id', { @@ -178,7 +181,8 @@ app.config(function ($routeProvider) { $scope.templateUrl = ('views/' + $routeParams.tree + '/' + $routeParams.method + '.html'); } - }, + }, + reloadOnSearch: false, resolve: canRoute(true) }) .otherwise({ redirectTo: '/login' }); diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/umb-photo-folder-helper.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/umb-photo-folder-helper.spec.js deleted file mode 100644 index a57d69beb2..0000000000 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/umb-photo-folder-helper.spec.js +++ /dev/null @@ -1,99 +0,0 @@ -describe('umbPhotoFolderHelper tests', function () { - var umbPhotoFolderHelper; - - beforeEach(module('umbraco.services')); - - beforeEach(inject(function ($injector) { - umbPhotoFolderHelper = $injector.get('umbPhotoFolderHelper'); - })); - - describe('Calculate row', function () { - - it('Builds a row by scaling the height to fit the width', function () { - - var images = [ - { "properties": [{ "id": 8737, "value": "/media/2173/Save-The-Date.jpg", "alias": "umbracoFile" }, { "id": 8738, "value": "443", "alias": "umbracoWidth" }, { "id": 8739, "value": "500", "alias": "umbracoHeight" }, { "id": 8740, "value": "30830", "alias": "umbracoBytes" }, { "id": 8741, "value": "jpg", "alias": "umbracoExtension" }], "updateDate": "2013-12-10 16:57:56", "createDate": "2013-12-10 14:21:26", "published": false, "owner": { "id": 0, "name": "admin" }, "updater": null, "contentTypeAlias": "Image", "sortOrder": 5, "name": "Save-The-Date.jpg", "id": 1349, "icon": "mediaPhoto.gif", "key": "8eb67ae3-49da-4a25-ab39-185667a9b412", "parentId": 1160, "alias": null, "path": "-1,1142,1160,1349", "metaData": {}, "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1349", "originalWidth": 443, "originalHeight": 500 }, - { "properties": [{ "id": 8742, "value": "/media/2174/IMG_2980.JPG", "alias": "umbracoFile" }, { "id": 8743, "value": "640", "alias": "umbracoWidth" }, { "id": 8744, "value": "480", "alias": "umbracoHeight" }, { "id": 8745, "value": "113311", "alias": "umbracoBytes" }, { "id": 8746, "value": "jpg", "alias": "umbracoExtension" }], "updateDate": "2013-12-10 16:57:51", "createDate": "2013-12-10 14:22:33", "published": false, "owner": { "id": 0, "name": "admin" }, "updater": null, "contentTypeAlias": "Image", "sortOrder": 6, "name": "IMG_2980.JPG", "id": 1350, "icon": "mediaPhoto.gif", "key": "0a9618ea-9b4a-4d34-bf53-e76a0d252048", "parentId": 1160, "alias": null, "path": "-1,1142,1160,1350", "metaData": {}, "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1350", "originalWidth": 640, "originalHeight": 480 }, - { "properties": [{ "id": 8747, "value": "/media/2175/IMG_3023.JPG", "alias": "umbracoFile" }, { "id": 8748, "value": "360", "alias": "umbracoWidth" }, { "id": 8749, "value": "480", "alias": "umbracoHeight" }, { "id": 8750, "value": "106365", "alias": "umbracoBytes" }, { "id": 8751, "value": "jpg", "alias": "umbracoExtension" }], "updateDate": "2013-12-10 16:57:46", "createDate": "2013-12-10 14:39:28", "published": false, "owner": { "id": 0, "name": "admin" }, "updater": null, "contentTypeAlias": "Image", "sortOrder": 7, "name": "IMG_3023.JPG", "id": 1351, "icon": "mediaPhoto.gif", "key": "44cb1ee0-e3d7-40f7-b27c-ae05fb1a8e0c", "parentId": 1160, "alias": null, "path": "-1,1142,1160,1351", "metaData": {}, "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1351", "originalWidth": 360, "originalHeight": 480 }, - { "properties": [{ "id": 8752, "value": "/media/2176/IMG_2055.JPG", "alias": "umbracoFile" }, { "id": 8753, "value": "1024", "alias": "umbracoWidth" }, { "id": 8754, "value": "630", "alias": "umbracoHeight" }, { "id": 8755, "value": "57046", "alias": "umbracoBytes" }, { "id": 8756, "value": "jpg", "alias": "umbracoExtension" }], "updateDate": "2013-12-10 16:57:41", "createDate": "2013-12-10 15:09:47", "published": false, "owner": { "id": 0, "name": "admin" }, "updater": null, "contentTypeAlias": "Image", "sortOrder": 8, "name": "IMG_2055.JPG", "id": 1352, "icon": "mediaPhoto.gif", "key": "8a45465c-251e-44d4-88c5-d86606377105", "parentId": 1160, "alias": null, "path": "-1,1142,1160,1352", "metaData": {}, "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1352", "originalWidth": 1024, "originalHeight": 630 }, - { "properties": [{ "id": 8757, "value": "/media/2177/Signature1.png", "alias": "umbracoFile" }, { "id": 8758, "value": "873", "alias": "umbracoWidth" }, { "id": 8759, "value": "269", "alias": "umbracoHeight" }, { "id": 8760, "value": "105616", "alias": "umbracoBytes" }, { "id": 8761, "value": "png", "alias": "umbracoExtension" }], "updateDate": "2013-12-10 16:57:36", "createDate": "2013-12-10 15:11:53", "published": false, "owner": { "id": 0, "name": "admin" }, "updater": null, "contentTypeAlias": "Image", "sortOrder": 9, "name": "Signature1.png", "id": 1353, "icon": "mediaPhoto.gif", "key": "e12d382a-56f8-4b85-b507-b82aa466cd6f", "parentId": 1160, "alias": null, "path": "-1,1142,1160,1353", "metaData": {}, "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1353", "originalWidth": 873, "originalHeight": 269 } - ]; - var maxRowHeight = 330; - var minDisplayHeight = 100; - var maxRowWidth = 851; - var idealImgPerRow = 5; - var margin = 5; - - var result = umbPhotoFolderHelper.buildRow(images, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin); - - - expect(result.images.length).toBe(5); - - }); - - it('Builds a row by removing an item to scale up to fit', function () { - - var images = [ - { "properties": [{ "id": 8737, "value": "/media/2173/Save-The-Date.jpg", "alias": "umbracoFile" }, { "id": 8738, "value": "443", "alias": "umbracoWidth" }, { "id": 8739, "value": "500", "alias": "umbracoHeight" }, { "id": 8740, "value": "30830", "alias": "umbracoBytes" }, { "id": 8741, "value": "jpg", "alias": "umbracoExtension" }], "updateDate": "2013-12-10 16:57:56", "createDate": "2013-12-10 14:21:26", "published": false, "owner": { "id": 0, "name": "admin" }, "updater": null, "contentTypeAlias": "Image", "sortOrder": 5, "name": "Save-The-Date.jpg", "id": 1349, "icon": "mediaPhoto.gif", "key": "8eb67ae3-49da-4a25-ab39-185667a9b412", "parentId": 1160, "alias": null, "path": "-1,1142,1160,1349", "metaData": {}, "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1349", "originalWidth": 443, "originalHeight": 500 }, - { "properties": [{ "id": 8742, "value": "/media/2174/IMG_2980.JPG", "alias": "umbracoFile" }, { "id": 8743, "value": "640", "alias": "umbracoWidth" }, { "id": 8744, "value": "480", "alias": "umbracoHeight" }, { "id": 8745, "value": "113311", "alias": "umbracoBytes" }, { "id": 8746, "value": "jpg", "alias": "umbracoExtension" }], "updateDate": "2013-12-10 16:57:51", "createDate": "2013-12-10 14:22:33", "published": false, "owner": { "id": 0, "name": "admin" }, "updater": null, "contentTypeAlias": "Image", "sortOrder": 6, "name": "IMG_2980.JPG", "id": 1350, "icon": "mediaPhoto.gif", "key": "0a9618ea-9b4a-4d34-bf53-e76a0d252048", "parentId": 1160, "alias": null, "path": "-1,1142,1160,1350", "metaData": {}, "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1350", "originalWidth": 640, "originalHeight": 480 }, - { "properties": [{ "id": 8747, "value": "/media/2175/IMG_3023.JPG", "alias": "umbracoFile" }, { "id": 8748, "value": "360", "alias": "umbracoWidth" }, { "id": 8749, "value": "480", "alias": "umbracoHeight" }, { "id": 8750, "value": "106365", "alias": "umbracoBytes" }, { "id": 8751, "value": "jpg", "alias": "umbracoExtension" }], "updateDate": "2013-12-10 16:57:46", "createDate": "2013-12-10 14:39:28", "published": false, "owner": { "id": 0, "name": "admin" }, "updater": null, "contentTypeAlias": "Image", "sortOrder": 7, "name": "IMG_3023.JPG", "id": 1351, "icon": "mediaPhoto.gif", "key": "44cb1ee0-e3d7-40f7-b27c-ae05fb1a8e0c", "parentId": 1160, "alias": null, "path": "-1,1142,1160,1351", "metaData": {}, "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1351", "originalWidth": 360, "originalHeight": 480 }, - { "properties": [{ "id": 8752, "value": "/media/2176/IMG_2055.JPG", "alias": "umbracoFile" }, { "id": 8753, "value": "1024", "alias": "umbracoWidth" }, { "id": 8754, "value": "630", "alias": "umbracoHeight" }, { "id": 8755, "value": "57046", "alias": "umbracoBytes" }, { "id": 8756, "value": "jpg", "alias": "umbracoExtension" }], "updateDate": "2013-12-10 16:57:41", "createDate": "2013-12-10 15:09:47", "published": false, "owner": { "id": 0, "name": "admin" }, "updater": null, "contentTypeAlias": "Image", "sortOrder": 8, "name": "IMG_2055.JPG", "id": 1352, "icon": "mediaPhoto.gif", "key": "8a45465c-251e-44d4-88c5-d86606377105", "parentId": 1160, "alias": null, "path": "-1,1142,1160,1352", "metaData": {}, "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1352", "originalWidth": 1024, "originalHeight": 630 }, - { "properties": [{ "id": 8757, "value": "/media/2177/Signature1.png", "alias": "umbracoFile" }, { "id": 8758, "value": "873", "alias": "umbracoWidth" }, { "id": 8759, "value": "269", "alias": "umbracoHeight" }, { "id": 8760, "value": "105616", "alias": "umbracoBytes" }, { "id": 8761, "value": "png", "alias": "umbracoExtension" }], "updateDate": "2013-12-10 16:57:36", "createDate": "2013-12-10 15:11:53", "published": false, "owner": { "id": 0, "name": "admin" }, "updater": null, "contentTypeAlias": "Image", "sortOrder": 9, "name": "Signature1.png", "id": 1353, "icon": "mediaPhoto.gif", "key": "e12d382a-56f8-4b85-b507-b82aa466cd6f", "parentId": 1160, "alias": null, "path": "-1,1142,1160,1353", "metaData": {}, "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1353", "originalWidth": 873, "originalHeight": 269 } - ]; - var maxRowHeight = 330; - var minDisplayHeight = 100; - var maxRowWidth = 802; - var idealImgPerRow = 5; - var margin = 5; - - var result = umbPhotoFolderHelper.buildRow(images, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin); - - - expect(result.images.length).toBe(4); - - }); - - it('Builds a row by removing an item to scale up to fit, then attempts to upscale remaining 2 images, but that doesnt fit so drops another and we end up with one', function () { - - var images = [ - { "properties": [{ "id": 8737, "value": "/media/2173/Save-The-Date.jpg", "alias": "umbracoFile" }, { "id": 8738, "value": "198", "alias": "umbracoWidth" }, { "id": 8739, "value": "220", "alias": "umbracoHeight" }, { "id": 8740, "value": "30830", "alias": "umbracoBytes" }, { "id": 8741, "value": "jpg", "alias": "umbracoExtension" }], "updateDate": "2013-12-10 16:57:56", "createDate": "2013-12-10 14:21:26", "published": false, "owner": { "id": 0, "name": "admin" }, "updater": null, "contentTypeAlias": "Image", "sortOrder": 5, "name": "Save-The-Date.jpg", "id": 1349, "icon": "mediaPhoto.gif", "key": "8eb67ae3-49da-4a25-ab39-185667a9b412", "parentId": 1160, "alias": null, "path": "-1,1142,1160,1349", "metaData": {}, "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1349", "originalWidth": 198, "originalHeight": 220 }, - { "properties": [{ "id": 8742, "value": "/media/2174/IMG_2980.JPG", "alias": "umbracoFile" }, { "id": 8743, "value": "211", "alias": "umbracoWidth" }, { "id": 8744, "value": "500", "alias": "umbracoHeight" }, { "id": 8745, "value": "113311", "alias": "umbracoBytes" }, { "id": 8746, "value": "jpg", "alias": "umbracoExtension" }], "updateDate": "2013-12-10 16:57:51", "createDate": "2013-12-10 14:22:33", "published": false, "owner": { "id": 0, "name": "admin" }, "updater": null, "contentTypeAlias": "Image", "sortOrder": 6, "name": "IMG_2980.JPG", "id": 1350, "icon": "mediaPhoto.gif", "key": "0a9618ea-9b4a-4d34-bf53-e76a0d252048", "parentId": 1160, "alias": null, "path": "-1,1142,1160,1350", "metaData": {}, "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1350", "originalWidth": 211, "originalHeight": 500 }, - { "properties": [{ "id": 8747, "value": "/media/2175/IMG_3023.JPG", "alias": "umbracoFile" }, { "id": 8748, "value": "940", "alias": "umbracoWidth" }, { "id": 8749, "value": "317", "alias": "umbracoHeight" }, { "id": 8750, "value": "106365", "alias": "umbracoBytes" }, { "id": 8751, "value": "jpg", "alias": "umbracoExtension" }], "updateDate": "2013-12-10 16:57:46", "createDate": "2013-12-10 14:39:28", "published": false, "owner": { "id": 0, "name": "admin" }, "updater": null, "contentTypeAlias": "Image", "sortOrder": 7, "name": "IMG_3023.JPG", "id": 1351, "icon": "mediaPhoto.gif", "key": "44cb1ee0-e3d7-40f7-b27c-ae05fb1a8e0c", "parentId": 1160, "alias": null, "path": "-1,1142,1160,1351", "metaData": {}, "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1351", "originalWidth": 940, "originalHeight": 317 } - ]; - var maxRowHeight = 250; - var minDisplayHeight = 105; - var maxRowWidth = 400; - var idealImgPerRow = 3; - var margin = 5; - - var result = umbPhotoFolderHelper.buildRow(images, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin); - - - expect(result.images.length).toBe(1); - - }); - - //SEE: http://issues.umbraco.org/issue/U4-5304 - it('When a row fits with width but its too short, we remove one and scale up, but that comes up too narrow, so we just render what we have', function () { - - var images = [ -{ "properties": [{ "value": "/test35.jpg", "alias": "umbracoFile" }, { "value": "1000", "alias": "umbracoWidth" }, { "value": "1041", "alias": "umbracoHeight" }], "contentTypeAlias": "Image", "name": "Test.jpg", "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1349", "originalWidth": 1000, "originalHeight": 1041 }, -{ "properties": [{ "value": "/test36.jpg", "alias": "umbracoFile" }, { "value": "1000", "alias": "umbracoWidth" }, { "value": "2013", "alias": "umbracoHeight" }], "contentTypeAlias": "Image", "name": "Test.jpg", "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1349", "originalWidth": 1000, "originalHeight": 2013 }, -{ "properties": [{ "value": "/test37.jpg", "alias": "umbracoFile" }, { "value": "840", "alias": "umbracoWidth" }, { "value": "360", "alias": "umbracoHeight" }], "contentTypeAlias": "Image", "name": "Test.jpg", "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1349", "originalWidth": 840, "originalHeight": 360 } - ]; - var maxRowHeight = 250; - var minDisplayHeight = 105; - var maxRowWidth = 400; - var idealImgPerRow = 3; - var margin = 5; - - var result = umbPhotoFolderHelper.buildRow(images, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin); - - expect(result.images.length).toBe(2); - - }); - - - }); - -}); \ No newline at end of file