Change button label from "editHold" to "Edit Hold"
[sitka/overdrive-evergreen-opac.git] / src / od_action.coffee
1 # TODO cannot auto-focus on close button of action dialog
2 # probably because it needs to be done asynchronously using setTimeout
3 #
4 define [
5         'jquery-noconflict'
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 'autoclose'
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                         f = @options._scenario.intent?.body
81                         @set_message 'intent', not intent, if $.isFunction f then f @options._action?._name else undefined
82
83                         @_on 'dialogactionclose': -> @_destroy()
84
85                 # Depending on the given scenario, the title and body of the dialog
86                 # screen may be set, and the close button may be shown or hidden.
87                 set_message: (scenario, close, body, title) ->
88                         @_close_button close
89                         # Get the scenario properties
90                         s = @options._scenario[scenario]
91                         # The body is a text string specified as an argument or as the
92                         # scenario's body property, or it defaults to a progress bar.
93                         @element.empty().append body or s?.body or $('<div>').progressbar value: false
94                         # The title is a text string specified as an argument or as the
95                         # scenario's title property, or it defaults to the etitle of the
96                         # attached action object.
97                         @option 'title', title or s?.title or @options._action._of._etitle()
98                         return @
99
100                 autoclose: ->
101                         @_on 'dialogactionclose': @options._scenario?.intent?.reroute or ->
102                         @close()
103                         return @
104
105                 # Respond to the Yes button by going ahead with the intended action
106                 yes_action: ->
107
108                         # At this point, dialog buttons are turned off.
109                         @option 'buttons', []
110
111                         # Define functions to close dialogue box and reroute to another window
112                         onclose_maybe_reroute = =>
113                                 @_on 'dialogactionclose': @options._scenario?.done?.reroute or ->
114                         close = =>
115                                 onclose_maybe_reroute()
116                                 @close()
117
118                         # Make an API call
119                         action = @options._action
120                         progress = => @set_message 'progress', true
121                         od.api action.href, action.method, fields: $('form', @element).serializeArray(), progress
122
123                         # Re-use the dialog to show notifications with a close button
124                         .then(
125                                 (x) =>
126                                         f = @options._scenario.done?.body
127                                         @set_message 'done', true, if $.isFunction f then f x else undefined
128                                         window.setTimeout close, @options._scenario.done?.timeout or 2000
129                                 (x) => @set_message 'fail', true, responseMessage x
130                         )
131
132                         # On done and when the user closes the dialog, reroute the page
133                         .done onclose_maybe_reroute
134
135                         return @
136
137                 # Show and focus, or hide, the dialog close button.
138                 _close_button: (show) ->
139                         $button = @element.parent().find('.ui-dialog-titlebar-close')
140                         if show then $button.show().focus() else $button.hide()
141                         return @
142
143
144         # Map action names to labels
145         # TODO also use this mapping in scenarios
146         Labels =
147                 hold: 'Place hold'
148                 addSuspension: 'Suspend'
149                 releaseSuspension: 'Activate'
150                 editHold: 'Edit Hold'
151                 removeHold: 'Cancel'
152                 checkout: 'Check out'
153                 earlyReturn: 'Return title'
154                 format: 'Select format'
155
156
157         # We define custom jQuery extensions to perform the actions defined by the
158         # Labels object.  The main role of each of these extensions is to build a
159         # scenario object that specifies the layout and behaviour of an instance of
160         # the action dialog widget.  The specification is dependent on the content
161         # of a given action object and hence the scenario object must be built
162         # dynamically.
163         #
164         $.fn.extend
165
166                 # Build a dialog to place a hold
167                 _hold: (action) ->
168
169                         scenario =
170                                 intent:
171                                         body: $('<div>')._action_fields action.fields
172                                         buttons: [ 'Place hold', 'Cancel' ]
173                                         # TODO clicking cancel should return to search results, not
174                                         # to rerouted page
175                                         reroute: -> window.history.back()
176                                 done:
177                                         body: 'Hold was successfully placed. Close this box to be redirected to your holds list.'
178                                         reroute: -> window.location.replace '/eg/opac/myopac/holds?e_items'
179                                 fail: body: 'Hold was not placed. There may have been a network or server problem. Please try again.'
180
181                         @dialogAction _scenario: scenario, _action: action
182
183                 # Build a dialog to cancel a hold
184                 _removeHold: (action) ->
185
186                         scenario =
187                                 intent: body: 'Are you sure you want to cancel this hold?'
188                                 done: body: 'Hold was successfully cancelled.'
189                                 fail: body: 'Hold was not cancelled. There may have been a network or server problem. Please try again.'
190
191                         @dialogAction _scenario: scenario, _action: action
192
193                 # Build a dialog to suspend a hold
194                 _addSuspension: (action) ->
195
196                         scenario =
197                                 intent:
198                                         body: $('<div>')._action_fields action.fields
199                                         buttons: [ 'Suspend', 'Cancel' ]
200                                 done: body: 'Hold was successfully suspended'
201                                 fail: body: 'Hold was not suspended. There may have been a network or server problem. Please try again.'
202
203                         @dialogAction _scenario: scenario, _action: action
204
205                 # Build a dialog to release a suspension
206                 _releaseSuspension: (action) ->
207
208                         scenario =
209                                 intent: body: 'Are you sure you want this hold to activate again?'
210                                 done:
211                                         body: 'Suspension was successfully released. The page will reload to update your account status.'
212                                         reroute: -> window.location.reload true
213                                 fail: body: 'Suspension was not released. There may have been a network or server problem. Please try again.'
214
215                         @dialogAction _scenario: scenario, _action: action
216
217                 # Build a dialog to checkout a title
218                 _checkout: (action) ->
219
220                         scenario =
221                                 intent:
222                                         body: $('<div>')._action_fields action.fields
223                                         buttons: [ 'Check out', 'Cancel' ]
224                                         reroute: ->
225                                                 # if at placehold page, go back; otherwise, stay on same page
226                                                 # TODO place_hold no longer relevant
227                                                 window.history.back() if brief_name() is 'place_hold'
228                                 done:
229                                         body: 'Title was successfully checked out. Close this page to be redirected to your checkouts list.'
230                                         reroute: -> window.location.replace '/eg/opac/myopac/circs?e_items'
231                                 fail: body: 'Title was not checked out. There may have been a network or server problem. Please try again.'
232
233                         @dialogAction _scenario: scenario, _action: action
234
235                 # Build a dialog to select a format of a title
236                 _format: (action) ->
237
238                         scenario =
239                                 intent:
240                                         body: $('<div>')._action_fields action.fields
241                                         buttons: [ 'Select format', 'Cancel' ]
242                                 done: body: 'Format was successfully selected.'
243                                 fail: body: 'Format was not selected. There may have been a network or server problem. Please try again.'
244
245                         @dialogAction _scenario: scenario, _action: action
246
247                 # Build a dialog to return a title early
248                 _earlyReturn: (action) ->
249
250                         scenario =
251                                 intent: body: 'Are you sure you want to return this title before it expires?'
252                                 done: body: 'Title was successfully returned.'
253                                 fail: body: 'Title was not returned. There may have been a network or server problem. Please try again.'
254
255                         @dialogAction _scenario: scenario, _action: action
256
257                 _downloadLink: (action) ->
258
259                         scenario =
260                                 intent: body: (formatType) ->
261                                         if formatType is 'ebook-overdrive'
262                                                 'Download content to read in the browser?'
263                                         else
264                                                 'Download content for an e-book reader?'
265                                 done:
266                                         body: (x) ->
267                                                 $ """
268                                                 <div>Content is ready.</div>
269                                                 <a href=#{x.links.contentlink.href}>Right-click to download in a new tab or window.</a>
270                                                 <div>The link is valid for the next 60 seconds.</div>
271                                                 """
272                                         timeout: 59000
273                                 fail: body: 'could not download'
274
275                         @dialogAction _scenario: scenario, _action: action
276
277                 # Build action buttons and dialogs given specifications as follows.
278                 # actions = [ { name: { href: h, method: m, fields: [ { name: n, value: v, options: [...] } ] } ]
279                 _actions: (actions, id, suspended) ->
280
281                         tpl = _.template """
282                         <div>
283                                 <a href="<%= href %>" class="opac-button <%= action_name %>" style="margin-top: 0px; margin-bottom: 0px"><%= label %></a>
284                         </div>
285                         """
286
287                         # Find the related row
288                         $tr = @closest('tr')
289
290                         $buttons = for n, action of actions
291
292                                 unless suspended? and n is 'addSuspension'
293
294                                         # Extend the action object with context
295                                         $.extend action, _of: $tr, _name: n, _id: id
296
297                                         # Create a button for this action
298                                         $ tpl href: action.href, action_name: n, label: Labels?[n] or n
299
300                                         # On clicking the button, build a new dialog using the extended action object
301                                         .on 'click', action, (ev) ->
302                                                 ev.preventDefault()
303                                                 $('<div>')['_' + ev.data._name] ev.data
304                                                 return false
305
306                         @empty().append $buttons
307
308                 # Build a form of input fields out of a list of action fields
309                 _action_fields: (fields) ->
310
311                         $('<form>')
312                         ._action_field_hidden  _.where(fields, name: 'reserveId')[0]
313                         ._action_field_email   _.where(fields, name: 'emailAddress')[0]
314                         ._action_field_radio   _.where(fields, name: 'formatType')[0]
315                         ._action_field_suspend _.where(fields, name: 'suspensionType')[0]
316                         ._action_field_date    _.where(fields, name: 'numberOfDays')[0]
317
318                         # Show a date field only if suspensionType of indefinite is selected
319                         .on 'click', '[name=suspensionType]', (ev) ->
320                                 $input = $('[name=numberOfDays]')
321                                 switch @defaultValue
322                                         when 'limited'    then $input.show()
323                                         when 'indefinite' then $input.hide()
324
325                         .on 'submit', (ev) ->
326                                 $input = $('[name=numberOfDays]')
327
328                 # Build a date field and initially hide it
329                 _action_field_date: (field) ->
330
331                         return @ unless field
332
333                         $input = $ """
334                         <input type="date" name="#{field.name}" value="#{field.value}" />
335                         """
336                         @append $input.hide()
337
338                 # Build a hidden input
339                 _action_field_hidden: (field) ->
340
341                         return @ unless field
342
343                         @append """
344                         <input type="hidden" name="#{field.name}" value="#{field.value}" />
345                         """
346
347                 # Build an email input
348                 _action_field_email: (field) ->
349
350                         return @ unless field
351
352                         $input = $ """
353                         <div>
354                                 You will be notified by email when a copy becomes available
355                         </div>
356                         <div>
357                                 <label>Email address: <input type="email" name="#{field.name}" value="#{field.value}" />
358                                 </label>
359                         </div>
360                         """
361                         $input.find('input').prop 'required', true unless Boolean field.optional
362
363                         @append $input
364
365                 # Build a group of radio buttons
366                 _action_field_radio: (field) ->
367
368                         return @ unless field
369
370                         # If one of the format types is ebook reader, omit it from the
371                         # list, because it is not a downloadable type
372                         _.remove field.options, (f) -> f is 'ebook-overdrive'
373
374                         # A hint specific to whether format types are optional or not is added to the form
375                         hint = switch
376                                 when field.options.length is 1 then """
377                                         <div>Only one #{field.name} is available and it has been selected for you</div>
378                                         """
379                                 when Boolean field.optional then """
380                                         <div>You may select a #{field.name} at this time</div>
381                                         """
382                                 else """
383                                         <div>Please select one of the available #{field.name}s</div>
384                                         """
385                         inputs = for v in field.options
386                                 $x = $ """
387                                 <div>
388                                         <input type="radio" name="#{field.name}" value="#{v}" />#{od.labels v}
389                                 </div>
390                                 """
391                                 $y = $x.find 'input'
392                                 $y.prop('required', true) unless Boolean field.optional
393                                 $y.prop('checked', true) unless field.options.length > 1
394                                 $x
395
396                         @append hint
397                         .append inputs
398
399                 _action_field_suspend: (field) ->
400
401                         return @ unless field
402
403                         label =
404                                 indefinite: 'Suspend this hold indefinitely'
405                                 limited: 'Suspend this hold for a limited time'
406
407                         inputs = for v in field.options
408                                 $x = $ """
409                                 <div><label>
410                                         <input type="radio" name="#{field.name}" value="#{v}" /> #{label[v]}
411                                 </label></div>
412                                 """
413                                 $y = $x.find 'input'
414                                 $y.prop('required', true) unless Boolean field.optional
415                                 $y.prop('checked', true) if v is 'indefinite'
416                                 $x
417
418                         @append inputs
419
420                 # Build format buttons given specifications as follows.
421                 # formats = [ { formatType: type, linkTemplates: { downloadLink: { href: href } } } ]
422                 #
423                 # We need to get from a download link to make the download request,
424                 # receive a content link as a response, and then perform a 'normal' get
425                 # of the content link.  A complexity is to handle the error responses.
426                 #
427                 # TODO The initial get could fail, in which case, the errorpageurl
428                 # will be used to convey the failure status as a query string. We
429                 # will redirect the error page to the current page and we recognize
430                 # the error condition by analysing the query parameters.
431                 #
432                 # TODO Another error condition could occur if an Overdrive Read
433                 # ebook is attempted to be downloaded.  Here, the odreadauthurl
434                 # will be used as a redirect location. We will also redirect to the
435                 # current page and hopefully will be able to discern the state and
436                 # show it accordingly.
437                 _formats: (formats, id) ->
438                         return @ unless formats
439
440                         tpl = _.template """
441                         <div>
442                                 <a href="<%= href %>" class="opac-button" style="margin-top: 0px; margin-bottom: 0px"><%= label %></a>
443                         </div>
444                         """
445
446                         $tr = @closest('tr')
447
448                         $buttons = for format in formats
449                                 {
450                                         formatType: n
451                                         linkTemplates:
452                                                 downloadLink:
453                                                         href: href
454                                                         type: type
455                                 } = format
456
457                                 href = href
458                                         .replace /\{errorpageurl\}/, encodeURIComponent window.location.href
459                                         .replace /\{odreadauthurl\}/, encodeURIComponent "#{window.location.href}&reserveid=[RESERVE_ID]&read_error=[READ_ERROR]"
460
461                                 action = format.linkTemplates.downloadLink
462                                 $.extend action, _of: $tr, _name: n, _id: id, href: href
463
464                                 # Create a button for this action
465                                 $ tpl href: href, label: "Download #{od.labels n}"
466
467                                 # On clicking the button, build a new dialog using the extended action object
468                                 .on 'click', action, (ev) ->
469                                         ev.preventDefault()
470                                         $('<div>')['_downloadLink'] ev.data
471                                         return false
472
473                         @empty().append $buttons
474
475
476                 _notify: (title, text) ->
477
478                         @dialog
479                                 #position: my: 'top', at: 'right top'
480                                 minHeight: 0
481                                 autoOpen: true
482                                 draggable: false
483                                 resizable: true
484                                 show: effect: 'slideDown'
485                                 hide: effect: 'slideUp'
486                                 close: -> $(@).dialog 'destroy'
487                                 title: title
488
489                         .html text
490
491                 # Get the title of e-item in a row context
492                 _etitle: -> @find('.title a').text()