describe 'up.modal', ->
u = up.util
describe 'JavaScript functions', ->
assumedScrollbarWidth = 15
describe 'up.modal.follow', ->
it "loads the given link's destination in a dialog window", (done) ->
$link = affix('a[href="/path/to"][up-modal=".middle"]').text('link')
promise = up.modal.follow($link)
expect(@lastRequest().url).toMatch /\/path\/to$/
@respondWith """
new-before
new-middle
new-after
"""
promise.then ->
expect($('.up-modal')).toExist()
expect($('.up-modal-dialog')).toExist()
expect($('.up-modal-dialog .middle')).toExist()
expect($('.up-modal-dialog .middle')).toHaveText('new-middle')
expect($('.up-modal-dialog .before')).not.toExist()
expect($('.up-modal-dialog .after')).not.toExist()
done()
describe 'up.modal.extract', ->
it 'opens a modal by extracting the given selector from the given HTML string', ->
oldHref = location.href
up.modal.extract '.middle', """
new-before
new-middle
new-after
"""
expect($('.up-modal')).toExist()
expect($('.up-modal-dialog')).toExist()
expect($('.up-modal-dialog .middle')).toExist()
expect($('.up-modal-dialog .middle')).toHaveText('new-middle')
expect($('.up-modal-dialog .before')).not.toExist()
expect($('.up-modal-dialog .after')).not.toExist()
# Can't change URLs
expect(location.href).toEqual(oldHref)
describe 'up.modal.visit', ->
it "requests the given URL and places the given selector into a modal", ->
up.modal.visit '/foo', target: '.middle'
@respondWith """
new-before
new-middle
new-after
"""
expect('.up-modal').toExist()
expect('.up-modal-dialog').toExist()
expect('.up-modal-dialog .middle').toExist()
expect('.up-modal-dialog .middle').toHaveText('new-middle')
expect('.up-modal-dialog .before').not.toExist()
expect('.up-modal-dialog .after').not.toExist()
expect(location.pathname).toEndWith('/foo')
it "doesn't create an .up-modal frame and replaces { failTarget } if the server returns a non-200 response", ->
affix('.error').text('old error')
up.modal.visit '/foo', target: '.target', failTarget: '.error'
@respondWith
status: 500
responseText: """
new target
new error
"""
expect('.up-modal').not.toExist()
expect('.error').toHaveText('new error')
describe 'preventing elements from jumping as scrollbars change', ->
it "brings its own scrollbar, padding the body on the right", (done) ->
promise = up.modal.visit('/foo', target: '.container')
@respondWith('text
')
promise.then ->
$modal = $('.up-modal')
$viewport = $modal.find('.up-modal-viewport')
$body = $('body')
expect($modal).toExist()
expect($viewport.css('overflow-y')).toEqual('scroll')
expect($body.css('overflow-y')).toEqual('hidden')
expect(parseInt($body.css('padding-right'))).toBeAround(assumedScrollbarWidth, 5)
up.modal.close().then ->
expect($body.css('overflow-y')).toEqual('scroll')
expect(parseInt($body.css('padding-right'))).toBe(0)
done()
it "gives the scrollbar to .up-modal instead of .up-modal-viewport while animating, so we don't see scaled scrollbars in a zoom-in animation", (done) ->
openPromise = up.modal.extract('.container', 'text
', animation: 'fade-in', duration: 100)
$modal = $('.up-modal')
$viewport = $modal.find('.up-modal-viewport')
expect($modal.css('overflow-y')).toEqual('scroll')
expect($viewport.css('overflow-y')).toEqual('hidden')
openPromise.then ->
expect($modal.css('overflow-y')).not.toEqual('scroll')
expect($viewport.css('overflow-y')).toEqual('scroll')
closePromise = up.modal.close(animation: 'fade-out', duration: 200)
u.nextFrame ->
expect($modal.css('overflow-y')).toEqual('scroll')
expect($viewport.css('overflow-y')).toEqual('hidden')
done()
it 'does not add right padding to the body if the body has overflow-y: hidden', (done) ->
restoreBody = u.temporaryCss($('body'), 'overflow-y': 'hidden')
up.modal.extract('.container', 'text
').then ->
$body = $('body')
expect($('.up-modal')).toExist()
expect(parseInt($body.css('padding-right'))).toBe(0)
up.modal.close().then ->
expect(parseInt($body.css('padding-right'))).toBe(0)
restoreBody()
done()
it 'does not add right padding to the body if the body has overflow-y: auto, but does not currently have scrollbars', (done) ->
restoreBody = u.temporaryCss($('body'), 'overflow-y': 'auto')
restoreReporter = u.temporaryCss($('.jasmine_html-reporter'), 'height': '100px', 'overflow-y': 'hidden')
up.modal.extract('.container', 'text
').then ->
$body = $('body')
expect($('.up-modal')).toExist()
expect(parseInt($body.css('padding-right'))).toBe(0)
up.modal.close().then ->
expect(parseInt($body.css('padding-right'))).toBe(0)
restoreReporter()
restoreBody()
done()
it 'pushes right-anchored elements away from the edge of the screen', (done) ->
$anchoredElement = affix('div[up-anchored=right]').css
position: 'absolute'
top: '0'
right: '30px'
promise = up.modal.visit('/foo', target: '.container')
@respondWith('text
')
promise.then ->
expect(parseInt($anchoredElement.css('right'))).toBeAround(30 + assumedScrollbarWidth, 10)
up.modal.close().then ->
expect(parseInt($anchoredElement.css('right'))).toBeAround(30 , 10)
done()
describe 'opening a modal while another modal is open', ->
it 'does not open multiple modals or pad the body twice if the user starts loading a second modal before the first was done loading', (done) ->
up.modal.config.closeDuration = 10
promise1 = up.modal.visit('/path1', target: '.container', animation: 'fade-in', duration: 50)
promise2 = up.modal.visit('/path2', target: '.container', animation: 'fade-in', duration: 50)
@respondWith('response1
')
u.setTimer 120, =>
# up.modal.visit (visitAsap) will start the request only after the last modal
# has finished opening and closing: 50 + 10 ms
@respondWith('response2
')
$.when(promise1, promise2).then ->
expect($('.up-modal').length).toBe(1)
expect($('.up-modal-dialog').length).toBe(1)
expect($('.container')).toHaveText('response2')
bodyPadding = parseInt($('body').css('padding-right'))
expect(bodyPadding).toBeAround(assumedScrollbarWidth, 10)
expect(bodyPadding).not.toBeAround(2 * assumedScrollbarWidth, 2 * 5)
done()
it 'closes the current modal and wait for its close animation to finish before starting the open animation of a second modal', (done) ->
up.modal.config.openAnimation = 'fade-in'
up.modal.config.openDuration = 5
up.modal.config.closeAnimation = 'fade-out'
up.modal.config.closeDuration = 60
events = []
u.each ['up:modal:open', 'up:modal:opened', 'up:modal:close', 'up:modal:closed'], (event) ->
up.on event, ->
events.push(event)
up.modal.extract('.target', 'response1
')
# First modal is starting opening animation
expect(events).toEqual ['up:modal:open']
expect($('.target')).toHaveText('response1')
u.setTimer 80, ->
# First modal has completed opening animation
expect(events).toEqual ['up:modal:open', 'up:modal:opened']
expect($('.target')).toHaveText('response1')
# We open another modal, which will cause the first modal to start closing
up.modal.extract('.target', 'response2
')
expect($('.target')).toHaveText('response1')
u.setTimer 20, ->
# Second modal is still waiting for first modal's closing animaton to finish.
expect(events).toEqual ['up:modal:open', 'up:modal:opened', 'up:modal:close']
expect($('.target')).toHaveText('response1')
u.setTimer 200, ->
# First modal has finished closing, second modal has finished opening.
expect(events).toEqual ['up:modal:open', 'up:modal:opened', 'up:modal:close', 'up:modal:closed', 'up:modal:open', 'up:modal:opened']
expect($('.target')).toHaveText('response2')
done()
it 'closes an opening modal if a second modal starts opening before the first modal has finished its open animation', (done) ->
up.modal.config.openAnimation = 'fade-in'
up.modal.config.openDuration = 50
up.modal.config.closeAnimation = 'fade-out'
up.modal.config.closeDuration = 50
# Open the first modal
up.modal.extract('.target', 'response1
')
u.setTimer 10, ->
# First modal is still in its opening animation
expect($('.target')).toHaveText('response1')
# Open a second modal
up.modal.extract('.target', 'response2
')
# First modal is starting close animation. Second modal waits for that.
expect($('.target')).toHaveText('response1')
u.setTimer 10, ->
# Second modal is still waiting for first modal's closing animaton to finish.
expect($('.target')).toHaveText('response1')
u.setTimer 150, ->
# First modal has finished closing, second modal has finished opening.
expect($('.target')).toHaveText('response2')
done()
it 'uses the correct flavor config for the first and second modal', (done) ->
up.modal.config.openAnimation = 'fade-in'
up.modal.config.openDuration = 20
up.modal.config.closeAnimation = 'fade-out'
up.modal.config.closeDuration = 20
up.modal.flavor 'custom-drawer',
openAnimation: 'move-from-right'
closeAnimation: 'move-to-right'
animations = []
spyOn(up, 'animate').and.callFake ($element, animation, options) ->
if $element.is('.up-modal-viewport')
animations.push
text: u.trim($element.find('.target').text())
animation: animation
deferred = $.Deferred()
u.setTimer options.duration, -> deferred.resolve()
deferred.promise()
up.modal.extract('.target', 'response1
')
expect(animations).toEqual [
{ animation: 'fade-in', text: 'response1' }
]
u.setTimer 30, ->
# first modal is now done animating
expect(animations).toEqual [
{ animation: 'fade-in', text: 'response1' }
]
up.modal.extract('.target', 'response2
', flavor: 'custom-drawer')
expect(animations).toEqual [
{ animation: 'fade-in', text: 'response1' },
{ animation: 'fade-out', text: 'response1' },
]
u.setTimer 30, ->
expect(animations).toEqual [
{ animation: 'fade-in', text: 'response1' },
{ animation: 'fade-out', text: 'response1' },
{ animation: 'move-from-right', text: 'response2' }
]
expect($('.up-modal').attr('up-flavor')).toEqual('custom-drawer')
done()
it 'does not explode if up.modal.close() was called before the response was received', ->
up.modal.visit('/foo', target: '.container')
up.modal.close()
respond = => @respondWith('text
')
expect(respond).not.toThrowError()
expect($('.up-error')).not.toExist()
describe 'up.modal.coveredUrl', ->
describeCapability 'canPushState', ->
it 'returns the URL behind the modal overlay', (done) ->
up.history.replace('/foo')
expect(up.modal.coveredUrl()).toBeMissing()
visitPromise = up.modal.visit('/bar', target: '.container')
@respondWith('text
')
visitPromise.then ->
expect(up.modal.coveredUrl()).toEndWith('/foo')
up.modal.close().then ->
expect(up.modal.coveredUrl()).toBeMissing()
done()
describe 'up.modal.flavors', ->
it 'allows to register new modal variants with its own default configuration', ->
up.modal.flavors.variant = { maxWidth: 200 }
$link = affix('a[href="/path"][up-modal=".target"][up-flavor="variant"]')
Trigger.click($link)
@respondWith('new text
')
$modal = $('.up-modal')
$dialog = $modal.find('.up-modal-dialog')
expect($modal).toBeInDOM()
expect($modal.attr('up-flavor')).toEqual('variant')
expect($dialog.attr('style')).toContain('max-width: 200px')
it 'does not change the configuration of non-flavored modals', ->
up.modal.flavors.variant = { maxWidth: 200 }
$link = affix('a[href="/path"][up-modal=".target"]')
Trigger.click($link)
@respondWith('new text
')
$modal = $('.up-modal')
$dialog = $modal.find('.up-modal-dialog')
expect($modal).toBeInDOM()
expect($dialog.attr('style')).toBeBlank()
describe 'up.modal.close', ->
it 'closes a currently open modal', (done) ->
up.modal.extract('.modal', 'Modal content
')
modalContent = $('.modal')
expect(modalContent).toBeInDOM()
up.modal.close().then ->
expect(modalContent).not.toBeInDOM()
done()
it 'does nothing if no modal is open', (done) ->
wasClosed = false
up.on 'up:modal:close', ->
wasClosed = true
up.modal.close().then ->
expect(wasClosed).toBe(false)
done()
describe 'unobtrusive behavior', ->
describe 'a[up-modal]', ->
beforeEach ->
@stubFollow = =>
@$link = affix('a[href="/path"][up-modal=".target"]')
@followSpy = up.modal.knife.mock('followAsap').and.returnValue(u.resolvedPromise())
@defaultSpy = up.link.knife.mock('allowDefault').and.callFake((event) -> event.preventDefault())
it 'opens the clicked link in a modal', ->
@stubFollow()
Trigger.click(@$link)
expect(@followSpy).toHaveBeenCalledWith(@$link)
# IE does not call JavaScript and always performs the default action on right clicks
unless navigator.userAgent.match(/Trident/)
it 'does nothing if the right mouse button is used', ->
@stubFollow()
Trigger.click(@$link, button: 2)
expect(@followSpy).not.toHaveBeenCalled()
it 'does nothing if shift is pressed during the click', ->
@stubFollow()
Trigger.click(@$link, shiftKey: true)
expect(@followSpy).not.toHaveBeenCalled()
it 'does nothing if ctrl is pressed during the click', ->
@stubFollow()
Trigger.click(@$link, ctrlKey: true)
expect(@followSpy).not.toHaveBeenCalled()
it 'does nothing if meta is pressed during the click', ->
@stubFollow()
Trigger.click(@$link, metaKey: true)
expect(@followSpy).not.toHaveBeenCalled()
describe 'with [up-instant] modifier', ->
beforeEach ->
@stubFollow()
@$link.attr('up-instant', '')
it 'opens the modal on mousedown (instead of on click)', ->
Trigger.mousedown(@$link)
expect(@followSpy.calls.mostRecent().args[0]).toEqual(@$link)
it 'does nothing on mouseup', ->
Trigger.mouseup(@$link)
expect(@followSpy).not.toHaveBeenCalled()
it 'does nothing on click', ->
Trigger.click(@$link)
expect(@followSpy).not.toHaveBeenCalled()
# IE does not call JavaScript and always performs the default action on right clicks
unless navigator.userAgent.match(/Trident/)
it 'does nothing if the right mouse button is pressed down', ->
Trigger.mousedown(@$link, button: 2)
expect(@followSpy).not.toHaveBeenCalled()
it 'does nothing if shift is pressed during mousedown', ->
Trigger.mousedown(@$link, shiftKey: true)
expect(@followSpy).not.toHaveBeenCalled()
it 'does nothing if ctrl is pressed during mousedown', ->
Trigger.mousedown(@$link, ctrlKey: true)
expect(@followSpy).not.toHaveBeenCalled()
it 'does nothing if meta is pressed during mousedown', ->
Trigger.mousedown(@$link, metaKey: true)
expect(@followSpy).not.toHaveBeenCalled()
describe 'with [up-method] modifier', ->
it 'honours the given method', ->
$link = affix('a[href="/path"][up-modal=".target"][up-method="post"]')
Trigger.click($link)
expect(@lastRequest().method).toEqual 'POST'
it 'adds a history entry and allows the user to use the back button', (done) ->
up.motion.config.enabled = false
up.history.push('/original-path')
up.history.config.popTargets = ['.container']
$container = affix('.container').text('old container content')
$link = $container.affix('a[href="/new-path"][up-modal=".target"]')
expect(location.pathname).toEqual('/original-path')
Trigger.clickSequence($link)
u.nextFrame =>
@respondWith('modal content
')
expect(location.pathname).toEqual('/new-path')
expect('.up-modal .target').toHaveText('modal content')
history.back()
u.setTimer (waitForBrowser = 70), =>
@respondWith('restored container content
')
u.nextFrame =>
expect(location.pathname).toEqual('/original-path')
expect('.container').toHaveText('restored container content')
expect('.up-modal').not.toExist()
done()
describe '[up-drawer]', ->
beforeEach ->
up.motion.config.enabled = false
it 'slides in a drawer that covers the full height of the screen', (done) ->
$link = affix('a[href="/foo"][up-drawer=".target"]').text('label')
up.hello($link)
Trigger.clickSequence($link)
u.nextFrame =>
@respondWith 'new text
'
expect(up.modal.isOpen()).toBe(true)
expect($('.up-modal').attr('up-flavor')).toEqual('drawer')
windowHeight = u.clientSize().height
modalHeight = $('.up-modal-content').outerHeight()
expect(modalHeight).toEqual(windowHeight)
expect($('.up-modal-content').offset()).toEqual(top: 0, left: 0)
done()
it 'puts the drawer on the right if the opening link sits in the right 50% of the screen', (done) ->
$link = affix('a[href="/foo"][up-drawer=".target"]').text('label')
$link.css
position: 'absolute'
right: '0'
up.hello($link)
Trigger.clickSequence($link)
u.nextFrame =>
@respondWith 'new text
'
expect(up.modal.isOpen()).toBe(true)
windowWidth = u.clientSize().width
modalWidth = $('.up-modal-content').outerWidth()
scrollbarWidth = u.scrollbarWidth()
expect($('.up-modal-content').offset().left).toBeAround(windowWidth - modalWidth - scrollbarWidth, 1.0)
done()
describe '[up-close]', ->
backgroundClicked = undefined
beforeEach ->
up.motion.config.enabled = false
backgroundClicked = jasmine.createSpy('background clicked')
up.on 'click', backgroundClicked
describe 'when clicked inside a modal', ->
it 'closes the open modal and halts the event chain', (done) ->
up.modal.extract('.target', '', animation: false)
$link = $('.up-modal a[up-close]') # link is within the modal
Trigger.clickSequence($link)
u.nextFrame ->
expect(up.modal.isOpen()).toBe(false)
expect(backgroundClicked).not.toHaveBeenCalled()
done()
describe 'when no modal is open', ->
it 'does nothing and allows the event chain to continue', (done) ->
$link = affix('a[up-close]') # link is outside the modal
up.hello($link)
Trigger.clickSequence($link)
u.nextFrame ->
expect(backgroundClicked).toHaveBeenCalled()
done()
describe 'template behavior', ->
it 'closes the modal on close icon click', (done) ->
up.modal.extract('.modal', 'Modal content
', animation: false)
$closeIcon = $('.up-modal-close')
Trigger.clickSequence($closeIcon)
u.nextFrame ->
expect(up.modal.isOpen()).toBe(false)
done()
it 'closes the modal on backdrop click', (done) ->
up.modal.extract('.modal', 'Modal content
', animation: false)
$backdrop = $('.up-modal-backdrop')
Trigger.clickSequence($backdrop)
u.nextFrame ->
expect(up.modal.isOpen()).toBe(false)
done()
it 'closes the modal when the user presses the escape key', (done) ->
wasClosed = false
up.modal.extract('.modal', 'Modal content
', animation: false)
up.on 'up:modal:close', ->
wasClosed = true
escapeEvent = $.Event('keydown', keyCode: 27)
$('body').trigger(escapeEvent)
u.nextFrame ->
expect(wasClosed).toBe(true)
done()
describe 'when opened with { closable: false }', ->
it 'does not render a close icon', ->
up.modal.extract('.modal', 'Modal content
', animation: false, closable: false)
modal = $('.up-modal')
expect(modal).not.toContainElement('.up-modal-close')
it 'does not close the modal on backdrop click', (done) ->
up.modal.extract('.modal', 'Modal content
', animation: false, closable: false)
$backdrop = $('.up-modal-backdrop')
Trigger.clickSequence($backdrop)
u.nextFrame ->
expect(up.modal.isOpen()).toBe(true)
done()
it 'does not close the modal when the user presses the escape key', (done) ->
up.modal.extract('.modal', 'Modal content
', animation: false, closable: false)
escapeEvent = $.Event('keydown', keyCode: 27)
$('body').trigger(escapeEvent)
u.nextFrame ->
expect(up.modal.isOpen()).toBe(true)
done()
describe 'when replacing content', ->
beforeEach ->
up.motion.config.enabled = false
it 'prefers to replace a selector within the modal', ->
$outside = affix('.foo').text('old outside')
up.modal.visit('/path', target: '.foo')
@respondWith("old inside
")
up.extract('.foo', "new text
")
expect($outside).toBeInDOM()
expect($outside).toHaveText('old outside')
expect($('.up-modal-content')).toHaveText('new text')
it 'auto-closes the modal when a replacement from inside the modal affects a selector behind the modal', ->
affix('.outside').text('old outside')
up.modal.visit('/path', target: '.inside')
@respondWith("old inside
")
up.extract('.outside', "new outside
", origin: $('.inside'))
expect($('.outside')).toHaveText('new outside')
expect($('.up-modal')).not.toExist()
it 'does not restore the covered URL when auto-closing', (done) ->
up.motion.config.enabled = true
up.modal.config.openDuration = 0
up.modal.config.closeDuration = 20
affix('.outside').text('old outside')
whenModalOpen = up.modal.visit('/path', target: '.inside')
@respondWith("old inside
") # Populate modal
whenModalOpen.then ->
up.extract('.outside', "new outside
",
origin: $('.inside'), history: '/new-location') # Provoke auto-close
u.setTimer 50, ->
expect(location.href).toEndWith '/new-location'
done()
it 'does not auto-close the modal when a replacement from inside the modal affects a selector inside the modal', ->
affix('.outside').text('old outside')
up.modal.visit('/path', target: '.inside')
@respondWith("old inside
")
up.extract('.inside', "new inside
", origin: $('.inside'))
expect($('.inside')).toHaveText('new inside')
expect($('.up-modal')).toExist()
it 'does not auto-close the modal when a replacement from outside the modal affects a selector outside the modal', ->
affix('.outside').text('old outside')
up.modal.visit('/path', target: '.inside')
@respondWith("old inside
")
up.extract('.outside', "new outside
", origin: $('.outside'))
expect($('.outside')).toHaveText('new outside')
expect($('.up-modal')).toExist()
it 'does not auto-close the modal when a replacement from outside the modal affects a selector inside the modal', ->
affix('.outside').text('old outside')
up.modal.visit('/path', target: '.inside')
@respondWith("old inside
")
up.extract('.inside', "new inside
", origin: $('.outside'))
expect($('.inside')).toHaveText('new inside')
expect($('.up-modal')).toExist()
it 'does not auto-close the modal when the new fragment is within a popup', ->
up.modal.visit('/modal', target: '.modal-content')
@respondWith("")
up.popup.attach('.modal-content', url: '/popup', target: '.popup-content')
@respondWith("")
expect($('.up-modal')).toExist()
expect($('.up-popup')).toExist()
it 'does not close the modal when a clicked [up-target] link within the modal links to cached content (bugfix)', (done) ->
up.modal.extract '.content', """
"""
$link = $('.up-modal .content a')
expect($link).toExist()
whenPreloaded = up.proxy.preload($link)
@respondWith """
new text
"""
whenPreloaded.then ->
Trigger.clickSequence($link)
u.nextFrame ->
expect($('.up-modal')).toExist()
expect($('.up-modal .content')).toHaveText('new text')
done()