Change button label from "editHold" to "Edit Hold"
[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 [
b63fc3d5 5 'jquery-noconflict'
8ebc3fff
SC
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()
f1fb1882 55 $(@).dialogAction 'autoclose'
8ebc3fff
SC
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
f1fb1882
SC
80 f = @options._scenario.intent?.body
81 @set_message 'intent', not intent, if $.isFunction f then f @options._action?._name else undefined
8ebc3fff
SC
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.
f46ca691 87 set_message: (scenario, close, body, title) ->
8ebc3fff 88 @_close_button close
f46ca691
SC
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()
8ebc3fff
SC
98 return @
99
f1fb1882
SC
100 autoclose: ->
101 @_on 'dialogactionclose': @options._scenario?.intent?.reroute or ->
8ebc3fff
SC
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
f1fb1882
SC
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
8ebc3fff
SC
118 # Make an API call
119 action = @options._action
8546cdf5
SC
120 progress = => @set_message 'progress', true
121 od.api action.href, action.method, fields: $('form', @element).serializeArray(), progress
8ebc3fff
SC
122
123 # Re-use the dialog to show notifications with a close button
124 .then(
f1fb1882
SC
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
8ebc3fff
SC
129 (x) => @set_message 'fail', true, responseMessage x
130 )
131
132 # On done and when the user closes the dialog, reroute the page
f1fb1882 133 .done onclose_maybe_reroute
8ebc3fff
SC
134
135 return @
136
f1fb1882
SC
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 @
8ebc3fff
SC
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'
c7992436 150 editHold: 'Edit Hold'
8ebc3fff
SC
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 #
8ebc3fff
SC
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.'
8ebc3fff
SC
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.'
8ebc3fff
SC
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.'
8ebc3fff
SC
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.'
8ebc3fff
SC
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.'
8ebc3fff
SC
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.'
8ebc3fff
SC
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.'
8ebc3fff
SC
254
255 @dialogAction _scenario: scenario, _action: action
256
f1fb1882 257 _downloadLink: (action) ->
8ebc3fff 258
f1fb1882
SC
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'
8ebc3fff 274
f1fb1882 275 @dialogAction _scenario: scenario, _action: action
8ebc3fff
SC
276
277 # Build action buttons and dialogs given specifications as follows.
278 # actions = [ { name: { href: h, method: m, fields: [ { name: n, value: v, options: [...] } ] } ]
53ade1ec 279 _actions: (actions, id, suspended) ->
8ebc3fff
SC
280
281 tpl = _.template """
282 <div>
07895f87 283 <a href="<%= href %>" class="opac-button <%= action_name %>" style="margin-top: 0px; margin-bottom: 0px"><%= label %></a>
8ebc3fff
SC
284 </div>
285 """
286
287 # Find the related row
288 $tr = @closest('tr')
289
290 $buttons = for n, action of actions
291
53ade1ec 292 unless suspended? and n is 'addSuspension'
8ebc3fff 293
53ade1ec
JD
294 # Extend the action object with context
295 $.extend action, _of: $tr, _name: n, _id: id
8ebc3fff 296
53ade1ec
JD
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
8ebc3fff
SC
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
f1fb1882
SC
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.
9b152782
SC
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.
f1fb1882
SC
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
8ebc3fff 456
f1fb1882 457 href = href
639929ef
SC
458 .replace /\{errorpageurl\}/, encodeURIComponent window.location.href
459 .replace /\{odreadauthurl\}/, encodeURIComponent "#{window.location.href}&reserveid=[RESERVE_ID]&read_error=[READ_ERROR]"
8ebc3fff 460
f1fb1882
SC
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
8ebc3fff
SC
474
475
476 _notify: (title, text) ->
477
478 @dialog
3865fb23 479 #position: my: 'top', at: 'right top'
8ebc3fff
SC
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
3865fb23 489 .html text
8ebc3fff
SC
490
491 # Get the title of e-item in a row context
492 _etitle: -> @find('.title a').text()