mirror of
https://github.com/ZwareBear/awx.git
synced 2026-04-01 13:41:48 -05:00
1346 lines
50 KiB
JavaScript
1346 lines
50 KiB
JavaScript
/*************************************************
|
|
* Copyright (c) 2015 Ansible, Inc.
|
|
*
|
|
* All Rights Reserved
|
|
*************************************************/
|
|
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name shared.function:directives
|
|
* @description
|
|
* Custom directives for form validation
|
|
*
|
|
*/
|
|
|
|
export default
|
|
angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
|
|
|
|
// awpassmatch: Add to password_confirm field. Will test if value
|
|
// matches that of 'input[name="password"]'
|
|
.directive('awpassmatch', function() {
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, elm, attrs, ctrl) {
|
|
ctrl.$parsers.unshift(function(viewValue) {
|
|
var associated = attrs.awpassmatch,
|
|
password = $('input[name="' + associated + '"]').val();
|
|
if (viewValue === password) {
|
|
// it is valid
|
|
ctrl.$setValidity('awpassmatch', true);
|
|
return viewValue;
|
|
}
|
|
// Invalid, return undefined (no model update)
|
|
ctrl.$setValidity('awpassmatch', false);
|
|
return viewValue;
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
// caplitalize Add to any input field where the first letter of each
|
|
// word should be capitalized. Use in place of css test-transform.
|
|
// For some reason "text-transform: capitalize" in breadcrumbs
|
|
// causes a break at each blank space. And of course,
|
|
// "autocapitalize='word'" only works in iOS. Use this as a fix.
|
|
.directive('capitalize', function() {
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, elm, attrs, ctrl) {
|
|
ctrl.$parsers.unshift(function(viewValue) {
|
|
var values = viewValue.split(" "),
|
|
result = "",
|
|
i;
|
|
for (i = 0; i < values.length; i++) {
|
|
result += values[i].charAt(0).toUpperCase() + values[i].substr(1) + ' ';
|
|
}
|
|
result = result.trim();
|
|
if (result !== viewValue) {
|
|
ctrl.$setViewValue(result);
|
|
ctrl.$render();
|
|
}
|
|
return result;
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
// chkPass
|
|
//
|
|
// Enables use of js/shared/pwdmeter.js to check strengh of passwords.
|
|
// See controllers/Users.js for example.
|
|
//
|
|
.directive('chkPass', [function() {
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, elm, attrs, ctrl) {
|
|
$(elm).keyup(function() {
|
|
var validity = true;
|
|
if (elm.val()) {
|
|
if ($AnsibleConfig.password_length) {
|
|
validity = (ctrl.$modelValue.length >= $AnsibleConfig.password_length);
|
|
ctrl.$setValidity('password_length', validity);
|
|
}
|
|
if ($AnsibleConfig.password_hasLowercase) {
|
|
validity = (/[a-z]/.test(ctrl.$modelValue));
|
|
ctrl.$setValidity('hasLowercase', validity);
|
|
}
|
|
if ($AnsibleConfig.password_hasUppercase) {
|
|
validity = (/[A-Z]/.test(ctrl.$modelValue));
|
|
ctrl.$setValidity('hasUppercase', validity);
|
|
}
|
|
if ($AnsibleConfig.password_hasNumber) {
|
|
validity = (/[0-9]/.test(ctrl.$modelValue));
|
|
ctrl.$setValidity('hasNumber', validity);
|
|
}
|
|
if ($AnsibleConfig.password_hasSymbol) {
|
|
validity = (/[\\#@$-/:-?{-~!"^_`\[\]]/.test(ctrl.$modelValue));
|
|
ctrl.$setValidity('hasSymbol', validity);
|
|
}
|
|
} else {
|
|
validity = true;
|
|
if ($AnsibleConfig.password_length) {
|
|
ctrl.$setValidity('password_length', validity);
|
|
}
|
|
if ($AnsibleConfig.password_hasLowercase) {
|
|
ctrl.$setValidity('hasLowercase', validity);
|
|
}
|
|
if ($AnsibleConfig.password_hasUppercase) {
|
|
ctrl.$setValidity('hasUppercase', validity);
|
|
}
|
|
if ($AnsibleConfig.password_hasNumber) {
|
|
ctrl.$setValidity('hasNumber', validity);
|
|
}
|
|
if ($AnsibleConfig.password_hasSymbol) {
|
|
ctrl.$setValidity('hasSymbol', validity);
|
|
}
|
|
}
|
|
if (!scope.$$phase) {
|
|
scope.$digest();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}])
|
|
|
|
// imageUpload
|
|
//
|
|
// Accepts image and returns base64 information with basic validation
|
|
// Can eventually expand to handle all uploads with different endpoints and handlers
|
|
//
|
|
.directive('imageUpload', ['ConfigurationUtils', 'i18n', '$rootScope',
|
|
function(ConfigurationUtils, i18n, $rootScope) {
|
|
var browseText = i18n._('BROWSE'),
|
|
placeholderText = i18n._('Choose file'),
|
|
uploadedText = i18n._('Current Image: '),
|
|
removeText = i18n._('REMOVE');
|
|
|
|
return {
|
|
restrict: 'E',
|
|
scope: {
|
|
key: '@'
|
|
},
|
|
template: `
|
|
<div class="input-group">
|
|
<label class="input-group-addon Form-filePicker--pickerButton" id="filePickerButton" for="filePicker" ng-click="update($event)">${browseText}</label>
|
|
<input type="text" class="form-control Form-filePicker--textBox" id="filePickerText" placeholder="${placeholderText}" readonly>
|
|
<input type="file" name="file" class="Form-filePicker" id="filePicker" onchange="angular.element(this).scope().fileChange(this.files)"/>
|
|
</div>
|
|
|
|
<div ng-if="imagePresent" class="Form-filePicker--selectedFile">
|
|
${uploadedText}
|
|
<img data-ng-src="{{imageData}}" alt="Current logo" class="Form-filePicker--thumbnail">
|
|
</div>
|
|
|
|
<div class="error" id="filePickerError"></div>`,
|
|
|
|
link: function(scope) {
|
|
var fieldKey = scope.key;
|
|
var filePickerText = angular.element(document.getElementById('filePickerText'));
|
|
var filePickerError = angular.element(document.getElementById('filePickerError'));
|
|
var filePickerButton = angular.element(document.getElementById('filePickerButton'));
|
|
|
|
scope.imagePresent = global.$AnsibleConfig.custom_logo || false;
|
|
scope.imageData = $rootScope.custom_logo;
|
|
|
|
scope.$on('loginUpdated', function() {
|
|
scope.imagePresent = global.$AnsibleConfig.custom_logo;
|
|
scope.imageData = $rootScope.custom_logo;
|
|
});
|
|
|
|
scope.update = function(e) {
|
|
if(scope.$parent[fieldKey]) {
|
|
e.preventDefault();
|
|
scope.$parent[fieldKey] = '';
|
|
filePickerButton.html(browseText);
|
|
filePickerText.val('');
|
|
}
|
|
else {
|
|
// Nothing exists so open file picker
|
|
}
|
|
};
|
|
|
|
scope.fileChange = function(file) {
|
|
filePickerError.html('');
|
|
|
|
ConfigurationUtils.imageProcess(file[0])
|
|
.then(function(result) {
|
|
scope.$parent[fieldKey] = result;
|
|
filePickerText.val(file[0].name);
|
|
filePickerButton.html(removeText);
|
|
}).catch(function(error) {
|
|
filePickerText.html(file[0].name);
|
|
filePickerError.text(error);
|
|
}).finally(function() {
|
|
|
|
});
|
|
};
|
|
|
|
}
|
|
};
|
|
}])
|
|
|
|
|
|
.directive('surveyCheckboxes', function() {
|
|
return {
|
|
restrict: 'E',
|
|
require: 'ngModel',
|
|
scope: { ngModel: '=ngModel' },
|
|
template: '<div class="survey_taker_input" ng-repeat="option in ngModel.options">' +
|
|
'<label style="font-weight:normal"><input type="checkbox" ng-model="cbModel[option.value]" ' +
|
|
'value="{{option.value}}" class="mc" ng-change="update(this.value)" />' +
|
|
'<span>' +
|
|
'{{option.value}}' +
|
|
'</span></label>' +
|
|
'</div>',
|
|
link: function(scope, element, attrs, ctrl) {
|
|
scope.cbModel = {};
|
|
ctrl.$setValidity('reqCheck', true);
|
|
angular.forEach(scope.ngModel.value, function(value) {
|
|
scope.cbModel[value] = true;
|
|
|
|
});
|
|
|
|
if (scope.ngModel.required === true && scope.ngModel.value.length === 0) {
|
|
ctrl.$setValidity('reqCheck', false);
|
|
}
|
|
|
|
ctrl.$parsers.unshift(function(viewValue) {
|
|
for (var c in scope.cbModel) {
|
|
if (scope.cbModel[c]) {
|
|
ctrl.$setValidity('checkbox', true);
|
|
}
|
|
}
|
|
ctrl.$setValidity('checkbox', false);
|
|
|
|
return viewValue;
|
|
});
|
|
|
|
scope.update = function() {
|
|
var val = [];
|
|
angular.forEach(scope.cbModel, function(v, k) {
|
|
if (v) {
|
|
val.push(k);
|
|
}
|
|
});
|
|
if (val.length > 0) {
|
|
scope.ngModel.value = val;
|
|
scope.$parent[scope.ngModel.name] = val;
|
|
ctrl.$setValidity('checkbox', true);
|
|
ctrl.$setValidity('reqCheck', true);
|
|
} else if (scope.ngModel.required === true) {
|
|
ctrl.$setValidity('checkbox', false);
|
|
}
|
|
};
|
|
}
|
|
};
|
|
})
|
|
|
|
|
|
.directive('awSurveyQuestion', function() {
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, elm, attrs, ctrl) {
|
|
ctrl.$parsers.unshift(function(viewValue) {
|
|
var values = viewValue.split(" "),
|
|
result = "",
|
|
i;
|
|
result += values[0].charAt(0).toUpperCase() + values[0].substr(1) + ' ';
|
|
for (i = 1; i < values.length; i++) {
|
|
result += values[i] + ' ';
|
|
}
|
|
result = result.trim();
|
|
if (result !== viewValue) {
|
|
ctrl.$setViewValue(result);
|
|
ctrl.$render();
|
|
}
|
|
return result;
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
.directive('awMin', ['Empty', function(Empty) {
|
|
return {
|
|
restrict: 'A',
|
|
require: 'ngModel',
|
|
link: function(scope, elem, attr, ctrl) {
|
|
ctrl.$parsers.unshift(function(viewValue) {
|
|
var min = (attr.awMin) ? scope.$eval(attr.awMin) : -Infinity;
|
|
if (!Empty(min) && !Empty(viewValue) && Number(viewValue) < min) {
|
|
ctrl.$setValidity('awMin', false);
|
|
return viewValue;
|
|
} else {
|
|
ctrl.$setValidity('awMin', true);
|
|
return viewValue;
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}])
|
|
|
|
.directive('awMax', ['Empty', function(Empty) {
|
|
return {
|
|
restrict: 'A',
|
|
require: 'ngModel',
|
|
link: function(scope, elem, attr, ctrl) {
|
|
ctrl.$parsers.unshift(function(viewValue) {
|
|
var max = (attr.awMax) ? scope.$eval(attr.awMax) : Infinity;
|
|
if (!Empty(max) && !Empty(viewValue) && Number(viewValue) > max) {
|
|
ctrl.$setValidity('awMax', false);
|
|
return viewValue;
|
|
} else {
|
|
ctrl.$setValidity('awMax', true);
|
|
return viewValue;
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}])
|
|
|
|
.directive('smartFloat', function() {
|
|
var FLOAT_REGEXP = /^\-?\d+((\.|\,)\d+)?$/;
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, elm, attrs, ctrl) {
|
|
ctrl.$parsers.unshift(function(viewValue) {
|
|
if (FLOAT_REGEXP.test(viewValue)) {
|
|
ctrl.$setValidity('float', true);
|
|
return parseFloat(viewValue.replace(',', '.'));
|
|
} else {
|
|
ctrl.$setValidity('float', false);
|
|
return undefined;
|
|
}
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
// integer Validate that input is of type integer. Taken from Angular developer
|
|
// guide, form examples. Add min and max directives, and this will check
|
|
// entered values is within the range.
|
|
//
|
|
// Use input type of 'text'. Use of 'number' casuses browser validation to
|
|
// override/interfere with this directive.
|
|
.directive('integer', function() {
|
|
return {
|
|
restrict: 'A',
|
|
require: 'ngModel',
|
|
link: function(scope, elm, attrs, ctrl) {
|
|
ctrl.$parsers.unshift(function(viewValue) {
|
|
ctrl.$setValidity('min', true);
|
|
ctrl.$setValidity('max', true);
|
|
if (/^\-?\d*$/.test(viewValue)) {
|
|
// it is valid
|
|
ctrl.$setValidity('integer', true);
|
|
if (viewValue === '-' || viewValue === '-0' || viewValue === null) {
|
|
ctrl.$setValidity('integer', false);
|
|
return viewValue;
|
|
}
|
|
if (elm.attr('min') &&
|
|
parseInt(viewValue, 10) < parseInt(elm.attr('min'), 10)) {
|
|
ctrl.$setValidity('min', false);
|
|
return viewValue;
|
|
}
|
|
if (elm.attr('max') && (parseInt(viewValue, 10) > parseInt(elm.attr('max'), 10))) {
|
|
ctrl.$setValidity('max', false);
|
|
return viewValue;
|
|
}
|
|
return viewValue;
|
|
}
|
|
// Invalid, return undefined (no model update)
|
|
ctrl.$setValidity('integer', false);
|
|
return viewValue;
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
//the awSurveyVariableName directive checks if the field contains any spaces.
|
|
// this could be elaborated in the future for other things we want to check this field against
|
|
.directive('awSurveyVariableName', function() {
|
|
var FLOAT_REGEXP = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
|
|
return {
|
|
restrict: 'A',
|
|
require: 'ngModel',
|
|
link: function(scope, elm, attrs, ctrl) {
|
|
ctrl.$setValidity('required', true); // we only want the error message for incorrect characters to be displayed
|
|
ctrl.$parsers.unshift(function(viewValue) {
|
|
if (viewValue.length !== 0) {
|
|
if (FLOAT_REGEXP.test(viewValue) && viewValue.indexOf(' ') === -1) { //check for a spaces
|
|
ctrl.$setValidity('variable', true);
|
|
return viewValue;
|
|
} else {
|
|
ctrl.$setValidity('variable', false); // spaces found, therefore throw error.
|
|
return viewValue;
|
|
}
|
|
} else {
|
|
ctrl.$setValidity('variable', true); // spaces found, therefore throw error.
|
|
return viewValue;
|
|
}
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
//
|
|
// awRequiredWhen: { reqExpression: "<expression to watch for true|false>", init: "true|false" }
|
|
//
|
|
// Make a field required conditionally using an expression. If the expression evaluates to true, the
|
|
// field will be required. Otherwise, the required attribute will be removed.
|
|
//
|
|
.directive('awRequiredWhen', function() {
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, elm, attrs, ctrl) {
|
|
|
|
function updateRequired() {
|
|
var isRequired = scope.$eval(attrs.awRequiredWhen);
|
|
|
|
var viewValue = elm.val(),
|
|
label, validity = true;
|
|
label = $(elm).closest('.form-group').find('label').first();
|
|
|
|
if (isRequired && (elm.attr('required') === null || elm.attr('required') === undefined)) {
|
|
$(elm).attr('required', 'required');
|
|
$(label).removeClass('prepend-asterisk').addClass('prepend-asterisk');
|
|
} else if (!isRequired) {
|
|
elm.removeAttr('required');
|
|
if (!attrs.awrequiredAlwaysShowAsterisk) {
|
|
$(label).removeClass('prepend-asterisk');
|
|
}
|
|
}
|
|
if (isRequired && (viewValue === undefined || viewValue === null || viewValue === '')) {
|
|
validity = false;
|
|
}
|
|
ctrl.$setValidity('required', validity);
|
|
}
|
|
|
|
scope.$watchGroup([attrs.awRequiredWhen, $(elm).attr('name')], function() {
|
|
// watch for the aw-required-when expression to change value
|
|
updateRequired();
|
|
});
|
|
|
|
if (attrs.awrequiredInit !== undefined && attrs.awrequiredInit !== null) {
|
|
// We already set a watcher on the attribute above so no need to call updateRequired() in here
|
|
scope[attrs.awRequiredWhen] = attrs.awrequiredInit;
|
|
}
|
|
|
|
}
|
|
};
|
|
})
|
|
|
|
// awPlaceholder: Dynamic placeholder set to a scope variable you want watched.
|
|
// Value will be place in field placeholder attribute.
|
|
.directive('awPlaceholder', [function() {
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, elm, attrs) {
|
|
$(elm).attr('placeholder', scope[attrs.awPlaceholder]);
|
|
scope.$watch(attrs.awPlaceholder, function(newVal) {
|
|
$(elm).attr('placeholder', newVal);
|
|
});
|
|
}
|
|
};
|
|
}])
|
|
|
|
// lookup Validate lookup value against API
|
|
.directive('awlookup', ['Rest', 'GetBasePath', '$q', function(Rest, GetBasePath, $q) {
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, elm, attrs, fieldCtrl) {
|
|
let query,
|
|
basePath,
|
|
defer = $q.defer(),
|
|
autopopulateLookup,
|
|
modelKey = attrs.ngModel,
|
|
modelName = attrs.source,
|
|
watcher = attrs.awRequiredWhen || undefined,
|
|
watchBasePath;
|
|
|
|
if (attrs.autopopulatelookup !== undefined) {
|
|
autopopulateLookup = JSON.parse(attrs.autopopulatelookup);
|
|
} else {
|
|
autopopulateLookup = true;
|
|
}
|
|
|
|
|
|
// The following block of code is for instances where the
|
|
// lookup field is reused by varying sub-forms. Example: The groups
|
|
// form will change it's credential lookup based on the
|
|
// source type. The basepath the lookup should utilize is dynamic
|
|
// in this case. You'd configure the "watchBasePath" key on the
|
|
// field's configuration in the form configuration field.
|
|
if (attrs.watchbasepath !== undefined) {
|
|
watchBasePath = attrs.watchbasepath;
|
|
scope.$watch(watchBasePath, (newValue) => {
|
|
if(newValue !== undefined && fieldIsAutopopulatable()){
|
|
_doAutoPopulate();
|
|
}
|
|
});
|
|
}
|
|
|
|
function _doAutoPopulate() {
|
|
let query = '?role_level=use_role';
|
|
|
|
if (attrs.watchbasepath !== undefined && scope[attrs.watchbasepath] !== undefined) {
|
|
basePath = scope[attrs.watchbasepath];
|
|
query = '&role_level=use_role';
|
|
}
|
|
else {
|
|
basePath = GetBasePath(elm.attr('data-basePath')) || elm.attr('data-basePath');
|
|
switch(modelName) {
|
|
case 'credential':
|
|
query = '?kind=ssh&role_level=use_role';
|
|
break;
|
|
case 'network_credential':
|
|
query = '?kind=net&role_level=use_role';
|
|
break;
|
|
case 'organization':
|
|
query = '?role_level=admin_role';
|
|
break;
|
|
case 'inventory_script':
|
|
query = '?role_level=admin_role';
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
Rest.setUrl(`${basePath}` + query);
|
|
Rest.get()
|
|
.success(function (data) {
|
|
if (data.count === 1) {
|
|
scope[modelKey] = data.results[0].name;
|
|
scope[modelName] = data.results[0].id;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (fieldIsAutopopulatable()) {
|
|
_doAutoPopulate();
|
|
}
|
|
|
|
// This checks to see if the field meets the criteria to
|
|
// autopopulate:
|
|
// Population rules:
|
|
// - add form only
|
|
// - lookup is required
|
|
// - lookup is not promptable
|
|
// - user must only have access to 1 item the lookup is for
|
|
function fieldIsAutopopulatable() {
|
|
if (autopopulateLookup === false) {
|
|
return false;
|
|
}
|
|
if (scope.mode === "add") {
|
|
if(watcher){
|
|
scope.$watch(watcher, () => {
|
|
if(Boolean(scope.$eval(watcher)) === true){
|
|
|
|
// if we get here then the field is required
|
|
// by way of awRequiredWhen
|
|
// and is a candidate for autopopulation
|
|
|
|
_doAutoPopulate();
|
|
}
|
|
});
|
|
}
|
|
else if (attrs.required === true) {
|
|
return true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// query the API to see if field value corresponds to a valid resource
|
|
// .ng-pending will be applied to the directive element while the request is outstanding
|
|
// form.$pending will contain object reference to any ngModelControllers with outstanding requests
|
|
fieldCtrl.$asyncValidators.validResource = function(modelValue, viewValue) {
|
|
|
|
applyValidationStrategy(viewValue, fieldCtrl);
|
|
|
|
return defer.promise;
|
|
};
|
|
|
|
function applyValidationStrategy(viewValue, ctrl) {
|
|
|
|
// use supplied data attributes to build an endpoint, query, resolve outstanding promise
|
|
function applyValidation(viewValue) {
|
|
basePath = GetBasePath(elm.attr('data-basePath')) || elm.attr('data-basePath');
|
|
query = elm.attr('data-query');
|
|
query = query.replace(/\:value/, encodeURIComponent(viewValue));
|
|
|
|
let base = ctrl.$name.split('_name')[0];
|
|
if (attrs.watchbasepath !== undefined && scope[attrs.watchbasepath] !== undefined) {
|
|
basePath = scope[attrs.watchbasepath];
|
|
query += '&role_level=use_role';
|
|
query = query.replace('?', '&');
|
|
}
|
|
else {
|
|
switch(base) {
|
|
case 'credential':
|
|
query += '&kind=ssh&role_level=use_role';
|
|
break;
|
|
case 'network_credential':
|
|
query += '&kind=net&role_level=use_role';
|
|
break;
|
|
case 'cloud_credential':
|
|
query += '&cloud=true&role_level=use_role';
|
|
break;
|
|
case 'organization':
|
|
query += '&role_level=admin_role';
|
|
break;
|
|
case 'inventory_script':
|
|
query += '&role_level=admin_role';
|
|
break;
|
|
default:
|
|
query += '&role_level=use_role';
|
|
}
|
|
}
|
|
|
|
Rest.setUrl(`${basePath}${query}`);
|
|
// https://github.com/ansible/ansible-tower/issues/3549
|
|
// capturing both success/failure conditions in .then() promise
|
|
// when #3549 is resolved, this will need to be partitioned into success/error or then/catch blocks
|
|
return Rest.get()
|
|
.then((res) => {
|
|
if (res.data.results.length > 0) {
|
|
scope[elm.attr('data-source')] = res.data.results[0].id;
|
|
return setValidity(ctrl, true);
|
|
} else {
|
|
scope[elm.attr('data-source')] = null;
|
|
return setValidity(ctrl, false);
|
|
}
|
|
});
|
|
}
|
|
|
|
function setValidity(ctrl, validity){
|
|
var isRequired;
|
|
if (attrs.required) {
|
|
isRequired = true;
|
|
} else {
|
|
isRequired = false;
|
|
}
|
|
if (attrs.awRequiredWhen) {
|
|
if (attrs.awRequiredWhen.charAt(0) === "!") {
|
|
isRequired = !scope[attrs.awRequiredWhen.slice(1, attrs.awRequiredWhen.length)];
|
|
} else {
|
|
isRequired = scope[attrs.awRequiredWhen];
|
|
}
|
|
}
|
|
if (!isRequired && (viewValue === undefined || viewValue === undefined || viewValue === "")) {
|
|
validity = true;
|
|
}
|
|
ctrl.$setValidity('awlookup', validity);
|
|
return defer.resolve(validity);
|
|
}
|
|
|
|
// Three common cases for clarity:
|
|
|
|
// 1) Field is not required & pristine. Pass validation & skip async $pending state
|
|
// 2) Field is required. Always validate & use async $pending state
|
|
// 3) Field is not required, but is not $pristine. Always validate & use async $pending state
|
|
|
|
// case 1
|
|
if (!ctrl.$validators.required && ctrl.$pristine) {
|
|
return setValidity(ctrl, true);
|
|
}
|
|
// case 2 & 3
|
|
else {
|
|
return applyValidation(viewValue);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}])
|
|
|
|
//
|
|
// awValidUrl
|
|
//
|
|
.directive('awValidUrl', [function() {
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, elm, attrs, ctrl) {
|
|
ctrl.$parsers.unshift(function(viewValue) {
|
|
var validity = true,
|
|
rgx, rgx2;
|
|
if (viewValue !== '') {
|
|
ctrl.$setValidity('required', true);
|
|
rgx = /^(https|http|ssh)\:\/\//;
|
|
rgx2 = /\@/g;
|
|
if (!rgx.test(viewValue) || rgx2.test(viewValue)) {
|
|
validity = false;
|
|
}
|
|
}
|
|
ctrl.$setValidity('awvalidurl', validity);
|
|
|
|
return viewValue;
|
|
});
|
|
}
|
|
};
|
|
}])
|
|
|
|
/*
|
|
* Enable TB tooltips. To add a tooltip to an element, include the following directive in
|
|
* the element's attributes:
|
|
*
|
|
* aw-tool-tip="<< tooltip text here >>"
|
|
*
|
|
* Include the standard TB data-XXX attributes to controll a tooltip's appearance. We will
|
|
* default placement to the left and delay to the config setting.
|
|
*/
|
|
.directive('awToolTip', [function() {
|
|
return {
|
|
link: function(scope, element, attrs) {
|
|
// if (attrs.class.indexOf("JobResultsStdOut") > -1) {
|
|
// debugger;
|
|
// }
|
|
var delay = { show: 200, hide: 0 },
|
|
placement,
|
|
stateChangeWatcher;
|
|
if (attrs.awTipPlacement) {
|
|
placement = attrs.awTipPlacement;
|
|
} else {
|
|
placement = (attrs.placement !== undefined && attrs.placement !== null) ? attrs.placement : 'left';
|
|
}
|
|
|
|
var template, custom_class;
|
|
if (attrs.tooltipInnerClass || attrs.tooltipinnerclass) {
|
|
custom_class = attrs.tooltipInnerClass || attrs.tooltipinnerclass;
|
|
template = '<div class="tooltip Tooltip" role="tooltip"><div class="tooltip-arrow Tooltip-arrow"></div><div class="tooltip-inner Tooltip-inner ' + custom_class + '"></div></div>';
|
|
} else {
|
|
template = '<div class="tooltip Tooltip" role="tooltip"><div class="tooltip-arrow Tooltip-arrow"></div><div class="tooltip-inner Tooltip-inner"></div></div>';
|
|
}
|
|
|
|
// This block helps clean up tooltips that may get orphaned by a click event
|
|
$(element).on('mouseenter', function() {
|
|
if (stateChangeWatcher) {
|
|
// Un-bind - we don't want a bunch of listeners firing
|
|
stateChangeWatcher();
|
|
}
|
|
stateChangeWatcher = scope.$on('$stateChangeStart', function() {
|
|
// Go ahead and force the tooltip setTimeout to expire (if it hasn't already fired)
|
|
$(element).tooltip('hide');
|
|
// Clean up any existing tooltips including this one
|
|
$('.tooltip').each(function() {
|
|
$(this).remove();
|
|
});
|
|
});
|
|
});
|
|
|
|
$(element).on('hidden.bs.tooltip', function() {
|
|
// TB3RC1 is leaving behind tooltip <div> elements. This will remove them
|
|
// after a tooltip fades away. If not, they lay overtop of other elements and
|
|
// honk up the page.
|
|
$('.tooltip').each(function() {
|
|
$(this).remove();
|
|
});
|
|
});
|
|
|
|
$(element).tooltip({
|
|
placement: placement,
|
|
delay: delay,
|
|
html: true,
|
|
title: attrs.awToolTip,
|
|
container: 'body',
|
|
trigger: 'hover',
|
|
template: template
|
|
});
|
|
|
|
if (attrs.tipWatch) {
|
|
// Add dataTipWatch: 'variable_name'
|
|
scope.$watch(attrs.tipWatch, function(newVal) {
|
|
// Where did fixTitle come from?:
|
|
// http://stackoverflow.com/questions/9501921/change-twitter-bootstrap-tooltip-content-on-click
|
|
$(element).tooltip('hide').attr('data-original-title', newVal).tooltip('fixTitle');
|
|
});
|
|
}
|
|
}
|
|
};
|
|
}])
|
|
|
|
/*
|
|
* Enable TB pop-overs. To add a pop-over to an element, include the following directive in
|
|
* the element's attributes:
|
|
*
|
|
* aw-pop-over="<< pop-over html here >>"
|
|
*
|
|
* Include the standard TB data-XXX attributes to controll the pop-over's appearance. We will
|
|
* default placement to the left, delay to 0 seconds, content type to HTML, and title to 'Help'.
|
|
*/
|
|
.directive('awPopOver', ['$compile', function($compile) {
|
|
return function(scope, element, attrs) {
|
|
var placement = (attrs.placement !== undefined && attrs.placement !== null) ? attrs.placement : 'left',
|
|
title = (attrs.overTitle) ? attrs.overTitle : (attrs.popoverTitle) ? attrs.popoverTitle : 'Help',
|
|
container = (attrs.container !== undefined) ? attrs.container : false,
|
|
trigger = (attrs.trigger !== undefined) ? attrs.trigger : 'manual',
|
|
template = '<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>',
|
|
id_to_close = "";
|
|
|
|
if (element[0].id) {
|
|
template = '<div id="' + element[0].id + '_popover_container" class="popover" role="tooltip"><div class="arrow"></div><h3 id="' + element[0].id + '_popover_title" class="popover-title"></h3><div id="' + element[0].id + '_popover_content" class="popover-content"></div></div>';
|
|
}
|
|
|
|
scope.triggerPopover = function(e) {
|
|
showPopover(e);
|
|
};
|
|
|
|
if (attrs.awPopOverWatch) {
|
|
$(element).popover({
|
|
placement: placement,
|
|
delay: 0,
|
|
title: title,
|
|
content: function() {
|
|
return scope[attrs.awPopOverWatch];
|
|
},
|
|
trigger: trigger,
|
|
html: true,
|
|
container: container,
|
|
template: template
|
|
});
|
|
} else {
|
|
$(element).popover({
|
|
placement: placement,
|
|
delay: 0,
|
|
title: title,
|
|
content: attrs.awPopOver,
|
|
trigger: trigger,
|
|
html: true,
|
|
container: container,
|
|
template: template
|
|
});
|
|
}
|
|
$(element).attr('tabindex', -1);
|
|
|
|
$(element).one('click', showPopover);
|
|
|
|
function bindPopoverDismiss() {
|
|
$('body').one('click.popover' + id_to_close, function(e) {
|
|
if ($(e.target).parents(id_to_close).length === 0) {
|
|
// case: you clicked to open the popover and then you
|
|
// clicked outside of it...hide it.
|
|
$(element).popover('hide');
|
|
} else {
|
|
// case: you clicked to open the popover and then you
|
|
// clicked inside the popover
|
|
bindPopoverDismiss();
|
|
}
|
|
});
|
|
}
|
|
|
|
$(element).on('shown.bs.popover', function() {
|
|
bindPopoverDismiss();
|
|
$(document).on('keydown.popover', dismissOnEsc);
|
|
});
|
|
|
|
$(element).on('hidden.bs.popover', function() {
|
|
$(element).off('click', dismissPopover);
|
|
$(element).off('click', showPopover);
|
|
$('body').off('click.popover.' + id_to_close);
|
|
$(element).one('click', showPopover);
|
|
$(document).off('keydown.popover', dismissOnEsc);
|
|
});
|
|
|
|
function showPopover(e) {
|
|
e.stopPropagation();
|
|
|
|
var self = $(element);
|
|
|
|
// remove tool-tip
|
|
try {
|
|
element.tooltip('hide');
|
|
} catch (ex) {
|
|
// ignore
|
|
}
|
|
|
|
// this is called on the help-link (over and over again)
|
|
$('.help-link, .help-link-white').each(function() {
|
|
if (self.attr('id') !== $(this).attr('id')) {
|
|
try {
|
|
// not sure what this does different than the method above
|
|
$(this).popover('hide');
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
}
|
|
});
|
|
|
|
$('.popover').each(function() {
|
|
// remove lingering popover <div>. Seems to be a bug in TB3 RC1
|
|
$(this).remove();
|
|
});
|
|
$('.tooltip').each(function() {
|
|
// close any lingering tool tips
|
|
$(this).hide();
|
|
});
|
|
|
|
// set id_to_close of the actual open element
|
|
id_to_close = "#" + $(element).attr('id') + "_popover_container";
|
|
|
|
// $(element).one('click', dismissPopover);
|
|
|
|
$(element).popover('toggle');
|
|
|
|
$('.popover').each(function() {
|
|
$compile($(this))(scope); //make nested directives work!
|
|
});
|
|
}
|
|
|
|
function dismissPopover(e) {
|
|
e.stopPropagation();
|
|
$(element).popover('hide');
|
|
}
|
|
|
|
function dismissOnEsc(e) {
|
|
if (e.keyCode === 27) {
|
|
$(element).popover('hide');
|
|
$('.popover').each(function() {
|
|
// remove lingering popover <div>. Seems to be a bug in TB3 RC1
|
|
// $(this).remove();
|
|
});
|
|
}
|
|
}
|
|
|
|
};
|
|
}])
|
|
|
|
//
|
|
// Enable jqueryui slider widget on a numeric input field
|
|
//
|
|
// <input type="number" aw-slider name="myfield" min="0" max="100" />
|
|
//
|
|
.directive('awSlider', [function() {
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, elm, attrs, ctrl) {
|
|
var name = elm.attr('name');
|
|
$('#' + name + '-slider').slider({
|
|
value: 0,
|
|
step: 1,
|
|
min: elm.attr('min'),
|
|
max: elm.attr('max'),
|
|
disabled: (elm.attr('readonly')) ? true : false,
|
|
slide: function(e, u) {
|
|
ctrl.$setViewValue(u.value);
|
|
ctrl.$setValidity('required', true);
|
|
ctrl.$setValidity('min', true);
|
|
ctrl.$setValidity('max', true);
|
|
ctrl.$dirty = true;
|
|
ctrl.$render();
|
|
if (!scope.$$phase) {
|
|
scope.$digest();
|
|
}
|
|
}
|
|
});
|
|
|
|
$('#' + name + '-number').change(function() {
|
|
$('#' + name + '-slider').slider('value', parseInt($(this).val(), 10));
|
|
});
|
|
|
|
}
|
|
};
|
|
}])
|
|
|
|
//
|
|
// Enable jqueryui spinner widget on a numeric input field
|
|
//
|
|
// <input type="number" aw-spinner name="myfield" min="0" max="100" />
|
|
//
|
|
.directive('awSpinner', [function() {
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, elm, attrs, ctrl) {
|
|
var disabled, opts;
|
|
disabled = elm.attr('data-disabled');
|
|
opts = {
|
|
value: 0,
|
|
step: 1,
|
|
min: elm.attr('min'),
|
|
max: elm.attr('max'),
|
|
numberFormat: "d",
|
|
disabled: (elm.attr('readonly')) ? true : false,
|
|
icons: {
|
|
down: "Form-numberInputButton fa fa-angle-down",
|
|
up: "Form-numberInputButton fa fa-angle-up"
|
|
},
|
|
spin: function(e, u) {
|
|
ctrl.$setViewValue(u.value);
|
|
ctrl.$setValidity('required', true);
|
|
ctrl.$setValidity('min', true);
|
|
ctrl.$setValidity('max', true);
|
|
ctrl.$dirty = true;
|
|
ctrl.$render();
|
|
if (scope.job_template_form) {
|
|
// need a way to find the parent form and mark it dirty
|
|
scope.job_template_form.$dirty = true;
|
|
}
|
|
if (!scope.$$phase) {
|
|
scope.$digest();
|
|
}
|
|
}
|
|
};
|
|
|
|
// hack to get ngDisabled to work
|
|
if (attrs.ngDisabled) {
|
|
scope.$watch(attrs.ngDisabled, function(val) {
|
|
opts.disabled = (val === true) ? true : false;
|
|
$(elm).spinner(opts);
|
|
});
|
|
}
|
|
|
|
if (disabled) {
|
|
opts.disabled = true;
|
|
}
|
|
$(elm).spinner(opts);
|
|
$('.ui-icon').text('');
|
|
$(".ui-icon").removeClass('ui-icon ui-icon-triangle-1-n ui-icon-triangle-1-s');
|
|
$(elm).on("click", function() {
|
|
$(elm).select();
|
|
});
|
|
}
|
|
};
|
|
}])
|
|
|
|
//
|
|
// awRefresh
|
|
//
|
|
// Creates a timer to call scope.refresh(iterator) ever N seconds, where
|
|
// N is a setting in config.js
|
|
//
|
|
.directive('awRefresh', ['$rootScope', function($rootScope) {
|
|
return {
|
|
link: function(scope) {
|
|
function msg() {
|
|
var num = '' + scope.refreshCnt;
|
|
while (num.length < 2) {
|
|
num = '0' + num;
|
|
}
|
|
return 'Refresh in ' + num + ' sec.';
|
|
}
|
|
scope.refreshCnt = $AnsibleConfig.refresh_rate;
|
|
scope.refreshMsg = msg();
|
|
if ($rootScope.timer) {
|
|
clearInterval($rootScope.timer);
|
|
}
|
|
$rootScope.timer = setInterval(function() {
|
|
scope.refreshCnt--;
|
|
if (scope.refreshCnt <= 0) {
|
|
scope.refresh();
|
|
scope.refreshCnt = $AnsibleConfig.refresh_rate;
|
|
}
|
|
scope.refreshMsg = msg();
|
|
if (!scope.$$phase) {
|
|
scope.$digest();
|
|
}
|
|
}, 1000);
|
|
}
|
|
};
|
|
}])
|
|
|
|
/*
|
|
* Make an element draggable. Used on inventory groups tree.
|
|
*
|
|
* awDraggable: boolean || {{ expression }}
|
|
*
|
|
*/
|
|
.directive('awDraggable', [function() {
|
|
return function(scope, element, attrs) {
|
|
|
|
if (attrs.awDraggable === "true") {
|
|
var containment = attrs.containment; //provide dataContainment:"#id"
|
|
$(element).draggable({
|
|
containment: containment,
|
|
scroll: true,
|
|
revert: "invalid",
|
|
helper: "clone",
|
|
start: function(e, ui) {
|
|
ui.helper.addClass('draggable-clone');
|
|
},
|
|
zIndex: 100,
|
|
cursorAt: { left: -1 }
|
|
});
|
|
}
|
|
};
|
|
}])
|
|
|
|
/*
|
|
* Make an element droppable- it can receive draggable elements
|
|
*
|
|
* awDroppable: boolean || {{ expression }}
|
|
*
|
|
*/
|
|
.directive('awDroppable', ['Find', function(Find) {
|
|
return function(scope, element, attrs) {
|
|
var node;
|
|
if (attrs.awDroppable === "true") {
|
|
$(element).droppable({
|
|
// the following is inventory specific accept checking and
|
|
// drop processing.
|
|
accept: function(draggable) {
|
|
if (draggable.attr('data-type') === 'group') {
|
|
// Dropped a group
|
|
if ($(this).attr('data-group-id') === draggable.attr('data-group-id')) {
|
|
// No dropping a node onto itself (or a copy)
|
|
return false;
|
|
}
|
|
// No dropping a node into a group that already has the node
|
|
node = Find({ list: scope.groups, key: 'id', val: parseInt($(this).attr('data-tree-id'), 10) });
|
|
if (node) {
|
|
var group = parseInt(draggable.attr('data-group-id'), 10),
|
|
found = false,
|
|
i;
|
|
// For whatever reason indexOf() would not work...
|
|
for (i = 0; i < node.children.length; i++) {
|
|
if (node.children[i] === group) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
return (found) ? false : true;
|
|
}
|
|
return false;
|
|
}
|
|
if (draggable.attr('data-type') === 'host') {
|
|
// Dropped a host
|
|
node = Find({ list: scope.groups, key: 'id', val: parseInt($(this).attr('data-tree-id'), 10) });
|
|
return (node.id > 1) ? true : false;
|
|
}
|
|
return false;
|
|
},
|
|
over: function() {
|
|
$(this).addClass('droppable-hover');
|
|
},
|
|
out: function() {
|
|
$(this).removeClass('droppable-hover');
|
|
},
|
|
drop: function(e, ui) {
|
|
// Drag-n-drop succeeded. Trigger a response from the inventory.edit controller
|
|
$(this).removeClass('droppable-hover');
|
|
if (ui.draggable.attr('data-type') === 'group') {
|
|
scope.$emit('CopyMoveGroup', parseInt(ui.draggable.attr('data-tree-id'), 10),
|
|
parseInt($(this).attr('data-tree-id'), 10));
|
|
} else if (ui.draggable.attr('data-type') === 'host') {
|
|
scope.$emit('CopyMoveHost', parseInt($(this).attr('data-tree-id'), 10),
|
|
parseInt(ui.draggable.attr('data-host-id'), 10));
|
|
}
|
|
},
|
|
tolerance: 'pointer'
|
|
});
|
|
}
|
|
};
|
|
}])
|
|
|
|
|
|
.directive('awAccordion', ['Empty', '$location', 'Store', function(Empty, $location, Store) {
|
|
return function(scope, element, attrs) {
|
|
var active,
|
|
list = Store('accordions'),
|
|
id, base;
|
|
|
|
if (!Empty(attrs.openFirst)) {
|
|
active = 0;
|
|
} else {
|
|
// Look in storage for last active panel
|
|
if (list) {
|
|
id = $(element).attr('id');
|
|
base = ($location.path().replace(/^\//, '').split('/')[0]);
|
|
list.every(function(elem) {
|
|
if (elem.base === base && elem.id === id) {
|
|
active = elem.active;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
active = (Empty(active)) ? 0 : active;
|
|
}
|
|
|
|
$(element).accordion({
|
|
collapsible: true,
|
|
heightStyle: "content",
|
|
active: active,
|
|
activate: function() {
|
|
// When a panel is activated update storage
|
|
var active = $(this).accordion('option', 'active'),
|
|
id = $(this).attr('id'),
|
|
base = ($location.path().replace(/^\//, '').split('/')[0]),
|
|
list = Store('accordions'),
|
|
found = false;
|
|
if (!list) {
|
|
list = [];
|
|
}
|
|
list.every(function(elem) {
|
|
if (elem.base === base && elem.id === id) {
|
|
elem.active = active;
|
|
found = true;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
if (found === false) {
|
|
list.push({
|
|
base: base,
|
|
id: id,
|
|
active: active
|
|
});
|
|
}
|
|
Store('accordions', list);
|
|
}
|
|
});
|
|
};
|
|
}])
|
|
|
|
// Toggle switch inspired by http://www.bootply.com/92189
|
|
.directive('awToggleButton', [function() {
|
|
return function(scope, element) {
|
|
$(element).click(function() {
|
|
var next, choice;
|
|
$(this).find('.btn').toggleClass('active');
|
|
if ($(this).find('.btn-primary').size() > 0) {
|
|
$(this).find('.btn').toggleClass('btn-primary');
|
|
}
|
|
if ($(this).find('.btn-danger').size() > 0) {
|
|
$(this).find('.btn').toggleClass('btn-danger');
|
|
}
|
|
if ($(this).find('.btn-success').size() > 0) {
|
|
$(this).find('.btn').toggleClass('btn-success');
|
|
}
|
|
if ($(this).find('.btn-info').size() > 0) {
|
|
$(this).find('.btn').toggleClass('btn-info');
|
|
}
|
|
$(this).find('.btn').toggleClass('btn-default');
|
|
|
|
// Add data-after-toggle="functionName" to the btn-group, and we'll
|
|
// execute here. The newly active choice is passed as a parameter.
|
|
if ($(this).attr('data-after-toggle')) {
|
|
next = $(this).attr('data-after-toggle');
|
|
choice = $(this).find('.active').text();
|
|
setTimeout(function() {
|
|
scope.$apply(function() {
|
|
scope[next](choice);
|
|
});
|
|
});
|
|
}
|
|
|
|
});
|
|
};
|
|
}])
|
|
|
|
//
|
|
// Support dropping files on an element. Used on credentials page for SSH/RSA private keys
|
|
// Inspired by https://developer.mozilla.org/en-US/docs/Using_files_from_web_applications
|
|
//
|
|
.directive('awDropFile', ['Alert', function(Alert) {
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, element, attrs, ctrl) {
|
|
$(element).on('dragenter', function(e) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
});
|
|
$(element).on('dragover', function(e) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
});
|
|
$(element).on('drop', function(e) {
|
|
var dt, files, reader;
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
dt = e.originalEvent.dataTransfer;
|
|
files = dt.files;
|
|
reader = new FileReader();
|
|
reader.onload = function() {
|
|
ctrl.$setViewValue(reader.result);
|
|
ctrl.$render();
|
|
ctrl.$setValidity('required', true);
|
|
ctrl.$dirty = true;
|
|
if (!scope.$$phase) {
|
|
scope.$digest();
|
|
}
|
|
};
|
|
reader.onerror = function() {
|
|
Alert('Error', 'There was an error reading the selected file.');
|
|
};
|
|
if (files[0].size < 10000) {
|
|
reader.readAsText(files[0]);
|
|
} else {
|
|
Alert('Error', 'There was an error reading the selected file.');
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}])
|
|
|
|
.directive('awPasswordToggle', [function() {
|
|
return {
|
|
restrict: 'A',
|
|
link: function(scope, element) {
|
|
$(element).click(function() {
|
|
var buttonInnerHTML = $(element).html();
|
|
if (buttonInnerHTML.indexOf("Show") > -1) {
|
|
$(element).html("Hide");
|
|
$(element).closest('.input-group').find('input').first().attr("type", "text");
|
|
} else {
|
|
$(element).html("Show");
|
|
$(element).closest('.input-group').find('input').first().attr("type", "password");
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}])
|
|
|
|
.directive('awEnterKey', [function() {
|
|
return {
|
|
restrict: 'A',
|
|
link: function(scope, element, attrs) {
|
|
element.bind("keydown keypress", function(event) {
|
|
var keyCode = event.which || event.keyCode;
|
|
if (keyCode === 13) {
|
|
scope.$apply(function() {
|
|
scope.$eval(attrs.awEnterKey);
|
|
});
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}])
|
|
|
|
.directive('awTruncateBreadcrumb', ['BreadCrumbService', function(BreadCrumbService) {
|
|
return {
|
|
restrict: 'A',
|
|
scope: {
|
|
breadcrumbStep: '='
|
|
},
|
|
link: function(scope) {
|
|
scope.$watch('breadcrumbStep.ncyBreadcrumbLabel', function(){
|
|
BreadCrumbService.truncateCrumbs();
|
|
});
|
|
}
|
|
};
|
|
}]);
|