588 lines
22 KiB
JavaScript
588 lines
22 KiB
JavaScript
'use strict';
|
|
|
|
/* global angular:false */
|
|
/* global moment:false */
|
|
/* global $:false */
|
|
/* global ERROR,ISTATES,HSTATES,RSTATES */
|
|
|
|
// deal with accessToken in the query, this is passed for example on password reset and account setup upon invite
|
|
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
|
if (search.accessToken) {
|
|
localStorage.token = search.accessToken;
|
|
|
|
// strip the accessToken and expiresAt, then preserve the rest
|
|
delete search.accessToken;
|
|
delete search.expiresAt;
|
|
|
|
// this will reload the page as this is not a hash change
|
|
window.location.search = encodeURIComponent(Object.keys(search).map(function (key) { return key + '=' + search[key]; }).join('&'));
|
|
}
|
|
|
|
// create main application module
|
|
var app = angular.module('Application', ['pascalprecht.translate', 'ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld', 'ui.multiselect']);
|
|
|
|
app.config(['NotificationProvider', function (NotificationProvider) {
|
|
NotificationProvider.setOptions({
|
|
delay: 5000,
|
|
startTop: 60,
|
|
positionX: 'left',
|
|
templateUrl: 'notification.html'
|
|
});
|
|
}]);
|
|
|
|
app.config(['$translateProvider', function ($translateProvider) {
|
|
$translateProvider.useStaticFilesLoader({
|
|
prefix: 'translation/',
|
|
suffix: '.json'
|
|
});
|
|
$translateProvider.preferredLanguage('en');
|
|
// $translateProvider.preferredLanguage('de');
|
|
}]);
|
|
|
|
// configure resourceUrlWhitelist https://code.angularjs.org/1.5.8/docs/api/ng/provider/$sceDelegateProvider#resourceUrlWhitelist
|
|
app.config(function ($sceDelegateProvider) {
|
|
$sceDelegateProvider.resourceUrlWhitelist([
|
|
// Allow same origin resource loads.
|
|
'self',
|
|
// Allow loading from our assets domain.
|
|
'https://cloudron.io/**',
|
|
'https://staging.cloudron.io/**',
|
|
'https://dev.cloudron.io/**',
|
|
// Allow local development against the appstore pages
|
|
'http://localhost:5000/**'
|
|
]);
|
|
});
|
|
|
|
// setup all major application routes
|
|
app.config(['$routeProvider', function ($routeProvider) {
|
|
$routeProvider.when('/', {
|
|
redirectTo: '/apps'
|
|
}).when('/users', {
|
|
controller: 'UsersController',
|
|
templateUrl: 'views/users.html?<%= revision %>'
|
|
}).when('/app/:appId/:view?', {
|
|
controller: 'AppController',
|
|
templateUrl: 'views/app.html?<%= revision %>'
|
|
}).when('/appstore', {
|
|
controller: 'AppStoreController',
|
|
templateUrl: 'views/appstore.html?<%= revision %>'
|
|
}).when('/appstore/:appId', {
|
|
controller: 'AppStoreController',
|
|
templateUrl: 'views/appstore.html?<%= revision %>'
|
|
}).when('/apps', {
|
|
controller: 'AppsController',
|
|
templateUrl: 'views/apps.html?<%= revision %>'
|
|
}).when('/profile', {
|
|
controller: 'ProfileController',
|
|
templateUrl: 'views/profile.html?<%= revision %>'
|
|
}).when('/backups', {
|
|
controller: 'BackupsController',
|
|
templateUrl: 'views/backups.html?<%= revision %>'
|
|
}).when('/branding', {
|
|
controller: 'BrandingController',
|
|
templateUrl: 'views/branding.html?<%= revision %>'
|
|
}).when('/network', {
|
|
controller: 'NetworkController',
|
|
templateUrl: 'views/network.html?<%= revision %>'
|
|
}).when('/domains', {
|
|
controller: 'DomainsController',
|
|
templateUrl: 'views/domains.html?<%= revision %>'
|
|
}).when('/email', {
|
|
controller: 'EmailsController',
|
|
templateUrl: 'views/emails.html?<%= revision %>'
|
|
}).when('/email/:domain', {
|
|
controller: 'EmailController',
|
|
templateUrl: 'views/email.html?<%= revision %>'
|
|
}).when('/notifications', {
|
|
controller: 'NotificationsController',
|
|
templateUrl: 'views/notifications.html?<%= revision %>'
|
|
}).when('/settings', {
|
|
controller: 'SettingsController',
|
|
templateUrl: 'views/settings.html?<%= revision %>'
|
|
}).when('/activity', {
|
|
controller: 'ActivityController',
|
|
templateUrl: 'views/activity.html?<%= revision %>'
|
|
}).when('/support', {
|
|
controller: 'SupportController',
|
|
templateUrl: 'views/support.html?<%= revision %>'
|
|
}).when('/system', {
|
|
controller: 'SystemController',
|
|
templateUrl: 'views/system.html?<%= revision %>'
|
|
}).when('/services', {
|
|
controller: 'ServicesController',
|
|
templateUrl: 'views/services.html?<%= revision %>'
|
|
}).when('/volumes', {
|
|
controller: 'VolumesController',
|
|
templateUrl: 'views/volumes.html?<%= revision %>'
|
|
}).otherwise({ redirectTo: '/'});
|
|
}]);
|
|
|
|
app.filter('installError', function () {
|
|
return function (app) {
|
|
if (!app) return false;
|
|
if (app.installationState === ISTATES.ERROR) return true;
|
|
if (app.installationState === ISTATES.INSTALLED) {
|
|
// app.health can also be null to indicate insufficient data
|
|
if (app.health === HSTATES.UNHEALTHY || app.health === HSTATES.ERROR || app.health === HSTATES.DEAD) return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
});
|
|
|
|
app.filter('activeTask', function () {
|
|
return function (app) {
|
|
if (!app) return false;
|
|
return app.taskId !== null;
|
|
};
|
|
});
|
|
|
|
app.filter('installSuccess', function () {
|
|
return function (app) {
|
|
if (!app) return false;
|
|
return app.installationState === ISTATES.INSTALLED;
|
|
};
|
|
});
|
|
|
|
app.filter('appIsInstalledAndHealthy', function () {
|
|
return function (app) {
|
|
if (!app) return false;
|
|
return (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && app.runState === RSTATES.RUNNING);
|
|
};
|
|
});
|
|
|
|
app.filter('applicationLink', function() {
|
|
return function(app) {
|
|
if (!app) return '';
|
|
|
|
if (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && app.runState === RSTATES.RUNNING && !app.pendingPostInstallConfirmation) {
|
|
return 'https://' + app.fqdn;
|
|
} else {
|
|
return '';
|
|
}
|
|
};
|
|
});
|
|
|
|
// this appears when an item in app grid is clicked
|
|
app.filter('prettyAppErrorMessage', function () {
|
|
return function (app) {
|
|
if (!app) return '';
|
|
|
|
if (app.installationState === ISTATES.INSTALLED) {
|
|
// app.health can also be null to indicate insufficient data
|
|
if (app.health === HSTATES.UNHEALTHY) return 'The app is not responding to health checks. Check the logs for any error messages.';
|
|
}
|
|
|
|
if (app.error.reason === 'Access Denied') {
|
|
if (app.error.domain) return 'The DNS record for this location is not setup correctly. Please verify your DNS settings and repair this app.';
|
|
} else if (app.error.reason === 'Already Exists') {
|
|
if (app.error.domain) return 'The DNS record for this location already exists. Cloudron does not remove existing DNS records. Manually remove the DNS record and then click on repair.';
|
|
}
|
|
|
|
return app.error.message;
|
|
};
|
|
});
|
|
|
|
// this appears as tool tip in app grid
|
|
app.filter('appProgressMessage', function () {
|
|
return function (app) {
|
|
var message = app.message || (app.error ? app.error.message : '');
|
|
return message;
|
|
};
|
|
});
|
|
|
|
// see apps.js $scope.states
|
|
app.filter('selectedStateFilter', function () {
|
|
return function selectedStateFilter(apps, selectedState) {
|
|
return apps.filter(function (app) {
|
|
if (!selectedState || !selectedState.state) return true;
|
|
|
|
if (selectedState.state === 'running') return app.runState === 'running' && app.health === 'healthy' && app.installationState === 'installed';
|
|
if (selectedState.state === 'stopped') return app.runState === 'stopped';
|
|
|
|
return app.runState === 'running' && (app.health !== 'healthy' || app.installationState !== 'installed'); // not responding
|
|
});
|
|
};
|
|
});
|
|
|
|
app.filter('selectedTagFilter', function () {
|
|
return function selectedTagFilter(apps, selectedTags) {
|
|
return apps.filter(function (app) {
|
|
if (selectedTags.length === 0) return true;
|
|
if (!app.tags) return false;
|
|
|
|
for (var i = 0; i < selectedTags.length; i++) {
|
|
if (app.tags.indexOf(selectedTags[i]) === -1) return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
};
|
|
});
|
|
|
|
app.filter('selectedDomainFilter', function () {
|
|
return function selectedDomainFilter(apps, selectedDomain) {
|
|
return apps.filter(function (app) {
|
|
if (!selectedDomain) return true;
|
|
if (selectedDomain._alldomains) return true; // magic domain for single select, see apps.js ALL_DOMAINS_DOMAIN
|
|
|
|
if (selectedDomain.domain === app.domain) return true;
|
|
return !!app.alternateDomains.find(function (ad) { return ad.domain === selectedDomain.domain; });
|
|
});
|
|
};
|
|
});
|
|
|
|
app.filter('appSearchFilter', function () {
|
|
return function appSearchFilter(apps, appSearch) {
|
|
return apps.filter(function (app) {
|
|
if (!appSearch) return true;
|
|
appSearch = appSearch.toLowerCase();
|
|
return app.fqdn.indexOf(appSearch) !== -1
|
|
|| (app.label && app.label.toLowerCase().indexOf(appSearch) !== -1)
|
|
|| (app.manifest.title && app.manifest.title.toLowerCase().indexOf(appSearch) !== -1);
|
|
});
|
|
};
|
|
});
|
|
|
|
app.filter('prettyDomains', function () {
|
|
return function prettyDomains(domains) {
|
|
return domains.map(function (d) { return d.domain; }).join(', ');
|
|
};
|
|
});
|
|
|
|
app.filter('installationActive', function () {
|
|
return function (app) {
|
|
if (app.installationState === ISTATES.ERROR) return false;
|
|
if (app.installationState === ISTATES.INSTALLED) return false;
|
|
return true;
|
|
};
|
|
});
|
|
|
|
// this appears in the app grid
|
|
app.filter('installationStateLabel', function () {
|
|
return function(app) {
|
|
if (!app) return '';
|
|
|
|
var waiting = app.progress === 0 ? ' (Queued)' : '';
|
|
|
|
switch (app.installationState) {
|
|
case ISTATES.PENDING_INSTALL:
|
|
return 'Installing' + waiting;
|
|
case ISTATES.PENDING_CLONE:
|
|
return 'Cloning' + waiting;
|
|
case ISTATES.PENDING_LOCATION_CHANGE:
|
|
case ISTATES.PENDING_CONFIGURE:
|
|
case ISTATES.PENDING_RECREATE_CONTAINER:
|
|
case ISTATES.PENDING_DEBUG:
|
|
return 'Configuring' + waiting;
|
|
case ISTATES.PENDING_RESIZE:
|
|
return 'Resizing' + waiting;
|
|
case ISTATES.PENDING_DATA_DIR_MIGRATION:
|
|
return 'Migrating data' + waiting;
|
|
case ISTATES.PENDING_UNINSTALL: return 'Uninstalling' + waiting;
|
|
case ISTATES.PENDING_RESTORE: return 'Restoring' + waiting;
|
|
case ISTATES.PENDING_UPDATE: return 'Updating' + waiting;
|
|
case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting;
|
|
case ISTATES.PENDING_START: return 'Starting' + waiting;
|
|
case ISTATES.PENDING_STOP: return 'Stopping' + waiting;
|
|
case ISTATES.PENDING_RESTART: return 'Restarting' + waiting;
|
|
case ISTATES.ERROR: {
|
|
if (app.error && app.error.message === 'ETRYAGAIN') return 'DNS Error';
|
|
return 'Error';
|
|
}
|
|
case ISTATES.INSTALLED: {
|
|
if (app.debugMode) {
|
|
return 'Recovery Mode';
|
|
} else if (app.runState === 'running') {
|
|
if (!app.health) return 'Starting...'; // no data yet
|
|
if (app.health === HSTATES.HEALTHY) return 'Running';
|
|
return 'Not responding'; // dead/exit/unhealthy
|
|
} else if (app.runState === 'stopped') return 'Stopped';
|
|
else return app.runState;
|
|
}
|
|
default: return app.installationState;
|
|
}
|
|
};
|
|
});
|
|
|
|
app.filter('taskName', function () {
|
|
return function(installationState) {
|
|
switch (installationState) {
|
|
case ISTATES.PENDING_INSTALL: return 'install';
|
|
case ISTATES.PENDING_CLONE: return 'clone';
|
|
case ISTATES.PENDING_LOCATION_CHANGE: return 'location change';
|
|
case ISTATES.PENDING_CONFIGURE: return 'configure';
|
|
case ISTATES.PENDING_RECREATE_CONTAINER: return 'create container';
|
|
case ISTATES.PENDING_DEBUG: return 'debug';
|
|
case ISTATES.PENDING_RESIZE: return 'resize';
|
|
case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'data migration';
|
|
case ISTATES.PENDING_UNINSTALL: return 'uninstall';
|
|
case ISTATES.PENDING_RESTORE: return 'restore';
|
|
case ISTATES.PENDING_UPDATE: return 'update';
|
|
case ISTATES.PENDING_BACKUP: return 'backup';
|
|
case ISTATES.PENDING_START: return 'start app';
|
|
case ISTATES.PENDING_STOP: return 'stop app';
|
|
case ISTATES.PENDING_RESTART: return 'restart app';
|
|
default: return installationState || '';
|
|
}
|
|
};
|
|
});
|
|
|
|
app.filter('errorSuggestion', function () {
|
|
return function (error) {
|
|
if (!error) return '';
|
|
|
|
switch (error.reason) {
|
|
case ERROR.ACCESS_DENIED:
|
|
if (error.domain) return 'Check the DNS credentials of ' + error.domain.domain + ' in the Domains & Certs view';
|
|
return '';
|
|
case ERROR.COLLECTD_ERROR: return 'Check if collectd is running on the server';
|
|
case ERROR.DATABASE_ERROR: return 'Check if MySQL database is running on the server';
|
|
case ERROR.DOCKER_ERROR: return 'Check if docker is running on the server';
|
|
case ERROR.DNS_ERROR: return 'Check if the DNS service of the domain is running';
|
|
case ERROR.LOGROTATE_ERROR: return 'Check if logrotate is running on the server';
|
|
case ERROR.NETWORK_ERROR: return 'Check if there are any network issues on the server';
|
|
case ERROR.REVERSEPROXY_ERROR: return 'Check if nginx is running on the server';
|
|
default: return '';
|
|
}
|
|
};
|
|
});
|
|
|
|
app.filter('readyToUpdate', function () {
|
|
return function (apps) {
|
|
return apps.every(function (app) {
|
|
return (app.installationState === ISTATES.ERROR) || (app.installationState === ISTATES.INSTALLED);
|
|
});
|
|
};
|
|
});
|
|
|
|
app.filter('inProgressApps', function () {
|
|
return function (apps) {
|
|
return apps.filter(function (app) {
|
|
return app.installationState !== ISTATES.ERROR && app.installationState !== ISTATES.INSTALLED;
|
|
});
|
|
};
|
|
});
|
|
|
|
app.filter('prettyHref', function () {
|
|
return function (input) {
|
|
if (!input) return input;
|
|
if (input.indexOf('http://') === 0) return input.slice('http://'.length);
|
|
if (input.indexOf('https://') === 0) return input.slice('https://'.length);
|
|
return input;
|
|
};
|
|
});
|
|
|
|
app.filter('prettyDate', function () {
|
|
// http://ejohn.org/files/pretty.js
|
|
return function prettyDate(utc) {
|
|
var date = new Date(utc), // this converts utc into browser timezone and not cloudron timezone!
|
|
diff = (((new Date()).getTime() - date.getTime()) / 1000) + 30, // add 30seconds for clock skew
|
|
day_diff = Math.floor(diff / 86400);
|
|
|
|
if (isNaN(day_diff) || day_diff < 0)
|
|
return 'just now';
|
|
|
|
return day_diff === 0 && (
|
|
diff < 60 && 'just now' ||
|
|
diff < 120 && '1 minute ago' ||
|
|
diff < 3600 && Math.floor( diff / 60 ) + ' minutes ago' ||
|
|
diff < 7200 && '1 hour ago' ||
|
|
diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') ||
|
|
day_diff === 1 && 'Yesterday' ||
|
|
day_diff < 7 && day_diff + ' days ago' ||
|
|
day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' ||
|
|
day_diff < 365 && Math.round( day_diff / 30 ) + ' months ago' ||
|
|
Math.round( day_diff / 365 ) + ' years ago';
|
|
};
|
|
});
|
|
|
|
app.filter('prettyLongDate', function () {
|
|
return function prettyLongDate(utc) {
|
|
return moment(utc).format('MMMM Do YYYY, h:mm:ss a'); // this converts utc into browser timezone and not cloudron timezone!
|
|
};
|
|
});
|
|
|
|
app.filter('prettyShortDate', function () {
|
|
return function prettyShortDate(utc) {
|
|
return moment(utc).format('MMMM Do YYYY'); // this converts utc into browser timezone and not cloudron timezone!
|
|
};
|
|
});
|
|
|
|
app.filter('prettyEmailAddresses', function () {
|
|
return function prettyEmailAddresses(addresses) {
|
|
if (!addresses || addresses === '<>') return '<>';
|
|
if (Array.isArray(addresses)) return addresses.map(function (a) { return a.slice(1, -1); }).join(', ');
|
|
return addresses.slice(1, -1);
|
|
};
|
|
});
|
|
|
|
// custom directive for dynamic names in forms
|
|
// See http://stackoverflow.com/questions/23616578/issue-registering-form-control-with-interpolated-name#answer-23617401
|
|
app.directive('laterName', function () { // (2)
|
|
return {
|
|
restrict: 'A',
|
|
require: ['?ngModel', '^?form'], // (3)
|
|
link: function postLink(scope, elem, attrs, ctrls) {
|
|
attrs.$set('name', attrs.laterName);
|
|
|
|
var modelCtrl = ctrls[0]; // (3)
|
|
var formCtrl = ctrls[1]; // (3)
|
|
if (modelCtrl && formCtrl) {
|
|
modelCtrl.$name = attrs.name; // (4)
|
|
formCtrl.$addControl(modelCtrl); // (2)
|
|
scope.$on('$destroy', function () {
|
|
formCtrl.$removeControl(modelCtrl); // (5)
|
|
});
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) {
|
|
var original = $location.path;
|
|
$location.path = function (path, reload) {
|
|
if (reload === false) {
|
|
var lastRoute = $route.current;
|
|
var un = $rootScope.$on('$locationChangeSuccess', function () {
|
|
$route.current = lastRoute;
|
|
un();
|
|
});
|
|
}
|
|
return original.apply($location, [path]);
|
|
};
|
|
}]);
|
|
|
|
app.directive('ngClickSelect', function () {
|
|
return {
|
|
restrict: 'AC',
|
|
link: function (scope, element/*, attrs */) {
|
|
element.bind('click', function () {
|
|
var selection = window.getSelection();
|
|
var range = document.createRange();
|
|
range.selectNodeContents(this);
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
app.directive('ngClickReveal', function () {
|
|
return {
|
|
restrict: 'A',
|
|
link: function (scope, element, attrs) {
|
|
element.addClass('hand');
|
|
|
|
var value = '';
|
|
|
|
scope.$watch(attrs.ngClickReveal, function (newValue, oldValue) {
|
|
if (newValue !== oldValue) {
|
|
element.html('<i>hidden</i>');
|
|
value = newValue;
|
|
}
|
|
});
|
|
|
|
element.bind('click', function () {
|
|
element.text(value);
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
// https://codepen.io/webmatze/pen/isuHh
|
|
app.directive('tagInput', function () {
|
|
return {
|
|
restrict: 'E',
|
|
scope: {
|
|
inputTags: '=taglist'
|
|
},
|
|
require: '^form',
|
|
link: function ($scope, element, attrs, formCtrl) {
|
|
$scope.defaultWidth = 200;
|
|
$scope.tagText = ''; // current tag being edited
|
|
$scope.placeholder = attrs.placeholder;
|
|
$scope.tagArray = function () {
|
|
if ($scope.inputTags === undefined) {
|
|
return [];
|
|
}
|
|
return $scope.inputTags.split(' ').filter(function (tag) {
|
|
return tag !== '';
|
|
});
|
|
};
|
|
$scope.addTag = function () {
|
|
var tagArray = $scope.tagArray();
|
|
|
|
// prevent adding empty or existing items
|
|
if ($scope.tagText.length === 0 || tagArray.indexOf($scope.tagText) !== -1) {
|
|
return $scope.tagText = '';
|
|
}
|
|
|
|
tagArray.push($scope.tagText);
|
|
$scope.inputTags = tagArray.join(' ');
|
|
return $scope.tagText = '';
|
|
};
|
|
$scope.deleteTag = function (key) {
|
|
var tagArray;
|
|
tagArray = $scope.tagArray();
|
|
if (tagArray.length > 0 && $scope.tagText.length === 0 && key === undefined) {
|
|
tagArray.pop();
|
|
} else {
|
|
if (key !== undefined) {
|
|
tagArray.splice(key, 1);
|
|
}
|
|
}
|
|
formCtrl.$setDirty();
|
|
return $scope.inputTags = tagArray.join(' ');
|
|
};
|
|
$scope.$watch('tagText', function (newVal, oldVal) {
|
|
var tempEl;
|
|
if (!(newVal === oldVal && newVal === undefined)) {
|
|
tempEl = $('<span>' + newVal + '</span>').appendTo('body');
|
|
$scope.inputWidth = tempEl.width() + 5;
|
|
if ($scope.inputWidth < $scope.defaultWidth) {
|
|
$scope.inputWidth = $scope.defaultWidth;
|
|
}
|
|
return tempEl.remove();
|
|
}
|
|
});
|
|
element.bind('click', function () {
|
|
element[0].firstChild.lastChild.focus();
|
|
});
|
|
element.bind('keydown', function (e) {
|
|
var key = e.which;
|
|
if (key === 9 || key === 13) {
|
|
e.preventDefault();
|
|
}
|
|
if (key === 8) {
|
|
return $scope.$apply('deleteTag()');
|
|
}
|
|
});
|
|
element.bind('keyup', function (e) {
|
|
var key = e.which;
|
|
if (key === 9 || key === 13 || key === 32) {
|
|
e.preventDefault();
|
|
return $scope.$apply('addTag()');
|
|
}
|
|
});
|
|
},
|
|
template:
|
|
'<div class="tag-input-container">' +
|
|
'<div class="btn-group input-tag" data-ng-repeat="tag in tagArray()">' +
|
|
'<button type="button" class="btn btn-xs btn-primary" disabled>{{ tag }}</button>' +
|
|
'<button type="button" class="btn btn-xs btn-primary" data-ng-click="deleteTag($index)">×</button>' +
|
|
'</div>' +
|
|
'<input type="text" data-ng-model="tagText" ng-blur="addTag()" placeholder="{{placeholder}}"/>' +
|
|
'</div>'
|
|
};
|
|
});
|
|
|
|
app.config(['fitTextConfigProvider', function (fitTextConfigProvider) {
|
|
fitTextConfigProvider.config = {
|
|
loadDelay: 250,
|
|
compressor: 0.9,
|
|
min: 8,
|
|
max: 24
|
|
};
|
|
}]);
|