diff --git a/src/Cofoundry.Core/Core/Settings/DebugSettings.cs b/src/Cofoundry.Core/Core/Settings/DebugSettings.cs index fa62d0de4..bd64a33bc 100644 --- a/src/Cofoundry.Core/Core/Settings/DebugSettings.cs +++ b/src/Cofoundry.Core/Core/Settings/DebugSettings.cs @@ -43,7 +43,7 @@ public partial class DebugSettings : CofoundryConfigurationSettingsBase public string EmbeddedContentPhysicalPathRootOverride { get; set; } /// - /// USe to determine if we should show the developer exception page, + /// Use to determine if we should show the developer exception page, /// taking the current environment into consideration. /// /// The current hosting environment. diff --git a/src/Cofoundry.Web.Admin/Admin/Modules/Shared/Content/js/shared.js b/src/Cofoundry.Web.Admin/Admin/Modules/Shared/Content/js/shared.js index f2c8daf80..939fd4c3b 100644 --- a/src/Cofoundry.Web.Admin/Admin/Modules/Shared/Content/js/shared.js +++ b/src/Cofoundry.Web.Admin/Admin/Modules/Shared/Content/js/shared.js @@ -8092,7 +8092,8 @@ function ( }); headers.common['X-Requested-With'] = 'XMLHttpRequest'; - + headers.common['Accept'] = 'application/json, text/html, text/plain, */*' + }]); /** * Fix to angular 1.5 > 1.6 upgrade where the default hashPrefix has changed. Here @@ -9229,63 +9230,160 @@ function ( function Controller() { } }]); -angular.module('cms.shared').directive('cmsFormFieldDirectorySelector', [ - '_', - 'shared.directiveUtilities', - 'shared.internalModulePath', - 'shared.directoryService', +angular.module('cms.shared').controller('ImageAssetEditorDialogController', [ + '$scope', + 'shared.LoadState', + 'shared.imageService', + 'shared.SearchQuery', + 'shared.urlLibrary', + 'options', + 'close', function ( - _, - directiveUtilities, - modulePath, - directoryService - ) { + $scope, + LoadState, + imageService, + SearchQuery, + urlLibrary, + options, + close) { + + var vm = $scope, + isAssetInitialized; - return { - restrict: 'E', - templateUrl: modulePath + 'UIComponents/Directories/FormFieldDirectorySelector.html', - scope: { - model: '=cmsModel', - title: '@cmsTitle', - onLoaded: '&cmsOnLoaded', - readonly: '=cmsReadonly' - }, - link: { - pre: preLink - }, - controller: Controller, - controllerAs: 'vm', - bindToController: true - }; + init(); + + /* INIT */ + + function init() { + angular.extend($scope, options); - /* COMPILE */ + vm.formLoadState = new LoadState(); + vm.saveLoadState = new LoadState(); - function preLink(scope, el, attrs) { - var vm = scope.vm; + vm.onInsert = onInsert; + vm.onCancel = onCancel; - if (angular.isDefined(attrs.required)) { - vm.isRequired = true; - } else { - vm.isRequired = false; - vm.defaultItemText = attrs.cmsDefaultItemText || 'None'; + vm.onImageChanged = onImageChanged; + vm.command = {}; + + setCurrentImage(); + } + + /* ACTIONS */ + + function setCurrentImage() { + // If we have an existing image, we need to find the asset id to set the command image + if (vm.imageAssetHtml && vm.imageAssetHtml.length) { + vm.command.imageAssetId = vm.imageAssetHtml.attr('data-image-asset-id'); + vm.command.altTag = vm.imageAssetHtml.attr('alt'); + vm.command.style = vm.imageAssetHtml.attr('style'); + + // If the image had any styles (mainly dimensions), pass them to the command so they are retained + if (vm.command.style) { + var styles = parseStyles(vm.command.style); + vm.command.width = styles['width']; + vm.command.height = styles['height']; + + // Else, look to see if the dimensions are stored as attibutes of the image + } else { + vm.command.width = vm.imageAssetHtml.attr('width'); + vm.command.height = vm.imageAssetHtml.attr('height'); + } + + // If we cannot find the asset id (could have removed the data attribute that this relies on), + // we try to work this out based on the image path (this might change in future versions of cofoundry so less reliable) + if (!vm.command.imageAssetId) { + var src = vm.imageAssetHtml.attr('src'); + var lastIndex = src.lastIndexOf('/'); + var extractId = src.substr(lastIndex + 1, ((src.indexOf('_') - lastIndex) - 1)); + vm.command.imageAssetId = extractId; + } } - vm.title = attrs.cmsTitle || 'Directory'; - vm.description = attrs.cmsDescription; - directiveUtilities.setModelName(vm, attrs); } + + /* EVENTS */ - /* CONTROLLER */ + function onCancel() { + close(); + } - function Controller() { - var vm = this; + function onImageChanged() { + vm.command.altTag = vm.command.imageAsset.title || vm.command.imageAsset.fileName; + } - directoryService.getAll().then(function (pageDirectories) { - vm.pageDirectories = pageDirectories; + function onInsert() { - if (vm.onLoaded) vm.onLoaded(); - }); + // Parse and hold dimensions + var dimensions = { + width: parseUnits(vm.command.width), + height: parseUnits(vm.command.height) + }; + + // If we have no sizes set, default to percentage respecting ratio + if (!dimensions.width && !dimensions.height) { + dimensions.width = '100%'; + dimensions.height = 'auto'; + } + + // Get the image path, including specific size options if nessessary + var path = urlLibrary.getImageUrl(vm.command.imageAsset, parseImageRequestSize(dimensions)); + + // Default the alt tag to an empty string if not specified + var alt = vm.command.altTag || ''; + + // Define an object thay holds formatted outputs, plus the model itself + var output = { + markdown: "![Alt " + alt + "](" + path + ")", + html: "" + alt + "", + model: vm.command + }; + + // Add css styles to output html + output.html = insertCssStyles(output.html, dimensions); + + // Call callback with output + vm.onSelected(output); + + // Close dialog + close(); + } + + /* PUBLIC HELPERS */ + + function insertCssStyles(html, styles) { + return angular.element(html).css(styles)[0].outerHTML; + } + + function parseImageRequestSize(dimensions) { + // If unit type is percent, use original image size + if ((dimensions.width || '').indexOf('%') > -1 || (dimensions.height || '').indexOf('%') > -1) return {}; + + // Else, return raw pixel sizes + return { + width: dimensions.width.replace('px', ''), + height: dimensions.height.replace('px', '') + }; + } + + function parseUnits(value) { + if (!value) return ''; + + // Default to pixels if not unit type specified + if (value.indexOf('px') == -1 && value.indexOf('%') == -1 && value.indexOf('auto') == -1) return value + 'px'; + + // Return original value if we get here + return value; + } + + function parseStyles(cssText) { + var regex = /([\w-]*)\s*:\s*([^;]*)/g; + var match, properties = {}; + while (match = regex.exec(cssText)) properties[match[1]] = match[2]; + return properties; } + }]); + angular.module('cms.shared').directive('cmsDocumentAsset', [ 'shared.internalModulePath', 'shared.urlLibrary', @@ -10030,171 +10128,74 @@ function ( }]); -angular.module('cms.shared').controller('ImageAssetEditorDialogController', [ +angular.module('cms.shared').directive('cmsFormFieldDirectorySelector', [ + '_', + 'shared.directiveUtilities', + 'shared.internalModulePath', + 'shared.directoryService', +function ( + _, + directiveUtilities, + modulePath, + directoryService + ) { + + return { + restrict: 'E', + templateUrl: modulePath + 'UIComponents/Directories/FormFieldDirectorySelector.html', + scope: { + model: '=cmsModel', + title: '@cmsTitle', + onLoaded: '&cmsOnLoaded', + readonly: '=cmsReadonly' + }, + link: { + pre: preLink + }, + controller: Controller, + controllerAs: 'vm', + bindToController: true + }; + + /* COMPILE */ + + function preLink(scope, el, attrs) { + var vm = scope.vm; + + if (angular.isDefined(attrs.required)) { + vm.isRequired = true; + } else { + vm.isRequired = false; + vm.defaultItemText = attrs.cmsDefaultItemText || 'None'; + } + vm.title = attrs.cmsTitle || 'Directory'; + vm.description = attrs.cmsDescription; + directiveUtilities.setModelName(vm, attrs); + } + + /* CONTROLLER */ + + function Controller() { + var vm = this; + + directoryService.getAll().then(function (pageDirectories) { + vm.pageDirectories = pageDirectories; + + if (vm.onLoaded) vm.onLoaded(); + }); + } +}]); +angular.module('cms.shared').controller('AddEntityAccessRuleController', [ '$scope', + '$q', 'shared.LoadState', - 'shared.imageService', - 'shared.SearchQuery', - 'shared.urlLibrary', + 'shared.roleService', + 'shared.userAreaService', 'options', 'close', function ( $scope, - LoadState, - imageService, - SearchQuery, - urlLibrary, - options, - close) { - - var vm = $scope, - isAssetInitialized; - - init(); - - /* INIT */ - - function init() { - angular.extend($scope, options); - - vm.formLoadState = new LoadState(); - vm.saveLoadState = new LoadState(); - - vm.onInsert = onInsert; - vm.onCancel = onCancel; - - vm.onImageChanged = onImageChanged; - vm.command = {}; - - setCurrentImage(); - } - - /* ACTIONS */ - - function setCurrentImage() { - // If we have an existing image, we need to find the asset id to set the command image - if (vm.imageAssetHtml && vm.imageAssetHtml.length) { - vm.command.imageAssetId = vm.imageAssetHtml.attr('data-image-asset-id'); - vm.command.altTag = vm.imageAssetHtml.attr('alt'); - vm.command.style = vm.imageAssetHtml.attr('style'); - - // If the image had any styles (mainly dimensions), pass them to the command so they are retained - if (vm.command.style) { - var styles = parseStyles(vm.command.style); - vm.command.width = styles['width']; - vm.command.height = styles['height']; - - // Else, look to see if the dimensions are stored as attibutes of the image - } else { - vm.command.width = vm.imageAssetHtml.attr('width'); - vm.command.height = vm.imageAssetHtml.attr('height'); - } - - // If we cannot find the asset id (could have removed the data attribute that this relies on), - // we try to work this out based on the image path (this might change in future versions of cofoundry so less reliable) - if (!vm.command.imageAssetId) { - var src = vm.imageAssetHtml.attr('src'); - var lastIndex = src.lastIndexOf('/'); - var extractId = src.substr(lastIndex + 1, ((src.indexOf('_') - lastIndex) - 1)); - vm.command.imageAssetId = extractId; - } - } - } - - /* EVENTS */ - - function onCancel() { - close(); - } - - function onImageChanged() { - vm.command.altTag = vm.command.imageAsset.title || vm.command.imageAsset.fileName; - } - - function onInsert() { - - // Parse and hold dimensions - var dimensions = { - width: parseUnits(vm.command.width), - height: parseUnits(vm.command.height) - }; - - // If we have no sizes set, default to percentage respecting ratio - if (!dimensions.width && !dimensions.height) { - dimensions.width = '100%'; - dimensions.height = 'auto'; - } - - // Get the image path, including specific size options if nessessary - var path = urlLibrary.getImageUrl(vm.command.imageAsset, parseImageRequestSize(dimensions)); - - // Default the alt tag to an empty string if not specified - var alt = vm.command.altTag || ''; - - // Define an object thay holds formatted outputs, plus the model itself - var output = { - markdown: "![Alt " + alt + "](" + path + ")", - html: "" + alt + "", - model: vm.command - }; - - // Add css styles to output html - output.html = insertCssStyles(output.html, dimensions); - - // Call callback with output - vm.onSelected(output); - - // Close dialog - close(); - } - - /* PUBLIC HELPERS */ - - function insertCssStyles(html, styles) { - return angular.element(html).css(styles)[0].outerHTML; - } - - function parseImageRequestSize(dimensions) { - // If unit type is percent, use original image size - if ((dimensions.width || '').indexOf('%') > -1 || (dimensions.height || '').indexOf('%') > -1) return {}; - - // Else, return raw pixel sizes - return { - width: dimensions.width.replace('px', ''), - height: dimensions.height.replace('px', '') - }; - } - - function parseUnits(value) { - if (!value) return ''; - - // Default to pixels if not unit type specified - if (value.indexOf('px') == -1 && value.indexOf('%') == -1 && value.indexOf('auto') == -1) return value + 'px'; - - // Return original value if we get here - return value; - } - - function parseStyles(cssText) { - var regex = /([\w-]*)\s*:\s*([^;]*)/g; - var match, properties = {}; - while (match = regex.exec(cssText)) properties[match[1]] = match[2]; - return properties; - } - -}]); - -angular.module('cms.shared').controller('AddEntityAccessRuleController', [ - '$scope', - '$q', - 'shared.LoadState', - 'shared.roleService', - 'shared.userAreaService', - 'options', - 'close', -function ( - $scope, - $q, + $q, LoadState, roleService, userAreaService, @@ -11027,1216 +11028,1223 @@ function ( }]); /** - * Base class for form fields that uses default conventions and includes integration with - * server validation. + * Validates that a field matches the value of another field. Set the came of the field + * in the attribute definition e.g. cms-match="vm.command.password" + * Adapted from http://ericpanorel.net/2013/10/05/angularjs-password-match-form-validation/ */ -angular.module('cms.shared').factory('baseFormFieldFactory', [ +angular.module('cms.shared').directive('cmsMatch', [ + '$parse', '$timeout', - 'shared.stringUtilities', + 'shared.internalModulePath', 'shared.directiveUtilities', - 'shared.validationErrorService', function ( + $parse, $timeout, - stringUtilities, - directiveUtilities, - validationErrorService + modulePath, + directiveUtilities ) { - var service = {}, - /* Here we can validation messages that can apply to all FormField controls */ - globalDefaultValidationMessages = [ - { - attr: 'required', - msg: 'This field is required' - }, - { - attr: 'maxlength', - msg: 'This field cannot be longer than {0} characters' - }, - { - attr: 'minlength', - msg: 'This must be at least {0} characters long' - } - ]; - - /* PUBLIC */ - - service.create = function (config) { - return angular.extend({}, service.defaultConfig, config); - } - - /* CONFIG */ + var DIRECTIVE_ID = 'cmsMatch'; + var DIRECTIVE_ATTRIBUTE = 'cms-match'; - /** - * Configuration defaults - */ - service.defaultConfig = { - restrict: 'E', - replace: true, - require: ['^^cmsForm'], - scope: { - title: '@cmsTitle', - description: '@cmsDescription', - change: '&cmsChange', - model: '=cmsModel', - disabled: '=cmsDisabled', - readonly: '=cmsReadonly' - }, - compile: compile, + return { link: link, - controller: function () { }, - controllerAs: 'vm', - bindToController: true, - - /* Custom Properties */ - - /** - * Should return the main input element that is displayed in edit mode. - * By default this returns the first child input element. - */ - getInputEl: getInputEl, - - /** - * a list of attributes that when defined on the directive are passed through to the element - * returned from getInputEl. - */ - passThroughAttributes: [], - - /** - * Default validation messages to use when none are provided on the element. Saves specifying common messages - * like 'This field is required' on every field. Each array element should be an object in the form of: - * { attr: 'name', msg: 'string message or formatter function' } - */ - defaultValidationMessages: [] + restrict: 'A', + require: ['^^cmsForm', '?ngModel'], }; + + function link(scope, el, attrs, controllers) { + // NB: ngModel may be null on an outer form control before it has been copied to the inner input. + if (!attrs[DIRECTIVE_ID] || !controllers[1]) return; - /* COMPILE */ - - function compile(el, attrs) { + var formController = controllers[0], + ngModelController = controllers[1], + form = formController.getFormScope().getForm(), + sourceField = directiveUtilities.parseModelName(attrs[DIRECTIVE_ID]); - initPassThroughAttributes.call(this, el, attrs); + var validator = function (value, otherVal) { + var formField = form[sourceField]; + if (!formField) return false; - return this.link.bind(this); - } + var sourceFieldValue = formField.$viewValue; - function link(scope, el, attrs, controllers) { - var vm = scope.vm, - formController = controllers[0]; + return value === sourceFieldValue; + } - // Model Properties - vm.formScope = formController.getFormScope(); - vm.form = vm.formScope.getForm(); - - directiveUtilities.setModelName(vm, attrs); - parseErrorMessages(vm, attrs); + ngModelController.$validators[DIRECTIVE_ID] = validator; + } +}]); +angular.module('cms.shared').directive('cmsFormFieldImageAnchorLocationSelector', [ + '_', + 'shared.internalModulePath', + function ( + _, + modulePath) { + return { + restrict: 'E', + templateUrl: modulePath + 'UIComponents/ImageAssets/FormFieldImageAnchorLocationSelector.html', + scope: { + model: '=cmsModel', + readonly: '=cmsReadonly' + }, + controller: Controller, + controllerAs: 'vm', + bindToController: true + }; - // Model Funcs - vm.onChange = onChange.bind(vm); - vm.resetCustomErrors = resetCustomErrors.bind(vm); - vm.addOrUpdateValidator = addOrUpdateValidator.bind(vm); + /* CONTROLLER */ - // Init Errors - vm.resetCustomErrors(); + function Controller() { + var vm = this; + + vm.options = [ + { name: 'Top Left', id: 'TopLeft' }, + { name: 'Top Center', id: 'TopCenter' }, + { name: 'Top Right', id: 'TopRight' }, + { name: 'Middle Left', id: 'MiddleLeft' }, + { name: 'Middle Center', id: 'MiddleCenter' }, + { name: 'Middle Right', id: 'MiddleRight' }, + { name: 'Bottom Left', id: 'BottomLeft' }, + { name: 'Bottom Center', id: 'BottomCenter' }, + { name: 'Bottom Right', id: 'BottomRight' } + ]; + } +}]); +/** + * A form field control for an image asset that uses a search and pick dialog + * to allow the user to change the selected file. + */ +angular.module('cms.shared').directive('cmsFormFieldImageAsset', [ + '_', + 'shared.internalModulePath', + 'shared.internalContentPath', + 'shared.modalDialogService', + 'shared.stringUtilities', + 'shared.imageService', + 'shared.urlLibrary', + 'baseFormFieldFactory', + function ( + _, + modulePath, + contentPath, + modalDialogService, + stringUtilities, + imageService, + urlLibrary, + baseFormFieldFactory) { - // Bind Validation Events - bindValidationEvents(scope, el); + /* VARS */ - // watches - scope.$watch('vm.model', function () { - vm.resetCustomErrors(); - }); - } + var assetReplacementPath = contentPath + 'img/AssetReplacement/', + noImagePath = assetReplacementPath + 'image-replacement.png', + baseConfig = baseFormFieldFactory.defaultConfig; - /* PUBLIC */ + /* CONFIG */ - function onChange() { - var vm = this; + var config = { + templateUrl: modulePath + 'UIComponents/ImageAssets/FormFieldImageAsset.html', + scope: _.extend(baseConfig.scope, { + asset: '=cmsAsset', // if we already have the full asset data we can set it here to save an api call + loadState: '=cmsLoadState', + updateAsset: '@cmsUpdateAsset' // update the asset property if it changes + }), + passThroughAttributes: [ + 'required' + ], + link: link + }; - vm.resetCustomErrors(); - if (vm.change) { - // run after digest cycle completes so the parent ngModel is updated - $timeout(vm.change, 0); - } - } + return baseFormFieldFactory.create(config); - function resetCustomErrors() { - var model = this.form[this.modelName]; + /* LINK */ - if (model) { - model.$setValidity('server', true); - } - this.customErrors = []; - } + function link(scope, el, attributes, controllers) { + var vm = scope.vm, + isRequired = _.has(attributes, 'required'), + isAssetInitialized; - function addOrUpdateValidator(validator) { - var vm = this; - var validators = _.filter(vm.validators, function (v) { - return v.name !== validator.name; - }); - validators.push(validator); - vm.validators = validators; - } + init(); + return baseConfig.link(scope, el, attributes, controllers); - /* HELPERS */ + /* INIT */ - /** - * Loop through attributes specified in config.passThroughAttributes and copy - * them onto the input control requrned by config.getInputEl - */ - function initPassThroughAttributes(rootEl, attrs) { - var config = this, - el = config.getInputEl(rootEl); + function init() { - (config.passThroughAttributes || []).forEach(function (passThroughAttribute) { - var name = passThroughAttribute.name || passThroughAttribute; - if (angular.isDefined(attrs[name])) { - el[0].setAttribute(attrs.$attr[name], attrs[name]); - } - else if (passThroughAttribute.default) { - el[0].setAttribute(name, passThroughAttribute.default); - } - }); - } + vm.urlLibrary = urlLibrary; + vm.showPicker = showPicker; + vm.remove = remove; + vm.isRemovable = _.isObject(vm.model) && !isRequired; - function getInputEl(rootEl) { - return rootEl.find('input'); - } + vm.filter = parseFilters(attributes); + vm.previewWidth = attributes['cmsPreviewWidth'] || 450; + vm.previewHeight = attributes['cmsPreviewHeight']; + + scope.$watch("vm.asset", setAsset); + scope.$watch("vm.model", setAssetById); + } - function bindValidationEvents(scope, el) { - var fn = _.partial(addErrors, scope.vm, el); + /* EVENTS */ - validationErrorService.addHandler(scope.vm.modelName, fn); + function remove() { + setAsset(null); + } - scope.$on('$destroy', function () { - validationErrorService.removeHandler(fn); - }); - } - - function parseErrorMessages(vm, attrs) { - var config = this, - postfix = 'ValMsg', - attrPostfix = '-val-msg'; + function showPicker() { - vm.validators = []; + modalDialogService.show({ + templateUrl: modulePath + 'UIComponents/ImageAssets/ImageAssetPickerDialog.html', + controller: 'ImageAssetPickerDialogController', + options: { + currentAsset: vm.previewAsset, + filter: vm.filter, + onSelected: onSelected + } + }); - _.each(attrs.$attr, function (value, key) { - var attributeToValidate, - msg; + function onSelected(newAsset) { - if (stringUtilities.endsWith(key, postfix)) { - // if the property is postfix '-val-msg' then pull in the message from the attribute - attributeToValidate = value.substring(0, value.length - attrPostfix.length); - msg = attrs[key]; - } else { + if (!newAsset && vm.previewAsset) { + setAsset(null); - attributeToValidate = value; + vm.onChange(); + } else if (!vm.previewAsset || (newAsset && vm.previewAsset.imageAssetId !== newAsset.imageAssetId)) { + setAsset(newAsset); - // check to see if we have a default message for the property - msg = getDefaultErrorMessage(config.defaultValidationMessages, key) || getDefaultErrorMessage(globalDefaultValidationMessages, key); - } + vm.onChange(); + } + } + } - if (msg) { - vm.validators.push({ - name: attrs.$normalize(attributeToValidate), - message: stringUtilities.format(msg, attrs[key]) - }); - } - }); + /** + * When the model is set without a preview asset, we need to go get the full + * asset details. This query can be bypassed by setting the cms-asset attribute + */ + function setAssetById(assetId) { - function getDefaultErrorMessage(defaultsToCheck, attr) { - var validator = _.find(defaultsToCheck, function (v) { - return v.attr === attr; - }); + // Remove the id if it is 0 or invalid to make sure required validation works + if (!assetId) { + vm.model = assetId = undefined; + } - if (validator) { - if (_.isFunction(validator.msg)) { - return validator.msg(vm.modelName, attrs); + if (assetId && (!vm.previewAsset || vm.previewAsset.imageAssetId != assetId)) { + imageService.getById(assetId).then(function (asset) { + if (asset) { + setAsset(asset); + } + }); + } } - return validator.msg; - } - } - } - function addErrors(vm, el, errors) { - var form = vm.formScope.getForm(); - var model = form[vm.modelName]; - vm.resetCustomErrors(); + /** + * Initialise the state when the asset is changed + */ + function setAsset(asset) { - model.$setValidity('server', false); + if (asset) { + vm.previewAsset = asset; + vm.isRemovable = !isRequired; + vm.model = asset.imageAssetId; - // make dirty to ensure css classes are applied - getInputEl(el).removeClass('ng-pristine').addClass('ng-dirty'); + if (vm.updateAsset) { + vm.asset = asset; + } + } else if (isAssetInitialized) { + // Ignore if we are running this first time to avoid overwriting the model with a null vlaue + vm.previewAsset = null; + vm.isRemovable = false; - errors.forEach(function (error) { - vm.customErrors.push(error); - }); - } + if (vm.model) { + vm.model = null; + } + if (vm.updateAsset) { + vm.asset = null; + } + } - /* DEFINITION */ + setButtonText(); - return service; -}]); -angular.module('cms.shared').directive('cmsFormFieldChar', [ - '_', - 'shared.internalModulePath', - 'baseFormFieldFactory', -function ( - _, - modulePath, - baseFormFieldFactory) { + isAssetInitialized = true; + } - var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldChar.html', - passThroughAttributes: [ - 'required', - 'placeholder', - 'pattern', - 'disabled', - 'cmsMatch' - ] - }; + /* Helpers */ + + function parseFilters(attributes) { + var filter = {}, + attributePrefix = 'cms'; + + setAttribute('Tags'); + setAttribute('Width', true); + setAttribute('Height', true); + setAttribute('MinWidth', true); + setAttribute('MinHeight', true); - return baseFormFieldFactory.create(config); -}]); -angular.module('cms.shared').directive('cmsFormFieldCheckbox', [ - 'shared.internalModulePath', - 'baseFormFieldFactory', -function ( - modulePath, - baseFormFieldFactory) { + return filter; - var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldCheckbox.html', - passThroughAttributes: [ - 'disabled' - ] - }; + function setAttribute(filterName, isInt) { + var value = attributes[attributePrefix + filterName]; - return baseFormFieldFactory.create(config); -}]); -angular.module('cms.shared').directive('cmsFormFieldCheckboxList', [ + if (value) { + filterName = stringUtilities.lowerCaseFirstWord(filterName); + filter[filterName] = isInt ? parseInt(value) : value; + } + } + } + + function setButtonText() { + vm.buttonText = vm.model ? 'Change' : 'Select'; + } + } + + }]); +angular.module('cms.shared').directive('cmsFormFieldImageAssetCollection', [ '_', - '$http', 'shared.internalModulePath', - 'shared.optionSourceService', + 'shared.LoadState', + 'shared.imageService', + 'shared.modalDialogService', + 'shared.arrayUtilities', + 'shared.stringUtilities', + 'shared.urlLibrary', 'baseFormFieldFactory', function ( _, - $http, modulePath, - optionSourceService, + LoadState, + imageService, + modalDialogService, + arrayUtilities, + stringUtilities, + urlLibrary, baseFormFieldFactory) { + /* VARS */ + + var IMAGE_ASSET_ID_PROP = 'imageAssetId', + baseConfig = baseFormFieldFactory.defaultConfig; + + /* CONFIG */ + var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldCheckboxList.html', - scope: _.extend(baseFormFieldFactory.defaultConfig.scope, { - options: '=cmsOptions', - optionValue: '@cmsOptionValue', - optionName: '@cmsOptionName', - optionsApi: '@cmsOptionsApi', - noValueText: '@cmsNoValueText', - required: '=cmsRequired', - disabled: '=cmsDisabled' - }), + templateUrl: modulePath + 'UIComponents/ImageAssets/FormFieldImageAssetCollection.html', passThroughAttributes: [ - 'placeholder', - 'disabled', - 'cmsMatch', - 'required' + 'required', + 'ngRequired' ], link: link }; return baseFormFieldFactory.create(config); - /* PRIVATE */ + /* LINK */ - function link(scope, element, attrs, controllers) { + function link(scope, el, attributes, controllers) { var vm = scope.vm; - init(); - // call base - baseFormFieldFactory.defaultConfig.link.apply(this, arguments); + init(); + return baseConfig.link(scope, el, attributes, controllers); - /* Init */ + /* INIT */ function init() { - vm.isRequiredAttributeDefined = angular.isDefined(attrs.required); - vm.onCheckChanged = updateModel; - if (vm.optionsApi) { - // options bound by api call - getOptions(vm.optionsApi); - - } else { - - // options provided as a model - var optionsWatch = scope.$watch('vm.options', function () { + vm.gridLoadState = new LoadState(); + vm.urlLibrary = urlLibrary; - bindOptions(vm.options); - updateDisplayValues(); - }); - } + vm.showPicker = showPicker; + vm.remove = remove; + vm.onDrop = onDrop; - scope.$watch('vm.model', onModelChanged); + scope.$watch("vm.model", setGridItems); } - /* Events */ + /* EVENTS */ - function onModelChanged() { - updateOptionSelectionFromModel(); - updateDisplayValues(); - } + function remove(image) { - /* Helpers */ + removeItemFromArray(vm.gridData, image); + removeItemFromArray(vm.model, image.imageAssetId); - function getOptions(apiPath) { - return optionSourceService.getFromApi(apiPath).then(loadOptions); + function removeItemFromArray(arr, item) { + var index = arr.indexOf(item); - function loadOptions(options) { - bindOptions(options); - updateDisplayValues(); + if (index >= 0) { + return arr.splice(index, 1); + } } } - /** - * Copies over the option collection so the original - * is not modified with selcted properties. - */ - function bindOptions(options) { - vm.listOptions = _.map(options, mapOption); + function showPicker() { - updateOptionSelectionFromModel(); + modalDialogService.show({ + templateUrl: modulePath + 'UIComponents/ImageAssets/ImageAssetPickerDialog.html', + controller: 'ImageAssetPickerDialogController', + options: { + selectedIds: vm.model || [], + filter: getFilter(), + onSelected: onSelected + } + }); - function mapOption(option) { - return { - text: option[vm.optionName], - value: option[vm.optionValue] - }; + function onSelected(newImageArr) { + vm.model = newImageArr; + setGridItems(newImageArr); } } - /** - * Updates the selected values in the listOptions - * collection from the model, - */ - function updateOptionSelectionFromModel() { - var isModelEmpty = !vm.model || !vm.model.length; - vm.displayValues = []; - - _.each(vm.listOptions, function (option) { - - option.selected = !isModelEmpty && !!_.find(vm.model, function (m) { - return m === option.value; - }); - }); - } + function onDrop($index, droppedEntity) { - function updateDisplayValues() { + arrayUtilities.moveObject(vm.gridData, droppedEntity, $index, IMAGE_ASSET_ID_PROP); - vm.displayValues = _.chain(vm.listOptions) - .filter(function (o) { return o.selected }) - .pluck('text') - .value(); + // Update model with new orering + setModelFromGridData(); } - function updateModel() { - - vm.model = _.chain(vm.listOptions) - .filter(function (o) { return o.selected }) - .pluck('value') - .value(); + function setModelFromGridData() { + vm.model = _.pluck(vm.gridData, IMAGE_ASSET_ID_PROP); } - } -}]); -angular.module('cms.shared').directive('cmsFormFieldColor', [ - 'shared.internalModulePath', - 'baseFormFieldFactory', -function ( - modulePath, - baseFormFieldFactory - ) { - - var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldColor.html', - passThroughAttributes: [ - 'required', - 'disabled' - ], - link: link - }; - - return baseFormFieldFactory.create(config); - - function link(scope) { - var vm = scope.vm; - - // call base - baseFormFieldFactory.defaultConfig.link.apply(this, arguments); - - // add custom error for value since its not attribute based like other validation messages - vm.validators.push({ - name: 'pattern', - message: vm.title + " must be a hexadecimal colour value e.g. '#EFEFEF' or '#fff'" - }); - } -}]); -angular.module('cms.shared').directive('cmsFormFieldContainer', [ - 'shared.internalModulePath', -function ( - modulePath - ) { - return { - restrict: 'E', - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldContainer.html', - require: ['^^cmsForm'], - replace: true, - transclude: true, - scope: { - title: '@cmsTitle', - description: '@cmsDescription' - } - }; -}]); -angular.module('cms.shared').directive('cmsFormFieldDate', [ - 'shared.internalModulePath', - 'baseFormFieldFactory', -function ( - modulePath, - baseFormFieldFactory - ) { + /* HELPERS */ - var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldDate.html', - passThroughAttributes: [ - 'required', - 'min', - 'max', - 'step', - 'disabled', - 'cmsMatch' - ], - link: link - }; + function getFilter() { + var filter = {}, + attributePrefix = 'cms'; - return baseFormFieldFactory.create(config); + setAttribute('Tags'); + setAttribute('Width', true); + setAttribute('Height', true); + setAttribute('MinWidth', true); + setAttribute('MinHeight', true); - function link(scope, element, attrs, controllers) { - var vm = scope.vm; + return filter; - // call base - baseFormFieldFactory.defaultConfig.link.apply(this, arguments); + function setAttribute(filterName, isInt) { + var value = attributes[attributePrefix + filterName]; - init(); + if (value) { + filterName = stringUtilities.lowerCaseFirstWord(filterName); + filter[filterName] = isInt ? parseInt(value) : value; + } + } + } - /* Init */ + /** + * Load the grid data if it is inconsistent with the Ids collection. + */ + function setGridItems(ids) { - function init() { - if (attrs.min) { - vm.addOrUpdateValidator({ - name: 'min', - message: "This date cannot be before " + attrs.min - }); + if (!ids || !ids.length) { + vm.gridData = []; } - if (attrs.max) { - vm.addOrUpdateValidator({ - name: 'max', - message: "This date cannot be after " + attrs.max + else if (!vm.gridData || _.pluck(vm.gridData, IMAGE_ASSET_ID_PROP).join() != ids.join()) { + + vm.gridLoadState.on(); + imageService.getByIdRange(ids).then(function (items) { + vm.gridData = items; + vm.gridLoadState.off(); }); } } } }]); -angular.module('cms.shared').directive('cmsFormFieldDateLocal', [ +/** + * File upload control for images. Uses https://github.com/danialfarid/angular-file-upload + */ +angular.module('cms.shared').directive('cmsFormFieldImageUpload', [ + '_', + '$timeout', 'shared.internalModulePath', + 'shared.internalContentPath', + 'shared.LoadState', + 'shared.stringUtilities', + 'shared.imageFileUtilities', + 'shared.imageService', + 'shared.urlLibrary', + 'shared.validationErrorService', 'baseFormFieldFactory', function ( + _, + $timeout, modulePath, - baseFormFieldFactory - ) { + contentPath, + LoadState, + stringUtilities, + imageFileUtilities, + imageService, + urlLibrary, + validationErrorService, + baseFormFieldFactory) { + + /* VARS */ + + var baseConfig = baseFormFieldFactory.defaultConfig, + assetReplacementPath = contentPath + 'img/AssetReplacement/', + noPreviewImagePath = assetReplacementPath + 'preview-not-supported.png', + noImagePath = assetReplacementPath + 'image-replacement.png'; + + /* CONFIG */ var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldDateLocal.html', - passThroughAttributes: [ - 'required', - 'min', - 'max', - 'disabled', - 'cmsMatch' - ], + templateUrl: modulePath + 'UIComponents/ImageAssets/FormFieldImageUpload.html', + scope: _.extend(baseFormFieldFactory.defaultConfig.scope, { + asset: '=cmsAsset', + loadState: '=cmsLoadState', + filter: '=cmsFilter', + onChange: '&cmsOnChange' + }), link: link }; return baseFormFieldFactory.create(config); - function link(scope, element, attrs, controllers) { - var vm = scope.vm; + /* LINK */ - // call base - baseFormFieldFactory.defaultConfig.link.apply(this, arguments); + function link(scope, el, attributes, controllers) { + var vm = scope.vm; init(); + return baseConfig.link(scope, el, attributes, controllers); - /* Init */ + /* INIT */ function init() { - if (attrs.min) { - vm.addOrUpdateValidator({ - name: 'min', - message: "This date cannot be before " + attrs.min - }); - } - if (attrs.max) { - vm.addOrUpdateValidator({ - name: 'max', - message: "This date cannot be after " + attrs.max + + vm.isRequired = _.has(attributes, 'required'); + vm.remove = remove; + vm.fileChanged = onFileChanged; + vm.isRemovable = _.isObject(vm.ngModel) && !vm.isRequired; + vm.mainLoadState = new LoadState(true); + + scope.$watch("vm.asset", setAsset); + + imageService + .getSettings() + .then(mapSettings) + .then(vm.mainLoadState.off); + } + + function mapSettings(settings) { + vm.settings = settings; + } + + /* EVENTS */ + + function remove() { + onFileChanged(); + } + + /** + * Initialise the state when the asset is changed + */ + function setAsset() { + var asset = vm.asset; + + if (asset) { + vm.previewUrl = urlLibrary.getImageUrl(asset, { + width: 450 }); + vm.isRemovable = !vm.isRequired; + + vm.model = { + name: asset.fileName + '.' + asset.extension, + size: asset.fileSizeInBytes, + isCurrentFile: true + }; + } else { + vm.previewUrl = noImagePath; + vm.isRemovable = false; + + if (vm.model) { + vm.model = undefined; + } } + + setButtonText(); } - } -}]); -angular.module('cms.shared').directive('cmsFormFieldDateTime', [ - 'shared.internalModulePath', - 'baseFormFieldFactory', -function ( - modulePath, - baseFormFieldFactory - ) { - var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldDateTime.html', - passThroughAttributes: [ - 'required', - 'min', - 'max', - 'disabled', - 'cmsMatch' - ], - link: link - }; + function onFileChanged($files) { - return baseFormFieldFactory.create(config); + if ($files && $files[0]) { + vm.mainLoadState.on(); - function link(scope, element, attrs, controllers) { - var vm = scope.vm; + // set the file if one is selected + imageFileUtilities + .getFileInfoAndResizeIfRequired($files[0], vm.settings) + .then(onImageInfoLoaded) + .finally(vm.mainLoadState.off); - // call base - baseFormFieldFactory.defaultConfig.link.apply(this, arguments); + } else if (!vm.ngModel || _.isUndefined($files)) { + onNoFileSelected(); + } - init(); + function onImageInfoLoaded(imgInfo) { + if (!imgInfo) { + onNoFileSelected(); + } - /* Init */ + vm.model = imgInfo.file; + validateSize(imgInfo); - function init() { - if (attrs.min) { - var minDate = attrs.min.replace('T', ' '); - vm.addOrUpdateValidator({ - name: 'min', - message: "This date cannot be before " + minDate - }); - } - if (attrs.max) { - var maxDate = attrs.min.replace('T', ' '); - vm.addOrUpdateValidator({ - name: 'max', - message: "This date cannot be after " + maxDate - }); + setPreviewImage(imgInfo.file); + vm.isRemovable = !vm.isRequired; + vm.isResized = imgInfo.isResized; + vm.resizeSize = imgInfo.width + 'x' + imgInfo.height; + onComplete(); } - } - } -}]); -angular.module('cms.shared').directive('cmsFormFieldDateTimeLocal', [ - 'shared.internalModulePath', - 'baseFormFieldFactory', -function ( - modulePath, - baseFormFieldFactory - ) { - var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldDateTimeLocal.html', - scope: _.extend(baseFormFieldFactory.defaultConfig.scope, { - minUtc: '@cmsMinUtc', - maxUtc: '@cmsMaxUtc' - }), - passThroughAttributes: [ - 'required', - 'min', - 'max', - 'disabled', - 'cmsMatch' - ], - link: link - }; + function validateSize(imgInfo) { + var filter = vm.filter; + if (!filter) return; - return baseFormFieldFactory.create(config); + if (!isMinSize(filter.minWidth, imgInfo.width) + || !isMinSize(filter.minHeight, imgInfo.height) + || !isSize(filter.width, imgInfo.width) + || !isSize(filter.height, imgInfo.height)) { + addError('The image is ' + imgInfo.width + 'x' + imgInfo.height + ' which does not meet the size requirements.'); + } - function link(scope, element, attrs, controllers) { - var vm = scope.vm; + function isMinSize(size, value) { + return !(size > 1) || value >= size; + } - // call base - baseFormFieldFactory.defaultConfig.link.apply(this, arguments); + function isSize(size, value) { + return !(size > 1) || value == size; + } + } - init(); + function addError(message) { + // Run in next digest cycle otherwise it will + // be overwirtten in the model change + $timeout(function () { + validationErrorService.raise([{ + message: message, + properties: [vm.modelName] + }]); + }); + } - /* Init */ + function onNoFileSelected() { - function init() { - scope.$watch("vm.minUtc", function (value) { - var date = parseDateAsUtc(value); - vm.minLocal = formatDate(date); - if (vm.minLocal) { - vm.addOrUpdateValidator({ - name: 'min', - message: "This date cannot be before " + date.toLocaleString() - }); - } - }); + // if we don't have an image loaded already, remove the file. + vm.model = undefined; + vm.previewUrl = noImagePath; + vm.isRemovable = false; + vm.isResized = false; + onComplete(); + } - scope.$watch("vm.maxUtc", function (value) { - var date = parseDateAsUtc(value); - vm.maxLocal = formatDate(date); - if (vm.maxLocal) { - vm.addOrUpdateValidator({ - name: 'max', - message: "This date cannot be after " + date.toLocaleString() - }); - } - }); + function onComplete() { + setButtonText(); + + // base onChange event + if (vm.onChange) vm.onChange(); + } } - function parseDateAsUtc(value) { - if (!value) return; + /* Helpers */ - // Format is expected to be 'yyyy-MM-ddT00:00' - return new Date(value + ':00.000Z'); + function setPreviewImage(file) { + if (!isPreviewSupported(file)) { + vm.previewUrl = noPreviewImagePath; + return; + } + + try { + vm.previewUrl = URL.createObjectURL(file); + } + catch (err) { + vm.previewUrl = noPreviewImagePath; + } } - function formatDate(date) { - if (!date) return; + function isPreviewSupported(file) { + var unsupportedPreviewFormats = ['.tiff', '.tif']; - return date.getFullYear() - + '-' + padTwoDigitNumber(date.getMonth() + 1) - + '-' + padTwoDigitNumber(date.getDate()) - + 'T' + padTwoDigitNumber(date.getHours()) - + ':' + padTwoDigitNumber(date.getMinutes()); + return !_.find(unsupportedPreviewFormats, function (format) { + return stringUtilities.endsWith(file.name, format); + }); } - function padTwoDigitNumber(number) { - return ("00" + number).slice(-2); + function setButtonText() { + vm.buttonText = vm.model ? 'Change' : 'Upload'; } } }]); -angular.module('cms.shared').directive('cmsFormFieldDropdown', [ - '_', - '$http', +angular.module('cms.shared').directive('cmsImageAsset', [ 'shared.internalModulePath', - 'baseFormFieldFactory', + 'shared.internalContentPath', + 'shared.urlLibrary', function ( - _, - $http, modulePath, - baseFormFieldFactory) { + contentPath, + urlLibrary) { - var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldDropdown.html', - scope: _.extend(baseFormFieldFactory.defaultConfig.scope, { - options: '=cmsOptions', - optionValue: '@cmsOptionValue', - optionName: '@cmsOptionName', - optionsApi: '@cmsOptionsApi', - defaultItemText: '@cmsDefaultItemText', - href: '@cmsHref', - required: '=cmsRequired', - disabled: '=cmsDisabled' - }), - passThroughAttributes: [ - 'placeholder', - 'disabled', - 'cmsMatch' - ], - getInputEl: getInputEl, - link: link + return { + restrict: 'E', + scope: { + image: '=cmsImage', + width: '@cmsWidth', + height: '@cmsHeight', + cropMode: '@cmsCropMode' + }, + templateUrl: modulePath + 'UIComponents/ImageAssets/ImageAsset.html', + link: function (scope, el, attributes) { + + scope.$watch('image', function (newValue, oldValue) { + if (newValue && newValue.imageAssetId) { + scope.src = urlLibrary.getImageUrl(newValue, { + width: scope.width, + height: scope.height, + mode: scope.cropMode + }); + } else { + scope.src = contentPath + 'img/AssetReplacement/image-replacement.png'; + } + }); + + + }, + replace: true }; +}]); +angular.module('cms.shared').controller('ImageAssetPickerDialogController', [ + '$scope', + 'shared.LoadState', + 'shared.imageService', + 'shared.SearchQuery', + 'shared.modalDialogService', + 'shared.internalModulePath', + 'shared.permissionValidationService', + 'options', + 'close', +function ( + $scope, + LoadState, + imageService, + SearchQuery, + modalDialogService, + modulePath, + permissionValidationService, + options, + close) { + + var vm = $scope; + init(); + + /* INIT */ + + function init() { + angular.extend($scope, options); - return baseFormFieldFactory.create(config); + vm.onOk = onOk; + vm.onCancel = onCancel; + vm.onSelect = onSelect; + vm.onUpload = onUpload; + vm.selectedAsset = vm.currentAsset; // currentAsset is null in single mode + vm.onSelectAndClose = onSelectAndClose; + vm.close = onCancel; - /* PRIVATE */ + vm.gridLoadState = new LoadState(); + vm.query = new SearchQuery({ + onChanged: onQueryChanged, + useHistory: false, + defaultParams: options.filter + }); + vm.presetFilter = options.filter; - function link(scope, element, attrs, controllers) { - var vm = scope.vm; - init(); + vm.filter = vm.query.getFilters(); + vm.toggleFilter = toggleFilter; - // call base - baseFormFieldFactory.defaultConfig.link.apply(this, arguments); + vm.isSelected = isSelected; + vm.multiMode = vm.selectedIds ? true : false; + vm.okText = vm.multiMode ? 'Ok' : 'Select'; - /* Init */ + vm.canCreate = permissionValidationService.canCreate('COFIMG'); - function init() { - vm.isRequiredAttributeDefined = angular.isDefined(attrs.required); + toggleFilter(false); + loadGrid(); + } - if (vm.optionsApi) { - // options bound by api call - getOptions(vm.optionsApi); + /* ACTIONS */ - } else { + function toggleFilter(show) { + vm.isFilterVisible = _.isUndefined(show) ? !vm.isFilterVisible : show; + } - // options provided as a model - var optionsWatch = scope.$watch('vm.options', function () { + function onQueryChanged() { + toggleFilter(false); + loadGrid(); + } - // need to copy b/c of assignment issue with bound attributes - vm.listOptions = vm.options; + function loadGrid() { + vm.gridLoadState.on(); + + return imageService.getAll(vm.query.getParameters()).then(function (result) { + vm.result = result; + vm.gridLoadState.off(); + }); + } + + /* EVENTS */ + + function onCancel() { + if (!vm.multiMode) { + // in single-mode reset the currentAsset + vm.onSelected(vm.currentAsset); + } + close(); + } + + function onSelect(image) { + if (!vm.multiMode) { + vm.selectedAsset = image; + return; + } - bindDisplayValue(); - }); - } + addOrRemove(image); + } - scope.$watch('vm.model', bindDisplayValue); + function onSelectAndClose(image) { + if (!vm.multiMode) { + vm.selectedAsset = image; + onOk(); + return; } - /* Helpers */ - - function getOptions(apiPath) { - return $http.get(apiPath).then(loadOptions); + addOrRemove(image); + onOk(); + } - function loadOptions(options) { - vm.listOptions = options; - bindDisplayValue(); - } + function onOk() { + if (!vm.multiMode) { + vm.onSelected(vm.selectedAsset); + } else { + vm.onSelected(vm.selectedIds); } - function bindDisplayValue() { + close(); + } - var selectedOption = _.find(vm.listOptions, function (option) { - return option[vm.optionValue] == vm.model; - }); + function onUpload() { + modalDialogService.show({ + templateUrl: modulePath + 'UIComponents/ImageAssets/UploadImageAssetDialog.html', + controller: 'UploadImageAssetDialogController', + options: { + filter: options.filter, + onUploadComplete: onUploadComplete + } + }); - vm.displayValue = selectedOption ? selectedOption[vm.optionName] : vm.defaultItemText; + function onUploadComplete(imageAssetId) { - // if the options and model are bound, and the option does not appear in the - // list, remove the value from the model. - if (!selectedOption && vm.model != undefined && vm.listOptions) { - vm.model = undefined; - } + imageService + .getById(imageAssetId) + .then(onSelectAndClose); } } - function getInputEl(rootEl) { - return rootEl.find('select'); - } - -}]); -angular.module('cms.shared').directive('cmsFormFieldEmailAddress', [ - 'shared.internalModulePath', - 'baseFormFieldFactory', -function ( - modulePath, - baseFormFieldFactory) { - - var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldEmailAddress.html', - passThroughAttributes: [ - 'required', - 'maxlength', - 'placeholder', - 'disabled', - 'cmsMatch' - ], - link: link - }; + /* PUBLIC HELPERS */ - return baseFormFieldFactory.create(config); + function isSelected(image) { + if (vm.selectedIds && image && vm.selectedIds.indexOf(image.imageAssetId) > -1) return true; - function link(scope) { - var vm = scope.vm; + if (!image || !vm.selectedAsset) return false; - // call base - baseFormFieldFactory.defaultConfig.link.apply(this, arguments); + return image.imageAssetId === vm.selectedAsset.imageAssetId; + } - // add custom error for email since its not attribute based like other validation messages - vm.validators.push({ - name: 'email', - message: 'Please enter a valid email address' - }); + function addOrRemove(image) { + if (!isSelected(image)) { + vm.selectedIds.push(image.imageAssetId); + } else { + var index = vm.selectedIds.indexOf(image.imageAssetId); + vm.selectedIds.splice(index, 1); + } } }]); -angular.module('cms.shared').directive('cmsFormFieldFilteredDropdown', [ - '$q', - '_', - 'shared.internalModulePath', + +angular.module('cms.shared').controller('UploadImageAssetDialogController', [ + '$scope', + 'shared.LoadState', + 'shared.imageService', + 'shared.SearchQuery', + 'shared.focusService', 'shared.stringUtilities', - 'baseFormFieldFactory', + 'options', + 'close', function ( - $q, - _, - modulePath, + $scope, + LoadState, + imageService, + SearchQuery, + focusService, stringUtilities, - baseFormFieldFactory) { - - var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldFilteredDropdown.html', - passThroughAttributes: [ - 'required', - 'disabled' - ], - scope: _.extend(baseFormFieldFactory.defaultConfig.scope, { - defaultItemText: '@cmsDefaultItemText', - href: '@cmsHref', - searchFunction: '&cmsSearchFunction', - initialItemFunction: '&cmsInitialItemFunction', - optionName: '@cmsOptionName', - optionValue: '@cmsOptionValue', - required: '=cmsRequired' - }), - require: _.union(baseFormFieldFactory.defaultConfig.require, ['?^^cmsFormDynamicFieldSet']), - link: link, - transclude: true - }; + options, + close) { + + var vm = $scope; + init(); + + /* INIT */ + function init() { + angular.extend($scope, options); - return baseFormFieldFactory.create(config); + initData(); - /* LINK */ + vm.onUpload = onUpload; + vm.onCancel = onCancel; + vm.close = onCancel; + vm.filter = options.filter; + $scope.$watch("command.file", setFileName); - function link(scope, element, attrs, controllers) { - var vm = scope.vm, - dynamicFormFieldController = _.last(controllers); + setFilter(options.filter); - init(); + vm.saveLoadState = new LoadState(); + } - // call base - baseFormFieldFactory.defaultConfig.link.apply(this, arguments); + /* EVENTS */ + function onUpload() { + vm.saveLoadState.on(); - /* Init */ + imageService + .add(vm.command) + .progress(vm.saveLoadState.setProgress) + .then(uploadComplete); + } - function init() { - vm.refreshDataSource = refreshDataSource; - vm.dataSource = []; - vm.hasRequiredAttribute = _.has(attrs, 'required'); - vm.placeholder = attrs['placeholder']; - vm.clearSelected = clearSelected; + function setFileName() { + var command = vm.command; - scope.$watch("vm.model", setSelectedText); + if (command.file && command.file.name) { + command.title = stringUtilities.capitaliseFirstLetter(stringUtilities.getFileNameWithoutExtension(command.file.name)); + focusService.focusById('title'); } + } - function setSelectedText(id) { - vm.selectedText = ''; - - if (id && vm.dataSource && vm.dataSource.length) { - var item = _.find(vm.dataSource, function (item) { - return id == item[vm.optionValue]; - }); - - if (item) vm.selectedText = item[vm.optionName]; - } + function onCancel() { + close(); + } - if (!vm.selectedText && id && vm.initialItemFunction) { - $q.when(vm.initialItemFunction({ id: id })).then(setSelectedItem); - } + /* PUBLIC HELPERS */ + function initData() { + vm.command = {}; - function setSelectedItem(item) { - if (item) { - vm.selectedText = item[vm.optionName]; - refreshDataSource(vm.selectedText); - } - } + if (vm.filter.tags) { + vm.command.tags = vm.filter.tags.split(','); } + } - function clearSelected() { - vm.selectedText = ''; + function setFilter(filter) { + var parts = []; - if (vm.model) { - vm.model = null; - } + if (filter) { + addSize(filter.width, filter.height); + addSize(filter.minWidth, filter.minHeight, 'min-'); } - function refreshDataSource(search) { - var query = { - text: search, - pageSize: 20 - } + vm.filterText = parts.join(', '); + vm.isFilterSet = parts.length > 0; - if (vm.localeId) { - query.localeId = vm.localeId; - } else if (dynamicFormFieldController && dynamicFormFieldController.additionalParameters) { - query.localeId = dynamicFormFieldController.additionalParameters.localeId; + function addSize(width, height, prefix) { + if (width && height) { + parts.push(prefix + 'size ' + width + 'x' + height); } + else { + addIfSet(prefix + 'width', width); + addIfSet(prefix + 'height', height); + } + } - return vm.searchFunction({ $query: query }).then(loadResults); - - function loadResults(results) { - if (!results || !results.items || !results.items.length) { - vm.dataSource = []; - } else { - vm.dataSource = results.items; - } + function addIfSet(name, value) { + if (value) { + parts.push(name + ' ' + value); } } } + function cancel() { + close(); + } + + function uploadComplete(imageAssetId) { + options.onUploadComplete(imageAssetId); + close(); + } + }]); + /** - * Allows editing of html. Note that in order to display html we have included ngSanitize in - * the module dependencies (https://docs.angularjs.org/api/ng/directive/ngBindHtml) + * Base class for form fields that uses default conventions and includes integration with + * server validation. */ -angular.module('cms.shared').directive('cmsFormFieldHtml', [ - '$sce', - '$q', - '$http', - '_', - 'shared.internalModulePath', - 'shared.internalContentPath', +angular.module('cms.shared').factory('baseFormFieldFactory', [ + '$timeout', 'shared.stringUtilities', - 'shared.modalDialogService', - 'baseFormFieldFactory', + 'shared.directiveUtilities', + 'shared.validationErrorService', function ( - $sce, - $q, - $http, - _, - modulePath, - contentPath, + $timeout, stringUtilities, - modalDialogService, - baseFormFieldFactory -) { + directiveUtilities, + validationErrorService + ) { - var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldHtml.html', - passThroughAttributes: [ - 'required', - 'maxlength', - 'disabled', - 'rows' - ], + var service = {}, + /* Here we can validation messages that can apply to all FormField controls */ + globalDefaultValidationMessages = [ + { + attr: 'required', + msg: 'This field is required' + }, + { + attr: 'maxlength', + msg: 'This field cannot be longer than {0} characters' + }, + { + attr: 'minlength', + msg: 'This must be at least {0} characters long' + } + ]; + + /* PUBLIC */ + + service.create = function (config) { + return angular.extend({}, service.defaultConfig, config); + } + + /* CONFIG */ + + /** + * Configuration defaults + */ + service.defaultConfig = { + restrict: 'E', + replace: true, + require: ['^^cmsForm'], + scope: { + title: '@cmsTitle', + description: '@cmsDescription', + change: '&cmsChange', + model: '=cmsModel', + disabled: '=cmsDisabled', + readonly: '=cmsReadonly' + }, + compile: compile, + link: link, + controller: function () { }, + controllerAs: 'vm', + bindToController: true, + + /* Custom Properties */ + + /** + * Should return the main input element that is displayed in edit mode. + * By default this returns the first child input element. + */ getInputEl: getInputEl, - scope: _.extend(baseFormFieldFactory.defaultConfig.scope, { - toolbarsConfig: '@cmsToolbars', - toolbarCustomConfig: '@cmsCustomToolbar', - options: '=cmsOptions', - configPath: '@cmsConfigPath', - }), - link: link + + /** + * a list of attributes that when defined on the directive are passed through to the element + * returned from getInputEl. + */ + passThroughAttributes: [], + + /** + * Default validation messages to use when none are provided on the element. Saves specifying common messages + * like 'This field is required' on every field. Each array element should be an object in the form of: + * { attr: 'name', msg: 'string message or formatter function' } + */ + defaultValidationMessages: [] }; - return baseFormFieldFactory.create(config); + /* COMPILE */ - /* OVERRIDES */ + function compile(el, attrs) { - function link(scope, el, attributes) { - var vm = scope.vm; + initPassThroughAttributes.call(this, el, attrs); - // call base - baseFormFieldFactory.defaultConfig.link.apply(this, arguments); + return this.link.bind(this); + } - loadTinyMCEOptions(vm, attributes).then(function (tinymceOptions) { - vm.tinymceOptions = tinymceOptions; - }); + function link(scope, el, attrs, controllers) { + var vm = scope.vm, + formController = controllers[0]; - scope.$watch("vm.model", setEditorModel); - scope.$watch("vm.editorModel", setCmsModel); + // Model Properties + vm.formScope = formController.getFormScope(); + vm.form = vm.formScope.getForm(); + + directiveUtilities.setModelName(vm, attrs); + parseErrorMessages(vm, attrs); - function setEditorModel(value) { - if (value !== vm.editorModel) { - vm.editorModel = value; - vm.rawHtml = $sce.trustAsHtml(value); - } - } + // Model Funcs + vm.onChange = onChange.bind(vm); + vm.resetCustomErrors = resetCustomErrors.bind(vm); + vm.addOrUpdateValidator = addOrUpdateValidator.bind(vm); - function setCmsModel(value) { - if (value !== vm.model) { - vm.model = value; - vm.rawHtml = $sce.trustAsHtml(value); - } - } - } + // Init Errors + vm.resetCustomErrors(); - function getInputEl(rootEl) { - return rootEl.find('textarea'); + // Bind Validation Events + bindValidationEvents(scope, el); + + // watches + scope.$watch('vm.model', function () { + vm.resetCustomErrors(); + }); } - /* HELPERS */ + /* PUBLIC */ - function loadTinyMCEOptions(vm, attributes) { - var rows = 20, - def = $q.defer(); + function onChange() { + var vm = this; - if (attributes.rows) { - rows = parseInt(attributes.rows); + vm.resetCustomErrors(); + if (vm.change) { + // run after digest cycle completes so the parent ngModel is updated + $timeout(vm.change, 0); } + } - var defaultOptions = { - toolbar: parseToolbarButtons(vm.toolbarsConfig, vm.toolbarCustomConfig), - plugins: 'link image media fullscreen imagetools code', - content_css: contentPath + "css/third-party/tinymce/content.min.css", - menubar: false, - min_height: rows * 16, - setup: function (editor) { - editor.addButton('cfimage', { - icon: 'image', - onclick: onEditorImageButtonClick.bind(null, editor) - }); - }, - browser_spellcheck: true, - convert_urls: false - }; - - if (vm.configPath) { - $http.get(vm.configPath).then(bindFileOptions); + function resetCustomErrors() { + var model = this.form[this.modelName]; - } else { - bindCodeOptionsAndResolve(); + if (model) { + model.$setValidity('server', true); } + this.customErrors = []; + } - return def.promise; + function addOrUpdateValidator(validator) { + var vm = this; + var validators = _.filter(vm.validators, function (v) { + return v.name !== validator.name; + }); + validators.push(validator); + vm.validators = validators; + } - function bindFileOptions(result) { - if (result && result.data) { - _.extend(defaultOptions, result.data); - } - bindCodeOptionsAndResolve(); - } + /* HELPERS */ - function bindCodeOptionsAndResolve() { - // Always apply last - if (vm.options) { - _.extend(defaultOptions, vm.options); + /** + * Loop through attributes specified in config.passThroughAttributes and copy + * them onto the input control requrned by config.getInputEl + */ + function initPassThroughAttributes(rootEl, attrs) { + var config = this, + el = config.getInputEl(rootEl); + + (config.passThroughAttributes || []).forEach(function (passThroughAttribute) { + var name = passThroughAttribute.name || passThroughAttribute; + if (angular.isDefined(attrs[name])) { + el[0].setAttribute(attrs.$attr[name], attrs[name]); + } + else if (passThroughAttribute.default) { + el[0].setAttribute(name, passThroughAttribute.default); } + }); + } - def.resolve(defaultOptions); - } + function getInputEl(rootEl) { + return rootEl.find('input'); } - function onEditorImageButtonClick(editor) { - var currentElement = editor.selection.getContent({ format: 'image' }), - currentImage = currentElement.length ? angular.element(currentElement) : null; + function bindValidationEvents(scope, el) { + var fn = _.partial(addErrors, scope.vm, el); - modalDialogService.show({ - templateUrl: modulePath + 'UIComponents/EditorDialogs/ImageAssetEditorDialog.html', - controller: 'ImageAssetEditorDialogController', - options: { - imageAssetHtml: currentImage, - onSelected: function (output) { - editor.insertContent(output.html); - } - } + validationErrorService.addHandler(scope.vm.modelName, fn); + + scope.$on('$destroy', function () { + validationErrorService.removeHandler(fn); }); } + + function parseErrorMessages(vm, attrs) { + var config = this, + postfix = 'ValMsg', + attrPostfix = '-val-msg'; - function parseToolbarButtons(toolbarsConfig, toolbarCustomConfig) { - var DEFAULT_CONFIG = 'headings,basicFormatting', - buttonConfig = { - headings: 'formatselect', - basicFormatting: 'fullscreen undo redo removeformat | bold italic underline | link unlink', - advancedFormatting: 'bullist numlist blockquote | alignleft aligncenter alignright alignjustify | strikethrough superscript subscript', - media: 'cfimage media', - source: 'code', - }, toolbars = []; + vm.validators = []; - toolbarsConfig = toolbarsConfig || DEFAULT_CONFIG; + _.each(attrs.$attr, function (value, key) { + var attributeToValidate, + msg; - toolbarsConfig.split(',').forEach(function (configItem) { - configItem = stringUtilities.lowerCaseFirstWord(configItem.trim()); + if (stringUtilities.endsWith(key, postfix)) { + // if the property is postfix '-val-msg' then pull in the message from the attribute + attributeToValidate = value.substring(0, value.length - attrPostfix.length); + msg = attrs[key]; + } else { - if (configItem === 'custom') { - toolbars = _.union(toolbars, parseCustomConfig(toolbarCustomConfig)); + attributeToValidate = value; - } else if (buttonConfig[configItem]) { - toolbars.push(buttonConfig[configItem]); + // check to see if we have a default message for the property + msg = getDefaultErrorMessage(config.defaultValidationMessages, key) || getDefaultErrorMessage(globalDefaultValidationMessages, key); + } + + if (msg) { + vm.validators.push({ + name: attrs.$normalize(attributeToValidate), + message: stringUtilities.format(msg, attrs[key]) + }); } }); - return toolbars.join(' | '); + function getDefaultErrorMessage(defaultsToCheck, attr) { + var validator = _.find(defaultsToCheck, function (v) { + return v.attr === attr; + }); - function parseCustomConfig(toolbarCustomConfig) { - var customToolbars; + if (validator) { + if (_.isFunction(validator.msg)) { + return validator.msg(vm.modelName, attrs); + } + return validator.msg; + } + } + } - if (toolbarCustomConfig) { + function addErrors(vm, el, errors) { + var form = vm.formScope.getForm(); + var model = form[vm.modelName]; + vm.resetCustomErrors(); - // old formatting allowed array parsing, but this is ambigous when mixed with other toolbars - // and so will be removed eventually. - if (!toolbarCustomConfig.startsWith("'") && !toolbarCustomConfig.startsWith("\"")) { - // single toolbar - return [toolbarCustomConfig]; - } - try { - // parse an array of toolbars, with each one surrounded by quotationmarks - customToolbars = JSON.parse('{"j":[' + toolbarCustomConfig.replace(/'/g, '"') + ']}').j; - } - catch (e) { } + model.$setValidity('server', false); - if (customToolbars && customToolbars.length) { - return customToolbars; - } - } + // make dirty to ensure css classes are applied + getInputEl(el).removeClass('ng-pristine').addClass('ng-dirty'); - return []; - } + errors.forEach(function (error) { + vm.customErrors.push(error); + }); } + + /* DEFINITION */ + + return service; }]); -angular.module('cms.shared').directive('cmsFormFieldNumber', [ +angular.module('cms.shared').directive('cmsFormFieldChar', [ + '_', 'shared.internalModulePath', 'baseFormFieldFactory', function ( + _, modulePath, - baseFormFieldFactory - ) { + baseFormFieldFactory) { var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldNumber.html', + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldChar.html', passThroughAttributes: [ 'required', - 'maxlength', - 'min', - 'max', - 'step', - 'disabled', 'placeholder', + 'pattern', + 'disabled', 'cmsMatch' ] }; return baseFormFieldFactory.create(config); }]); -var numericTypes = [ - { name: 'Byte', min: 0, max: 255, step: 1 }, - { name: 'SByte', min: -128, max: 127, step: 1 }, - { name: 'Int16', min: -32768, max: 32767, step: 1 }, - { name: 'UInt16', min: 0, max: 65535, step: 1 }, - { name: 'Int32', min: -2147483648, max: 2147483647, step: 1 }, - { name: 'UInt32', min: 0, max: 4294967295, step: 1 }, - { name: 'Int64', step: 1 }, - { name: 'UInt64', min: 0, step: 1 } -]; - -numericTypes.forEach(function (numericType) { - - angular.module('cms.shared').directive('cmsFormField' + numericType.name, [ - 'shared.internalModulePath', - 'baseFormFieldFactory', - function ( - modulePath, - baseFormFieldFactory - ) { - - var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldNumber.html', - passThroughAttributes: [ - 'required', - 'maxlength', - { name: 'min', default: numericType.min }, - { name: 'max', default: numericType.max }, - { name: 'step', default: numericType.step }, - 'disabled', - 'placeholder', - 'cmsMatch' - ] - }; - - return baseFormFieldFactory.create(config); - }]); -}); -angular.module('cms.shared').directive('cmsFormFieldPassword', [ +angular.module('cms.shared').directive('cmsFormFieldCheckbox', [ 'shared.internalModulePath', 'baseFormFieldFactory', function ( @@ -12244,113 +12252,43 @@ function ( baseFormFieldFactory) { var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldPassword.html', - scope: _.extend(baseFormFieldFactory.defaultConfig.scope, { - passwordPolicy: '=cmsPasswordPolicy' - }), + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldCheckbox.html', passThroughAttributes: [ - 'required', - 'minlength', - 'maxlength', - 'pattern', - 'passwordrules', - 'disabled', - 'cmsMatch' - ], - link: link + 'disabled' + ] }; return baseFormFieldFactory.create(config); - - - function link(scope, element, attrs, controllers) { - var vm = scope.vm; - - // call base - baseFormFieldFactory.defaultConfig.link.apply(this, arguments); - - init(); - - /* Init */ - - function init() { - scope.$watch('vm.passwordPolicy', onPasswordPolicyChange); - } - - function onPasswordPolicyChange(policy) { - - vm.policyAttributes = {}; - - processAttribute('minlength', true); - processAttribute('maxlength', true); - processAttribute('placeholder'); - processAttribute('pattern', true); - processAttribute('passwordrules'); - processAttribute('title', false, policy ? policy.description : null); - - function processAttribute(attribute, addValidator, defaultValue) { - var value; - - if (attrs[attribute]) { - value = attrs[attribute] - } - else if (policy && policy.attributes[attribute]) { - value = policy.attributes[attribute]; - } - - setAttributeValue(attribute, value, addValidator, defaultValue); - } - - function setAttributeValue(attribute, value, addValidator, defaultValue) { - - vm.policyAttributes[attribute] = value || defaultValue || ''; - if (!addValidator) return; - - var validator = _.findWhere(vm.validators, { name: attribute }); - - if (validator && (!value || !policy)) { - vm.validators = _.without(vm.validators, validator); - } - else if (policy && value && validator) { - validator.message = policy.description; - } - else if (policy && value && !validator) - { - vm.validators.push({ - name: attribute, - message: policy.description - }); - } - } - } - } }]); -angular.module('cms.shared').directive('cmsFormFieldRadioList', [ +angular.module('cms.shared').directive('cmsFormFieldCheckboxList', [ '_', '$http', 'shared.internalModulePath', + 'shared.optionSourceService', 'baseFormFieldFactory', function ( _, $http, modulePath, + optionSourceService, baseFormFieldFactory) { var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldRadioList.html', + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldCheckboxList.html', scope: _.extend(baseFormFieldFactory.defaultConfig.scope, { options: '=cmsOptions', optionValue: '@cmsOptionValue', optionName: '@cmsOptionName', optionsApi: '@cmsOptionsApi', - defaultItemText: '@cmsDefaultItemText', + noValueText: '@cmsNoValueText', required: '=cmsRequired', disabled: '=cmsDisabled' }), passThroughAttributes: [ 'placeholder', 'disabled', - 'cmsMatch' + 'cmsMatch', + 'required' ], link: link }; @@ -12370,6 +12308,7 @@ function ( function init() { vm.isRequiredAttributeDefined = angular.isDefined(attrs.required); + vm.onCheckChanged = updateModel; if (vm.optionsApi) { // options bound by api call @@ -12380,66 +12319,115 @@ function ( // options provided as a model var optionsWatch = scope.$watch('vm.options', function () { - // need to copy b/c of assignment issue with bound attributes - vm.listOptions = vm.options; - - bindDisplayValue(); + bindOptions(vm.options); + updateDisplayValues(); }); } - scope.$watch('vm.model', bindDisplayValue); + scope.$watch('vm.model', onModelChanged); + } + + /* Events */ + + function onModelChanged() { + updateOptionSelectionFromModel(); + updateDisplayValues(); } /* Helpers */ function getOptions(apiPath) { - return $http.get(apiPath).then(loadOptions); + return optionSourceService.getFromApi(apiPath).then(loadOptions); function loadOptions(options) { - vm.listOptions = options; - bindDisplayValue(); + bindOptions(options); + updateDisplayValues(); } } - function bindDisplayValue() { + /** + * Copies over the option collection so the original + * is not modified with selcted properties. + */ + function bindOptions(options) { + vm.listOptions = _.map(options, mapOption); - var selectedOption = _.find(vm.listOptions, function (option) { - return option[vm.optionValue] == vm.model; + updateOptionSelectionFromModel(); + + function mapOption(option) { + return { + text: option[vm.optionName], + value: option[vm.optionValue] + }; + } + } + + /** + * Updates the selected values in the listOptions + * collection from the model, + */ + function updateOptionSelectionFromModel() { + var isModelEmpty = !vm.model || !vm.model.length; + vm.displayValues = []; + + _.each(vm.listOptions, function (option) { + + option.selected = !isModelEmpty && !!_.find(vm.model, function (m) { + return m === option.value; + }); }); + } - vm.displayValue = selectedOption ? selectedOption[vm.optionName] : vm.defaultItemText; + function updateDisplayValues() { - // if the options and model are bound, and the option does not appear in the - // list, remove the value from the model. - if (!selectedOption && vm.model != undefined && vm.listOptions) { - vm.model = undefined; - } + vm.displayValues = _.chain(vm.listOptions) + .filter(function (o) { return o.selected }) + .pluck('text') + .value(); } - } + function updateModel() { + + vm.model = _.chain(vm.listOptions) + .filter(function (o) { return o.selected }) + .pluck('value') + .value(); + } + } }]); -angular.module('cms.shared').directive('cmsFormFieldReadonly', [ +angular.module('cms.shared').directive('cmsFormFieldColor', [ 'shared.internalModulePath', + 'baseFormFieldFactory', function ( - modulePath + modulePath, + baseFormFieldFactory ) { - return { - restrict: 'E', - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldReadonly.html', - replace: true, - require: '^^cmsForm', - scope: { - title: '@cmsTitle', - description: '@cmsDescription', - model: '=cmsModel' - }, - controller: function () { }, - controllerAs: 'vm', - bindToController: true + var config = { + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldColor.html', + passThroughAttributes: [ + 'required', + 'disabled' + ], + link: link }; + + return baseFormFieldFactory.create(config); + + function link(scope) { + var vm = scope.vm; + + // call base + baseFormFieldFactory.defaultConfig.link.apply(this, arguments); + + // add custom error for value since its not attribute based like other validation messages + vm.validators.push({ + name: 'pattern', + message: vm.title + " must be a hexadecimal colour value e.g. '#EFEFEF' or '#fff'" + }); + } }]); -angular.module('cms.shared').directive('cmsFormFieldSubHeading', [ +angular.module('cms.shared').directive('cmsFormFieldContainer', [ 'shared.internalModulePath', function ( modulePath @@ -12447,89 +12435,128 @@ function ( return { restrict: 'E', - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldSubHeading.html', + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldContainer.html', require: ['^^cmsForm'], replace: true, - transclude: true + transclude: true, + scope: { + title: '@cmsTitle', + description: '@cmsDescription' + } }; }]); -angular.module('cms.shared').directive('cmsFormFieldText', [ - '_', +angular.module('cms.shared').directive('cmsFormFieldDate', [ 'shared.internalModulePath', 'baseFormFieldFactory', function ( - _, modulePath, - baseFormFieldFactory) { + baseFormFieldFactory + ) { var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldText.html', + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldDate.html', passThroughAttributes: [ 'required', - 'minlength', - 'maxlength', - 'placeholder', - 'pattern', + 'min', + 'max', + 'step', 'disabled', - 'readonly', 'cmsMatch' - ] + ], + link: link }; return baseFormFieldFactory.create(config); + + function link(scope, element, attrs, controllers) { + var vm = scope.vm; + + // call base + baseFormFieldFactory.defaultConfig.link.apply(this, arguments); + + init(); + + /* Init */ + + function init() { + if (attrs.min) { + vm.addOrUpdateValidator({ + name: 'min', + message: "This date cannot be before " + attrs.min + }); + } + if (attrs.max) { + vm.addOrUpdateValidator({ + name: 'max', + message: "This date cannot be after " + attrs.max + }); + } + } + } }]); -angular.module('cms.shared').directive('cmsFormFieldTextArea', [ - 'shared.internalModulePath', - 'shared.stringUtilities', - 'baseFormFieldFactory', +angular.module('cms.shared').directive('cmsFormFieldDateLocal', [ + 'shared.internalModulePath', + 'baseFormFieldFactory', function ( - modulePath, - stringUtilities, - baseFormFieldFactory) { + modulePath, + baseFormFieldFactory + ) { var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldTextArea.html', + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldDateLocal.html', passThroughAttributes: [ 'required', - 'maxlength', - 'placeholder', - 'ngMinlength', - 'ngMaxlength', - 'ngPattern', + 'min', + 'max', 'disabled', - 'rows', - 'cols', - 'wrap' + 'cmsMatch' ], - getInputEl: getInputEl + link: link }; return baseFormFieldFactory.create(config); - function getInputEl(rootEl) { - return rootEl.find('textarea'); - } + function link(scope, element, attrs, controllers) { + var vm = scope.vm; + // call base + baseFormFieldFactory.defaultConfig.link.apply(this, arguments); + + init(); + + /* Init */ + + function init() { + if (attrs.min) { + vm.addOrUpdateValidator({ + name: 'min', + message: "This date cannot be before " + attrs.min + }); + } + if (attrs.max) { + vm.addOrUpdateValidator({ + name: 'max', + message: "This date cannot be after " + attrs.max + }); + } + } + } }]); -angular.module('cms.shared').directive('cmsFormFieldTime', [ +angular.module('cms.shared').directive('cmsFormFieldDateTime', [ 'shared.internalModulePath', - 'shared.timeUtilities', 'baseFormFieldFactory', function ( modulePath, - timeUtilities, baseFormFieldFactory ) { var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldTime.html', + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldDateTime.html', passThroughAttributes: [ 'required', 'min', - 'step', 'max', 'disabled', - 'readonly', 'cmsMatch' ], link: link @@ -12548,1035 +12575,1009 @@ function ( /* Init */ function init() { - - // Use watch instead of formatters/parsers here due to issues - // with utc dates - scope.$watch("vm.model", setEditorModel); - scope.$watch("vm.editorModel", setCmsModel); - - function setEditorModel(value) { - if (value !== vm.editorModel) { - vm.editorModel = timeUtilities.toDate(value); - } + if (attrs.min) { + var minDate = attrs.min.replace('T', ' '); + vm.addOrUpdateValidator({ + name: 'min', + message: "This date cannot be before " + minDate + }); } - - function setCmsModel(value) { - if (value !== vm.model) { - vm.model = timeUtilities.format(value); - } + if (attrs.max) { + var maxDate = attrs.min.replace('T', ' '); + vm.addOrUpdateValidator({ + name: 'max', + message: "This date cannot be after " + maxDate + }); } } } }]); -angular.module('cms.shared').directive('cmsFormFieldUrl', [ - '_', +angular.module('cms.shared').directive('cmsFormFieldDateTimeLocal', [ 'shared.internalModulePath', 'baseFormFieldFactory', function ( - _, modulePath, - baseFormFieldFactory) { + baseFormFieldFactory + ) { var config = { - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldUrl.html', + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldDateTimeLocal.html', + scope: _.extend(baseFormFieldFactory.defaultConfig.scope, { + minUtc: '@cmsMinUtc', + maxUtc: '@cmsMaxUtc' + }), passThroughAttributes: [ 'required', - 'minlength', - 'maxlength', - 'placeholder', - 'pattern', + 'min', + 'max', 'disabled', 'cmsMatch' - ] + ], + link: link }; return baseFormFieldFactory.create(config); -}]); -angular.module('cms.shared').directive('cmsFormFieldValidationSummary', [ - 'shared.internalModulePath', -function ( - modulePath - ) { + function link(scope, element, attrs, controllers) { + var vm = scope.vm; - return { - restrict: 'E', - templateUrl: modulePath + 'UIComponents/FormFields/FormFieldValidationSummary.html', - replace: true - }; -}]); -angular.module('cms.shared').directive('cmsHttpPrefix', function () { - return { - restrict: 'A', - require: 'ngModel', - link: function (scope, element, attrs, controller) { - function ensureHttpPrefix(value) { - var httpPrefix = 'http://'; + // call base + baseFormFieldFactory.defaultConfig.link.apply(this, arguments); - if (value - && !/^(https?):\/\//i.test(value) - && httpPrefix.indexOf(value) === -1 - && 'https://'.indexOf(value) === -1) { + init(); - controller.$setViewValue(httpPrefix + value); - controller.$render(); + /* Init */ - return httpPrefix + value; + function init() { + scope.$watch("vm.minUtc", function (value) { + var date = parseDateAsUtc(value); + vm.minLocal = formatDate(date); + if (vm.minLocal) { + vm.addOrUpdateValidator({ + name: 'min', + message: "This date cannot be before " + date.toLocaleString() + }); } - else { - return value; + }); + + scope.$watch("vm.maxUtc", function (value) { + var date = parseDateAsUtc(value); + vm.maxLocal = formatDate(date); + if (vm.maxLocal) { + vm.addOrUpdateValidator({ + name: 'max', + message: "This date cannot be after " + date.toLocaleString() + }); } - } - controller.$formatters.push(ensureHttpPrefix); - controller.$parsers.splice(0, 0, ensureHttpPrefix); + }); } - }; -}); -/** - * Validates that a field matches the value of another field. Set the came of the field - * in the attribute definition e.g. cms-match="vm.command.password" - * Adapted from http://ericpanorel.net/2013/10/05/angularjs-password-match-form-validation/ - */ -angular.module('cms.shared').directive('cmsMatch', [ - '$parse', - '$timeout', - 'shared.internalModulePath', - 'shared.directiveUtilities', -function ( - $parse, - $timeout, - modulePath, - directiveUtilities - ) { - - var DIRECTIVE_ID = 'cmsMatch'; - var DIRECTIVE_ATTRIBUTE = 'cms-match'; - - return { - link: link, - restrict: 'A', - require: ['^^cmsForm', '?ngModel'], - }; - - function link(scope, el, attrs, controllers) { - // NB: ngModel may be null on an outer form control before it has been copied to the inner input. - if (!attrs[DIRECTIVE_ID] || !controllers[1]) return; - - var formController = controllers[0], - ngModelController = controllers[1], - form = formController.getFormScope().getForm(), - sourceField = directiveUtilities.parseModelName(attrs[DIRECTIVE_ID]); - - var validator = function (value, otherVal) { - var formField = form[sourceField]; - if (!formField) return false; - var sourceFieldValue = formField.$viewValue; + function parseDateAsUtc(value) { + if (!value) return; - return value === sourceFieldValue; + // Format is expected to be 'yyyy-MM-ddT00:00' + return new Date(value + ':00.000Z'); } - ngModelController.$validators[DIRECTIVE_ID] = validator; - } -}]); -angular.module('cms.shared').directive('cmsFormFieldImageAnchorLocationSelector', [ - '_', - 'shared.internalModulePath', - function ( - _, - modulePath) { - return { - restrict: 'E', - templateUrl: modulePath + 'UIComponents/ImageAssets/FormFieldImageAnchorLocationSelector.html', - scope: { - model: '=cmsModel', - readonly: '=cmsReadonly' - }, - controller: Controller, - controllerAs: 'vm', - bindToController: true - }; + function formatDate(date) { + if (!date) return; - /* CONTROLLER */ + return date.getFullYear() + + '-' + padTwoDigitNumber(date.getMonth() + 1) + + '-' + padTwoDigitNumber(date.getDate()) + + 'T' + padTwoDigitNumber(date.getHours()) + + ':' + padTwoDigitNumber(date.getMinutes()); + } - function Controller() { - var vm = this; - - vm.options = [ - { name: 'Top Left', id: 'TopLeft' }, - { name: 'Top Center', id: 'TopCenter' }, - { name: 'Top Right', id: 'TopRight' }, - { name: 'Middle Left', id: 'MiddleLeft' }, - { name: 'Middle Center', id: 'MiddleCenter' }, - { name: 'Middle Right', id: 'MiddleRight' }, - { name: 'Bottom Left', id: 'BottomLeft' }, - { name: 'Bottom Center', id: 'BottomCenter' }, - { name: 'Bottom Right', id: 'BottomRight' } - ]; + function padTwoDigitNumber(number) { + return ("00" + number).slice(-2); + } } }]); -/** - * A form field control for an image asset that uses a search and pick dialog - * to allow the user to change the selected file. - */ -angular.module('cms.shared').directive('cmsFormFieldImageAsset', [ - '_', - 'shared.internalModulePath', - 'shared.internalContentPath', - 'shared.modalDialogService', - 'shared.stringUtilities', - 'shared.imageService', - 'shared.urlLibrary', - 'baseFormFieldFactory', - function ( - _, - modulePath, - contentPath, - modalDialogService, - stringUtilities, - imageService, - urlLibrary, - baseFormFieldFactory) { - - /* VARS */ - - var assetReplacementPath = contentPath + 'img/AssetReplacement/', - noImagePath = assetReplacementPath + 'image-replacement.png', - baseConfig = baseFormFieldFactory.defaultConfig; - - /* CONFIG */ - - var config = { - templateUrl: modulePath + 'UIComponents/ImageAssets/FormFieldImageAsset.html', - scope: _.extend(baseConfig.scope, { - asset: '=cmsAsset', // if we already have the full asset data we can set it here to save an api call - loadState: '=cmsLoadState', - updateAsset: '@cmsUpdateAsset' // update the asset property if it changes - }), - passThroughAttributes: [ - 'required' - ], - link: link - }; - - return baseFormFieldFactory.create(config); - - /* LINK */ - - function link(scope, el, attributes, controllers) { - var vm = scope.vm, - isRequired = _.has(attributes, 'required'), - isAssetInitialized; - - init(); - return baseConfig.link(scope, el, attributes, controllers); - - /* INIT */ - - function init() { +angular.module('cms.shared').directive('cmsFormFieldDropdown', [ + '_', + '$http', + 'shared.internalModulePath', + 'baseFormFieldFactory', +function ( + _, + $http, + modulePath, + baseFormFieldFactory) { - vm.urlLibrary = urlLibrary; - vm.showPicker = showPicker; - vm.remove = remove; - vm.isRemovable = _.isObject(vm.model) && !isRequired; + var config = { + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldDropdown.html', + scope: _.extend(baseFormFieldFactory.defaultConfig.scope, { + options: '=cmsOptions', + optionValue: '@cmsOptionValue', + optionName: '@cmsOptionName', + optionsApi: '@cmsOptionsApi', + defaultItemText: '@cmsDefaultItemText', + href: '@cmsHref', + required: '=cmsRequired', + disabled: '=cmsDisabled' + }), + passThroughAttributes: [ + 'placeholder', + 'disabled', + 'cmsMatch' + ], + getInputEl: getInputEl, + link: link + }; - vm.filter = parseFilters(attributes); - vm.previewWidth = attributes['cmsPreviewWidth'] || 450; - vm.previewHeight = attributes['cmsPreviewHeight']; - - scope.$watch("vm.asset", setAsset); - scope.$watch("vm.model", setAssetById); - } + return baseFormFieldFactory.create(config); - /* EVENTS */ + /* PRIVATE */ - function remove() { - setAsset(null); - } + function link(scope, element, attrs, controllers) { + var vm = scope.vm; + init(); - function showPicker() { + // call base + baseFormFieldFactory.defaultConfig.link.apply(this, arguments); - modalDialogService.show({ - templateUrl: modulePath + 'UIComponents/ImageAssets/ImageAssetPickerDialog.html', - controller: 'ImageAssetPickerDialogController', - options: { - currentAsset: vm.previewAsset, - filter: vm.filter, - onSelected: onSelected - } - }); + /* Init */ - function onSelected(newAsset) { + function init() { + vm.isRequiredAttributeDefined = angular.isDefined(attrs.required); - if (!newAsset && vm.previewAsset) { - setAsset(null); + if (vm.optionsApi) { + // options bound by api call + getOptions(vm.optionsApi); - vm.onChange(); - } else if (!vm.previewAsset || (newAsset && vm.previewAsset.imageAssetId !== newAsset.imageAssetId)) { - setAsset(newAsset); + } else { - vm.onChange(); - } - } - } + // options provided as a model + var optionsWatch = scope.$watch('vm.options', function () { - /** - * When the model is set without a preview asset, we need to go get the full - * asset details. This query can be bypassed by setting the cms-asset attribute - */ - function setAssetById(assetId) { + // need to copy b/c of assignment issue with bound attributes + vm.listOptions = vm.options; - // Remove the id if it is 0 or invalid to make sure required validation works - if (!assetId) { - vm.model = assetId = undefined; - } + bindDisplayValue(); + }); + } - if (assetId && (!vm.previewAsset || vm.previewAsset.imageAssetId != assetId)) { - imageService.getById(assetId).then(function (asset) { - if (asset) { - setAsset(asset); - } - }); - } - } + scope.$watch('vm.model', bindDisplayValue); + } - /** - * Initialise the state when the asset is changed - */ - function setAsset(asset) { + /* Helpers */ - if (asset) { - vm.previewAsset = asset; - vm.isRemovable = !isRequired; - vm.model = asset.imageAssetId; + function getOptions(apiPath) { + return $http.get(apiPath).then(loadOptions); - if (vm.updateAsset) { - vm.asset = asset; - } - } else if (isAssetInitialized) { - // Ignore if we are running this first time to avoid overwriting the model with a null vlaue - vm.previewAsset = null; - vm.isRemovable = false; + function loadOptions(options) { + vm.listOptions = options; + bindDisplayValue(); + } + } - if (vm.model) { - vm.model = null; - } - if (vm.updateAsset) { - vm.asset = null; - } - } + function bindDisplayValue() { - setButtonText(); + var selectedOption = _.find(vm.listOptions, function (option) { + return option[vm.optionValue] == vm.model; + }); - isAssetInitialized = true; - } + vm.displayValue = selectedOption ? selectedOption[vm.optionName] : vm.defaultItemText; - /* Helpers */ + // if the options and model are bound, and the option does not appear in the + // list, remove the value from the model. + if (!selectedOption && vm.model != undefined && vm.listOptions) { + vm.model = undefined; + } + } + } - function parseFilters(attributes) { - var filter = {}, - attributePrefix = 'cms'; + function getInputEl(rootEl) { + return rootEl.find('select'); + } - setAttribute('Tags'); - setAttribute('Width', true); - setAttribute('Height', true); - setAttribute('MinWidth', true); - setAttribute('MinHeight', true); +}]); +angular.module('cms.shared').directive('cmsFormFieldEmailAddress', [ + 'shared.internalModulePath', + 'baseFormFieldFactory', +function ( + modulePath, + baseFormFieldFactory) { - return filter; + var config = { + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldEmailAddress.html', + passThroughAttributes: [ + 'required', + 'maxlength', + 'placeholder', + 'disabled', + 'cmsMatch' + ], + link: link + }; - function setAttribute(filterName, isInt) { - var value = attributes[attributePrefix + filterName]; + return baseFormFieldFactory.create(config); - if (value) { - filterName = stringUtilities.lowerCaseFirstWord(filterName); - filter[filterName] = isInt ? parseInt(value) : value; - } - } - } + function link(scope) { + var vm = scope.vm; - function setButtonText() { - vm.buttonText = vm.model ? 'Change' : 'Select'; - } - } + // call base + baseFormFieldFactory.defaultConfig.link.apply(this, arguments); - }]); -angular.module('cms.shared').directive('cmsFormFieldImageAssetCollection', [ + // add custom error for email since its not attribute based like other validation messages + vm.validators.push({ + name: 'email', + message: 'Please enter a valid email address' + }); + } +}]); +angular.module('cms.shared').directive('cmsFormFieldFilteredDropdown', [ + '$q', '_', 'shared.internalModulePath', - 'shared.LoadState', - 'shared.imageService', - 'shared.modalDialogService', - 'shared.arrayUtilities', 'shared.stringUtilities', - 'shared.urlLibrary', 'baseFormFieldFactory', function ( + $q, _, modulePath, - LoadState, - imageService, - modalDialogService, - arrayUtilities, stringUtilities, - urlLibrary, baseFormFieldFactory) { - /* VARS */ - - var IMAGE_ASSET_ID_PROP = 'imageAssetId', - baseConfig = baseFormFieldFactory.defaultConfig; - - /* CONFIG */ - var config = { - templateUrl: modulePath + 'UIComponents/ImageAssets/FormFieldImageAssetCollection.html', + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldFilteredDropdown.html', passThroughAttributes: [ 'required', - 'ngRequired' + 'disabled' ], - link: link + scope: _.extend(baseFormFieldFactory.defaultConfig.scope, { + defaultItemText: '@cmsDefaultItemText', + href: '@cmsHref', + searchFunction: '&cmsSearchFunction', + initialItemFunction: '&cmsInitialItemFunction', + optionName: '@cmsOptionName', + optionValue: '@cmsOptionValue', + required: '=cmsRequired' + }), + require: _.union(baseFormFieldFactory.defaultConfig.require, ['?^^cmsFormDynamicFieldSet']), + link: link, + transclude: true }; return baseFormFieldFactory.create(config); /* LINK */ - function link(scope, el, attributes, controllers) { - var vm = scope.vm; - - init(); - return baseConfig.link(scope, el, attributes, controllers); - - /* INIT */ - - function init() { - - vm.gridLoadState = new LoadState(); - vm.urlLibrary = urlLibrary; - - vm.showPicker = showPicker; - vm.remove = remove; - vm.onDrop = onDrop; - - scope.$watch("vm.model", setGridItems); - } - - /* EVENTS */ - - function remove(image) { - - removeItemFromArray(vm.gridData, image); - removeItemFromArray(vm.model, image.imageAssetId); - - function removeItemFromArray(arr, item) { - var index = arr.indexOf(item); - - if (index >= 0) { - return arr.splice(index, 1); - } - } - } - - function showPicker() { + function link(scope, element, attrs, controllers) { + var vm = scope.vm, + dynamicFormFieldController = _.last(controllers); - modalDialogService.show({ - templateUrl: modulePath + 'UIComponents/ImageAssets/ImageAssetPickerDialog.html', - controller: 'ImageAssetPickerDialogController', - options: { - selectedIds: vm.model || [], - filter: getFilter(), - onSelected: onSelected - } - }); + init(); - function onSelected(newImageArr) { - vm.model = newImageArr; - setGridItems(newImageArr); - } - } + // call base + baseFormFieldFactory.defaultConfig.link.apply(this, arguments); - function onDrop($index, droppedEntity) { + /* Init */ - arrayUtilities.moveObject(vm.gridData, droppedEntity, $index, IMAGE_ASSET_ID_PROP); + function init() { + vm.refreshDataSource = refreshDataSource; + vm.dataSource = []; + vm.hasRequiredAttribute = _.has(attrs, 'required'); + vm.placeholder = attrs['placeholder']; + vm.clearSelected = clearSelected; - // Update model with new orering - setModelFromGridData(); + scope.$watch("vm.model", setSelectedText); } - function setModelFromGridData() { - vm.model = _.pluck(vm.gridData, IMAGE_ASSET_ID_PROP); - } + function setSelectedText(id) { + vm.selectedText = ''; - /* HELPERS */ + if (id && vm.dataSource && vm.dataSource.length) { + var item = _.find(vm.dataSource, function (item) { + return id == item[vm.optionValue]; + }); - function getFilter() { - var filter = {}, - attributePrefix = 'cms'; + if (item) vm.selectedText = item[vm.optionName]; + } - setAttribute('Tags'); - setAttribute('Width', true); - setAttribute('Height', true); - setAttribute('MinWidth', true); - setAttribute('MinHeight', true); + if (!vm.selectedText && id && vm.initialItemFunction) { + $q.when(vm.initialItemFunction({ id: id })).then(setSelectedItem); + } - return filter; + function setSelectedItem(item) { + if (item) { + vm.selectedText = item[vm.optionName]; + refreshDataSource(vm.selectedText); + } + } + } - function setAttribute(filterName, isInt) { - var value = attributes[attributePrefix + filterName]; + function clearSelected() { + vm.selectedText = ''; - if (value) { - filterName = stringUtilities.lowerCaseFirstWord(filterName); - filter[filterName] = isInt ? parseInt(value) : value; - } + if (vm.model) { + vm.model = null; } } - /** - * Load the grid data if it is inconsistent with the Ids collection. - */ - function setGridItems(ids) { + function refreshDataSource(search) { + var query = { + text: search, + pageSize: 20 + } - if (!ids || !ids.length) { - vm.gridData = []; + if (vm.localeId) { + query.localeId = vm.localeId; + } else if (dynamicFormFieldController && dynamicFormFieldController.additionalParameters) { + query.localeId = dynamicFormFieldController.additionalParameters.localeId; } - else if (!vm.gridData || _.pluck(vm.gridData, IMAGE_ASSET_ID_PROP).join() != ids.join()) { - vm.gridLoadState.on(); - imageService.getByIdRange(ids).then(function (items) { - vm.gridData = items; - vm.gridLoadState.off(); - }); + return vm.searchFunction({ $query: query }).then(loadResults); + + function loadResults(results) { + if (!results || !results.items || !results.items.length) { + vm.dataSource = []; + } else { + vm.dataSource = results.items; + } } } } + }]); /** - * File upload control for images. Uses https://github.com/danialfarid/angular-file-upload + * Allows editing of html. Note that in order to display html we have included ngSanitize in + * the module dependencies (https://docs.angularjs.org/api/ng/directive/ngBindHtml) */ -angular.module('cms.shared').directive('cmsFormFieldImageUpload', [ +angular.module('cms.shared').directive('cmsFormFieldHtml', [ + '$sce', + '$q', + '$http', '_', - '$timeout', - 'shared.internalModulePath', + 'shared.internalModulePath', 'shared.internalContentPath', - 'shared.LoadState', 'shared.stringUtilities', - 'shared.imageFileUtilities', - 'shared.imageService', - 'shared.urlLibrary', - 'shared.validationErrorService', + 'shared.modalDialogService', 'baseFormFieldFactory', function ( + $sce, + $q, + $http, _, - $timeout, - modulePath, + modulePath, contentPath, - LoadState, stringUtilities, - imageFileUtilities, - imageService, - urlLibrary, - validationErrorService, - baseFormFieldFactory) { - - /* VARS */ - - var baseConfig = baseFormFieldFactory.defaultConfig, - assetReplacementPath = contentPath + 'img/AssetReplacement/', - noPreviewImagePath = assetReplacementPath + 'preview-not-supported.png', - noImagePath = assetReplacementPath + 'image-replacement.png'; - - /* CONFIG */ + modalDialogService, + baseFormFieldFactory +) { var config = { - templateUrl: modulePath + 'UIComponents/ImageAssets/FormFieldImageUpload.html', + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldHtml.html', + passThroughAttributes: [ + 'required', + 'maxlength', + 'disabled', + 'rows' + ], + getInputEl: getInputEl, scope: _.extend(baseFormFieldFactory.defaultConfig.scope, { - asset: '=cmsAsset', - loadState: '=cmsLoadState', - filter: '=cmsFilter', - onChange: '&cmsOnChange' + toolbarsConfig: '@cmsToolbars', + toolbarCustomConfig: '@cmsCustomToolbar', + options: '=cmsOptions', + configPath: '@cmsConfigPath', }), link: link }; return baseFormFieldFactory.create(config); - /* LINK */ + /* OVERRIDES */ - function link(scope, el, attributes, controllers) { + function link(scope, el, attributes) { var vm = scope.vm; - init(); - return baseConfig.link(scope, el, attributes, controllers); + // call base + baseFormFieldFactory.defaultConfig.link.apply(this, arguments); - /* INIT */ + loadTinyMCEOptions(vm, attributes).then(function (tinymceOptions) { + vm.tinymceOptions = tinymceOptions; + }); - function init() { + scope.$watch("vm.model", setEditorModel); + scope.$watch("vm.editorModel", setCmsModel); + + function setEditorModel(value) { + if (value !== vm.editorModel) { + vm.editorModel = value; + vm.rawHtml = $sce.trustAsHtml(value); + } + } + + function setCmsModel(value) { + if (value !== vm.model) { + vm.model = value; + vm.rawHtml = $sce.trustAsHtml(value); + } + } + } + + function getInputEl(rootEl) { + return rootEl.find('textarea'); + } + + /* HELPERS */ + + function loadTinyMCEOptions(vm, attributes) { + var rows = 20, + def = $q.defer(); + + if (attributes.rows) { + rows = parseInt(attributes.rows); + } + + var defaultOptions = { + toolbar: parseToolbarButtons(vm.toolbarsConfig, vm.toolbarCustomConfig), + plugins: 'link image media fullscreen imagetools code', + content_css: contentPath + "css/third-party/tinymce/content.min.css", + menubar: false, + min_height: rows * 16, + setup: function (editor) { + editor.addButton('cfimage', { + icon: 'image', + onclick: onEditorImageButtonClick.bind(null, editor) + }); + }, + browser_spellcheck: true, + convert_urls: false + }; + + if (vm.configPath) { + $http.get(vm.configPath).then(bindFileOptions); + + } else { + bindCodeOptionsAndResolve(); + } + + return def.promise; + + function bindFileOptions(result) { + if (result && result.data) { + _.extend(defaultOptions, result.data); + } + bindCodeOptionsAndResolve(); + } + + function bindCodeOptionsAndResolve() { + // Always apply last + if (vm.options) { + _.extend(defaultOptions, vm.options); + } + + def.resolve(defaultOptions); + } + } + + function onEditorImageButtonClick(editor) { + var currentElement = editor.selection.getContent({ format: 'image' }), + currentImage = currentElement.length ? angular.element(currentElement) : null; + + modalDialogService.show({ + templateUrl: modulePath + 'UIComponents/EditorDialogs/ImageAssetEditorDialog.html', + controller: 'ImageAssetEditorDialogController', + options: { + imageAssetHtml: currentImage, + onSelected: function (output) { + editor.insertContent(output.html); + } + } + }); + } - vm.isRequired = _.has(attributes, 'required'); - vm.remove = remove; - vm.fileChanged = onFileChanged; - vm.isRemovable = _.isObject(vm.ngModel) && !vm.isRequired; - vm.mainLoadState = new LoadState(true); + function parseToolbarButtons(toolbarsConfig, toolbarCustomConfig) { + var DEFAULT_CONFIG = 'headings,basicFormatting', + buttonConfig = { + headings: 'formatselect', + basicFormatting: 'fullscreen undo redo removeformat | bold italic underline | link unlink', + advancedFormatting: 'bullist numlist blockquote | alignleft aligncenter alignright alignjustify | strikethrough superscript subscript', + media: 'cfimage media', + source: 'code', + }, toolbars = []; - scope.$watch("vm.asset", setAsset); + toolbarsConfig = toolbarsConfig || DEFAULT_CONFIG; - imageService - .getSettings() - .then(mapSettings) - .then(vm.mainLoadState.off); - } + toolbarsConfig.split(',').forEach(function (configItem) { + configItem = stringUtilities.lowerCaseFirstWord(configItem.trim()); - function mapSettings(settings) { - vm.settings = settings; - } + if (configItem === 'custom') { + toolbars = _.union(toolbars, parseCustomConfig(toolbarCustomConfig)); - /* EVENTS */ + } else if (buttonConfig[configItem]) { + toolbars.push(buttonConfig[configItem]); + } + }); - function remove() { - onFileChanged(); - } + return toolbars.join(' | '); - /** - * Initialise the state when the asset is changed - */ - function setAsset() { - var asset = vm.asset; + function parseCustomConfig(toolbarCustomConfig) { + var customToolbars; - if (asset) { - vm.previewUrl = urlLibrary.getImageUrl(asset, { - width: 450 - }); - vm.isRemovable = !vm.isRequired; + if (toolbarCustomConfig) { - vm.model = { - name: asset.fileName + '.' + asset.extension, - size: asset.fileSizeInBytes, - isCurrentFile: true - }; - } else { - vm.previewUrl = noImagePath; - vm.isRemovable = false; + // old formatting allowed array parsing, but this is ambigous when mixed with other toolbars + // and so will be removed eventually. + if (!toolbarCustomConfig.startsWith("'") && !toolbarCustomConfig.startsWith("\"")) { + // single toolbar + return [toolbarCustomConfig]; + } + try { + // parse an array of toolbars, with each one surrounded by quotationmarks + customToolbars = JSON.parse('{"j":[' + toolbarCustomConfig.replace(/'/g, '"') + ']}').j; + } + catch (e) { } - if (vm.model) { - vm.model = undefined; + if (customToolbars && customToolbars.length) { + return customToolbars; } } - setButtonText(); + return []; } + } +}]); +angular.module('cms.shared').directive('cmsFormFieldNumber', [ + 'shared.internalModulePath', + 'baseFormFieldFactory', +function ( + modulePath, + baseFormFieldFactory + ) { - function onFileChanged($files) { - - if ($files && $files[0]) { - vm.mainLoadState.on(); + var config = { + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldNumber.html', + passThroughAttributes: [ + 'required', + 'maxlength', + 'min', + 'max', + 'step', + 'disabled', + 'placeholder', + 'cmsMatch' + ] + }; - // set the file if one is selected - imageFileUtilities - .getFileInfoAndResizeIfRequired($files[0], vm.settings) - .then(onImageInfoLoaded) - .finally(vm.mainLoadState.off); + return baseFormFieldFactory.create(config); +}]); +var numericTypes = [ + { name: 'Byte', min: 0, max: 255, step: 1 }, + { name: 'SByte', min: -128, max: 127, step: 1 }, + { name: 'Int16', min: -32768, max: 32767, step: 1 }, + { name: 'UInt16', min: 0, max: 65535, step: 1 }, + { name: 'Int32', min: -2147483648, max: 2147483647, step: 1 }, + { name: 'UInt32', min: 0, max: 4294967295, step: 1 }, + { name: 'Int64', step: 1 }, + { name: 'UInt64', min: 0, step: 1 } +]; - } else if (!vm.ngModel || _.isUndefined($files)) { - onNoFileSelected(); - } +numericTypes.forEach(function (numericType) { - function onImageInfoLoaded(imgInfo) { - if (!imgInfo) { - onNoFileSelected(); - } + angular.module('cms.shared').directive('cmsFormField' + numericType.name, [ + 'shared.internalModulePath', + 'baseFormFieldFactory', + function ( + modulePath, + baseFormFieldFactory + ) { - vm.model = imgInfo.file; - validateSize(imgInfo); + var config = { + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldNumber.html', + passThroughAttributes: [ + 'required', + 'maxlength', + { name: 'min', default: numericType.min }, + { name: 'max', default: numericType.max }, + { name: 'step', default: numericType.step }, + 'disabled', + 'placeholder', + 'cmsMatch' + ] + }; - setPreviewImage(imgInfo.file); - vm.isRemovable = !vm.isRequired; - vm.isResized = imgInfo.isResized; - vm.resizeSize = imgInfo.width + 'x' + imgInfo.height; - onComplete(); - } + return baseFormFieldFactory.create(config); + }]); +}); +angular.module('cms.shared').directive('cmsFormFieldPassword', [ + 'shared.internalModulePath', + 'baseFormFieldFactory', +function ( + modulePath, + baseFormFieldFactory) { - function validateSize(imgInfo) { - var filter = vm.filter; - if (!filter) return; + var config = { + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldPassword.html', + scope: _.extend(baseFormFieldFactory.defaultConfig.scope, { + passwordPolicy: '=cmsPasswordPolicy' + }), + passThroughAttributes: [ + 'required', + 'minlength', + 'maxlength', + 'pattern', + 'passwordrules', + 'disabled', + 'cmsMatch' + ], + link: link + }; - if (!isMinSize(filter.minWidth, imgInfo.width) - || !isMinSize(filter.minHeight, imgInfo.height) - || !isSize(filter.width, imgInfo.width) - || !isSize(filter.height, imgInfo.height)) { - addError('The image is ' + imgInfo.width + 'x' + imgInfo.height + ' which does not meet the size requirements.'); - } + return baseFormFieldFactory.create(config); - function isMinSize(size, value) { - return !(size > 1) || value >= size; - } + + function link(scope, element, attrs, controllers) { + var vm = scope.vm; - function isSize(size, value) { - return !(size > 1) || value == size; - } - } + // call base + baseFormFieldFactory.defaultConfig.link.apply(this, arguments); + + init(); - function addError(message) { - // Run in next digest cycle otherwise it will - // be overwirtten in the model change - $timeout(function () { - validationErrorService.raise([{ - message: message, - properties: [vm.modelName] - }]); - }); - } + /* Init */ - function onNoFileSelected() { + function init() { + scope.$watch('vm.passwordPolicy', onPasswordPolicyChange); + } - // if we don't have an image loaded already, remove the file. - vm.model = undefined; - vm.previewUrl = noImagePath; - vm.isRemovable = false; - vm.isResized = false; - onComplete(); - } + function onPasswordPolicyChange(policy) { + + vm.policyAttributes = {}; - function onComplete() { - setButtonText(); + processAttribute('minlength', true); + processAttribute('maxlength', true); + processAttribute('placeholder'); + processAttribute('pattern', true); + processAttribute('passwordrules'); + processAttribute('title', false, policy ? policy.description : null); - // base onChange event - if (vm.onChange) vm.onChange(); - } - } + function processAttribute(attribute, addValidator, defaultValue) { + var value; - /* Helpers */ + if (attrs[attribute]) { + value = attrs[attribute] + } + else if (policy && policy.attributes[attribute]) { + value = policy.attributes[attribute]; + } - function setPreviewImage(file) { - if (!isPreviewSupported(file)) { - vm.previewUrl = noPreviewImagePath; - return; + setAttributeValue(attribute, value, addValidator, defaultValue); } - try { - vm.previewUrl = URL.createObjectURL(file); - } - catch (err) { - vm.previewUrl = noPreviewImagePath; - } - } + function setAttributeValue(attribute, value, addValidator, defaultValue) { - function isPreviewSupported(file) { - var unsupportedPreviewFormats = ['.tiff', '.tif']; + vm.policyAttributes[attribute] = value || defaultValue || ''; + if (!addValidator) return; - return !_.find(unsupportedPreviewFormats, function (format) { - return stringUtilities.endsWith(file.name, format); - }); - } + var validator = _.findWhere(vm.validators, { name: attribute }); - function setButtonText() { - vm.buttonText = vm.model ? 'Change' : 'Upload'; + if (validator && (!value || !policy)) { + vm.validators = _.without(vm.validators, validator); + } + else if (policy && value && validator) { + validator.message = policy.description; + } + else if (policy && value && !validator) + { + vm.validators.push({ + name: attribute, + message: policy.description + }); + } + } } } }]); -angular.module('cms.shared').directive('cmsImageAsset', [ +angular.module('cms.shared').directive('cmsFormFieldRadioList', [ + '_', + '$http', 'shared.internalModulePath', - 'shared.internalContentPath', - 'shared.urlLibrary', + 'baseFormFieldFactory', function ( + _, + $http, modulePath, - contentPath, - urlLibrary) { - - return { - restrict: 'E', - scope: { - image: '=cmsImage', - width: '@cmsWidth', - height: '@cmsHeight', - cropMode: '@cmsCropMode' - }, - templateUrl: modulePath + 'UIComponents/ImageAssets/ImageAsset.html', - link: function (scope, el, attributes) { - - scope.$watch('image', function (newValue, oldValue) { - if (newValue && newValue.imageAssetId) { - scope.src = urlLibrary.getImageUrl(newValue, { - width: scope.width, - height: scope.height, - mode: scope.cropMode - }); - } else { - scope.src = contentPath + 'img/AssetReplacement/image-replacement.png'; - } - }); - + baseFormFieldFactory) { - }, - replace: true + var config = { + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldRadioList.html', + scope: _.extend(baseFormFieldFactory.defaultConfig.scope, { + options: '=cmsOptions', + optionValue: '@cmsOptionValue', + optionName: '@cmsOptionName', + optionsApi: '@cmsOptionsApi', + defaultItemText: '@cmsDefaultItemText', + required: '=cmsRequired', + disabled: '=cmsDisabled' + }), + passThroughAttributes: [ + 'placeholder', + 'disabled', + 'cmsMatch' + ], + link: link }; -}]); -angular.module('cms.shared').controller('ImageAssetPickerDialogController', [ - '$scope', - 'shared.LoadState', - 'shared.imageService', - 'shared.SearchQuery', - 'shared.modalDialogService', - 'shared.internalModulePath', - 'shared.permissionValidationService', - 'options', - 'close', -function ( - $scope, - LoadState, - imageService, - SearchQuery, - modalDialogService, - modulePath, - permissionValidationService, - options, - close) { - - var vm = $scope; - init(); - - /* INIT */ - - function init() { - angular.extend($scope, options); - vm.onOk = onOk; - vm.onCancel = onCancel; - vm.onSelect = onSelect; - vm.onUpload = onUpload; - vm.selectedAsset = vm.currentAsset; // currentAsset is null in single mode - vm.onSelectAndClose = onSelectAndClose; - vm.close = onCancel; + return baseFormFieldFactory.create(config); - vm.gridLoadState = new LoadState(); - vm.query = new SearchQuery({ - onChanged: onQueryChanged, - useHistory: false, - defaultParams: options.filter - }); - vm.presetFilter = options.filter; + /* PRIVATE */ - vm.filter = vm.query.getFilters(); - vm.toggleFilter = toggleFilter; + function link(scope, element, attrs, controllers) { + var vm = scope.vm; + init(); - vm.isSelected = isSelected; - vm.multiMode = vm.selectedIds ? true : false; - vm.okText = vm.multiMode ? 'Ok' : 'Select'; + // call base + baseFormFieldFactory.defaultConfig.link.apply(this, arguments); - vm.canCreate = permissionValidationService.canCreate('COFIMG'); + /* Init */ - toggleFilter(false); - loadGrid(); - } + function init() { + vm.isRequiredAttributeDefined = angular.isDefined(attrs.required); - /* ACTIONS */ + if (vm.optionsApi) { + // options bound by api call + getOptions(vm.optionsApi); - function toggleFilter(show) { - vm.isFilterVisible = _.isUndefined(show) ? !vm.isFilterVisible : show; - } + } else { - function onQueryChanged() { - toggleFilter(false); - loadGrid(); - } + // options provided as a model + var optionsWatch = scope.$watch('vm.options', function () { - function loadGrid() { - vm.gridLoadState.on(); + // need to copy b/c of assignment issue with bound attributes + vm.listOptions = vm.options; - return imageService.getAll(vm.query.getParameters()).then(function (result) { - vm.result = result; - vm.gridLoadState.off(); - }); - } - - /* EVENTS */ + bindDisplayValue(); + }); + } - function onCancel() { - if (!vm.multiMode) { - // in single-mode reset the currentAsset - vm.onSelected(vm.currentAsset); + scope.$watch('vm.model', bindDisplayValue); } - close(); - } - function onSelect(image) { - if (!vm.multiMode) { - vm.selectedAsset = image; - return; - } + /* Helpers */ - addOrRemove(image); - } + function getOptions(apiPath) { + return $http.get(apiPath).then(loadOptions); - function onSelectAndClose(image) { - if (!vm.multiMode) { - vm.selectedAsset = image; - onOk(); - return; + function loadOptions(options) { + vm.listOptions = options; + bindDisplayValue(); + } } - addOrRemove(image); - onOk(); - } + function bindDisplayValue() { - function onOk() { - if (!vm.multiMode) { - vm.onSelected(vm.selectedAsset); - } else { - vm.onSelected(vm.selectedIds); - } + var selectedOption = _.find(vm.listOptions, function (option) { + return option[vm.optionValue] == vm.model; + }); - close(); - } + vm.displayValue = selectedOption ? selectedOption[vm.optionName] : vm.defaultItemText; - function onUpload() { - modalDialogService.show({ - templateUrl: modulePath + 'UIComponents/ImageAssets/UploadImageAssetDialog.html', - controller: 'UploadImageAssetDialogController', - options: { - filter: options.filter, - onUploadComplete: onUploadComplete + // if the options and model are bound, and the option does not appear in the + // list, remove the value from the model. + if (!selectedOption && vm.model != undefined && vm.listOptions) { + vm.model = undefined; } - }); - - function onUploadComplete(imageAssetId) { - - imageService - .getById(imageAssetId) - .then(onSelectAndClose); } } - /* PUBLIC HELPERS */ +}]); +angular.module('cms.shared').directive('cmsFormFieldReadonly', [ + 'shared.internalModulePath', +function ( + modulePath + ) { - function isSelected(image) { - if (vm.selectedIds && image && vm.selectedIds.indexOf(image.imageAssetId) > -1) return true; + return { + restrict: 'E', + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldReadonly.html', + replace: true, + require: '^^cmsForm', + scope: { + title: '@cmsTitle', + description: '@cmsDescription', + model: '=cmsModel' + }, + controller: function () { }, + controllerAs: 'vm', + bindToController: true + }; +}]); +angular.module('cms.shared').directive('cmsFormFieldSubHeading', [ + 'shared.internalModulePath', +function ( + modulePath + ) { - if (!image || !vm.selectedAsset) return false; + return { + restrict: 'E', + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldSubHeading.html', + require: ['^^cmsForm'], + replace: true, + transclude: true + }; +}]); +angular.module('cms.shared').directive('cmsFormFieldText', [ + '_', + 'shared.internalModulePath', + 'baseFormFieldFactory', +function ( + _, + modulePath, + baseFormFieldFactory) { - return image.imageAssetId === vm.selectedAsset.imageAssetId; - } + var config = { + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldText.html', + passThroughAttributes: [ + 'required', + 'minlength', + 'maxlength', + 'placeholder', + 'pattern', + 'disabled', + 'readonly', + 'cmsMatch' + ] + }; - function addOrRemove(image) { - if (!isSelected(image)) { - vm.selectedIds.push(image.imageAssetId); - } else { - var index = vm.selectedIds.indexOf(image.imageAssetId); - vm.selectedIds.splice(index, 1); - } - } + return baseFormFieldFactory.create(config); }]); - -angular.module('cms.shared').controller('UploadImageAssetDialogController', [ - '$scope', - 'shared.LoadState', - 'shared.imageService', - 'shared.SearchQuery', - 'shared.focusService', +angular.module('cms.shared').directive('cmsFormFieldTextArea', [ + 'shared.internalModulePath', 'shared.stringUtilities', - 'options', - 'close', + 'baseFormFieldFactory', function ( - $scope, - LoadState, - imageService, - SearchQuery, - focusService, + modulePath, stringUtilities, - options, - close) { - - var vm = $scope; - init(); - - /* INIT */ - function init() { - angular.extend($scope, options); - - initData(); + baseFormFieldFactory) { - vm.onUpload = onUpload; - vm.onCancel = onCancel; - vm.close = onCancel; - vm.filter = options.filter; - $scope.$watch("command.file", setFileName); + var config = { + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldTextArea.html', + passThroughAttributes: [ + 'required', + 'maxlength', + 'placeholder', + 'ngMinlength', + 'ngMaxlength', + 'ngPattern', + 'disabled', + 'rows', + 'cols', + 'wrap' + ], + getInputEl: getInputEl + }; - setFilter(options.filter); + return baseFormFieldFactory.create(config); - vm.saveLoadState = new LoadState(); + function getInputEl(rootEl) { + return rootEl.find('textarea'); } - /* EVENTS */ - function onUpload() { - vm.saveLoadState.on(); - - imageService - .add(vm.command) - .progress(vm.saveLoadState.setProgress) - .then(uploadComplete); - } +}]); +angular.module('cms.shared').directive('cmsFormFieldTime', [ + 'shared.internalModulePath', + 'shared.timeUtilities', + 'baseFormFieldFactory', +function ( + modulePath, + timeUtilities, + baseFormFieldFactory + ) { - function setFileName() { - var command = vm.command; + var config = { + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldTime.html', + passThroughAttributes: [ + 'required', + 'min', + 'step', + 'max', + 'disabled', + 'readonly', + 'cmsMatch' + ], + link: link + }; - if (command.file && command.file.name) { - command.title = stringUtilities.capitaliseFirstLetter(stringUtilities.getFileNameWithoutExtension(command.file.name)); - focusService.focusById('title'); - } - } + return baseFormFieldFactory.create(config); - function onCancel() { - close(); - } + function link(scope, element, attrs, controllers) { + var vm = scope.vm; - /* PUBLIC HELPERS */ - function initData() { - vm.command = {}; + // call base + baseFormFieldFactory.defaultConfig.link.apply(this, arguments); - if (vm.filter.tags) { - vm.command.tags = vm.filter.tags.split(','); - } - } + init(); - function setFilter(filter) { - var parts = []; + /* Init */ - if (filter) { - addSize(filter.width, filter.height); - addSize(filter.minWidth, filter.minHeight, 'min-'); - } + function init() { - vm.filterText = parts.join(', '); - vm.isFilterSet = parts.length > 0; + // Use watch instead of formatters/parsers here due to issues + // with utc dates + scope.$watch("vm.model", setEditorModel); + scope.$watch("vm.editorModel", setCmsModel); - function addSize(width, height, prefix) { - if (width && height) { - parts.push(prefix + 'size ' + width + 'x' + height); - } - else { - addIfSet(prefix + 'width', width); - addIfSet(prefix + 'height', height); + function setEditorModel(value) { + if (value !== vm.editorModel) { + vm.editorModel = timeUtilities.toDate(value); + } } - } - function addIfSet(name, value) { - if (value) { - parts.push(name + ' ' + value); + function setCmsModel(value) { + if (value !== vm.model) { + vm.model = timeUtilities.format(value); + } } } } +}]); +angular.module('cms.shared').directive('cmsFormFieldUrl', [ + '_', + 'shared.internalModulePath', + 'baseFormFieldFactory', +function ( + _, + modulePath, + baseFormFieldFactory) { - function cancel() { - close(); - } + var config = { + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldUrl.html', + passThroughAttributes: [ + 'required', + 'minlength', + 'maxlength', + 'placeholder', + 'pattern', + 'disabled', + 'cmsMatch' + ] + }; - function uploadComplete(imageAssetId) { - options.onUploadComplete(imageAssetId); - close(); - } + return baseFormFieldFactory.create(config); + +}]); +angular.module('cms.shared').directive('cmsFormFieldValidationSummary', [ + 'shared.internalModulePath', +function ( + modulePath + ) { + return { + restrict: 'E', + templateUrl: modulePath + 'UIComponents/FormFields/FormFieldValidationSummary.html', + replace: true + }; }]); +angular.module('cms.shared').directive('cmsHttpPrefix', function () { + return { + restrict: 'A', + require: 'ngModel', + link: function (scope, element, attrs, controller) { + function ensureHttpPrefix(value) { + var httpPrefix = 'http://'; + + if (value + && !/^(https?):\/\//i.test(value) + && httpPrefix.indexOf(value) === -1 + && 'https://'.indexOf(value) === -1) { + + controller.$setViewValue(httpPrefix + value); + controller.$render(); + return httpPrefix + value; + } + else { + return value; + } + } + controller.$formatters.push(ensureHttpPrefix); + controller.$parsers.splice(0, 0, ensureHttpPrefix); + } + }; +}); angular.module('cms.shared').directive('cmsField', [ '$timeout', 'shared.internalModulePath', @@ -13834,259 +13835,31 @@ function ( localeService.getAll().then(function (locales) { - vm.locales = _.map(locales, function (locale) { - return { - name: locale.name + ' (' + locale.ietfLanguageTag + ')', - id: locale.localeId - } - }); - - if (vm.onLoaded) vm.onLoaded(); - }); - } -}]); -angular.module('cms.shared').directive('cmsMenu', [ - 'shared.internalModulePath', -function ( - modulePath) { - - return { - restrict: 'E', - replace: true, - transclude: true, - templateUrl: modulePath + 'UIComponents/Menus/Menu.html', - scope: { - icon: '@cmsIcon' - } - }; -}]); -angular.module('cms.shared').controller('AlertController', [ - '$scope', - 'options', - 'close', function ( - $scope, - options, - close) { - - angular.extend($scope, options); - $scope.close = close; -}]); -angular.module('cms.shared').controller('ConfirmDialogController', ['$scope', 'options', 'close', function ($scope, options, close) { - angular.extend($scope, options); - $scope.close = resolve; - - /* helpers */ - - function resolve(result) { - var resolver = result ? options.ok : options.cancel; - - if (resolver) { - resolver() - .then(closeIfRequired) - .finally(options.onCancel); - } - } - - function closeIfRequired() { - if (options.autoClose) { - close(); - } - } - -}]); -angular.module('cms.shared').controller('DeveloperExceptionController', [ - '$scope', - '$sce', - 'shared.internalContentPath', - 'options', - 'close', -function ( - $scope, - $sce, - internalContentPath, - options, - close) { - - var html = options.response.data; - - var iframe = document.createElement('iframe'); - iframe.setAttribute('srcdoc', html); - iframe.setAttribute('src', internalContentPath + 'developer-exception-not-supported.html'); - iframe.setAttribute('sandbox', 'allow-scripts'); - $scope.messageHtml = $sce.trustAsHtml(iframe.outerHTML); - - angular.extend($scope, options); - $scope.close = close; - -}]); -angular.module('cms.shared').directive('cmsModalDialogActions', ['shared.internalModulePath', function (modulePath) { - return { - restrict: 'E', - templateUrl: modulePath + 'UIComponents/Modals/ModalDialogActions.html', - transclude: true - }; -}]); -angular.module('cms.shared').directive('cmsModalDialogBody', ['shared.internalModulePath', function (modulePath) { - return { - restrict: 'E', - templateUrl: modulePath + 'UIComponents/Modals/ModalDialogBody.html', - transclude: true, - }; -}]); -angular.module('cms.shared').directive('cmsModalDialogContainer', [ - 'shared.internalModulePath', - '$timeout', -function ( - modulePath, - $timeout) { - - return { - restrict: 'E', - templateUrl: modulePath + 'UIComponents/Modals/ModalDialogContainer.html', - transclude: true, - link: link, - controller: angular.noop - }; - - function link(scope, el, attributes) { - var cls = attributes.cmsModalSize === 'large' ? 'modal-lg' : ''; - cls += (scope.isRootModal ? ' is-root-modal' : ' is-child-modal'); - if (attributes.cmsModalSize === 'large') { - scope.sizeCls = cls; - } - $timeout(function () { - scope.sizeCls = cls + ' modal--show'; - }, 1); - } -}]); -angular.module('cms.shared').directive('cmsModalDialogHeader', ['shared.internalModulePath', function (modulePath) { - return { - restrict: 'E', - templateUrl: modulePath + 'UIComponents/Modals/ModalDialogHeader.html', - transclude: true, - }; -}]); -angular.module('cms.shared').factory('shared.modalDialogService', [ - '$q', - '_', - 'ModalService', - 'shared.internalModulePath', - 'shared.LoadState', -function ( - $q, - _, - ModalService, - modulePath, - LoadState) { - - var service = {}; - - /* PUBLIC */ - - /** - * Displays a modal message with a button to dismiss the message. - */ - service.alert = function (optionsOrMessage) { - var deferred = $q.defer(), - options = optionsOrMessage || {}; - - if (_.isString(optionsOrMessage)) { - options = { - message: optionsOrMessage - } - } - - ModalService.showModal({ - templateUrl: modulePath + "UIComponents/Modals/Alert.html", - controller: "AlertController", - inputs: { - options: options - } - }).then(function (modal) { - - // Apres-creation stuff - modal.close.then(deferred.resolve); - }); - - return deferred.promise; - } - - /** - * Displays a custom modal popup using a template at the specified url. - */ - service.show = function (modalOptions) { - return ModalService.showModal({ - templateUrl: modalOptions.templateUrl, - controller: modalOptions.controller, - inputs: { - options: modalOptions.options - } - }); - } - - /** - * Displays a modal message with a button options to ok/cancel an action. - */ - service.confirm = function (optionsOrMessage) { - var returnDeferred = $q.defer(), - onOkLoadState = new LoadState(), - options = initOptions(optionsOrMessage); - - ModalService.showModal({ - templateUrl: modulePath + "UIComponents/Modals/ConfirmDialog.html", - controller: "ConfirmDialogController", - inputs: { - options: options - } - }); - - return returnDeferred.promise; - - /* helpers */ - - function initOptions(optionsOrMessage) { - var options = optionsOrMessage || {}, - defaults = { - okButtonTitle: 'OK', - cancelButtonTitle: 'Cancel', - autoClose: true, - // onCancel: fn or promise - // onOk: fn or promise - }, - internalScope = { - ok: resolve.bind(null, true), - cancel: resolve.bind(null, false), - onOkLoadState: onOkLoadState - }; - - if (_.isString(optionsOrMessage)) { - options = { - message: optionsOrMessage - } - } - - return _.defaults(internalScope, options, defaults); - } - - function resolve(isSuccess) { - var optionToExec = isSuccess ? options.onOk : options.onOk.onCancel, - deferredAction = isSuccess ? returnDeferred.resolve : returnDeferred.reject, - optionResult; - - // run the action - if (_.isFunction(optionToExec)) { - onOkLoadState.on(); - optionResult = optionToExec(); - } + vm.locales = _.map(locales, function (locale) { + return { + name: locale.name + ' (' + locale.ietfLanguageTag + ')', + id: locale.localeId + } + }); - // Wait for the result to resolve if its a promise - // Then resolve/reject promise we returned to the callee - return $q.when(optionResult) - .then(deferredAction); - } + if (vm.onLoaded) vm.onLoaded(); + }); } +}]); +angular.module('cms.shared').directive('cmsMenu', [ + 'shared.internalModulePath', +function ( + modulePath) { - return service; + return { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: modulePath + 'UIComponents/Menus/Menu.html', + scope: { + icon: '@cmsIcon' + } + }; }]); /** * Helper used for working with collections of dynamic model data that @@ -14358,6 +14131,234 @@ function ( } }]); +angular.module('cms.shared').controller('AlertController', [ + '$scope', + 'options', + 'close', function ( + $scope, + options, + close) { + + angular.extend($scope, options); + $scope.close = close; +}]); +angular.module('cms.shared').controller('ConfirmDialogController', ['$scope', 'options', 'close', function ($scope, options, close) { + angular.extend($scope, options); + $scope.close = resolve; + + /* helpers */ + + function resolve(result) { + var resolver = result ? options.ok : options.cancel; + + if (resolver) { + resolver() + .then(closeIfRequired) + .finally(options.onCancel); + } + } + + function closeIfRequired() { + if (options.autoClose) { + close(); + } + } + +}]); +angular.module('cms.shared').controller('DeveloperExceptionController', [ + '$scope', + '$sce', + 'shared.internalContentPath', + 'options', + 'close', +function ( + $scope, + $sce, + internalContentPath, + options, + close) { + + var html = options.response.data; + + var iframe = document.createElement('iframe'); + iframe.setAttribute('srcdoc', html); + iframe.setAttribute('src', internalContentPath + 'developer-exception-not-supported.html'); + iframe.setAttribute('sandbox', 'allow-scripts'); + $scope.messageHtml = $sce.trustAsHtml(iframe.outerHTML); + + angular.extend($scope, options); + $scope.close = close; + +}]); +angular.module('cms.shared').directive('cmsModalDialogActions', ['shared.internalModulePath', function (modulePath) { + return { + restrict: 'E', + templateUrl: modulePath + 'UIComponents/Modals/ModalDialogActions.html', + transclude: true + }; +}]); +angular.module('cms.shared').directive('cmsModalDialogBody', ['shared.internalModulePath', function (modulePath) { + return { + restrict: 'E', + templateUrl: modulePath + 'UIComponents/Modals/ModalDialogBody.html', + transclude: true, + }; +}]); +angular.module('cms.shared').directive('cmsModalDialogContainer', [ + 'shared.internalModulePath', + '$timeout', +function ( + modulePath, + $timeout) { + + return { + restrict: 'E', + templateUrl: modulePath + 'UIComponents/Modals/ModalDialogContainer.html', + transclude: true, + link: link, + controller: angular.noop + }; + + function link(scope, el, attributes) { + var cls = attributes.cmsModalSize === 'large' ? 'modal-lg' : ''; + cls += (scope.isRootModal ? ' is-root-modal' : ' is-child-modal'); + if (attributes.cmsModalSize === 'large') { + scope.sizeCls = cls; + } + $timeout(function () { + scope.sizeCls = cls + ' modal--show'; + }, 1); + } +}]); +angular.module('cms.shared').directive('cmsModalDialogHeader', ['shared.internalModulePath', function (modulePath) { + return { + restrict: 'E', + templateUrl: modulePath + 'UIComponents/Modals/ModalDialogHeader.html', + transclude: true, + }; +}]); +angular.module('cms.shared').factory('shared.modalDialogService', [ + '$q', + '_', + 'ModalService', + 'shared.internalModulePath', + 'shared.LoadState', +function ( + $q, + _, + ModalService, + modulePath, + LoadState) { + + var service = {}; + + /* PUBLIC */ + + /** + * Displays a modal message with a button to dismiss the message. + */ + service.alert = function (optionsOrMessage) { + var deferred = $q.defer(), + options = optionsOrMessage || {}; + + if (_.isString(optionsOrMessage)) { + options = { + message: optionsOrMessage + } + } + + ModalService.showModal({ + templateUrl: modulePath + "UIComponents/Modals/Alert.html", + controller: "AlertController", + inputs: { + options: options + } + }).then(function (modal) { + + // Apres-creation stuff + modal.close.then(deferred.resolve); + }); + + return deferred.promise; + } + + /** + * Displays a custom modal popup using a template at the specified url. + */ + service.show = function (modalOptions) { + return ModalService.showModal({ + templateUrl: modalOptions.templateUrl, + controller: modalOptions.controller, + inputs: { + options: modalOptions.options + } + }); + } + + /** + * Displays a modal message with a button options to ok/cancel an action. + */ + service.confirm = function (optionsOrMessage) { + var returnDeferred = $q.defer(), + onOkLoadState = new LoadState(), + options = initOptions(optionsOrMessage); + + ModalService.showModal({ + templateUrl: modulePath + "UIComponents/Modals/ConfirmDialog.html", + controller: "ConfirmDialogController", + inputs: { + options: options + } + }); + + return returnDeferred.promise; + + /* helpers */ + + function initOptions(optionsOrMessage) { + var options = optionsOrMessage || {}, + defaults = { + okButtonTitle: 'OK', + cancelButtonTitle: 'Cancel', + autoClose: true, + // onCancel: fn or promise + // onOk: fn or promise + }, + internalScope = { + ok: resolve.bind(null, true), + cancel: resolve.bind(null, false), + onOkLoadState: onOkLoadState + }; + + if (_.isString(optionsOrMessage)) { + options = { + message: optionsOrMessage + } + } + + return _.defaults(internalScope, options, defaults); + } + + function resolve(isSuccess) { + var optionToExec = isSuccess ? options.onOk : options.onOk.onCancel, + deferredAction = isSuccess ? returnDeferred.resolve : returnDeferred.reject, + optionResult; + + // run the action + if (_.isFunction(optionToExec)) { + onOkLoadState.on(); + optionResult = optionToExec(); + } + + // Wait for the result to resolve if its a promise + // Then resolve/reject promise we returned to the callee + return $q.when(optionResult) + .then(deferredAction); + } + } + + return service; +}]); angular.module('cms.shared').controller('EditNestedDataModelDialogController', [ '$scope', 'shared.nestedDataModelSchemaService', diff --git a/src/Cofoundry.Web.Admin/Admin/Modules/Shared/Content/js/shared_min.js b/src/Cofoundry.Web.Admin/Admin/Modules/Shared/Content/js/shared_min.js index 51a434161..129f43c30 100644 --- a/src/Cofoundry.Web.Admin/Admin/Modules/Shared/Content/js/shared_min.js +++ b/src/Cofoundry.Web.Admin/Admin/Modules/Shared/Content/js/shared_min.js @@ -472,7 +472,7 @@ angular.module("cms.shared").factory("shared.errorService",function(){var e={},n angular.module("cms.shared").factory("shared.focusService",["$document",function(e){var c={},u=e[0];return c.focusById=function(e){e=u.getElementById(e);e&&e.focus()},c}]); angular.module("cms.shared").factory("httpInterceptor",["$q","$rootScope","_","shared.validationErrorService","authenticationService","shared.errorService","shared.stringUtilities",function(t,e,r,a,s,i,n){var o={response:function(e){return e=!r.isUndefined(e.data.data)?e.data.data:e},responseError:function(e){var r;return n.startsWith(e.config.url,"/")&&function(){switch(e.status){case 400:!1===e.data.isValid&&a.raise(e.data.errors);break;case 401:s.redirectToLogin();break;case 403:i.raise({title:"Permission Denied",message:"This action is not authorized"});break;default:404==e.status&&"GET"===e.config.method||(r={title:e.statusText,message:"An unexpected server error has occurred.",response:e},i.raise(r))}}(),t.reject(e)}};return o}]); -angular.module("cms.shared").config(["$httpProvider","csrfToken","csrfHeaderName",function(e,t,r){var o=e.defaults.headers;e.interceptors.push("httpInterceptor"),["put","post","patch","delete"].forEach(function(e){o[e]=o[e]||{},o[e][r]=t}),o.common["X-Requested-With"]="XMLHttpRequest"}]); +angular.module("cms.shared").config(["$httpProvider","csrfToken","csrfHeaderName",function(t,e,o){var c=t.defaults.headers;t.interceptors.push("httpInterceptor"),["put","post","patch","delete"].forEach(function(t){c[t]=c[t]||{},c[t][o]=e}),c.common["X-Requested-With"]="XMLHttpRequest",c.common.Accept="application/json, text/html, text/plain, */*"}]); angular.module("cms.shared").config(["$locationProvider",function(o){o.hashPrefix("")}]); angular.module("cms.shared").factory("shared.permissionValidationService",["_","shared.currentUser",function(e,r){var s={};return s.hasPermission=function(n){return e.contains(r.permissionCodes,n)},s.canRead=function(n){return s.hasPermission(n+"COMRED")},s.canViewModule=function(n){return s.hasPermission(n+"COMMOD")},s.canCreate=function(n){return s.hasPermission(n+"COMCRT")},s.canUpdate=function(n){return s.hasPermission(n+"COMUPD")},s.canDelete=function(n){return s.hasPermission(n+"COMDEL")},s}]); angular.module("cms.shared").factory("shared.validationErrorService",["_",function(t){var n={},i=[];return n.raise=function(n){var r,o=[];n.forEach(function(e){var n=t.filter(i,function(r){return t.find(e.properties,function(n){return!(!n||!r.prop)&&r.prop.toLowerCase()===n.toLowerCase()})});n.length?n.forEach(function(n){n.fn([e])}):o.push(e)}),o.length&&((n=t.filter(i,function(n){return!n.prop})).length?(r=o,n.forEach(function(n){n.fn(r)})):function(){throw new Error("An unhandled validation exception has occurred")}())},n.addHandler=function(n,r){r={prop:n,fn:r};return i.push(r),r},n.removeHandler=function(n){n=t.isFunction(n)?t.where(i,{fn:n}):t.where(i,{prop:n});i=t.difference(i,n)},n}]); @@ -486,7 +486,7 @@ angular.module("cms.shared").controller("CustomEntityPickerDialogController",["$ angular.module("cms.shared").directive("cmsFormFieldCustomEntityCollection",["_","shared.internalModulePath","shared.LoadState","shared.customEntityService","shared.modalDialogService","shared.arrayUtilities","shared.ModelPreviewFieldset","shared.ImagePreviewFieldCollection","baseFormFieldFactory",function(h,v,y,C,I,E,p,F,e){var w="customEntityId",S=e.defaultConfig,t={templateUrl:v+"UIComponents/CustomEntities/FormFieldCustomEntityCollection.html",scope:h.extend(S.scope,{customEntityDefinitionCode:"@cmsCustomEntityDefinitionCode",localeId:"=cmsLocaleId",orderable:"=cmsOrderable"}),require:h.union(S.require,["?^^cmsFormDynamicFieldSet"]),passThroughAttributes:["required"],link:function(e,t,i,o){var n,r,a,d=e.vm,l=(h.has(i,"required"),h.last(o));return function(){d.gridLoadState=new y,d.showPicker=s,d.remove=c,d.onDrop=m,d.onDropSuccess=u,n=C.getDefinition(d.customEntityDefinitionCode).then(function(e){d.customEntityDefinition=e}),r=C.getDataModelSchema(d.customEntityDefinitionCode).then(f),e.$watch("vm.model",D)}(),S.link(e,t,i,o);function c(e,t){E.removeObject(d.gridData,e),E.removeObject(d.model,e[w]),d.gridImages.remove(t)}function s(){I.show({templateUrl:v+"UIComponents/CustomEntities/CustomEntityPickerDialog.html",controller:"CustomEntityPickerDialogController",options:{selectedIds:d.model||[],customEntityDefinition:d.customEntityDefinition,filter:function(){var e,t={};d.localeId?e=d.localeId:l&&l.additionalParameters&&(e=l.additionalParameters.localeId);e&&(t.localeId=e);return t}(),onSelected:function(e){D(d.model=e)}}})}function m(e,t){a=e}function u(e){E.move(d.gridData,e,a),d.gridImages.move(e,a),g()}function g(){d.model=h.pluck(d.gridData,w)}function f(e){d.previewFields=new p(e)}function D(e){e&&e.length?d.gridData&&h.pluck(d.gridData,w).join()==e.join()||(d.gridLoadState.on(),e=C.getByIdRange(e).then(function(e){d.gridData=e,d.orderable||(d.gridData=h.sortBy(d.gridData,"title"),g()),r.then(function(){return d.gridImages=new F,d.gridImages.load(d.gridData,d.previewFields)})}),d.gridLoadState.offWhen(n,e)):d.gridData=[]}}};return e.create(t)}]); angular.module("cms.shared").directive("cmsFormFieldCustomEntityMultiTypeCollection",["_","shared.internalModulePath","shared.LoadState","shared.customEntityService","shared.modalDialogService","shared.arrayUtilities","shared.ModelPreviewFieldset","shared.ImagePreviewFieldCollection","baseFormFieldFactory",function(p,h,y,D,v,E,I,w,e){var F="customEntityId",H="customEntityDefinitionCode",P=e.defaultConfig,t={templateUrl:h+"UIComponents/CustomEntities/FormFieldCustomEntityMultiTypeCollection.html",scope:p.extend(P.scope,{customEntityDefinitionCodes:"@cmsCustomEntityDefinitionCodes",localeId:"=cmsLocaleId",orderable:"=cmsOrderable",titleColumnHeader:"@cmsTitleColumnHeader",descriptionColumnHeader:"@cmsDescriptionColumnHeader",imageColumnHeader:"@cmsImageColumnHeader",typeColumnHeader:"@cmsTypeColumnHeader"}),require:p.union(P.require,["?^^cmsFormDynamicFieldSet"]),passThroughAttributes:["required"],link:function(t,e,i,o){var n,r,d,a=t.vm,l=(p.has(i,"required"),p.last(o));return function(){var e=a.customEntityDefinitionCodes?a.customEntityDefinitionCodes.split(","):[];a.gridLoadState=new y,a.showPicker=m,a.remove=s,a.onDrop=c,a.onDropSuccess=u,n=D.getDefinitionsByIdRange(e).then(function(e){a.customEntityDefinitions={},p.each(e,function(e){(a.customEntityDefinitions[e.customEntityDefinitionCode]=e).autoPublish||(a.showPublishColumn=!0)})}),r=D.getDataModelSchemasByCodeRange(e).then(g),t.$watch("vm.model",C),void 0===a.titleColumnHeader&&(a.titleColumnHeader="Title"),void 0===a.descriptionColumnHeader&&(a.descriptionColumnHeader="Description"),void(void 0===a.typeColumnHeader&&(a.typeColumnHeader="Type"))}(),P.link(t,e,i,o);function s(e,t){E.removeObject(a.gridData,e),E.remove(a.model,t),a.gridImages.remove(t)}function m(o){v.show({templateUrl:h+"UIComponents/CustomEntities/CustomEntityPickerDialog.html",controller:"CustomEntityPickerDialogController",options:{selectedIds:p.chain(a.model).where({customEntityDefinitionCode:o[H]}).map(function(e){return e[F]}).value(),customEntityDefinition:o,filter:function(){var e,t={};a.localeId?e=a.localeId:l&&l.additionalParameters&&(e=l.additionalParameters.localeId);e&&(t.localeId=e);return t}(),onSelected:function(e){a.model||(a.model=[]);if(0",model:s.command};t.html=(a=t.html,e=e,angular.element(a).css(e)[0].outerHTML),s.onSelected(t),d()}function h(e){return e?-1==e.indexOf("px")&&-1==e.indexOf("%")&&-1==e.indexOf("auto")?e+"px":e:""}angular.extend(e,n),s.formLoadState=new t,s.saveLoadState=new t,s.onInsert=g,s.onCancel=o,s.onImageChanged=r,s.command={},function(){var e,t;s.imageAssetHtml&&s.imageAssetHtml.length&&(s.command.imageAssetId=s.imageAssetHtml.attr("data-image-asset-id"),s.command.altTag=s.imageAssetHtml.attr("alt"),s.command.style=s.imageAssetHtml.attr("style"),s.command.style?(t=function(e){var t,a=/([\w-]*)\s*:\s*([^;]*)/g,m={};for(;t=a.exec(e);)m[t[1]]=t[2];return m}(s.command.style),s.command.width=t.width,s.command.height=t.height):(s.command.width=s.imageAssetHtml.attr("width"),s.command.height=s.imageAssetHtml.attr("height")),s.command.imageAssetId||(e=s.imageAssetHtml.attr("src"),t=e.lastIndexOf("/"),t=e.substr(t+1,e.indexOf("_")-t-1),s.command.imageAssetId=t))}()}]); angular.module("cms.shared").directive("cmsDocumentAsset",["shared.internalModulePath","shared.urlLibrary",function(e,r){return{restrict:"E",scope:{document:"=cmsDocument"},templateUrl:e+"UIComponents/DocumentAssets/DocumentAsset.html",link:function(e,t,n){e.getDocumentUrl=r.getDocumentUrl}}}]); angular.module("cms.shared").controller("DocumentAssetPickerDialogController",["$scope","shared.LoadState","shared.documentService","shared.SearchQuery","shared.modalDialogService","shared.internalModulePath","shared.permissionValidationService","shared.urlLibrary","options","close",function(e,t,s,o,n,l,d,r,c,i){var u=e;function a(e){u.isFilterVisible=_.isUndefined(e)?!u.isFilterVisible:e}function m(){a(!1),f()}function f(){return u.gridLoadState.on(),s.getAll(u.query.getParameters()).then(function(e){u.result=e,u.gridLoadState.off()})}function A(){u.multiMode||u.onSelected(u.currentAsset),i()}function g(e){u.multiMode?C(e):u.selectedAsset=e}function h(e){if(!u.multiMode)return u.selectedAsset=e,void S();C(e),S()}function S(){u.multiMode?u.onSelected(u.selectedIds):u.onSelected(u.selectedAsset),i()}function p(){n.show({templateUrl:l+"UIComponents/DocumentAssets/UploadDocumentAssetDialog.html",controller:"UploadDocumentAssetDialogController",options:{filter:c.filter,onUploadComplete:function(e){h({documentAssetId:e})}}})}function I(e){return!!(u.selectedIds&&e&&-1",model:s.command};t.html=(a=t.html,e=e,angular.element(a).css(e)[0].outerHTML),s.onSelected(t),d()}function h(e){return e?-1==e.indexOf("px")&&-1==e.indexOf("%")&&-1==e.indexOf("auto")?e+"px":e:""}angular.extend(e,n),s.formLoadState=new t,s.saveLoadState=new t,s.onInsert=g,s.onCancel=o,s.onImageChanged=r,s.command={},function(){var e,t;s.imageAssetHtml&&s.imageAssetHtml.length&&(s.command.imageAssetId=s.imageAssetHtml.attr("data-image-asset-id"),s.command.altTag=s.imageAssetHtml.attr("alt"),s.command.style=s.imageAssetHtml.attr("style"),s.command.style?(t=function(e){var t,a=/([\w-]*)\s*:\s*([^;]*)/g,m={};for(;t=a.exec(e);)m[t[1]]=t[2];return m}(s.command.style),s.command.width=t.width,s.command.height=t.height):(s.command.width=s.imageAssetHtml.attr("width"),s.command.height=s.imageAssetHtml.attr("height")),s.command.imageAssetId||(e=s.imageAssetHtml.attr("src"),t=e.lastIndexOf("/"),t=e.substr(t+1,e.indexOf("_")-t-1),s.command.imageAssetId=t))}()}]); +angular.module("cms.shared").directive("cmsFormFieldDirectorySelector",["_","shared.directiveUtilities","shared.internalModulePath","shared.directoryService",function(e,i,r,t){return{restrict:"E",templateUrl:r+"UIComponents/Directories/FormFieldDirectorySelector.html",scope:{model:"=cmsModel",title:"@cmsTitle",onLoaded:"&cmsOnLoaded",readonly:"=cmsReadonly"},link:{pre:function(e,r,t){e=e.vm;angular.isDefined(t.required)?e.isRequired=!0:(e.isRequired=!1,e.defaultItemText=t.cmsDefaultItemText||"None");e.title=t.cmsTitle||"Directory",e.description=t.cmsDescription,i.setModelName(e,t)}},controller:function(){var r=this;t.getAll().then(function(e){r.pageDirectories=e,r.onLoaded&&r.onLoaded()})},controllerAs:"vm",bindToController:!0}}]); angular.module("cms.shared").controller("AddEntityAccessRuleController",["$scope","$q","shared.LoadState","shared.roleService","shared.userAreaService","options","close",function(e,o,n,r,a,t,d){var s=e;function u(){s.command.roleId=null}function l(e){if(s.command.userAreaCode)return e.userAreaCode=s.command.userAreaCode,e.excludeAnonymous=!0,r.search(e);e=o.defer();return e.resolve(),e.promise}function c(){t.onSave(s.command),d()}function f(){d()}s.globalLoadState=new n,s.saveLoadState=new n,s.formLoadState=new n,function(e){s.globalLoadState.on(),e&&_.isFunction(e.on)&&e.on()}(s.formLoadState),s.onAdd=c,s.onCancel=f,s.onUserAreaChanged=u,s.searchRoles=l,s.command={},u(),a.getAll().then(function(e){s.userAreas=_.filter(e,function(e){return"COF"!==e.userAreaCode}),1==s.userAreas.length&&(s.command.userAreaCode=s.userAreas[0].userAreaCode)}).finally(function(e){s.globalLoadState.off(),e&&_.isFunction(e.off)&&e.off()}.bind(null,s.formLoadState))}]); angular.module("cms.shared").controller("EntityAccessEditorController",["$scope","$q","shared.LoadState","shared.userAreaService","shared.roleService","shared.modalDialogService","shared.arrayUtilities","shared.internalModulePath","shared.permissionValidationService","shared.urlLibrary","options","close",function(e,o,n,t,i,r,s,a,c,u,d,l){var A=e;function f(){A.command.accessRules=_.map(A.accessRuleSet.accessRules,function(e){var n=d.entityIdPrefix+"AccessRuleId",r={userAreaCode:e.userArea.userAreaCode,roleId:e.role?e.role.roleId:null};return r[n]=e[n],r}),A.command.redirectToSignIn||(A.command.userAreaCodeForSignInRedirect=null),I(A.saveLoadState),d.saveAccess(A.command).then(function(e,n){return h(n).then(A.mainForm.formStatus.success.bind(null,e))}.bind(null,"Access rules updated successfully")).then(l).finally(C.bind(null,A.saveLoadState))}function R(){r.show({templateUrl:a+"UIComponents/EntityAccess/AddEntityAccessRule.html",controller:"AddEntityAccessRuleController",options:{onSave:S}})}function m(e,n){s.removeObject(A.accessRuleSet.accessRules,e),g()}function S(r){var n={};_.find(A.accessRuleSet.accessRules,function(e){var n=e.role?e.role.roleId:null;return e.userArea.userAreaCode===r.userAreaCode&&n==r.roleId})||(I(),o.all([r.roleId?i.getById(r.roleId).then(function(e){n.role=e}):o(function(e){e()}),t.getByCode(r.userAreaCode).then(function(e){n.userArea=e})]).then(function(){A.accessRuleSet.accessRules.push(n),A.accessRuleSet.accessRules=_(A.accessRuleSet.accessRules).chain().sortBy(function(e){return e.role?e.role.roleId:-1}).sortBy(function(e){return e.userArea.userAreaCode}).value(),g()}).finally(C))}function h(e){return A.entityDefinitionName=d.entityDefinitionName,A.entityDefinitionNameLower=d.entityDefinitionName.toLowerCase(),A.entityDescription=d.entityDescription,A.violationActions=[{id:"Error",name:"Error",description:"Error (403: Forbidden)"},{id:"NotFound",name:"Not Found",description:"Not Found (404: Not Found)"}],d.entityAccessLoader().then(function(e){A.accessRuleSet=e,A.command=function(e){var n=_.pick(e,d.entityIdPrefix+"Id","userAreaCodeForSignInRedirect","violationAction");e.userAreaForSignInRedirect&&(n.userAreaCodeForSignInRedirect=e.userAreaForSignInRedirect.userAreaCode,n.redirectToSignIn=!0);return n}(e),A.inheritedRules=[],_.each(A.accessRuleSet.inheritedAccessRules,function(n){n.violationAction=_.findWhere(A.violationActions,{id:n.violationAction}),n.userAreaForSignInRedirect?(n.signInRedirect="Yes",n.signInRedirectDescription="If the user is not signed in, then they will be redirected to the sign in page associated with the "+n.userAreaForSignInRedirect.name+" user area."):(n.signInRedirect="No",n.signInRedirectDescription="No sign in redirection, the default action will trigger instead."),_.each(n.accessRules,function(e){e.accessRuleSet=n,A.inheritedRules.push(e)})}),g()}).then(C.bind(null,e))}function g(){A.userAreasInRules=_(A.accessRuleSet.accessRules).chain().map(function(e){return e.userArea}).uniq(function(e){return e.userAreaCode}).sortBy("userAreaCode").value(),A.userAreasInRules.length&&_.find(A.userAreasInRules,function(e){return e.userAreaCode===A.command.userAreaCodeForSignInRedirect})||(A.command.redirectToSignIn=!1,A.command.userAreaCodeForSignInRedirect=null),!A.command.userAreaCodeForSignInRedirect&&A.userAreasInRules.length&&(A.command.userAreaCodeForSignInRedirect=A.userAreasInRules[0].userAreaCode,console.log("setting vm.command.userAreaCodeForSignInRedirect",A.command.userAreaCodeForSignInRedirect))}function I(e){A.globalLoadState.on(),e&&_.isFunction(e.on)&&e.on()}function C(e){A.globalLoadState.off(),e&&_.isFunction(e.off)&&e.off()}A.save=f,A.close=l,A.add=R,A.deleteRule=m,A.globalLoadState=new n,A.saveLoadState=new n,A.formLoadState=new n(!0),A.urlLibrary=u,A.canManage=c.hasPermission(d.entityDefinitionCode+"ACCRUL"),A.editMode=A.canManage,h(A.formLoadState)}]); angular.module("cms.shared").factory("shared.entityVersionModalDialogService",["shared.entityVersionService","shared.modalDialogService",function(s,a){var t={},l={entityNameSingular:"Page"};return t.publish=function(t,e,i){var n=i||l,i={title:"Publish "+n.entityNameSingular,message:"Are you sure you want to publish this "+n.entityNameSingular.toLowerCase()+"?",okButtonTitle:"Yes, publish it",onOk:function(){return e(),s.publish(n.isCustomEntity,t)}};return a.confirm(i)},t.unpublish=function(t,e,i){var n=i||l,i={title:"Unpublish "+n.entityNameSingular,message:"Unpublishing this "+n.entityNameSingular.toLowerCase()+" will remove it from the live site and put it into draft status. Are you sure you want to continue?",okButtonTitle:"Yes, unpublish it",onOk:function(){return e(),s.unpublish(n.isCustomEntity,t)}};return a.confirm(i)},t.copyToDraft=function(t,e,i,n,r){var o=r||l,r={title:"Copy "+o.entityNameSingular+" Version",message:"A draft version of this "+o.entityNameSingular.toLowerCase()+" already exists. Copying this version will delete the current draft. Do you wish to continue?",okButtonTitle:"Yes, replace it",onOk:function(){return n(),s.removeDraft(o.isCustomEntity,t).then(u)}};return i?a.confirm(r):(n(),u());function u(){return s.duplicateDraft(o.isCustomEntity,t,e)}},t}]); @@ -505,6 +505,14 @@ angular.module("cms.shared").directive("cmsFormSectionActions",["shared.internal angular.module("cms.shared").directive("cmsFormSectionAuditData",["shared.internalModulePath",function(t){return{restrict:"E",templateUrl:t+"UIComponents/Form/FormSectionAuditData.html",scope:{auditData:"=cmsAuditData"}}}]); angular.module("cms.shared").directive("cmsFormStatus",["_","shared.validationErrorService","shared.internalModulePath",function(t,s,r){return{restrict:"E",templateUrl:r+"UIComponents/Form/FormStatus.html",require:["^^cmsForm"],replace:!0,scope:!0,link:{post:function(r,e,n,o){(function(r,e){e=e.getFormScope().getForm(),r.success=function(r){i(this,r,"success")}.bind(r),r.error=function(r){i(this,r,"error")}.bind(r),r.errors=function(r,e){r=t.uniq(r,function(r){return r.message});i(this,e,"error",r)}.bind(r),r.clear=function(){i(this)}.bind(r),e.formStatus=r})(r,o[0]),function(r){s.addHandler("",r.errors),r.$on("$destroy",function(){s.removeHandler(r.errors)})}(r)}}};function i(r,e,n,o){r.message=e,r.errors=o,r.cls=n}}]); angular.module("cms.shared").directive("cmsFormDynamicFieldSet",["$compile","_","shared.stringUtilities","shared.internalModulePath","shared.LoadState",function(i,c,d,e,t){return{restrict:"E",replace:!0,scope:{dataSource:"=cmsDataSource",additionalParameters:"=cmsAdditionalParameters"},link:function(t,a,e,r){t.vm;var n=new l;function o(e){var r="";a.empty(),e&&e.modelMetaData.dataModelProperties.length&&(e.modelMetaData.dataModelProperties.forEach(function(e,a){var t=function(e){var t="cms-form-field-";switch(e.dataTemplateName){case"Single":case"Double":case"Decimal":return t+"number";case"String":return t+"text";case"Boolean":return t+"checkbox";case"MultilineText":return t+"text-area"}return t+d.toSnakeCase(e.dataTemplateName)}(e);r+="<"+t,r+=n.map("model",d.lowerCaseFirstWord(e.name)),r+=n.map("title",e.displayName),r+=n.map("required",e.isRequired&&!e.additionalAttributes.readonly),r+=n.map("description",e.description),e.additionalAttributes&&c.each(e.additionalAttributes,function(e,t){r+=n.map(t,e,a)}),r+=">"}),a.append(i(r)(t)))}t.$watch("vm.dataSource",function(e){o(e)})},controller:["$scope",function(e){}],bindToController:!0,controllerAs:"vm"};function l(){var a="cms-",o={maxlength:i,minlength:i,min:i,max:i,pattern:i,step:i,placeholder:i,match:e,model:e,options:function(e,t,a){return c(e,"vm.dataSource.modelMetaData.dataModelProperties["+a+"].additionalAttributes['"+e+"']")},required:function(e,t){if(t)return r(e.toLowerCase());return""},rows:i,cols:i};function e(e,t){return c(e,t="vm.dataSource.model['"+t+"']")}function i(e,t){return r(d.toSnakeCase(e),t)}function c(e,t){return r(e=a+d.toSnakeCase(e),t)}function r(e,t){return t?" "+e+'="'+t+'"':" "+e}this.map=function(e,t,a){var r="ValMsg",n=o[e];return!n&&d.endsWith(e,r)?(r=e.substring(0,e.length-r.length),t&&o[r]===i&&(n=i)):n=n||c,n?n(e,t,a):""}}}]); +angular.module("cms.shared").directive("cmsMatch",["$parse","$timeout","shared.internalModulePath","shared.directiveUtilities",function(e,r,t,o){var s="cmsMatch";return{link:function(e,r,t,a){if(!t[s]||!a[1])return;var i=a[0],a=a[1],n=i.getFormScope().getForm(),c=o.parseModelName(t[s]);a.$validators[s]=function(e,r){var t=n[c];return!!t&&e===t.$viewValue}},restrict:"A",require:["^^cmsForm","?ngModel"]}}]); +angular.module("cms.shared").directive("cmsFormFieldImageAnchorLocationSelector",["_","shared.internalModulePath",function(e,t){return{restrict:"E",templateUrl:t+"UIComponents/ImageAssets/FormFieldImageAnchorLocationSelector.html",scope:{model:"=cmsModel",readonly:"=cmsReadonly"},controller:function(){this.options=[{name:"Top Left",id:"TopLeft"},{name:"Top Center",id:"TopCenter"},{name:"Top Right",id:"TopRight"},{name:"Middle Left",id:"MiddleLeft"},{name:"Middle Center",id:"MiddleCenter"},{name:"Middle Right",id:"MiddleRight"},{name:"Bottom Left",id:"BottomLeft"},{name:"Bottom Center",id:"BottomCenter"},{name:"Bottom Right",id:"BottomRight"}]},controllerAs:"vm",bindToController:!0}}]); +angular.module("cms.shared").directive("cmsFormFieldImageAsset",["_","shared.internalModulePath","shared.internalContentPath","shared.modalDialogService","shared.stringUtilities","shared.imageService","shared.urlLibrary","baseFormFieldFactory",function(c,u,e,h,g,v,A,t){var p=t.defaultConfig,e={templateUrl:u+"UIComponents/ImageAssets/FormFieldImageAsset.html",scope:c.extend(p.scope,{asset:"=cmsAsset",loadState:"=cmsLoadState",updateAsset:"@cmsUpdateAsset"}),passThroughAttributes:["required"],link:function(e,t,s,i){var r,a=e.vm,n=c.has(s,"required");return function(){a.urlLibrary=A,a.showPicker=l,a.remove=o,a.isRemovable=c.isObject(a.model)&&!n,a.filter=function(i){var r={};return e("Tags"),e("Width",!0),e("Height",!0),e("MinWidth",!0),e("MinHeight",!0),r;function e(e,t){var s=i["cms"+e];s&&(e=g.lowerCaseFirstWord(e),r[e]=t?parseInt(s):s)}}(s),a.previewWidth=s.cmsPreviewWidth||450,a.previewHeight=s.cmsPreviewHeight,e.$watch("vm.asset",d),e.$watch("vm.model",m)}(),p.link(e,t,s,i);function o(){d(null)}function l(){h.show({templateUrl:u+"UIComponents/ImageAssets/ImageAssetPickerDialog.html",controller:"ImageAssetPickerDialogController",options:{currentAsset:a.previewAsset,filter:a.filter,onSelected:function(e){!e&&a.previewAsset?(d(null),a.onChange()):(!a.previewAsset||e&&a.previewAsset.imageAssetId!==e.imageAssetId)&&(d(e),a.onChange())}}})}function m(e){e||(a.model=e=void 0),!e||a.previewAsset&&a.previewAsset.imageAssetId==e||v.getById(e).then(function(e){e&&d(e)})}function d(e){e?(a.previewAsset=e,a.isRemovable=!n,a.model=e.imageAssetId,a.updateAsset&&(a.asset=e)):r&&(a.previewAsset=null,a.isRemovable=!1,a.model&&(a.model=null),a.updateAsset&&(a.asset=null)),a.buttonText=a.model?"Change":"Select",r=!0}}};return t.create(e)}]); +angular.module("cms.shared").directive("cmsFormFieldImageAssetCollection",["_","shared.internalModulePath","shared.LoadState","shared.imageService","shared.modalDialogService","shared.arrayUtilities","shared.stringUtilities","shared.urlLibrary","baseFormFieldFactory",function(l,g,m,c,u,h,f,I,e){var p="imageAssetId",v=e.defaultConfig,t={templateUrl:g+"UIComponents/ImageAssets/FormFieldImageAssetCollection.html",passThroughAttributes:["required","ngRequired"],link:function(e,t,i,r){var o=e.vm;return o.gridLoadState=new m,o.urlLibrary=I,o.showPicker=n,o.remove=a,o.onDrop=s,e.$watch("vm.model",d),v.link(e,t,i,r);function a(e){function t(e,t){t=e.indexOf(t);return 0<=t&&e.splice(t,1)}t(o.gridData,e),t(o.model,e.imageAssetId)}function n(){function e(e,t){var r=i["cms"+e];r&&(e=f.lowerCaseFirstWord(e),a[e]=t?parseInt(r):r)}var a;u.show({templateUrl:g+"UIComponents/ImageAssets/ImageAssetPickerDialog.html",controller:"ImageAssetPickerDialogController",options:{selectedIds:o.model||[],filter:(a={},e("Tags"),e("Width",!0),e("Height",!0),e("MinWidth",!0),e("MinHeight",!0),a),onSelected:function(e){d(o.model=e)}}})}function s(e,t){h.moveObject(o.gridData,t,e,p),o.model=l.pluck(o.gridData,p)}function d(e){e&&e.length?o.gridData&&l.pluck(o.gridData,p).join()==e.join()||(o.gridLoadState.on(),c.getByIdRange(e).then(function(e){o.gridData=e,o.gridLoadState.off()})):o.gridData=[]}}};return e.create(t)}]); +angular.module("cms.shared").directive("cmsFormFieldImageUpload",["_","$timeout","shared.internalModulePath","shared.internalContentPath","shared.LoadState","shared.stringUtilities","shared.imageFileUtilities","shared.imageService","shared.urlLibrary","shared.validationErrorService","baseFormFieldFactory",function(m,h,e,i,f,u,c,g,v,p,t){var R=t.defaultConfig,i=i+"img/AssetReplacement/",U=i+"preview-not-supported.png",w=i+"image-replacement.png",e={templateUrl:e+"UIComponents/ImageAssets/FormFieldImageUpload.html",scope:m.extend(t.defaultConfig.scope,{asset:"=cmsAsset",loadState:"=cmsLoadState",filter:"=cmsFilter",onChange:"&cmsOnChange"}),link:function(e,i,t,n){var a=e.vm;return a.isRequired=m.has(t,"required"),a.remove=o,a.fileChanged=d,a.isRemovable=m.isObject(a.ngModel)&&!a.isRequired,a.mainLoadState=new f(!0),e.$watch("vm.asset",s),g.getSettings().then(r).then(a.mainLoadState.off),R.link(e,i,t,n);function r(e){a.settings=e}function o(){d()}function s(){var e=a.asset;e?(a.previewUrl=v.getImageUrl(e,{width:450}),a.isRemovable=!a.isRequired,a.model={name:e.fileName+"."+e.extension,size:e.fileSizeInBytes,isCurrentFile:!0}):(a.previewUrl=w,a.isRemovable=!1,a.model&&(a.model=void 0)),l()}function d(e){function i(){a.model=void 0,a.previewUrl=w,a.isRemovable=!1,a.isResized=!1,t()}function t(){l(),a.onChange&&a.onChange()}e&&e[0]?(a.mainLoadState.on(),c.getFileInfoAndResizeIfRequired(e[0],a.settings).then(function(e){e||i();a.model=e.file,function(e){var i=a.filter;function t(e,i){return!(1',replace:!0,transclude:!0}}); @@ -552,6 +552,8 @@ angular.module("cms.shared").factory("shared.LoadState",["$q","$rootScope","_",f angular.module("cms.shared").directive("cmsProgressBar",["shared.internalModulePath",function(e){return{restrict:"E",scope:{loadState:"="},templateUrl:e+"UIComponents/Loader/ProgressBar.html"}}]); angular.module("cms.shared").directive("cmsFormFieldLocaleSelector",["_","shared.internalModulePath","shared.localeService","shared.directiveUtilities",function(l,e,n,t){return{restrict:"E",templateUrl:e+"UIComponents/Locales/FormFieldLocaleSelector.html",scope:{model:"=cmsModel",onLoaded:"&cmsOnLoaded",readonly:"=cmsReadonly"},link:{pre:function(e,o,l){e=e.vm;angular.isDefined(l.required)?e.isRequired=!0:(e.isRequired=!1,e.defaultItemText=l.cmsDefaultItemText||"None");t.setModelName(e,l)}},controller:function(){var o=this;n.getAll().then(function(e){o.locales=l.map(e,function(e){return{name:e.name+" ("+e.ietfLanguageTag+")",id:e.localeId}}),o.onLoaded&&o.onLoaded()})},controllerAs:"vm",bindToController:!0}}]); angular.module("cms.shared").directive("cmsMenu",["shared.internalModulePath",function(e){return{restrict:"E",replace:!0,transclude:!0,templateUrl:e+"UIComponents/Menus/Menu.html",scope:{icon:"@cmsIcon"}}}]); +angular.module("cms.shared").factory("shared.ImagePreviewFieldCollection",["$q","_","shared.arrayUtilities","shared.imageService",function(s,m,d,c){return function(n){var i,r,a=this,t="previewImage";function u(e){if(n){e=e[n],e=r[e].fields[t];return e?e.lowerName:void 0}return i=!i&&r.fields[t]?r.fields[t].lowerName:i}function o(e,n,i){var r=u(e);if(r){r=f(e,r);if(!i){var t,i=a.images[n];if(r==(t=i?i.imageAssetId:t))return;if(!r)return void(a.images[n]=void 0)}return c.getById(r).then(function(e){a.images[n]=e})}}function f(e,n){if(n)return(e.model||e)[n]}a.load=function(e,n){if(r=n,!e||!e.length||!n)return i();n=m.chain(e).map(function(e){return f(e,u(e))}).filter(function(e){return!!e}).uniq().value();return n.length?c.getByIdRange(n).then(function(i){a.images=[],m.each(e,function(e){var n,e=f(e,u(e));e&&(n=m.find(i,{imageAssetId:e})),a.images.push(n)})}):i();function i(){var e=s.defer();return e.resolve(),a.images=[],e.promise}},a.move=function(e,n){d.move(a.images,e,n)},a.add=function(e,n){return o(e,n,!0)},a.update=function(e,n){return o(e,n)},a.remove=function(e){d.remove(a.images,e)}}}]); +angular.module("cms.shared").factory("shared.ModelPreviewFieldset",["$q","_","shared.stringUtilities","shared.imageService",function(e,o,d,i){return function(e){var i=this,n="previewTitle",s="previewDescription",a="previewImage";i.modelMetaData=e,i.fields=function(r){var t={};return e(n),e(s),e(a),t;function e(i){var e=o.find(r.dataModelProperties,function(e){return e.additionalAttributes[i]});e&&(e.lowerName=d.lowerCaseFirstWord(e.name),t[i]=e,t.hasFields=!0)}}(e),i.showTitle=function(e){return e[n]||!e.hasFields}(i.fields),i.titleTerm=function(e){return e[n]?e[n].displayName:"Title"}(i.fields),i.on=function(){i.isLoading=!0,100===i.progress&&(i.progress=0)}}}]); angular.module("cms.shared").controller("AlertController",["$scope","options","close",function(o,e,l){angular.extend(o,e),o.close=l}]); angular.module("cms.shared").controller("ConfirmDialogController",["$scope","options","close",function(o,n,l){function e(){n.autoClose&&l()}angular.extend(o,n),o.close=function(o){o=o?n.ok:n.cancel;o&&o().then(e).finally(n.onCancel)}}]); angular.module("cms.shared").controller("DeveloperExceptionController",["$scope","$sce","shared.internalContentPath","options","close",function(e,t,o,r,s){var n=r.response.data,l=document.createElement("iframe");l.setAttribute("srcdoc",n),l.setAttribute("src",o+"developer-exception-not-supported.html"),l.setAttribute("sandbox","allow-scripts"),e.messageHtml=t.trustAsHtml(l.outerHTML),angular.extend(e,r),e.close=s}]); @@ -560,8 +562,6 @@ angular.module("cms.shared").directive("cmsModalDialogBody",["shared.internalMod angular.module("cms.shared").directive("cmsModalDialogContainer",["shared.internalModulePath","$timeout",function(o,i){return{restrict:"E",templateUrl:o+"UIComponents/Modals/ModalDialogContainer.html",transclude:!0,link:function(o,l,a){var e="large"===a.cmsModalSize?"modal-lg":"";e+=o.isRootModal?" is-root-modal":" is-child-modal","large"===a.cmsModalSize&&(o.sizeCls=e);i(function(){o.sizeCls=e+" modal--show"},1)},controller:angular.noop}}]); angular.module("cms.shared").directive("cmsModalDialogHeader",["shared.internalModulePath",function(e){return{restrict:"E",templateUrl:e+"UIComponents/Modals/ModalDialogHeader.html",transclude:!0}}]); angular.module("cms.shared").factory("shared.modalDialogService",["$q","_","ModalService","shared.internalModulePath","shared.LoadState",function(i,s,u,c,e){var o={alert:function(o){var e=i.defer(),n=o||{};return s.isString(o)&&(n={message:o}),u.showModal({templateUrl:c+"UIComponents/Modals/Alert.html",controller:"AlertController",inputs:{options:n}}).then(function(o){o.close.then(e.resolve)}),e.promise},show:function(o){return u.showModal({templateUrl:o.templateUrl,controller:o.controller,inputs:{options:o.options}})}};return o.confirm=function(o){var t=i.defer(),r=new e,l=function(o){var e=o||{},n={ok:a.bind(null,!0),cancel:a.bind(null,!1),onOkLoadState:r};s.isString(o)&&(e={message:o});return s.defaults(n,e,{okButtonTitle:"OK",cancelButtonTitle:"Cancel",autoClose:!0})}(o);return u.showModal({templateUrl:c+"UIComponents/Modals/ConfirmDialog.html",controller:"ConfirmDialogController",inputs:{options:l}}),t.promise;function a(o){var e,n=o?l.onOk:l.onOk.onCancel,o=o?t.resolve:t.reject;return s.isFunction(n)&&(r.on(),e=n()),i.when(e).then(o)}},o}]); -angular.module("cms.shared").factory("shared.ImagePreviewFieldCollection",["$q","_","shared.arrayUtilities","shared.imageService",function(s,m,d,c){return function(n){var i,r,a=this,t="previewImage";function u(e){if(n){e=e[n],e=r[e].fields[t];return e?e.lowerName:void 0}return i=!i&&r.fields[t]?r.fields[t].lowerName:i}function o(e,n,i){var r=u(e);if(r){r=f(e,r);if(!i){var t,i=a.images[n];if(r==(t=i?i.imageAssetId:t))return;if(!r)return void(a.images[n]=void 0)}return c.getById(r).then(function(e){a.images[n]=e})}}function f(e,n){if(n)return(e.model||e)[n]}a.load=function(e,n){if(r=n,!e||!e.length||!n)return i();n=m.chain(e).map(function(e){return f(e,u(e))}).filter(function(e){return!!e}).uniq().value();return n.length?c.getByIdRange(n).then(function(i){a.images=[],m.each(e,function(e){var n,e=f(e,u(e));e&&(n=m.find(i,{imageAssetId:e})),a.images.push(n)})}):i();function i(){var e=s.defer();return e.resolve(),a.images=[],e.promise}},a.move=function(e,n){d.move(a.images,e,n)},a.add=function(e,n){return o(e,n,!0)},a.update=function(e,n){return o(e,n)},a.remove=function(e){d.remove(a.images,e)}}}]); -angular.module("cms.shared").factory("shared.ModelPreviewFieldset",["$q","_","shared.stringUtilities","shared.imageService",function(e,o,d,i){return function(e){var i=this,n="previewTitle",s="previewDescription",a="previewImage";i.modelMetaData=e,i.fields=function(r){var t={};return e(n),e(s),e(a),t;function e(i){var e=o.find(r.dataModelProperties,function(e){return e.additionalAttributes[i]});e&&(e.lowerName=d.lowerCaseFirstWord(e.name),t[i]=e,t.hasFields=!0)}}(e),i.showTitle=function(e){return e[n]||!e.hasFields}(i.fields),i.titleTerm=function(e){return e[n]?e[n].displayName:"Title"}(i.fields),i.on=function(){i.isLoading=!0,100===i.progress&&(i.progress=0)}}}]); angular.module("cms.shared").controller("EditNestedDataModelDialogController",["$scope","shared.nestedDataModelSchemaService","shared.LoadState","options","close",function(e,a,o,t,l){var d=e;function n(){d.saveLoadState.on(),a.validate(d.formDataSource.modelMetaData.typeName,d.formDataSource.model).then(function(){t.onSave&&t.onSave(d.formDataSource.model);l()}).finally(d.saveLoadState.off)}function r(){l()}angular.extend(e,t),d.save=n,d.onCancel=r,d.close=r,d.isNew=!t.model,d.title=d.isNew?"Add Item":"Edit Item",d.saveLoadState=new o,function(e){var a=e.model||{},e=e.modelMetaData;d.isNew&&e.defaultValue&&e.defaultValue.value&&(a=angular.copy(e.defaultValue.value)),d.formDataSource={model:a,modelMetaData:e}}(t)}]); angular.module("cms.shared").directive("cmsFormFieldNestedDataModelCollection",["_","shared.internalModulePath","shared.LoadState","shared.nestedDataModelSchemaService","shared.modalDialogService","shared.arrayUtilities","shared.ModelPreviewFieldset","shared.ImagePreviewFieldCollection","baseFormFieldFactory",function(p,h,e,f,D,M,I,F,t){var w=t.defaultConfig,o={templateUrl:h+"UIComponents/NestedDataModels/FormFieldNestedDataModelCollection.html",scope:p.extend(w.scope,{minItems:"@cmsMinItems",maxItems:"@cmsMaxItems",modelType:"@cmsModelType",orderable:"=cmsOrderable"}),passThroughAttributes:["required"],link:function(e,t,o,a){var d,l=e.vm,i=p.last(a);return function(){l.add=s,l.edit=m,l.remove=n,l.onDrop=u,l.onDropSuccess=g,l.getTitle=v,f.getByName(l.modelType).then(function(e){l.modelMetaData=e,l.previewFields=new I(e),l.gridImages=new F,l.gridImages.load(l.model,l.previewFields)})}(),w.link(e,t,o,a);function r(){l.model=l.model.slice(0)}function n(e,t){M.removeObject(l.model,e),l.gridImages.remove(t)}function m(e,t){c({model:e,onSave:function(){l.gridImages.update(e,t),r()}})}function s(){c({onSave:function(e,t){l.model=l.model||[],l.model.push(e),l.gridImages.add(e,l.model.length-1),r()}})}function c(e){e.modelMetaData=l.modelMetaData,i&&(e.additionalParameters=i.additionalParameters),D.show({templateUrl:h+"UIComponents/NestedDataModels/EditNestedDataModelDialog.html",controller:"EditNestedDataModelDialogController",options:e})}function u(e,t){d=e}function g(e){M.move(l.model,e,d),l.gridImages.move(e,d)}function v(e,t){var o=l.previewFields.fields.previewTitle;return o?e[o.lowerName]:e.title||"Item "+(t+1)}}};return t.create(o)}]); angular.module("cms.shared").directive("cmsFormFieldNestedDataModelMultiTypeCollection",["_","shared.internalModulePath","shared.LoadState","shared.nestedDataModelSchemaService","shared.modalDialogService","shared.arrayUtilities","shared.ModelPreviewFieldset","shared.ImagePreviewFieldCollection","baseFormFieldFactory",function(C,D,e,w,M,I,F,N,o){var H=o.defaultConfig,t={templateUrl:D+"UIComponents/NestedDataModels/FormFieldNestedDataModelMultiTypeCollection.html",scope:C.extend(H.scope,{minItems:"@cmsMinItems",maxItems:"@cmsMaxItems",modelTypes:"@cmsModelTypes",orderable:"=cmsOrderable",titleColumnHeader:"@cmsTitleColumnHeader",descriptionColumnHeader:"@cmsDescriptionColumnHeader",imageColumnHeader:"@cmsImageColumnHeader",typeColumnHeader:"@cmsTypeColumnHeader"}),passThroughAttributes:["required"],link:function(e,o,t,a){var d,i=e.vm,l=C.last(a),r="previewTitle",m="previewDescription",n="previewImage";return function(){var e=i.modelTypes?i.modelTypes.split(","):[];i.add=v,i.edit=u,i.remove=c,i.onDrop=h,i.onDropSuccess=y,i.getTitle=f,w.getByNames(e).then(p),void 0===i.titleColumnHeader&&(i.titleColumnHeader="Title"),void 0===i.descriptionColumnHeader&&(i.descriptionColumnHeader="Description"),void(void 0===i.typeColumnHeader&&(i.typeColumnHeader="Type"))}(),H.link(e,o,t,a);function s(){i.model=i.model.slice(0)}function p(e){var t=!1,a=!1;i.modelMetaDataLookup={},i.previewFields={},C.chain(e).sortBy("displayName").each(function(e){i.modelMetaDataLookup[e.typeName]=e;var o=new F(e);o.fields[r]&&(t=!0),o.fields[m]&&(i.previewFields.showDescription=!0,a=!0),o.fields[n]&&(i.previewFields.showImage=!0,a=!0),i.previewFields[e.typeName]=o}),!t&&a||(i.previewFields.showTitle=!0),i.gridImages=new N("typeName"),i.gridImages.load(i.model,i.previewFields)}function c(e,o){I.removeObject(i.model,e),i.gridImages.remove(o)}function u(e,o){g({model:e.model,onSave:function(){i.gridImages.update(e,o),s()},modelMetaData:i.modelMetaDataLookup[e.typeName]})}function v(o){g({onSave:function(e){i.model=i.model||[];e={model:e,typeName:o.typeName};i.model.push(e),i.gridImages.add(e,i.model.length-1,!0),s()},modelMetaData:o})}function g(e){l&&(e.additionalParameters=l.additionalParameters),M.show({templateUrl:D+"UIComponents/NestedDataModels/EditNestedDataModelDialog.html",controller:"EditNestedDataModelDialogController",options:e})}function h(e,o){d=e}function y(e){I.move(i.model,e,d),i.gridImages.move(e,d)}function f(e,o){var t=i.previewFields[e.typeName].fields[r];return t?e.model[t.lowerName]:e.model.title||"Item "+(o+1)}}};return o.create(t)}]); diff --git a/src/Cofoundry.Web.Admin/Admin/Modules/Shared/Js/Framework/HttpProviderConfig.js b/src/Cofoundry.Web.Admin/Admin/Modules/Shared/Js/Framework/HttpProviderConfig.js index a4ea694d6..a238f0aca 100644 --- a/src/Cofoundry.Web.Admin/Admin/Modules/Shared/Js/Framework/HttpProviderConfig.js +++ b/src/Cofoundry.Web.Admin/Admin/Modules/Shared/Js/Framework/HttpProviderConfig.js @@ -27,5 +27,6 @@ function ( }); headers.common['X-Requested-With'] = 'XMLHttpRequest'; - + headers.common['Accept'] = 'application/json, text/html, text/plain, */*' + }]); \ No newline at end of file diff --git a/src/Cofoundry.Web/App_Start/StartupTasks/ConfigurationTasks/ErrorHandlingMiddlewareConfigurationTask.cs b/src/Cofoundry.Web/App_Start/StartupTasks/ConfigurationTasks/ErrorHandlingMiddlewareConfigurationTask.cs index 987ec8de0..1e72650ad 100644 --- a/src/Cofoundry.Web/App_Start/StartupTasks/ConfigurationTasks/ErrorHandlingMiddlewareConfigurationTask.cs +++ b/src/Cofoundry.Web/App_Start/StartupTasks/ConfigurationTasks/ErrorHandlingMiddlewareConfigurationTask.cs @@ -29,7 +29,6 @@ public void Configure(IApplicationBuilder app) if (_debugSettings.CanShowDeveloperExceptionPage(env)) { app.UseDeveloperExceptionPage(); - app.UseDatabaseErrorPage(); } else {