forms-angular 0.12.0-beta.264 → 0.12.0-beta.265

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,8 +12,60 @@ var fng;
12
12
  (function (fng) {
13
13
  var directives;
14
14
  (function (directives) {
15
- /*@ngInject*/
16
15
  modelControllerDropdown.$inject = ["SecurityService"];
16
+ function dropDownItem($compile) {
17
+ return {
18
+ restrict: "AE",
19
+ replace: true,
20
+ link: function (scope, element) {
21
+ var template = ' <a ng-show="choice.text || choice.textFunc" class="dropdown-option" ng-href="{{choice.url || choice.urlFunc()}}" ng-click="doClick($index, $event, choice)">' +
22
+ " {{ choice.text || choice.textFunc() }}" +
23
+ " </a>";
24
+ element.replaceWith($compile(template)(scope));
25
+ },
26
+ };
27
+ }
28
+ directives.dropDownItem = dropDownItem;
29
+ function dropDownSubMenu($compile) {
30
+ return {
31
+ restrict: "AE",
32
+ replace: true,
33
+ link: function (scope, element) {
34
+ var parent = element[0].parentElement;
35
+ var template = ' <a ng-show="choice.text || choice.textFunc" class="dropdown-option open-sub-menu" data-ng-mouseenter="mouseenter()">' +
36
+ " {{ choice.text || choice.textFunc() }}" +
37
+ " </a>" +
38
+ // for now, the remainder of this template does not honour RBAC - come back to this if anyone ever reports it
39
+ ' <ul class="uib-dropdown-menu dropdown-menu sub-menu">' +
40
+ " <li ng-repeat=\"choice in choice.items\" ng-attr-id=\"{{choice.id}}\">" + // ${itemVisibilityStr} ${itemDisabledStr}
41
+ ' <drop-down-item data-ng-if="::!choice.items"></drop-down-item>' +
42
+ ' <drop-down-sub-menu data-ng-if="::choice.items"></drop-down-sub-menu>' +
43
+ " </li>" +
44
+ " </ul>";
45
+ var us = $compile(template)(scope);
46
+ element.replaceWith(us);
47
+ scope.mouseenter = function () {
48
+ // generally-speaking, we want the bottom of the sub-menu to line-up with the bottom of its parent menu
49
+ // item. we could achieve that using css alone, but the problem comes when the sub menu is too tall, and bottom-
50
+ // aligning it like that causes the top of the menu to overflow the navbar (or even higher).
51
+ // so we need to detect that case, and cap the top of the sub-menu at the top of the parent menu.
52
+ // this is harder than it sounds, as the top of the sub menu needs to be expressed (as a negative number)
53
+ // relative to the top of the parent menu item (because that is the closest relative-positioned parent
54
+ // to the submenu, and therefore what it - as an absolutely-positioned element - is expressed in relation to)
55
+ var parentRect = parent.getBoundingClientRect();
56
+ var ourHeight = us[2].getBoundingClientRect().height;
57
+ var offset = ourHeight - 5; // this is the top padding
58
+ if (offset > parentRect.top) {
59
+ offset -= offset - parentRect.top + parentRect.height;
60
+ }
61
+ offset -= parentRect.height;
62
+ us[2].style.top = "-".concat(offset, "px");
63
+ };
64
+ },
65
+ };
66
+ }
67
+ directives.dropDownSubMenu = dropDownSubMenu;
68
+ /*@ngInject*/
17
69
  function modelControllerDropdown(SecurityService) {
18
70
  var menuVisibilityStr;
19
71
  var menuDisabledStr;
@@ -24,7 +76,7 @@ var fng;
24
76
  var oneTimeBinding = fng.formsAngular.elemSecurityFuncBinding !== "normal";
25
77
  var bindingStr = oneTimeBinding ? "::" : "";
26
78
  if (SecurityService.canDoSecurity("hidden")) {
27
- menuVisibilityStr = "ng-if=\"contextMenuId && !contextMenuHidden\" ".concat(SecurityService.getHideableAttrs('{{ ::contextMenuId }}'));
79
+ menuVisibilityStr = "ng-if=\"contextMenuId && !contextMenuHidden\" ".concat(SecurityService.getHideableAttrs("{{ ::contextMenuId }}"));
28
80
  if (oneTimeBinding) {
29
81
  // because the isHidden(...) logic is highly likely to be model dependent, that cannot be one-time bound. to
30
82
  // be able to combine one-time and regular binding, we'll use ng-if for the one-time bound stuff and ng-hide for the rest
@@ -33,20 +85,20 @@ var fng;
33
85
  else if (fng.formsAngular.elemSecurityFuncBinding === "normal") {
34
86
  itemVisibilityStr = "ng-hide=\"".concat(itemVisibilityStr, " || (!choice.divider && isSecurelyHidden(choice.id))\"");
35
87
  }
36
- itemVisibilityStr += " ".concat(SecurityService.getHideableAttrs('{{ ::choice.id }}'));
88
+ itemVisibilityStr += " ".concat(SecurityService.getHideableAttrs("{{ ::choice.id }}"));
37
89
  }
38
90
  else {
39
91
  menuVisibilityStr = "";
40
92
  itemVisibilityStr = "ng-hide=\"".concat(itemVisibilityStr, "\"");
41
93
  }
42
94
  if (SecurityService.canDoSecurity("disabled")) {
43
- menuDisabledStr = "disableable-link ng-disabled=\"contextMenuDisabled\" ".concat(SecurityService.getDisableableAttrs('{{ ::contextMenuId }}'));
95
+ menuDisabledStr = "disableable-link ng-disabled=\"contextMenuDisabled\" ".concat(SecurityService.getDisableableAttrs("{{ ::contextMenuId }}"));
44
96
  // as ng-class is already being used, we'll add the .disabled class if the menu item is securely disabled using
45
97
  // class="{{ }}". note that we "prevent" a disabled menu item from being clicked by checking for the DISABLED
46
98
  // attribute in the doClick(...) function, and aborting if this is found.
47
99
  // note that the 'normal' class introduced here might not actually do anything, but for one-time binding to work
48
100
  // properly, we need a truthy value
49
- itemDisabledStr += " class=\"{{ ".concat(bindingStr, "(!choice.divider && isSecurelyDisabled(choice.id)) ? 'disabled' : 'normal' }}\" ").concat(SecurityService.getDisableableAttrs('{{ ::choice.id }}'));
101
+ itemDisabledStr += " class=\"{{ ".concat(bindingStr, "(!choice.divider && isSecurelyDisabled(choice.id)) ? 'disabled' : 'normal' }}\" ").concat(SecurityService.getDisableableAttrs("{{ ::choice.id }}"));
50
102
  }
51
103
  else {
52
104
  menuDisabledStr = "";
@@ -60,9 +112,8 @@ var fng;
60
112
  " </a>" +
61
113
  ' <ul class="uib-dropdown-menu dropdown-menu">' +
62
114
  " <li ng-repeat=\"choice in items\" ng-attr-id=\"{{choice.id}}\" ".concat(itemVisibilityStr, " ").concat(itemDisabledStr, ">") +
63
- ' <a ng-show="choice.text || choice.textFunc" class="dropdown-option" ng-href="{{choice.url || choice.urlFunc()}}" ng-click="doClick($index, $event)">' +
64
- " {{ choice.text || choice.textFunc() }}" +
65
- " </a>" +
115
+ ' <drop-down-item data-ng-if="!choice.items"></drop-down-item>' +
116
+ ' <drop-down-sub-menu data-ng-if="choice.items"></drop-down-sub-menu>' +
66
117
  " </li>" +
67
118
  " </ul>" +
68
119
  "</li>",
@@ -914,8 +965,13 @@ var fng;
914
965
  }
915
966
  }
916
967
  else {
968
+ // NB: Any changes to what properties we support @@-wrapped pseudonyms for here should be repeated in
969
+ // PluginHelperService.extractFromAttr
970
+ info.help = FormMarkupHelperService.handlePseudos(scope, info.help);
971
+ info.label = FormMarkupHelperService.handlePseudos(scope, info.label);
972
+ info.popup = FormMarkupHelperService.handlePseudos(scope, info.popup);
917
973
  // Handle arrays here
918
- var controlDivClasses = FormMarkupHelperService.controlDivClasses(options);
974
+ var controlDivClasses = FormMarkupHelperService.controlDivClasses(options, info);
919
975
  if (info.array) {
920
976
  controlDivClasses.push('fng-array');
921
977
  if (options.formstyle === 'inline' || options.formstyle === 'stacked') {
@@ -1116,179 +1172,201 @@ var fng;
1116
1172
  }
1117
1173
  return result;
1118
1174
  };
1175
+ scope.$on("regenerateFormMarkup", function (event, formId) {
1176
+ if (!attrs.formid || attrs.formid !== formId) {
1177
+ return;
1178
+ }
1179
+ generateForm(scope[attrs.schema]);
1180
+ });
1119
1181
  var unwatch = scope.$watch(attrs.schema, function (newValue) {
1120
1182
  if (newValue) {
1121
1183
  var newArrayValue = angular.isArray(newValue) ? newValue : [newValue]; // otherwise some old tests stop working for no real reason
1122
1184
  if (newArrayValue.length > 0 && typeof unwatch === "function") {
1123
1185
  unwatch();
1124
1186
  unwatch = null;
1125
- var elementHtml = '';
1126
- var recordAttribute = attrs.model || 'record'; // By default data comes from scope.record
1127
- var theRecord = scope[recordAttribute];
1128
- var hideableTabs = newArrayValue.filter(function (s) { return s && s.containerType === "tab" && s.hideable; });
1129
- var hiddenTabArrayProp = void 0;
1130
- var hiddenTabReintroductionMethod = void 0;
1131
- for (var _i = 0, hideableTabs_1 = hideableTabs; _i < hideableTabs_1.length; _i++) {
1132
- var tab = hideableTabs_1[_i];
1133
- if (tab.hiddenTabArrayProp) {
1134
- if (hiddenTabArrayProp && tab.hiddenTabArrayProp !== hiddenTabArrayProp) {
1135
- throw new Error("Currently, tab sets with more than one value for hiddenTabArrayProp are not supported");
1136
- }
1137
- hiddenTabArrayProp = tab.hiddenTabArrayProp;
1138
- }
1139
- if (tab.hiddenTabReintroductionMethod) {
1140
- if (hiddenTabReintroductionMethod && tab.hiddenTabReintroductionMethod !== hiddenTabReintroductionMethod) {
1141
- throw new Error("Currently, tab sets with more than one value for hiddenTabReintroductionMethod are not supported");
1142
- }
1143
- hiddenTabReintroductionMethod = tab.hiddenTabReintroductionMethod;
1144
- }
1145
- }
1146
- // now we have established that we don't have more than one value for hiddenTabArrayProp, apply a default if no
1147
- // value has been provided at all...
1148
- if (!hiddenTabArrayProp) {
1149
- hiddenTabArrayProp = "record.hiddenTabs";
1150
- }
1151
- // ...and then replace all blanks with this value so the processInstructions() call made below can deal with
1152
- // each tab independently of the others
1153
- for (var _a = 0, hideableTabs_2 = hideableTabs; _a < hideableTabs_2.length; _a++) {
1154
- var tab = hideableTabs_2[_a];
1155
- tab.hiddenTabArrayProp = hiddenTabArrayProp;
1187
+ generateForm(newArrayValue);
1188
+ }
1189
+ }
1190
+ }, true);
1191
+ var formElement;
1192
+ var generateForm = function (schema) {
1193
+ var elementHtml = '';
1194
+ var recordAttribute = attrs.model || 'record'; // By default data comes from scope.record
1195
+ var theRecord = scope[recordAttribute];
1196
+ var hideableTabs = schema.filter(function (s) { return s && s.containerType === "tab" && s.hideable; });
1197
+ var hiddenTabArrayProp;
1198
+ var hiddenTabReintroductionMethod;
1199
+ for (var _i = 0, hideableTabs_1 = hideableTabs; _i < hideableTabs_1.length; _i++) {
1200
+ var tab = hideableTabs_1[_i];
1201
+ if (tab.hiddenTabArrayProp) {
1202
+ if (hiddenTabArrayProp && tab.hiddenTabArrayProp !== hiddenTabArrayProp) {
1203
+ throw new Error("Currently, tab sets with more than one value for hiddenTabArrayProp are not supported");
1156
1204
  }
1157
- if (hiddenTabReintroductionMethod === "tab") {
1158
- newArrayValue.push({
1159
- containerType: "+tab",
1160
- hiddenTabArrayProp: hiddenTabArrayProp,
1161
- content: []
1162
- });
1205
+ hiddenTabArrayProp = tab.hiddenTabArrayProp;
1206
+ }
1207
+ if (tab.hiddenTabReintroductionMethod) {
1208
+ if (hiddenTabReintroductionMethod && tab.hiddenTabReintroductionMethod !== hiddenTabReintroductionMethod) {
1209
+ throw new Error("Currently, tab sets with more than one value for hiddenTabReintroductionMethod are not supported");
1163
1210
  }
1164
- theRecord = theRecord || {};
1165
- if ((attrs.subschema || attrs.model) && !attrs.forceform) {
1166
- elementHtml = '';
1211
+ hiddenTabReintroductionMethod = tab.hiddenTabReintroductionMethod;
1212
+ }
1213
+ }
1214
+ // now we have established that we don't have more than one value for hiddenTabArrayProp, apply a default if no
1215
+ // value has been provided at all...
1216
+ if (!hiddenTabArrayProp) {
1217
+ hiddenTabArrayProp = "record.hiddenTabs";
1218
+ }
1219
+ // ...and then replace all blanks with this value so the processInstructions() call made below can deal with
1220
+ // each tab independently of the others
1221
+ for (var _a = 0, hideableTabs_2 = hideableTabs; _a < hideableTabs_2.length; _a++) {
1222
+ var tab = hideableTabs_2[_a];
1223
+ tab.hiddenTabArrayProp = hiddenTabArrayProp;
1224
+ }
1225
+ if (hiddenTabReintroductionMethod === "tab") {
1226
+ schema.push({
1227
+ containerType: "+tab",
1228
+ hiddenTabArrayProp: hiddenTabArrayProp,
1229
+ content: []
1230
+ });
1231
+ }
1232
+ theRecord = theRecord || {};
1233
+ if ((attrs.subschema || attrs.model) && !attrs.forceform) {
1234
+ elementHtml = '';
1235
+ }
1236
+ else {
1237
+ scope.topLevelFormName = attrs.name || 'myForm'; // Form name defaults to myForm
1238
+ // Copy attrs we don't process into form
1239
+ var customAttrs = '';
1240
+ for (var thisAttr in attrs) {
1241
+ if (attrs.hasOwnProperty(thisAttr)) {
1242
+ if (thisAttr[0] !== '$' && ['name', 'formstyle', 'schema', 'subschema', 'model', 'viewform'].indexOf(thisAttr) === -1) {
1243
+ customAttrs += ' ' + attrs.$attr[thisAttr] + '="' + attrs[thisAttr] + '"';
1244
+ }
1167
1245
  }
1168
- else {
1169
- scope.topLevelFormName = attrs.name || 'myForm'; // Form name defaults to myForm
1170
- // Copy attrs we don't process into form
1171
- var customAttrs = '';
1172
- for (var thisAttr in attrs) {
1173
- if (attrs.hasOwnProperty(thisAttr)) {
1174
- if (thisAttr[0] !== '$' && ['name', 'formstyle', 'schema', 'subschema', 'model', 'viewform'].indexOf(thisAttr) === -1) {
1175
- customAttrs += ' ' + attrs.$attr[thisAttr] + '="' + attrs[thisAttr] + '"';
1176
- }
1246
+ }
1247
+ var tag = attrs.forceform ? 'ng-form' : 'form';
1248
+ elementHtml = "<".concat(tag, " name=\"").concat(scope.topLevelFormName, "\" class=\"").concat(convertFormStyleToClass(attrs.formstyle), "\" novalidate ").concat(customAttrs, ">");
1249
+ }
1250
+ if (theRecord === scope.topLevelFormName) {
1251
+ throw new Error('Model and Name must be distinct - they are both ' + theRecord);
1252
+ }
1253
+ elementHtml += processInstructions(schema, true, attrs);
1254
+ if (tabsSetup === tabsSetupState.Forced) {
1255
+ elementHtml += '</uib-tabset>';
1256
+ }
1257
+ elementHtml += attrs.subschema ? '' : '</form>';
1258
+ var compiledFormElement = $compile(elementHtml)(scope);
1259
+ if (formElement) {
1260
+ var topLevelForm = scope[scope.topLevelFormName];
1261
+ formElement.replaceWith(compiledFormElement);
1262
+ // the replaceWith will cause the scope[scope.topLevelFormName] to be cleared, so we need to...:
1263
+ scope[scope.topLevelFormName] = topLevelForm;
1264
+ }
1265
+ else {
1266
+ element.replaceWith(compiledFormElement);
1267
+ }
1268
+ // remember the element that we're now represented by (which is no longer the value
1269
+ // of element passed to our link function). This will enable us to replace the
1270
+ // correct element upon receipt of a regenerateFormMarkup request.
1271
+ formElement = compiledFormElement;
1272
+ // If there are subkeys we need to fix up ng-model references when record is read
1273
+ // If we have modelControllers we need to let them know when we have form + data
1274
+ var sharedData = scope[attrs.shared || 'sharedData'];
1275
+ var modelControllers = sharedData ? sharedData.modelControllers : [];
1276
+ if ((subkeys.length > 0 || modelControllers.length > 0) && !scope.phaseWatcher) {
1277
+ var unwatch2 = scope.$watch('phase', function (newValue) {
1278
+ scope.phaseWatcher = true;
1279
+ if (newValue === 'ready' && typeof unwatch2 === "function") {
1280
+ unwatch2();
1281
+ unwatch2 = null;
1282
+ // Tell the 'model controllers' that the form and data are there
1283
+ for (var i = 0; i < modelControllers.length; i++) {
1284
+ if (modelControllers[i].onAllReady) {
1285
+ modelControllers[i].onAllReady(scope);
1177
1286
  }
1178
1287
  }
1179
- var tag = attrs.forceform ? 'ng-form' : 'form';
1180
- elementHtml = "<".concat(tag, " name=\"").concat(scope.topLevelFormName, "\" class=\"").concat(convertFormStyleToClass(attrs.formstyle), "\" novalidate ").concat(customAttrs, ">");
1181
- }
1182
- if (theRecord === scope.topLevelFormName) {
1183
- throw new Error('Model and Name must be distinct - they are both ' + theRecord);
1184
- }
1185
- elementHtml += processInstructions(newArrayValue, true, attrs);
1186
- if (tabsSetup === tabsSetupState.Forced) {
1187
- elementHtml += '</uib-tabset>';
1188
- }
1189
- elementHtml += attrs.subschema ? '' : '</form>';
1190
- //console.log(elementHtml);
1191
- element.replaceWith($compile(elementHtml)(scope));
1192
- // If there are subkeys we need to fix up ng-model references when record is read
1193
- // If we have modelControllers we need to let them know when we have form + data
1194
- var sharedData = scope[attrs.shared || 'sharedData'];
1195
- var modelControllers_1 = sharedData ? sharedData.modelControllers : [];
1196
- if ((subkeys.length > 0 || modelControllers_1.length > 0) && !scope.phaseWatcher) {
1197
- var unwatch2 = scope.$watch('phase', function (newValue) {
1198
- scope.phaseWatcher = true;
1199
- if (newValue === 'ready' && typeof unwatch2 === "function") {
1200
- unwatch2();
1201
- unwatch2 = null;
1202
- // Tell the 'model controllers' that the form and data are there
1203
- for (var i = 0; i < modelControllers_1.length; i++) {
1204
- if (modelControllers_1[i].onAllReady) {
1205
- modelControllers_1[i].onAllReady(scope);
1288
+ // For each one of the subkeys sets in the form we need to fix up ng-model references
1289
+ for (var subkeyCtr = 0; subkeyCtr < subkeys.length; subkeyCtr++) {
1290
+ var info = subkeys[subkeyCtr];
1291
+ var arrayOffset;
1292
+ var matching;
1293
+ var arrayToProcess = angular.isArray(info.subkey) ? info.subkey : [info.subkey];
1294
+ var parts = info.name.split('.');
1295
+ var dataVal = theRecord;
1296
+ while (parts.length > 1) {
1297
+ dataVal = dataVal[parts.shift()] || {};
1298
+ }
1299
+ dataVal = dataVal[parts[0]] = dataVal[parts[0]] || [];
1300
+ // For each of the required subkeys of this type
1301
+ for (var thisOffset = 0; thisOffset < arrayToProcess.length; thisOffset++) {
1302
+ if (arrayToProcess[thisOffset].selectFunc) {
1303
+ // Get the array offset from a function
1304
+ if (!scope[arrayToProcess[thisOffset].selectFunc] || typeof scope[arrayToProcess[thisOffset].selectFunc] !== 'function') {
1305
+ throw new Error('Subkey function ' + arrayToProcess[thisOffset].selectFunc + ' is not properly set up');
1206
1306
  }
1307
+ arrayOffset = scope[arrayToProcess[thisOffset].selectFunc](theRecord, info);
1207
1308
  }
1208
- // For each one of the subkeys sets in the form we need to fix up ng-model references
1209
- for (var subkeyCtr = 0; subkeyCtr < subkeys.length; subkeyCtr++) {
1210
- var info = subkeys[subkeyCtr];
1211
- var arrayOffset;
1212
- var matching;
1213
- var arrayToProcess = angular.isArray(info.subkey) ? info.subkey : [info.subkey];
1214
- var parts = info.name.split('.');
1215
- var dataVal = theRecord;
1216
- while (parts.length > 1) {
1217
- dataVal = dataVal[parts.shift()] || {};
1218
- }
1219
- dataVal = dataVal[parts[0]] = dataVal[parts[0]] || [];
1220
- // For each of the required subkeys of this type
1221
- for (var thisOffset = 0; thisOffset < arrayToProcess.length; thisOffset++) {
1222
- if (arrayToProcess[thisOffset].selectFunc) {
1223
- // Get the array offset from a function
1224
- if (!scope[arrayToProcess[thisOffset].selectFunc] || typeof scope[arrayToProcess[thisOffset].selectFunc] !== 'function') {
1225
- throw new Error('Subkey function ' + arrayToProcess[thisOffset].selectFunc + ' is not properly set up');
1226
- }
1227
- arrayOffset = scope[arrayToProcess[thisOffset].selectFunc](theRecord, info);
1228
- }
1229
- else if (arrayToProcess[thisOffset].keyList) {
1230
- // We are choosing the array element by matching one or more keys
1231
- var thisSubkeyList = arrayToProcess[thisOffset].keyList;
1232
- for (arrayOffset = 0; arrayOffset < dataVal.length; arrayOffset++) {
1233
- matching = true;
1234
- for (var keyField in thisSubkeyList) {
1235
- if (thisSubkeyList.hasOwnProperty(keyField)) {
1236
- // Not (currently) concerned with objects here - just simple types and lookups
1237
- if (dataVal[arrayOffset][keyField] !== thisSubkeyList[keyField] &&
1238
- (typeof dataVal[arrayOffset][keyField] === 'undefined' || !dataVal[arrayOffset][keyField].text || dataVal[arrayOffset][keyField].text !== thisSubkeyList[keyField])) {
1239
- matching = false;
1240
- break;
1241
- }
1242
- }
1243
- }
1244
- if (matching) {
1309
+ else if (arrayToProcess[thisOffset].keyList) {
1310
+ // We are choosing the array element by matching one or more keys
1311
+ var thisSubkeyList = arrayToProcess[thisOffset].keyList;
1312
+ for (arrayOffset = 0; arrayOffset < dataVal.length; arrayOffset++) {
1313
+ matching = true;
1314
+ for (var keyField in thisSubkeyList) {
1315
+ if (thisSubkeyList.hasOwnProperty(keyField)) {
1316
+ // Not (currently) concerned with objects here - just simple types and lookups
1317
+ if (dataVal[arrayOffset][keyField] !== thisSubkeyList[keyField] &&
1318
+ (typeof dataVal[arrayOffset][keyField] === 'undefined' || !dataVal[arrayOffset][keyField].text || dataVal[arrayOffset][keyField].text !== thisSubkeyList[keyField])) {
1319
+ matching = false;
1245
1320
  break;
1246
1321
  }
1247
1322
  }
1248
- if (!matching) {
1249
- // There is no matching array element
1250
- switch (arrayToProcess[thisOffset].onNotFound) {
1251
- case 'error':
1252
- var errorMessage = 'Cannot find matching ' + (arrayToProcess[thisOffset].title || arrayToProcess[thisOffset].path);
1253
- //Have to do this async as setPristine clears it
1254
- $timeout(function () {
1255
- scope.showError(errorMessage, 'Unable to set up form correctly');
1256
- });
1257
- arrayOffset = -1;
1258
- //throw new Error(scope.errorMessage);
1259
- break;
1260
- case 'create':
1261
- default:
1262
- var nameElements = info.name.split('.');
1263
- var lastPart = nameElements.pop();
1264
- var possibleArray = nameElements.join('.');
1265
- var obj = theRecord;
1266
- // Should loop here when / if we re-introduce nesting
1267
- if (possibleArray) {
1268
- obj = obj[possibleArray];
1269
- }
1270
- arrayOffset = obj[lastPart].push(thisSubkeyList) - 1;
1271
- break;
1272
- }
1273
- }
1274
1323
  }
1275
- else {
1276
- throw new Error('Invalid subkey setup for ' + info.name);
1324
+ if (matching) {
1325
+ break;
1326
+ }
1327
+ }
1328
+ if (!matching) {
1329
+ // There is no matching array element
1330
+ switch (arrayToProcess[thisOffset].onNotFound) {
1331
+ case 'error':
1332
+ var errorMessage = 'Cannot find matching ' + (arrayToProcess[thisOffset].title || arrayToProcess[thisOffset].path);
1333
+ //Have to do this async as setPristine clears it
1334
+ $timeout(function () {
1335
+ scope.showError(errorMessage, 'Unable to set up form correctly');
1336
+ });
1337
+ arrayOffset = -1;
1338
+ //throw new Error(scope.errorMessage);
1339
+ break;
1340
+ case 'create':
1341
+ default:
1342
+ var nameElements = info.name.split('.');
1343
+ var lastPart = nameElements.pop();
1344
+ var possibleArray = nameElements.join('.');
1345
+ var obj = theRecord;
1346
+ // Should loop here when / if we re-introduce nesting
1347
+ if (possibleArray) {
1348
+ obj = obj[possibleArray];
1349
+ }
1350
+ arrayOffset = obj[lastPart].push(thisSubkeyList) - 1;
1351
+ break;
1277
1352
  }
1278
- scope['$_arrayOffset_' + info.name.replace(/\./g, '_') + '_' + thisOffset] = arrayOffset;
1279
1353
  }
1280
1354
  }
1355
+ else {
1356
+ throw new Error('Invalid subkey setup for ' + info.name);
1357
+ }
1358
+ scope['$_arrayOffset_' + info.name.replace(/\./g, '_') + '_' + thisOffset] = arrayOffset;
1281
1359
  }
1282
- });
1283
- }
1284
- $rootScope.$broadcast('formInputDone', attrs.name);
1285
- if (FormGeneratorService.updateDataDependentDisplay && theRecord && Object.keys(theRecord).length > 0) {
1286
- // If this is not a test force the data dependent updates to the DOM
1287
- FormGeneratorService.updateDataDependentDisplay(theRecord, null, true, scope);
1360
+ }
1288
1361
  }
1289
- }
1362
+ });
1290
1363
  }
1291
- }, true);
1364
+ $rootScope.$broadcast('formInputDone', attrs.name);
1365
+ if (FormGeneratorService.updateDataDependentDisplay && theRecord && Object.keys(theRecord).length > 0) {
1366
+ // If this is not a test force the data dependent updates to the DOM
1367
+ FormGeneratorService.updateDataDependentDisplay(theRecord, null, true, scope);
1368
+ }
1369
+ };
1292
1370
  }
1293
1371
  };
1294
1372
  }
@@ -2851,9 +2929,78 @@ var fng;
2851
2929
  function isArrayElement(scope, info, options) {
2852
2930
  return scope["$index"] !== undefined || !!options.subschema;
2853
2931
  }
2932
+ function performPseudoReplacements(scope, str, substitutionSrc) {
2933
+ while (str.includes("@@")) {
2934
+ var firstCharPos = str.indexOf("@@") + 2;
2935
+ var lastCharPos = str.indexOf("@@", firstCharPos) - 1;
2936
+ var token = str.substring(firstCharPos, lastCharPos + 1);
2937
+ var plural = token.endsWith("s");
2938
+ if (plural) {
2939
+ token = token.slice(0, -1);
2940
+ }
2941
+ var upperStr = token[0].toUpperCase() === token[0] ? "true" : "false";
2942
+ token = token.toLocaleLowerCase();
2943
+ var replacement = void 0;
2944
+ if (substitutionSrc === "global") {
2945
+ replacement = fng.formsAngular.pseudo(token, upperStr === "true");
2946
+ }
2947
+ else if (substitutionSrc === "scopeStatic") {
2948
+ replacement = scope.sharedData.pseudo(token, upperStr === "true");
2949
+ }
2950
+ else if (substitutionSrc === "scopeDynamic") {
2951
+ replacement = "{{ sharedData.pseudo('".concat(token, "', ").concat(upperStr, ") }}");
2952
+ }
2953
+ else {
2954
+ replacement = "";
2955
+ }
2956
+ str =
2957
+ str.substring(0, firstCharPos - 2) +
2958
+ replacement +
2959
+ (plural ? "s" : "") +
2960
+ str.substring(lastCharPos + 3);
2961
+ }
2962
+ return str;
2963
+ }
2964
+ // Text surrounded by @@ @@ is assumed to be something that can have a pseudonym:
2965
+ // - If the sharedData object has been assigned a pseudo() function:
2966
+ // - then if dynamicFuncName has a value, we will set up a function on scope that will call that function to perform
2967
+ // the necessary substitutions. This would be useful for directives that use a template where labels, hints etc. already use {{ }}
2968
+ // notation (which would prevent any additional {{ }} that we nested in our result from working as intended).
2969
+ // - otherwise, we will set up the necessary dynamic calls to that sharedData.pseudo function using {{ }} notation
2970
+ // - Otherwise, if a pseudo callback has been provided in the IFng options, we will use that to perform a one-off (static) replacement.
2971
+ // - Otherwise (though not expected to happen), we will just remove the @@ tags, leaving the token(s) unchanged.
2972
+ // If the first character of the pseudonym token is upper case, then its replacement will use
2973
+ // titlecase, otherwise its replacement will be in lowercase.
2974
+ // If the last character of the pseudonym token is "s", then its replacement will be pluralised.
2975
+ function handlePseudos(scope, str, dynamicFuncName) {
2976
+ var _a;
2977
+ if (!(str === null || str === void 0 ? void 0 : str.includes("@@"))) {
2978
+ return str;
2979
+ }
2980
+ var substitutionSrc;
2981
+ if (typeof ((_a = scope === null || scope === void 0 ? void 0 : scope.sharedData) === null || _a === void 0 ? void 0 : _a.pseudo) === "function") {
2982
+ if (dynamicFuncName) {
2983
+ scope[dynamicFuncName] = function () {
2984
+ return performPseudoReplacements(scope, str, "scopeStatic");
2985
+ };
2986
+ substitutionSrc = "none"; // now the dynamic function has been set up, remove the @@ @@ tags (probably not essential, but might avoid confusion)
2987
+ }
2988
+ else {
2989
+ substitutionSrc = "scopeDynamic";
2990
+ }
2991
+ }
2992
+ else if (typeof fng.formsAngular.pseudo === "function") {
2993
+ substitutionSrc = "global";
2994
+ }
2995
+ else {
2996
+ substitutionSrc = "none";
2997
+ }
2998
+ return performPseudoReplacements(scope, str, substitutionSrc);
2999
+ }
2854
3000
  return {
2855
3001
  isHorizontalStyle: isHorizontalStyle,
2856
3002
  isArrayElement: isArrayElement,
3003
+ handlePseudos: handlePseudos,
2857
3004
  fieldChrome: function fieldChrome(scope, info, options) {
2858
3005
  var insert = '';
2859
3006
  if (info.id && typeof info.id.replace === "function") {
@@ -2909,7 +3056,7 @@ var fng;
2909
3056
  classes += ' form-group';
2910
3057
  if (options.formstyle === 'vertical' && info.size !== 'block-level') {
2911
3058
  template += '<div class="row">';
2912
- classes += ' col-sm-' + InputSizeHelperService.sizeAsNumber(info.size);
3059
+ classes += ' col-sm-' + InputSizeHelperService.sizeAsNumber(info);
2913
3060
  closeTag += '</div>';
2914
3061
  }
2915
3062
  var modelControllerName;
@@ -2955,7 +3102,12 @@ var fng;
2955
3102
  classes += ' ' + fieldInfo.labelDefaultClass;
2956
3103
  }
2957
3104
  else if (CssFrameworkService.framework() === 'bs3') {
2958
- classes += ' col-sm-3';
3105
+ if (fieldInfo.coloffset) {
3106
+ classes += ' col-sm-' + (3 - fieldInfo.coloffset).toString();
3107
+ }
3108
+ else {
3109
+ classes += ' col-sm-3';
3110
+ }
2959
3111
  }
2960
3112
  }
2961
3113
  else if (['inline', 'stacked'].includes(options.formstyle)) {
@@ -2992,7 +3144,7 @@ var fng;
2992
3144
  var formControl = '';
2993
3145
  if (CssFrameworkService.framework() === 'bs3') {
2994
3146
  compactClass = (['horizontal', 'vertical', 'inline'].indexOf(options.formstyle) === -1) ? ' input-sm' : '';
2995
- sizeClassBS3 = 'col-sm-' + InputSizeHelperService.sizeAsNumber(fieldInfo.size);
3147
+ sizeClassBS3 = 'col-sm-' + InputSizeHelperService.sizeAsNumber(fieldInfo);
2996
3148
  formControl = ' form-control';
2997
3149
  }
2998
3150
  else {
@@ -3069,10 +3221,18 @@ var fng;
3069
3221
  result += ' />';
3070
3222
  return result;
3071
3223
  },
3072
- controlDivClasses: function controlDivClasses(options) {
3224
+ controlDivClasses: function controlDivClasses(options, fieldInfo) {
3073
3225
  var result = [];
3074
3226
  if (isHorizontalStyle(options.formstyle, false)) {
3075
- result.push(CssFrameworkService.framework() === 'bs2' ? 'controls' : 'col-sm-9');
3227
+ if (CssFrameworkService.framework() === 'bs2') {
3228
+ result.push('controls');
3229
+ }
3230
+ else if (fieldInfo.coloffset) {
3231
+ result.push('col-sm-' + (9 + fieldInfo.coloffset).toString());
3232
+ }
3233
+ else {
3234
+ result.push('col-sm-9');
3235
+ }
3076
3236
  }
3077
3237
  return result;
3078
3238
  },
@@ -3134,8 +3294,12 @@ var fng;
3134
3294
  sizeMapping: sizeMapping,
3135
3295
  sizeDescriptions: sizeDescriptions,
3136
3296
  defaultSizeOffset: defaultSizeOffset,
3137
- sizeAsNumber: function (fieldSizeAsText) {
3138
- return sizeMapping[fieldSizeAsText ? sizeDescriptions.indexOf(fieldSizeAsText) : defaultSizeOffset];
3297
+ sizeAsNumber: function (info) {
3298
+ var result = sizeMapping[info.size ? sizeDescriptions.indexOf(info.size) : defaultSizeOffset];
3299
+ if (info.coloffset) {
3300
+ result += info.coloffset;
3301
+ }
3302
+ return result;
3139
3303
  }
3140
3304
  };
3141
3305
  }
@@ -3198,33 +3362,8 @@ var fng;
3198
3362
  function internalGenDisabledStr(scope, id, processedAttrs, idSuffix, params) {
3199
3363
  return internalGenDisabledAttrs(scope, id, processedAttrs, idSuffix, params).join(" ");
3200
3364
  }
3201
- // Text surrounded by @@ @@ is assumed to be something that can have a pseudonym. We'll rely
3202
- // upon the relevant controller assigning a pseudo() function to baseScope.
3203
- // If the first character of the pseudonym token is upper case, then its replacement will use
3204
- // titlecase, otherwise its replacement will be in lowercase.
3205
- // If the last character of the pseudonym token is "s", then its replacement will be pluralised.
3206
- function handlePseudos(str) {
3207
- if (!str) {
3208
- return str;
3209
- }
3210
- var result = str;
3211
- while (result.includes("@@")) {
3212
- var firstCharPos = result.indexOf("@@") + 2;
3213
- var lastCharPos = result.indexOf("@@", firstCharPos) - 1;
3214
- var token = result.substring(firstCharPos, lastCharPos + 1);
3215
- var plural = token.endsWith("s");
3216
- if (plural) {
3217
- token = token.slice(0, -1);
3218
- }
3219
- var upperStr = token[0].toUpperCase() === token[0] ? "true" : "false";
3220
- token = token.toLocaleLowerCase();
3221
- result =
3222
- result.substring(0, firstCharPos - 2) +
3223
- "{{ baseScope.pseudo('".concat(token, "', ").concat(upperStr, ") }}") +
3224
- (plural ? "s" : "") +
3225
- result.substring(lastCharPos + 3);
3226
- }
3227
- return result;
3365
+ function handlePseudos(scope, str, dynamicFuncName) {
3366
+ return FormMarkupHelperService.handlePseudos(scope, str, dynamicFuncName);
3228
3367
  }
3229
3368
  function makeIdStringUniqueForArrayElements(scope, processedAttrs, idString) {
3230
3369
  if (FormMarkupHelperService.isArrayElement(scope, processedAttrs.info, processedAttrs.options)) {
@@ -3287,7 +3426,7 @@ var fng;
3287
3426
  // of whether or not the datetime picker is actually disabled) to indicate that it potentially could be
3288
3427
  return disabledStr + " " + rawDisabledAttrs[1];
3289
3428
  }
3290
- function extractFromAttr(attr, directiveName) {
3429
+ function extractFromAttr(attr, directiveName, scope, opts) {
3291
3430
  function deserialize(str) {
3292
3431
  var retVal = str.replace(/&quot;/g, '"');
3293
3432
  if (retVal === "true") {
@@ -3326,12 +3465,9 @@ var fng;
3326
3465
  }
3327
3466
  }
3328
3467
  var result = { info: info, options: options, directiveOptions: directiveOptions };
3329
- // any part of the help text or label that is surrounded by @@ @@ is assumed to be something that can have
3330
- // a pseudonym. We'll be relying upon the parent controller assigning a pseudo() function to baseScope to
3331
- // actually perform the translation.
3332
- // TODO - do this better when fng is re-written!
3333
- result.info.help = handlePseudos(result.info.help);
3334
- result.info.label = handlePseudos(result.info.label);
3468
+ result.info.help = handlePseudos(scope, result.info.help, (opts === null || opts === void 0 ? void 0 : opts.setUpDynamicHelpFunc) ? "help" : undefined);
3469
+ result.info.label = handlePseudos(scope, result.info.label, (opts === null || opts === void 0 ? void 0 : opts.setUpDynamicLabelFunc) ? "label" : undefined);
3470
+ result.info.popup = handlePseudos(scope, result.info.popup);
3335
3471
  return result;
3336
3472
  }
3337
3473
  function genIdAndDisabledStr(scope, processedAttrs, idSuffix, params) {
@@ -3342,7 +3478,7 @@ var fng;
3342
3478
  return {
3343
3479
  extractFromAttr: extractFromAttr,
3344
3480
  buildInputMarkup: function buildInputMarkup(scope, attrs, params, generateInputControl) {
3345
- var processedAttrs = params.processedAttrs || extractFromAttr(attrs, "");
3481
+ var processedAttrs = params.processedAttrs || extractFromAttr(attrs, "", scope);
3346
3482
  var info = {};
3347
3483
  if (!params.ignoreFieldInfoFromAttrs) {
3348
3484
  Object.assign(info, processedAttrs.info);
@@ -3355,7 +3491,7 @@ var fng;
3355
3491
  if (fieldChrome.omit) {
3356
3492
  return "";
3357
3493
  }
3358
- var controlDivClasses = FormMarkupHelperService.controlDivClasses(options);
3494
+ var controlDivClasses = FormMarkupHelperService.controlDivClasses(options, info);
3359
3495
  var elementHtml = fieldChrome.template + FormMarkupHelperService.label(scope, info, params.addButtons, options);
3360
3496
  var idString = info.id;
3361
3497
  if (info.array || options.subschema) {
@@ -3972,8 +4108,20 @@ var fng;
3972
4108
  $scope.$watch("record", function (newValue, oldValue) {
3973
4109
  if (newValue !== oldValue) {
3974
4110
  if (Object.keys(oldValue).length > 0 && $scope.dropConversionWatcher) {
3975
- $scope.dropConversionWatcher(); // Don't want to convert changed data
4111
+ // We don't want to convert changed data, so we need to stop watching the conversions
4112
+ // after the record has finished its initialisation and this watch has begun detecting actual changes.
4113
+ // In some rare cases, it is possible that a "change" to the record can be made programatically
4114
+ // before all of the directives that might need to add conversions have finished doing so.
4115
+ // This can happen in situations where promise resolutions interrupt the compilation process.
4116
+ // If we're not careful, we can end up with <select> inputs that are blank even when the underlying
4117
+ // field does have a value.
4118
+ // To avoid this, we'll use a $timeout to give for the digest queue to fully clear out - this should
4119
+ // ensure that all directives have been compiled before the conversion watcher is actually dropped.
4120
+ var dropWatcherFunc_1 = $scope.dropConversionWatcher;
3976
4121
  $scope.dropConversionWatcher = null;
4122
+ $timeout(function () {
4123
+ dropWatcherFunc_1();
4124
+ });
3977
4125
  }
3978
4126
  force = formGeneratorInstance.updateDataDependentDisplay(newValue, oldValue, force, $scope);
3979
4127
  processLookupHandlers(newValue, oldValue);
@@ -5492,9 +5640,9 @@ var fng;
5492
5640
  $scope.$on('fngControllersUnloaded', function (evt) {
5493
5641
  clearContextMenu();
5494
5642
  });
5495
- $scope.doClick = function (index, event) {
5643
+ $scope.doClick = function (index, event, item) {
5496
5644
  var option = angular.element(event.target);
5497
- var item = $scope.items[index];
5645
+ item = item || $scope.items[index];
5498
5646
  if (item.divider || option.parent().hasClass('disabled')) {
5499
5647
  event.preventDefault();
5500
5648
  }
@@ -5602,6 +5750,8 @@ var fng;
5602
5750
  .controller('ModelCtrl', fng.controllers.ModelCtrl)
5603
5751
  .controller('NavCtrl', fng.controllers.NavCtrl)
5604
5752
  .directive('modelControllerDropdown', fng.directives.modelControllerDropdown)
5753
+ .directive('dropDownItem', fng.directives.dropDownItem)
5754
+ .directive('dropDownSubMenu', fng.directives.dropDownSubMenu)
5605
5755
  .directive('errorDisplay', fng.directives.errorDisplay)
5606
5756
  .directive('fngLink', fng.directives.fngLink)
5607
5757
  .directive('formInput', fng.directives.formInput)
@@ -5637,5 +5787,5 @@ $templateCache.put('error-display-bs3.html','<div id="display-error" ng-show="er
5637
5787
  $templateCache.put('error-messages.html','<div ng-message="required">A value is required for this field</div>\n<div ng-message="minlength">Too few characters entered</div>\n<div ng-message="maxlength">Too many characters entered</div>\n<div ng-message="min">That value is too small</div>\n<div ng-message="max">That value is too large</div>\n<div ng-message="email">You need to enter a valid email address</div>\n<div ng-message="pattern">This field does not match the expected pattern</div>\n');
5638
5788
  $templateCache.put('form-button-bs2.html','<div class="form-btn-grp">\n <div class="btn-group pull-right">\n <button id="saveButton" class="btn btn-mini btn-primary form-btn" ng-click="save()" ng-disabled="isSaveDisabled()"><i class="icon-ok"></i> Save</button>\n <div id="why-disabled" ng-class="{showwhy:!!whyDisabled}" ng-bind-html="whyDisabled"></div>\n <button id="cancelButton" class="btn btn-mini btn-warning form-btn" ng-click="cancel()" ng-disabled="isCancelDisabled()"><i class="icon-remove"></i> Cancel</button>\n </div>\n <div class="btn-group pull-right">\n <button id="newButton" class="btn btn-mini btn-success form-btn" ng-click="newClick()" ng-disabled="isNewDisabled()"><i class="icon-plus"></i> New</button>\n <button id="deleteButton" class="btn btn-mini btn-danger form-btn" ng-click="deleteClick()" ng-disabled="isDeleteDisabled()"><i class="icon-minus"></i> Delete</button>\n </div>\n</div>\n');
5639
5789
  $templateCache.put('form-button-bs3.html','<div class="form-btn-grp">\n <div class="btn-group pull-right">\n <button id="saveButton" class="btn btn-primary form-btn btn-xs" ng-click="save()" ng-disabled="isSaveDisabled()"><i class="glyphicon glyphicon-ok"></i> Save</button>\n <div id="why-disabled" ng-class="{showwhy:!!whyDisabled}" ng-bind-html="whyDisabled"></div>\n <button id="cancelButton" class="btn btn-warning form-btn btn-xs" ng-click="cancel()" ng-disabled="isCancelDisabled()"><i class="glyphicon glyphicon-remove"></i> Cancel</button>\n </div>\n <div class="btn-group pull-right">\n <button id="newButton" class="btn btn-success form-btn btn-xs" ng-click="newClick()" ng-disabled="isNewDisabled()"><i class="glyphicon glyphicon-plus"></i> New</button>\n <button id="deleteButton" class="btn btn-danger form-btn btn-xs" ng-click="deleteClick()" ng-disabled="isDeleteDisabled()"><i class="glyphicon glyphicon-minus"></i> Delete</button>\n </div>\n</div>\n');
5640
- $templateCache.put('search-bs2.html','<form class="navbar-search pull-right">\n <div id="search-cg" class="control-group" ng-class="errorClass">\n <input type="text" spellcheck="false" autocomplete="off" id="searchinput" ng-model="searchTarget" ng-model-options="{debounce:500}" class="search-query" placeholder="{{searchPlaceholder}}" ng-keyup="handleKey($event)">\n </div>\n</form>\n<div class="results-container" ng-show="results.length >= 1">\n <div class="search-results">\n <div ng-repeat="result in results">\n <a href="{{result.href}}" ng-class="resultClass($index)" title="{{result.additional}}">{{result.resourceText}} {{result.text}}</a>\n </div>\n <div ng-show="moreCount > 0">(plus more - continue typing to narrow down search...)\n </div>\n </div>\n</div>\n');
5641
- $templateCache.put('search-bs3.html','<form class="pull-right navbar-form">\n <div id="search-cg" class="form-group" ng-class="errorClass">\n <input type="text" spellcheck="false" autocomplete="off" id="searchinput" ng-model="searchTarget" ng-model-options="{debounce:500}" class="search-query form-control" placeholder="{{searchPlaceholder}}" ng-keyup="handleKey($event)">\n </div>\n</form>\n<div class="results-container" ng-show="results.length >= 1">\n <div class="search-results">\n <div ng-repeat="result in results">\n <a href="{{result.href}}" ng-class="resultClass($index)" title="{{result.additional}}">{{result.resourceText}} {{result.text}}</a>\n </div>\n <div ng-show="moreCount > 0">(plus more - continue typing to narrow down search...)\n </div>\n </div>\n</div>\n');}]);
5790
+ $templateCache.put('search-bs2.html','<form class="navbar-search pull-right">\n <div id="search-cg" class="control-group" ng-class="errorClass">\n <input type="text" spellcheck="false" autocomplete="off" id="searchinput" ng-model="searchTarget" ng-model-options="{debounce:250}" class="search-query" placeholder="{{searchPlaceholder}}" ng-keyup="handleKey($event)">\n </div>\n</form>\n<div class="results-container" ng-show="results.length >= 1">\n <div class="search-results">\n <div ng-repeat="result in results">\n <a href="{{result.href}}" ng-class="resultClass($index)" title="{{result.additional}}">{{result.resourceText}} {{result.text}}</a>\n </div>\n <div ng-show="moreCount > 0">(plus more - continue typing to narrow down search...)\n </div>\n </div>\n</div>\n');
5791
+ $templateCache.put('search-bs3.html','<form class="pull-right navbar-form">\n <div id="search-cg" class="form-group" ng-class="errorClass">\n <input type="text" spellcheck="false" autocomplete="off" id="searchinput" ng-model="searchTarget" ng-model-options="{debounce:250}" class="search-query form-control" placeholder="{{searchPlaceholder}}" ng-keyup="handleKey($event)">\n </div>\n</form>\n<div class="results-container" ng-show="results.length >= 1">\n <div class="search-results">\n <div ng-repeat="result in results">\n <a href="{{result.href}}" ng-class="resultClass($index)" title="{{result.additional}}">{{result.resourceText}} {{result.text}}</a>\n </div>\n <div ng-show="moreCount > 0">(plus more - continue typing to narrow down search...)\n </div>\n </div>\n</div>\n');}]);