const templateUrl = require('./map/extent/extent-dialog.html');
/**
*
* @name formService
* @module app.ui
*
* @description common JavaScript methods for forms
*
*/
angular
.module('app.ui')
.factory('formService', formService);
function formService($timeout, $rootScope, events, $mdDialog, $translate, commonService, constants, projectionService) {
const service = {
showAdvance,
advanceModel: false,
toggleSection,
toggleAll,
setExtent,
setErrorMessage,
copyValueToFormIndex,
initValueToFormIndex,
copyValueToModelIndex,
updateLinkValues
};
// if show advance is true we need to toggle the hidden because the form has been reset
events.$on(events.avSchemaUpdate, () => { resestShowAdvance(); });
events.$on(events.avLoadModel, () => { resestShowAdvance(); });
events.$on(events.avSwitchLanguage, () => { resestShowAdvance(); });
return service;
/***/
/**
* Reset show advance fields if needed when there is a new model or language switch
* @function resestShowAdvance
*/
function resestShowAdvance() {
if (service.advanceModel) { $timeout(() => showAdvance(), constants.delayAccordion); }
}
/**
* Show advance fields
* @function showAdvance
*/
function showAdvance() {
// manage the show advance configuration (add 'htmlClass': 'av-form-advance hidden' to fields who need advance config)
const elems = document.getElementsByClassName('av-form-advance');
for (let elem of elems) {
elem.classList.toggle('hidden');
}
}
/**
* Toggle one section of accordion panel
* @function toggleSection
* @param {Object} event event who trigger the action
*/
function toggleSection(event) {
const targetParent = event.currentTarget.parentElement;
$(targetParent.getElementsByClassName('av-accordion-content')).slideToggle(400, 'swing');
const icons = targetParent.getElementsByTagName('md-icon');
for (let elem of icons) {
elem.classList.toggle('hidden');
}
}
/**
* Toggle all sections of accordion panel (array of accordion items)
* @function toggleAll
* @param {Object} event event who trigger the action
* @param {Bolean} collapse true if collapse false if expand
*/
function toggleAll(event, collapse) {
const targetParent = event.currentTarget.parentElement;
const iconsExp = targetParent.getElementsByClassName('av-accordion-expand');
const iconsCol = targetParent.getElementsByClassName('av-accordion-collapse');
if (collapse) {
$(targetParent.getElementsByClassName('av-accordion-content')).slideUp(400, 'swing');
for (let elem of iconsExp) {
elem.classList.remove('hidden');
}
for (let elem of iconsCol) {
elem.classList.add('hidden');
}
} else {
$(targetParent.getElementsByClassName('av-accordion-content')).slideDown(400, 'swing');
for (let elem of iconsExp) {
elem.classList.add('hidden');
}
for (let elem of iconsCol) {
elem.classList.remove('hidden');
}
}
}
/**
* Set extent from the viewer itself open in an iFrame
* @function setExtent
* @param {String} type type of extent ('default', 'full' or 'maximum')
* @param {Array} extentSets array of extent set to set the extent for
*/
function setExtent(type, extentSets) {
$mdDialog.show({
controller: extentController,
controllerAs: 'self',
templateUrl: templateUrl,
parent: $('.fgpa'),
disableParentScroll: false,
clickOutsideToClose: true,
fullscreen: false,
onRemoving: () => {
// get the extent from local storage
const extent = JSON.parse(localStorage.getItem('mapextent'));
localStorage.removeItem('mapextent');
// for each extent set, project the extent in the proper projection then set values
extentSets.forEach(extentSet => {
// project extent
const ext = projectionService.projectExtent(extent, extentSet.spatialReference);
// itnitialze the value because it will not work if it doesn't exist then apply values
extentSet[type] = {};
extentSet[type].xmin = ext.x0;
extentSet[type].ymin = ext.y0;
extentSet[type].xmax = ext.x1;
extentSet[type].ymax = ext.y1;
})
}
});
function extentController($mdDialog) {
'ngInject';
const self = this;
self.close = $mdDialog.hide;
}
}
/**
* Set custom validation error message
* inside translation.csv the variable to replace needs to be there inside {}
* @function setErrorMessage
* @param {Object} form form object to get value from
* @param {String} message message to get from translation.csv
* @param {Array} variables variables to replace
* @return {String} mess the updated message
*/
function setErrorMessage(form, message, variables) {
let mess = $translate.instant(message);
for (let variable of variables) {
// get the replacing value from form object
let replace = form;
variable.split('.').map(item => { replace = replace[item] });
// replace value in the message
mess = mess.replace(`{${variable}}`, replace);
}
return mess;
}
/**
* Assign the value to the form element. Use active element to get target tp update value for
*
* Need to have on the item:
* 'targetLink': 'legend.0', the target tag inside inside target parent and the array index for children
* 'targetParent': 'av-accordion-toggle', the parent element target class
* 'default': a default value for the tag when model value is empty
* @function copyValueToFormIndex
* @private
* @param {Object} model value to set
* @param {String} item item from the form
*/
function copyValueToFormIndex(model, item) {
// get targetLink info
const itemLink = item.targetLink.split('.');
// get active element to retrieve targetParent (loop throught parent element to find the target)
let flag = false;
let element = document.activeElement;
while(!flag) {
element = element.parentElement;
flag = element.classList.contains(item.targetParent);
}
// update form html element
const modelValue = (model !== '') ? model : item.default;
element.getElementsByTagName(itemLink[0])[itemLink[1]].innerHTML = modelValue;
}
/**
* Copy a value from the model to a form element who contain index when form loads
* @function initValueToFormIndex
* @param {Array} modelArray model array of values to apply
* @param {Array} classEl array of classes/index on the form element to retrieve it
* @param {String} field field on the model to get the value to apply
* @param {String} targetLink target element info to apply value to
*/
function initValueToFormIndex(modelArray, classEl, field, targetLink) {
// get targetLink info
const itemLink = targetLink.split('.');
// loop throught classEl elements to get proper html item
let elements = document;
for (let elemClass of classEl) {
// get the element from the class (all li elements (the html array))
elements = $(elements.getElementsByClassName(elemClass.cls)).children('ol').children('li');
// if needed, get the array index to set as the right element
elements = (elemClass.ind === -1) ? elements : elements[elemClass.ind];
}
// loop throught the model and assign value
for (let [index, model] of modelArray.entries()) {
elements[index].getElementsByTagName(itemLink[0])[itemLink[1]].innerHTML = model[field];
// FIXME: because we can't remove fields from array (ASF problem), we need to remove the hidden class
// if there is a field when we load a new model
if (typeof model.table !== 'undefined' && model.table.columns.length > 0) {
if (typeof model.table.columns[0].title === 'string') {
elements[index].getElementsByClassName('av-columns')[0].classList.remove('hidden');
}
}
}
}
/**
* Copy a value from the model to a model element who contain index
*
* Need to have on the item:
* 'targetElement': ['layers', 'layerType'], array of keys to to get the element to update
* 'targetParent': 'av-accordion-content', the parent element target class to find index of
* @function copyValueToModelIndex
* @param {Object} modelValue model value
* @param {String} item form item
* @param {String} model model to update
*/
function copyValueToModelIndex(modelValue, item, model) {
// get active element to retrieve targetParent (loop throught parent element to find the target)
let flag = false;
let element = document.activeElement;
while (!flag) {
element = element.parentElement;
flag = element.classList.contains(item.targetParent);
}
// get active index
const index = element.getAttribute('sf-index');
// find the key to update, if it is an array, use the index found before
let update = model;
for (let key of item.targetElement) {
update = commonService.isArray(update[key]) ? update[key][index] :
(typeof update[key] === 'object') ? update[key] : update[key] = modelValue;
}
}
/**
* Update scope element use inside a dynamic-select drop dowm. The value is use to link field together
* e.g. in TitleSchema, we have extentSetId who is the value of one extent set id. This function Will
* populate the scope element with all extent set id so user don't have to type them in
*
* Need an item to have the selection dropdown:
* { 'key': 'tileSchemas[].extentSetId', 'type': 'dynamic-select', 'optionData': 'extentId', 'model': 'extentSetId' }
* 'type' must be type-select, it will trigger the add on.
* 'optionData' is the variable name to create on scope object
* 'model' must be the last key in the path for this element (it is use inside dynamicSelect.module for ngModel)
*
* Need items with an values to updateModel
* { 'key': 'extentSets[].id', 'onChange': () => { debounceService.registerDebounce(self.formService.updateLinkValues(scope, ['extentSets', 'id'], 'extentId'), constants.debInput, false); } },
* 'onChange' function to use the self.formService.updateLinkValues where
* scope is the form scope
* Array of keys made from key element
* The same variable name created for the first element
*
* For a sample with a broadcast event, look at avLayersIdupdateEvents
*
* Known issue: onChange is not fired on the last item delete inside an array. Will need to find a workaround if need be
* @function updateLinkValues
* @param {Object} scope form scope
* @param {Array} keys the path to the key to get value from
* @param {String} link the value to update (need to be the same on optionData as the the field who receive the link)
* @param {String} broadcast optional - the event to broadcast. This will be use to update link in another scope model
*/
function updateLinkValues(scope, keys, link, broadcast = false) {
// find values then remove undefined
scope[link] = findValues(scope.model, keys, 0, []).filter(val => (typeof val !== 'undefined'));
// if array of options is empty, add a message. This way validation will apply
// when options are empty, the last item removed doesn't trigger validation
if (scope[link].length === 0) {
scope[link].push($translate.instant('options.dynamicselect'));
}
if (broadcast !== false) {
$rootScope.$broadcast(events[broadcast], scope[link]);
}
}
/**
* Find values from the model element
* @function findValues
* @private
* @param {Object} model model to find from
* @param {Array} keys the path to key
* @param {Integer} index index in the array
* @param {Array} returnValues array of values
* @return {Array} returnValues array of values
*/
function findValues(model, keys, index, returnValues) {
// get the key to check for
const key = keys.shift();
// deal differently if it is an array or not
if (commonService.isArray(model[key])) {
for (let i = 0; i < model[key].length; i++) {
// loop the array. Make a copy of keys so we don't empty it on first element
findValues(model[key][i], keys.slice(0), i, returnValues);
}
} else {
// if there is key in the array of keys, walk a level deeper
let item = keys.length > 0 ? findValues(model[key], keys, index, returnValues) : model[key];
// if the item equal the value to search, add the index to the return value
returnValues.push(item);
}
return returnValues;
}
}