Proxy URLs soon rather than late
[sitka/overdrive-evergreen-opac.git] / src / od_action.coffee
CommitLineData
8ebc3fff
SC
1# TODO cannot auto-focus on close button of action dialog
2# probably because it needs to be done asynchronously using setTimeout
3#
4define [
5 'jquery'
6 'lodash'
7 'json'
8 'od_api'
9 'jquery-ui'
10], ($, _, json, od) ->
11
12 # Load a CSS file related to our use of the jqueryui dialog widget.
13 # We manually load the file in order to avoid modifying any .tt2 files.
14 do (url = '//ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/themes/smoothness/jquery-ui.min.css') ->
15 link = document.createElement('link')
16 link.type = 'text/css'
17 link.rel = 'stylesheet'
18 link.href = url
19 document.getElementsByTagName('head')[0].appendChild(link)
20
21 # Return an abbreviation of the pathname of the current page,
22 # eg, if window.location.pathname equals 'eg/opac/record' or
23 # 'eg/opac/record/123', then return 'record', otherwise return ''
24 brief_name = ->
25 xs = window.location.pathname.match /eg\/opac\/(.+)/
26 if xs then xs[1].replace /\/\d+/, '' else ''
27 # TODO also defined in od_page_rewrite, but we don't want this module to
28 # depend on that module, because it depends on this module.
29
30 # Pluck out a sensible message from the reply to an action request
31 responseMessage = (x) -> (json.parse x.responseText).message
32
33 # Customize the dialog widget to guide a user through the intention of
34 # making a transaction
35 #
36 # Usage: $('<div>').dialogAction action: action, scenario: scenario
37 #
38 $.widget 'ui.dialogAction', $.ui.dialog,
39 options:
40 draggable: false
41 resizable: true
42 modal: true
43 buttons: [
44 {
45 text: 'Yes'
46 click: (ev) ->
47 ev.stopPropagation()
48 $(@).dialogAction 'yes_action'
49 return
50 }
51 {
52 text: 'No'
53 click: (ev) ->
54 ev.stopPropagation()
55 $(@).dialogAction 'non_action'
56 return
57 }
58 ]
59
60 # On create, perform custom positioning, and show custom title and
61 # body. When the dialog finally closes, destroy it.
62 _create: ->
63
64 intent = @options._scenario?.intent
65 position = @options._action?._of
66
67 # Text of Yes/No buttons in the intent scenario may be overridden
68 if intent
69 ob = @options.buttons
70 ib = intent.buttons
71 ob[0].text = ib?[0] or 'Yes'
72 ob[1].text = ib?[1] or 'No'
73
74 # Position of dialog box may be overridden
75 @options.position = of: position, at: 'top', my: 'top' if position
76
77 @_super()
78
79 # On creation, dialog message may be overidden by the intent scenario
80 @set_message 'intent', false if intent
81
82 @_on 'dialogactionclose': -> @_destroy()
83
84 # Depending on the given scenario, the title and body of the dialog
85 # screen may be set, and the close button may be shown or hidden.
f46ca691 86 set_message: (scenario, close, body, title) ->
8ebc3fff 87 @_close_button close
f46ca691
SC
88 # Get the scenario properties
89 s = @options._scenario[scenario]
90 # The body is a text string specified as an argument or as the
91 # scenario's body property, or it defaults to a progress bar.
92 @element.empty().append body or s?.body or $('<div>').progressbar value: false
93 # The title is a text string specified as an argument or as the
94 # scenario's title property, or it defaults to the etitle of the
95 # attached action object.
96 @option 'title', title or s?.title or @options._action._of._etitle()
8ebc3fff
SC
97 return @
98
99 non_action: ->
100 @_on 'dialogactionclose': reroute if reroute = @options._scenario?.intent?.reroute
101 @close()
102 return @
103
104 # Respond to the Yes button by going ahead with the intended action
105 yes_action: ->
106
107 # At this point, dialog buttons are turned off.
108 @option 'buttons', []
109
110 # Make an API call
111 action = @options._action
8546cdf5
SC
112 progress = => @set_message 'progress', true
113 od.api action.href, action.method, fields: $('form', @element).serializeArray(), progress
8ebc3fff
SC
114
115 # Re-use the dialog to show notifications with a close button
116 .then(
117 (x) => @set_message 'done', true
118 (x) => @set_message 'fail', true, responseMessage x
119 )
120
121 # On done and when the user closes the dialog, reroute the page
122 .done =>
123 @_on 'dialogactionclose': reroute if reroute = this.options._scenario?.done?.reroute
124
125 return @
126
127 # Show or hide the dialog close button
128 _close_button: (close) ->
129 @element.parent()
130 .find('.ui-dialog-titlebar-close')[if close then 'show' else 'hide']()
131 .end()
132 .end()
133
134
135 # Map action names to labels
136 # TODO also use this mapping in scenarios
137 Labels =
138 hold: 'Place hold'
139 addSuspension: 'Suspend'
140 releaseSuspension: 'Activate'
141 removeHold: 'Cancel'
142 checkout: 'Check out'
143 earlyReturn: 'Return title'
144 format: 'Select format'
145
146
147 # We define custom jQuery extensions to perform the actions defined by the
148 # Labels object. The main role of each of these extensions is to build a
149 # scenario object that specifies the layout and behaviour of an instance of
150 # the action dialog widget. The specification is dependent on the content
151 # of a given action object and hence the scenario object must be built
152 # dynamically.
153 #
154 # TODO Since all of these extensions end with making an identical call to
155 # the dialogAction widget, it would be good to abstract the call to the
156 # outside environment, perhaps redefine the extensions as simple functions.
157 # eg, fn:: action -> scenario
158 #
159 # TODO Map action names to re-routed page names. The rerouting function
160 # depends on current page, current action, and current scenario
161 #
162 $.fn.extend
163
164 # Build a dialog to place a hold
165 _hold: (action) ->
166
167 scenario =
168 intent:
169 body: $('<div>')._action_fields action.fields
170 buttons: [ 'Place hold', 'Cancel' ]
171 # TODO clicking cancel should return to search results, not
172 # to rerouted page
173 reroute: -> window.history.back()
174 done:
175 body: 'Hold was successfully placed. Close this box to be redirected to your holds list.'
176 reroute: -> window.location.replace '/eg/opac/myopac/holds?e_items'
177 fail: body: 'Hold was not placed. There may have been a network or server problem. Please try again.'
8ebc3fff
SC
178
179 @dialogAction _scenario: scenario, _action: action
180
181 # Build a dialog to cancel a hold
182 _removeHold: (action) ->
183
184 scenario =
185 intent: body: 'Are you sure you want to cancel this hold?'
186 done: body: 'Hold was successfully cancelled.'
187 fail: body: 'Hold was not cancelled. There may have been a network or server problem. Please try again.'
8ebc3fff
SC
188
189 @dialogAction _scenario: scenario, _action: action
190
191 # Build a dialog to suspend a hold
192 _addSuspension: (action) ->
193
194 scenario =
195 intent:
196 body: $('<div>')._action_fields action.fields
197 buttons: [ 'Suspend', 'Cancel' ]
198 done: body: 'Hold was successfully suspended'
199 fail: body: 'Hold was not suspended. There may have been a network or server problem. Please try again.'
8ebc3fff
SC
200
201 @dialogAction _scenario: scenario, _action: action
202
203 # Build a dialog to release a suspension
204 _releaseSuspension: (action) ->
205
206 scenario =
207 intent: body: 'Are you sure you want this hold to activate again?'
208 done:
209 body: 'Suspension was successfully released. The page will reload to update your account status.'
210 reroute: -> window.location.reload true
211 fail: body: 'Suspension was not released. There may have been a network or server problem. Please try again.'
8ebc3fff
SC
212
213 @dialogAction _scenario: scenario, _action: action
214
215 # Build a dialog to checkout a title
216 _checkout: (action) ->
217
218 scenario =
219 intent:
220 body: $('<div>')._action_fields action.fields
221 buttons: [ 'Check out', 'Cancel' ]
222 reroute: ->
223 # if at placehold page, go back; otherwise, stay on same page
224 # TODO place_hold no longer relevant
225 window.history.back() if brief_name() is 'place_hold'
226 done:
227 body: 'Title was successfully checked out. Close this page to be redirected to your checkouts list.'
228 reroute: -> window.location.replace '/eg/opac/myopac/circs?e_items'
229 fail: body: 'Title was not checked out. There may have been a network or server problem. Please try again.'
8ebc3fff
SC
230
231 @dialogAction _scenario: scenario, _action: action
232
233 # Build a dialog to select a format of a title
234 _format: (action) ->
235
236 scenario =
237 intent:
238 body: $('<div>')._action_fields action.fields
239 buttons: [ 'Select format', 'Cancel' ]
240 done: body: 'Format was successfully selected.'
241 fail: body: 'Format was not selected. There may have been a network or server problem. Please try again.'
8ebc3fff
SC
242
243 @dialogAction _scenario: scenario, _action: action
244
245 # Build a dialog to return a title early
246 _earlyReturn: (action) ->
247
248 scenario =
249 intent: body: 'Are you sure you want to return this title before it expires?'
250 done: body: 'Title was successfully returned.'
251 fail: body: 'Title was not returned. There may have been a network or server problem. Please try again.'
8ebc3fff
SC
252
253 @dialogAction _scenario: scenario, _action: action
254
255 # Build format buttons given specifications as follows.
256 # formats = [ { formatType: type, linkTemplates: { downloadLink: { href: href } } } ]
257 # actions = { downloadLink: { href: href, method: get, type: type } }
258 #
259 # TODO no need to define an action dialog because this is the only example of a get action
260 # and we can allow the default behaviour to occur.
261 #
262 # Do we need a dialogDownload widget?
263 # Confirm -> HTTP GET downloadLink.
264 # Fail -> Browser navigates to errorURL and shows error status.
265 # Done -> Response is a contentLink. HTTP Get contentLink.
266
267 _formats: (formats) ->
268 return @ unless formats
269
270 tpl = _.template """
271 <div>
272 <a href="<%= href %>" class="opac-button" style="margin-top: 0px; margin-bottom: 0px"><%= label %></a>
273 </div>
274 """
275
276 $buttons = for format in formats
277 {
278 formatType: n
279 linkTemplates:
280 downloadLink:
281 href: href
282 type: type
283 } = format
284
285 # Create a button for this action
286 $ tpl href: href, label: "Download #{od.labels n}"
287
288 @empty().append $buttons
289
290 # Build action buttons and dialogs given specifications as follows.
291 # actions = [ { name: { href: h, method: m, fields: [ { name: n, value: v, options: [...] } ] } ]
292 _actions: (actions) ->
293
294 tpl = _.template """
295 <div>
07895f87 296 <a href="<%= href %>" class="opac-button <%= action_name %>" style="margin-top: 0px; margin-bottom: 0px"><%= label %></a>
8ebc3fff
SC
297 </div>
298 """
299
300 # Find the related row
301 $tr = @closest('tr')
302
303 $buttons = for n, action of actions
304
305 # Extend the action object with context
306 $.extend action, _of: $tr, _name: n
307
308 # Create a button for this action
07895f87 309 $ tpl href: action.href, action_name: n, label: Labels?[n] or n
8ebc3fff
SC
310
311 # On clicking the button, build a new dialog using the extended action object
312 .on 'click', action, (ev) ->
313 ev.preventDefault()
314 $('<div>')['_' + ev.data._name] ev.data
315 # TODO apply dialogAction method directly as follows.
316 #$('<div>').dialogAction _scenario: Actions.scenario[ev.data._name], _action: ev.data
317 return false
318
319 @empty().append $buttons
320
321 # Build a form of input fields out of a list of action fields
322 _action_fields: (fields) ->
323
324 $('<form>')
325 ._action_field_hidden _.where(fields, name: 'reserveId')[0]
326 ._action_field_email _.where(fields, name: 'emailAddress')[0]
327 ._action_field_radio _.where(fields, name: 'formatType')[0]
328 ._action_field_suspend _.where(fields, name: 'suspensionType')[0]
329 ._action_field_date _.where(fields, name: 'numberOfDays')[0]
330
331 # Show a date field only if suspensionType of indefinite is selected
332 .on 'click', '[name=suspensionType]', (ev) ->
333 $input = $('[name=numberOfDays]')
334 switch @defaultValue
335 when 'limited' then $input.show()
336 when 'indefinite' then $input.hide()
337
338 .on 'submit', (ev) ->
339 $input = $('[name=numberOfDays]')
340
341 # Build a date field and initially hide it
342 _action_field_date: (field) ->
343
344 return @ unless field
345
346 $input = $ """
347 <input type="date" name="#{field.name}" value="#{field.value}" />
348 """
349 @append $input.hide()
350
351 # Build a hidden input
352 _action_field_hidden: (field) ->
353
354 return @ unless field
355
356 @append """
357 <input type="hidden" name="#{field.name}" value="#{field.value}" />
358 """
359
360 # Build an email input
361 _action_field_email: (field) ->
362
363 return @ unless field
364
365 $input = $ """
366 <div>
367 You will be notified by email when a copy becomes available
368 </div>
369 <div>
370 <label>Email address: <input type="email" name="#{field.name}" value="#{field.value}" />
371 </label>
372 </div>
373 """
374 $input.find('input').prop 'required', true unless Boolean field.optional
375
376 @append $input
377
378 # Build a group of radio buttons
379 _action_field_radio: (field) ->
380
381 return @ unless field
382
383 # If one of the format types is ebook reader, omit it from the
384 # list, because it is not a downloadable type
385 _.remove field.options, (f) -> f is 'ebook-overdrive'
386
387 # A hint specific to whether format types are optional or not is added to the form
388 hint = switch
389 when field.options.length is 1 then """
390 <div>Only one #{field.name} is available and it has been selected for you</div>
391 """
392 when Boolean field.optional then """
393 <div>You may select a #{field.name} at this time</div>
394 """
395 else """
396 <div>Please select one of the available #{field.name}s</div>
397 """
398 inputs = for v in field.options
399 $x = $ """
400 <div>
401 <input type="radio" name="#{field.name}" value="#{v}" />#{od.labels v}
402 </div>
403 """
404 $y = $x.find 'input'
405 $y.prop('required', true) unless Boolean field.optional
406 $y.prop('checked', true) unless field.options.length > 1
407 $x
408
409 @append hint
410 .append inputs
411
412 _action_field_suspend: (field) ->
413
414 return @ unless field
415
416 label =
417 indefinite: 'Suspend this hold indefinitely'
418 limited: 'Suspend this hold for a limited time'
419
420 inputs = for v in field.options
421 $x = $ """
422 <div><label>
423 <input type="radio" name="#{field.name}" value="#{v}" /> #{label[v]}
424 </label></div>
425 """
426 $y = $x.find 'input'
427 $y.prop('required', true) unless Boolean field.optional
428 $y.prop('checked', true) if v is 'indefinite'
429 $x
430
431 @append inputs
432
433 # We will delegate the handling of download links to the page's
434 # tbody. The sequence of operation is as follows. We need to get
435 # from a download link to make the download request, receive a
436 # content link as a response, and then perform a 'normal' get of
437 # the content link. A complexity is to handle the error responses.
438 #
439 # TODO The initial get could fail, in which case, the errorpageurl
440 # will be used to convey the failure status as a query string. We
441 # will redirect the error page to the current page and we recognize
442 # the error condition by analysing the query parameters.
443 #
444 # TODO Another error condition could occur if an Overdrive Read
445 # ebook is attempted to be downloaded. Here, the odreadauthurl
446 # will be used as a redirect location. We will also redirect to the
447 # current page and hopefully will be able to discern the state and
448 # show it accordingly.
449 #
450 _download_format: ->
451 @on 'click', 'td.formats a', (ev) ->
452 ev.preventDefault()
453
454 # We will return to the current page to handle errors
455 x = encodeURIComponent window.location.href
456 dl = @href
457 .replace /\{errorpageurl\}/, x
458 .replace /\{odreadauthurl\}/, x
459
460 od.api dl
461 .then(
462 (x) ->
463 window.open(
508513a1 464 x.links.contentlink.href # url
8ebc3fff
SC
465 '_blank' #'Overdrive Read format' # title
466 'resizable, scrollbars, status, menubar, toolbar, personalbar' # features
467 )
468 -> console.log 'failed to get download link'
469 )
470 .then(
471 -> # not expected to arrive here ever
472 -> console.log 'failed to get contentLink'
473 )
474
475 return
476
477
478 _notify: (title, text) ->
479
480 @dialog
481 position: my: 'top', at: 'right top'
482 minHeight: 0
483 autoOpen: true
484 draggable: false
485 resizable: true
486 show: effect: 'slideDown'
487 hide: effect: 'slideUp'
488 close: -> $(@).dialog 'destroy'
489 title: title
490
491 .text text
492
493 # Get the title of e-item in a row context
494 _etitle: -> @find('.title a').text()