-
Notifications
You must be signed in to change notification settings - Fork 6
/
response.go
505 lines (443 loc) · 15.3 KB
/
response.go
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
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
package page
import (
"encoding/json"
"fmt"
"github.com/goradd/gengen/pkg/maps"
"github.com/goradd/goradd/pkg/html"
"github.com/goradd/goradd/pkg/javascript"
"sync"
)
const (
ResponseControls = "controls"
ResponseCommandsHigh = "commandsHigh"
ResponseCommandsMedium = "commands"
ResponseCommandsLow = "commandsLow"
ResponseCommandsFinal = "commandsFinal"
ResponseRegC = "regc" // register control list
ResponseHtml = "html"
ResponseValue = "value"
ResponseId = "id"
ResponseAttributes = "attributes"
ResponseCss = "css"
ResponseClose = "winclose"
ResponseLocation = "loc"
ResponseAlert = "alert"
ResponseStyleSheets = "ss"
ResponseJavaScripts = "js"
)
// Priority orders the various responses to an Ajax request so that the framework can control the order they are processed,
// and not necessarily order the responses in the order they are sent.
type Priority int
const (
PriorityExclusive Priority = iota
PriorityHigh
PriorityStandard
PriorityLow
PriorityFinal // TODO: Note that this currently requires a preliminary ajax command, or it will not fire. Should fix that, but its tricky.
)
// responseCommand is a response packet that leads to execution of a javascript function
type responseCommand struct {
Script string `json:"script,omitempty"` // if just straight javascript
Selector string `json:"selector,omitempty"`
Id string `json:"id,omitempty"`
JqueryId string `json:"jqueryId,omitempty"`
Function string `json:"func,omitempty"`
Args []interface{} `json:"params,omitempty"`
Final bool `json:"final,omitempty"`
}
// responseControl is the response packet that leads to the manipulation or replacement of an html object
type responseControl struct {
Html string `json:"html,omitempty"` // replaces the entire control's html
Attributes map[string]string `json:"attributes,omitempty"`// replace only specific attributes of the control
Value string `json:"value,omitempty"`// sets the control's value. See goradd.js val:
}
// Response contains the various commands you can send to the client in response to a goradd event.
// These commands are packed as JSON (for an Ajax response) or JavaScript (for a Server response),
// sent to the client, unpacked by JavaScript code in the goradd.js file, and then acted upon.
type Response struct {
sync.RWMutex // This was inserted here for very rare situations of simultaneous access, like in the test harness.
// exclusiveCommand is a single command that is sent by itself, overriding all other commands
exclusiveCommand *responseCommand
// highPriorityCommands are sent first
highPriorityCommands []*responseCommand
// mediumPriorityCommands are sent after high priority commands
mediumPriorityCommands []*responseCommand
// lowPriorityCommands are sent after medium priority commands
lowPriorityCommands []*responseCommand
// finalCommands are acted on after all other commands have been processed
finalCommands []*responseCommand
// jsFiles are JavaScript files that should be inserted into the page. This should rarely be used,
// but is needed in case the programmer inserts a control widget in response to an Ajax event,
// and that control depends on javascript that has not yet been sent to the client.
jsFiles *maps.SliceMap // Use slicemap to preserve the order
// styleSheets are css files that should be inserted into the page.
styleSheets *maps.SliceMap
// alerts are strings that should be shown to the user in a javascript alert
alerts []string
// newLocation is a URL that the client should be redirected to.
newLocation string
// winClose directs the browser to close the current window.
winClose bool
// controls are goraddControls that should be inserted or replaced
controls map[string]responseControl
// profileHtml is the html sent from the database profiling tool to display in a special window
// TODO: This is not used currently, and is here for future ajax db profiling
profileHtml string
}
// NewResponse creates a new event response.
func NewResponse() Response {
return Response{}
}
func (r *Response) displayAlert(message string) {
r.Lock()
r.alerts = append(r.alerts, message)
r.Unlock()
}
func (r *Response) AddClass(id string, class string, priorities ...Priority) {
r.ExecuteControlCommand(id, "class", "+" + class, priorities)
}
func (r *Response) RemoveClass(id string, class string, priorities ...Priority) {
r.ExecuteControlCommand(id, "class", "-" + class, priorities)
}
func (r *Response) SetClass(id string, class string, priorities ...Priority) {
r.ExecuteControlCommand(id, "class", class, priorities)
}
// ExecuteJavaScript will execute the given code with the given priority. Note that all javascript code is run in
// strict mode.
func (r *Response) ExecuteJavaScript(js string, priorities ...Priority) {
var priority = PriorityStandard
if priorities != nil {
if len(priorities) == 1 {
priority = priorities[0]
} else {
panic("Don't call ExecuteJavaScript with arguments")
}
}
c := responseCommand{Script: js}
r.postCommand(&c, priority)
}
// ExecuteControlCommand executes the named command on the given control. Possible commands are defined
// by the goradd widget class in the javascript file.
func (r *Response) ExecuteControlCommand(controlID string, functionName string, args ...interface{}) {
args2,priority := r.extractPriority(args...)
c := responseCommand{Id: controlID, Function: functionName, Args: args2}
r.postCommand(&c, priority)
}
// ExecuteJqueryCommand executes the named jquery command on the given jquery control.
func (r *Response) ExecuteJqueryCommand(controlID string, functionName string, args ...interface{}) {
args2,priority := r.extractPriority(args...)
c := responseCommand{JqueryId: controlID, Function: functionName, Args: args2}
r.postCommand(&c, priority)
}
// ExecuteSelectorFunction calls a goradd function on a group of objects defined by a selector.
func (r *Response) ExecuteSelectorFunction(selector string, functionName string, args ...interface{}) {
args2,priority := r.extractPriority(args...)
c := responseCommand{Selector: selector, Function: functionName, Args: args2}
r.postCommand(&c, priority)
}
// ExecuteJsFunction calls the given JavaScript function with the given arguments.
// If the function name has a dot(.) in it, the items preceeding the dot will be considered global objects
// to call the function on. If the named function just a function label, then the function is called on the window object.
func (r *Response) ExecuteJsFunction(functionName string, args ...interface{}) {
args2,priority := r.extractPriority(args...)
c := responseCommand{Function: functionName, Args: args2}
r.postCommand(&c, priority)
}
func (r *Response) postCommand(c *responseCommand, priority Priority) {
r.Lock()
switch priority {
case PriorityExclusive:
r.exclusiveCommand = c
case PriorityHigh:
r.highPriorityCommands = append(r.highPriorityCommands, c)
case PriorityStandard:
r.mediumPriorityCommands = append(r.mediumPriorityCommands, c)
case PriorityLow:
r.lowPriorityCommands = append(r.lowPriorityCommands, c)
case PriorityFinal:
c.Final = true
r.finalCommands = append(r.finalCommands, c)
}
r.Unlock()
}
func (r *Response) extractPriority (args ...interface{}) (args2 []interface{}, priority Priority) {
for i,a := range args {
if p,ok := a.(Priority); ok {
priority = p
args2 = append(args[:i], args[i+1:]...)
return
}
}
priority = PriorityStandard
args2 = args
return
}
// One time add of style sheets, to be used by FormBase only for last minute style sheet injection.
func (r *Response) addStyleSheet(path string, attributes html.Attributes) {
if r.styleSheets == nil {
r.styleSheets = maps.NewSliceMap()
}
r.styleSheets.Set(path, attributes)
}
// Add javascript files to the response.
func (r *Response) addJavaScriptFile(path string, attributes html.Attributes) {
if r.jsFiles == nil {
r.jsFiles = maps.NewSliceMap()
}
r.jsFiles.Set(path, attributes)
}
// JavaScript renders the Response object as JavaScript that will be inserted into the page sent back to the
// client in response to a Server action.
func (r *Response) JavaScript() (script string) {
r.Lock()
// Style sheet injection by a control. Not very common, as other ways of adding style sheets would normally be done first.
if r.styleSheets != nil {
for _, s := range r.styleSheets.Keys() {
script += `goradd.loadStyleSheetFile("` + s + `", "all);\n"`
}
r.styleSheets = nil
}
// alerts
if r.alerts != nil {
for _, a := range r.alerts {
b, err := json.Marshal(a)
if err != nil {
panic(err)
}
script += fmt.Sprintf("goradd.msg(%s);\n", b[:])
}
r.alerts = nil
}
if r.highPriorityCommands != nil {
script += r.renderCommandArray(r.highPriorityCommands)
r.highPriorityCommands = nil
}
if r.mediumPriorityCommands != nil {
script += r.renderCommandArray(r.mediumPriorityCommands)
r.mediumPriorityCommands = nil
}
if r.lowPriorityCommands != nil {
script += r.renderCommandArray(r.lowPriorityCommands)
r.lowPriorityCommands = nil
}
// A redirect
if r.newLocation != "" {
script += fmt.Sprintf(`goradd.redirect("%s");`+"\n", r.newLocation)
r.newLocation = ""
}
// A window close
if r.winClose {
script += "window.close();\n"
r.winClose = false
}
r.Unlock()
return script
}
func (r *Response) renderCommandArray(commands []*responseCommand) string {
var script string
for _, command := range commands {
if command.Script != "" {
script += command.Script + ";\n"
} else {
com := make(map[string]interface{})
if command.Selector != "" {
com["selector"] = command.Selector
}
if command.Id != "" {
com["id"] = command.Id
}
if command.JqueryId != "" {
com["jqueryId"] = command.Id
}
if command.Function != "" {
com["func"] = command.Function
}
if command.Args != nil {
com["params"] = command.Args
}
script += fmt.Sprintf("goradd.processCommand(%s);\n", javascript.ToJavaScript(com))
}
}
return script
}
// GetAjaxResponse returns the JSON for use by the form ajax response.
// It will also reset the response
func (r *Response) GetAjaxResponse() (buf []byte, err error) {
var reply = map[string]interface{}{}
r.Lock()
if r.exclusiveCommand != nil {
// only render This one;
reply[ResponseCommandsMedium] = []responseCommand{*r.exclusiveCommand}
r.exclusiveCommand = nil
} else {
var commands []*responseCommand
if r.highPriorityCommands != nil {
commands = append(commands, r.highPriorityCommands...)
r.highPriorityCommands = nil
}
if r.mediumPriorityCommands != nil {
commands = append(commands, r.mediumPriorityCommands...)
r.mediumPriorityCommands = nil
}
if r.lowPriorityCommands != nil {
commands = append(commands, r.lowPriorityCommands...)
r.lowPriorityCommands = nil
}
if r.finalCommands != nil {
commands = append(commands, r.finalCommands...)
r.finalCommands = nil
}
if commands != nil && len(commands) > 0 {
reply["commands"] = commands
}
if r.jsFiles != nil {
reply[ResponseJavaScripts] = r.jsFiles
r.jsFiles = nil
}
if r.styleSheets != nil {
reply[ResponseStyleSheets] = r.styleSheets
r.styleSheets = nil
}
// alerts
if r.alerts != nil {
reply[ResponseAlert] = r.alerts
r.alerts = nil
}
if r.controls != nil {
reply[ResponseControls] = r.controls
r.controls = nil
}
if r.newLocation != "" {
reply[ResponseLocation] = r.newLocation
r.newLocation = ""
}
if r.winClose {
reply[ResponseClose] = 1
r.winClose = false
}
}
r.Unlock()
return json.Marshal(reply)
}
// Call SetLocation to change the url of the browser.
func (r *Response) SetLocation(newLocation string) {
r.Lock()
r.newLocation = newLocation
r.Unlock()
}
// Call CloseWindow to close the current window.
func (r *Response) CloseWindow() {
r.Lock()
r.winClose = true
r.Unlock()
}
func (r *Response) hasExclusiveCommand() bool {
r.RLock()
v := r.exclusiveCommand != nil
r.RUnlock()
return v
}
// SetControlHtml will cause the given control's html to be completely replaced by the given HTML.
func (r *Response) SetControlHtml(id string, html string) {
r.Lock()
if r.controls == nil {
r.controls = map[string]responseControl{}
}
if v, ok := r.controls[id]; ok && v.Html != "" {
r.Unlock()
panic("Setting ajax html twice on same control: " + id)
}
r.controls[id] = responseControl{Html: html}
r.Unlock()
}
// SetControlAttribute sets the named html attribute on the control to the given value.
func (r *Response) SetControlAttribute(id string, attribute string, value string) {
r.Lock()
if r.controls == nil {
r.controls = map[string]responseControl{}
}
if v, ok := r.controls[id]; ok {
if v.Html == "" { // only do attributes if whole control is not being redrawn
if v.Attributes != nil {
v.Attributes[attribute] = value
} else {
v.Attributes = map[string]string{attribute: value}
}
}
} else {
r.controls[id] = responseControl{Attributes: map[string]string{attribute: value}}
}
r.Unlock()
}
// SetControlValue calls the jQuery ".val()" function on the given control, passing it the given value.
func (r *Response) SetControlValue(id string, value string) {
r.Lock()
if r.controls == nil {
r.controls = map[string]responseControl{}
}
r.controls[id] = responseControl{Value: value}
r.Unlock()
}
func (r *Response) setProfileInfo(info string) {
r.Lock()
r.profileHtml = info
r.Unlock()
}
// use an encoder since some fields could be nil
type responseEncoded struct {
ExclusiveCommand *responseCommand
HighPriorityCommands []*responseCommand
MediumPriorityCommands []*responseCommand
LowPriorityCommands []*responseCommand
FinalCommands []*responseCommand
JsFiles *maps.SliceMap
StyleSheets *maps.SliceMap
Alerts []string
NewLocation string
WinClose bool
Controls map[string]responseControl
ProfileHtml string
}
// Serialize encodes the response for the pagestate. Currently, serialization of the response is only
// used by the testing framework.
func (r *Response) Serialize(e Encoder) (err error) {
enc := responseEncoded{
ExclusiveCommand: r.exclusiveCommand,
HighPriorityCommands: r.highPriorityCommands,
MediumPriorityCommands: r.mediumPriorityCommands,
LowPriorityCommands: r.lowPriorityCommands,
FinalCommands: r.finalCommands,
JsFiles: r.jsFiles,
StyleSheets: r.styleSheets,
Alerts: r.alerts,
NewLocation: r.newLocation,
WinClose: r.winClose,
Controls: r.controls,
ProfileHtml: r.profileHtml,
}
if err = e.Encode(enc); err != nil {
panic(err)
}
return
}
// Deserialize unpacks the response from the pagestate. Currently the response is only serialized
// in the testing framework.
func (r *Response) Deserialize(d Decoder) (err error) {
enc := responseEncoded{}
if err = d.Decode(&enc); err != nil {
panic(err)
}
r.exclusiveCommand = enc.ExclusiveCommand
r.highPriorityCommands = enc.HighPriorityCommands
r.mediumPriorityCommands = enc.MediumPriorityCommands
r.lowPriorityCommands = enc.LowPriorityCommands
r.finalCommands = enc.FinalCommands
r.jsFiles = enc.JsFiles
r.styleSheets = enc.StyleSheets
r.alerts = enc.Alerts
r.newLocation = enc.NewLocation
r.winClose = enc.WinClose
r.controls = enc.Controls
r.profileHtml = enc.ProfileHtml
return
}