-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathqs-emergo-triggers.js
378 lines (320 loc) · 9.92 KB
/
qs-emergo-triggers.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
/**
* E-mergo Triggers Extension
*
* @since 20180821
* @author Laurens Offereins <https://github.com/lmoffereins>
*
* @param {Object} $q Angular's promise library
* @param {Object} props Property panel definition
* @param {Object} initProps Initial properties
* @param {Object} emergoEvents E-mergo Events API
* @param {Object} emergoActions E-mergo Actions API
* @param {Object} util E-mergo utility functions
* @param {String} css Extension stylesheet
* @param {String} tmpl Extension template file
* @return {Object} Extension structure
*/
define([
"ng!$q",
"./properties",
"./initial-properties",
"./util/qs-emergo-events",
"./util/qs-emergo-actions",
"./util/util",
"text!./style.css",
"text!./template.ng.html"
], function( $q, props, initProps, emergoEvents, emergoActions, util, css, tmpl ) {
// Add global styles to the page
util.registerStyle("qs-emergo-triggers", css);
/**
* Extension controller function
*
* @param {Object} $scope Extension scope
* @return {Void}
*/
var controller = ["$scope", function( $scope ) {
/**
* Holds the list of event objects (from triggers)
*
* @type {Array}
*/
var events = [],
/**
* Cache functions for this controller
*
* @type {Object} Cache functions
*/
cache = util.createCache("qs-emergo-triggers/" + $scope.$id),
/**
* Deregister method after registering scoped extension object css
*
* @type {Function}
*/
deregisterStyle = util.registerObjStyle($scope.layout.qInfo.qId, {
hideNav: true // Hide the object's nav items
});
/**
* Trigger select handler
*
* @param {Object} trigger Trigger data
* @return {Void}
*/
$scope.do = function( trigger ) {
// Execute button actions when not editing the sheet and interaction is allowed
if (! $scope.object.inEditState() && ! ($scope.options && $scope.options.noInteraction)) {
emergoActions.doMany(trigger, $scope).then( function( done ) {
// Evaluate navigation settings
return (false !== done) && emergoActions.doNavigation(trigger, $scope);
}).catch(console.error);
}
};
/**
* Run setup methods on all event listeners
*
* @param {Array} triggerIds Optional. Trigger ids for which to setup event listeners. Defaults to all triggers.
* @return {Promise} Event listeners are setup
*/
$scope.setupEventListeners = function( triggerIds ) {
var setupable = [];
// Get the selected triggers from the extension's layout
if (! _.isEmpty(triggerIds)) {
setupable = $scope.layout.props.triggers.filter( function( i ) {
return -1 !== triggerIds.indexOf(i.cId);
});
// Get all registered triggers from the extension's layout
} else if ("undefined" === typeof triggerIds ) {
setupable = $scope.layout.props.triggers;
}
// Walk all setupables
return setupable.reduce( function( promise, trigger ) {
return promise.then( function() {
var setupTrigger = emergoEvents.options[trigger.event];
/**
* Setup event listener by mounting it's trigger logic
*
* The `setupTrigger` method uses the registered layout properties in the `trigger` object
* to setup the event listener. Using the `mount` method on the trigger's `event` object
* the eventual do-action callback gets registered. This callback first checks at the moment
* the actual event is fired whether the trigger's conditions are still met (enabled,
* run-trigger-if).
*/
return setupTrigger && setupTrigger(trigger, $scope).then( function( event ) {
var dfd = $q.defer();
// Add event to the set of registered events
if (event) {
event.cId = trigger.cId;
events.push(event);
}
// Mount event listener
if (event && event.mount && trigger.enabled) {
event.mount.call($scope, function() {
// Get the fresh trigger data from the layout at this point. Various parts of the
// trigger data could have been re-evaluated in between mounting and triggering.
var item = $scope.layout.props.triggers.find( function( i ) {
return i.cId === trigger.cId;
});
// Run trigger when it is enabled and considered active
if (item && isTriggerActive(item)) {
$scope.do(item);
}
}).then(dfd.resolve).catch(console.error);
} else {
dfd.resolve();
}
return dfd.promise;
});
});
}, $q.resolve());
};
/**
* Run destroy methods on event listeners
*
* @param {Array} triggerIds Optional. Trigger ids of which to destroy their event listeners. Defaults to all triggers.
* @return {Promise} Event listeners are destroyed
*/
$scope.destroyEventListeners = function( triggerIds ) {
var destroyable = [];
// Get the registered events for selected triggers
if (! _.isEmpty(triggerIds)) {
destroyable = events.filter( function( event ) {
return !! triggerIds.find( function( cId ) {
return cId === event.cId;
});
});
// Get all registered events
} else if ("undefined" === typeof triggerIds) {
destroyable = events;
}
// Walk all destroyables
return destroyable.reduce( function( promise, event ) {
return promise.then( function() {
// Run destroy sequence for the event
if ("function" === typeof event.destroy) {
event.destroy.call($scope);
}
// Remove this event
events = _.reject(events, function( i ) {
return i.cId === event.cId;
});
});
}, $q.resolve());
};
/**
* Reset methods on all event listeners
*
* @return {Promise} Event listeners are reset
*/
$scope.resetEventListeners = function() {
return $scope.destroyEventListeners().then( function() {
return $scope.setupEventListeners();
});
};
/**
* Return the text displaying the registered trigger count
*
* @return {String} Trigger count message
*/
$scope.triggerCount = function() {
var count = $scope.layout.props.triggers.length;
return count
? (count > 1 ? "There are " + count + " triggers registered." : "There is 1 trigger registered.")
: "There are no triggers registered.";
};
/**
* Clean up when the controller is destroyed
*
* @return {Void}
*/
$scope.$on("$destroy", function() {
// Clear the controller's cache
cache.clear();
// Remove the registered style
deregisterStyle();
});
}],
/**
* Return whether the trigger's calculation condition is positive
*
* @param {Object} trigger Trigger data
* @return {Boolean} Is the trigger active?
*/
isTriggerActive = function( trigger ) {
return util.booleanFromExpression(trigger.active);
},
/**
* List of event properties to monitor for changes
*
* @type {Array}
*/
monitoredEventProps = ["eitherOr", "enabled", "event", "field", "state", "theme", "timePassedDuration", "timePassedStartAfter", "value", "variable"],
/**
* Determine which triggers should be added or removed
*
* @param {Array} newTriggers New list of triggers
* @param {Array} oldTriggers Old list of triggers
* @return {Object} Changed triggers
*/
getChangedTriggers = function( newTriggers, oldTriggers ) {
var toAdd = [], toRemove = [], prevTriggers;
// Walk the new triggers
newTriggers.forEach( function( i ) {
// Find the previous version
prevTriggers = oldTriggers.find( function( j ) {
return j.cId === i.cId;
});
// Trigger is new
if (! prevTriggers) {
toAdd.push(i.cId); // Use ids for referencing...
// Trigger is maybe changed
} else {
// Check the list of relevant trigger properties
var isChanged = monitoredEventProps.reduce( function( is, prop ) {
return is || i[prop] !== prevTriggers[prop];
}, false);
// Trigger is changed
if (isChanged) {
toRemove.push(i.cId);
toAdd.push(i.cId);
}
}
});
// Walk the old triggers
oldTriggers.forEach( function( i ) {
// Trigger is not in the new set
if (! newTriggers.find( function( j ) {
return j.cId === i.cId;
})) {
toRemove.push(i.cId);
return;
}
});
return {
to: {
add: toAdd,
remove: toRemove
}
};
};
return {
definition: props,
initialProperties: initProps,
template: tmpl,
controller: controller,
/**
* Setup listeners and watchers when the object is mounted
*
* NOTE: this hook is available since QS April 2018
*
* @return {Void}
*/
mounted: function() {
var context = this;
// Initial setup of existing event listeners
this.$scope.setupEventListeners();
// Act when the list of triggers is updated
this.$scope.$watchCollection("layout.props.triggers", function( newValue, oldValue ) {
var triggers = getChangedTriggers(newValue, oldValue);
// Bail when no changes are noticed
if (_.isEmpty(triggers.to.add) && _.isEmpty(triggers.to.remove)) {
return;
}
// Tear down the existing event listeners
context.$scope.destroyEventListeners(triggers.to.remove).then( function() {
// Setup the event listeners
context.$scope.setupEventListeners(triggers.to.add);
});
});
// Act when the object's state is updated
this.$scope.$watch("layout.qStateName", function( newValue, oldValue ) {
// On change, do a full reset of all event listeners
if (newValue !== oldValue) {
context.$scope.resetEventListeners();
}
});
// Run events mounter
emergoEvents.mount(this.$scope);
// Run actions mounter
emergoActions.mount(this.$scope);
},
/**
* Clean-up before the extension object is destroyed
*
* NOTE: this hook is available since QS April 2018
*
* @return {Void}
*/
beforeDestroy: function() {
// Remove all event listeners
this.$scope.destroyEventListeners();
// Run events destroyer
emergoEvents.destroy(this.$scope);
// Run actions destroyer
emergoActions.destroy(this.$scope);
},
support: {
snapshot: false,
export: false,
exportData: false
}
};
});