# **AmpleAssets** is drag and drop file management for Rails applications.
#
class window.AmpleAssetsToolbar extends CoffeeCup
default_options:
debug: false
expanded: false
id: "ample-assets"
handle_text: 'Assets'
expanded_height: 180
collapsed_height: 0
base_url: '/ample_assets'
search_url: '/files/search'
thumb_url: '/files/thumbs'
show_url: '/files/{{ id }}'
touch_url: '/files/{{ id }}/touch'
gravity_url: '/files/{{ id }}/gravity'
onInit: ->
onExpand: ->
onCollapse: ->
panels_options:
debug: false
width: 950
height: 100
orientation: 'vertical'
key_orientation: 'vertical'
keyboard_nav: true
auto: false
parent: 'div'
children: 'div.page'
pages_options:
interval: 5000
width: 81
height: 81
enabled: true
distance: 10
auto: false
orientation: 'horizontal'
key_orientation: 'horizontal'
per_page: 10
pages: [
{
id: 'recent-assets',
title: 'Recently Viewed',
url: '',
panels: false
}
]
# Initialize product toolbar and drop targets.
init: ->
@options.onInit()
super
@setup()
@events()
# Setup global parameters & class options.
set_options: (opts) ->
@current = 0
@keys_enabled = true
@reloading = false
@searching = false
@loaded = false
super
# Build structure and stylize layout of toolbar, setup drag, drop and search logic.
# Opens first tab if toolbar is expanded on init.
setup: ->
id = @options.id
layout = Mustache.to_html(@tpl('layout'),{ id: id, pages: @get_pages(), tabs: @get_pages('tab') })
@handle = Mustache.to_html(@tpl('handle'),{ id: id, title: @options.handle_text })
html = $(layout)
$('body').append(html).append(@handle)
@style()
@drag_drop()
@search()
@goto(0) if @options.expanded
# Set initial styles on toolbar elements.
style: ->
handle_opts =
position: 'absolute'
bottom: 0
right: 0
$("##{@options.id}-handle").css(handle_opts)
@loading = $("##{@options.id}-tabs span.asset-loading")
$("##{@options.id} .container").css('height',200)
if @options.expanded
$("##{@options.id}").css({height:@options.expanded_height});
# Reloads tab identified by `i`
reload: (i) ->
@log "reload(#{i})"
@reloading = true
if i < @options.pages.length - 1
@empty(i)
@options.pages[i]['loaded'] = false
@options.pages[i]['pages_loaded'] = false
@options.pages[i]['last_request_empty'] = false
$(@options.pages[i]['panel_selector']).amplePanels('goto', 0)
@goto(i)
@enable_panel(i)
# Empties the contents of the page identified by `i`
empty: (i) ->
@log "empty(#{i})"
selector = "##{@options.id} .pages .page:nth-child(#{(i+1)})"
selector += " ul" if @options.pages[i]['panels']
$(selector).empty()
# Hide and deactivate current page, load next page identified by `i`
goto: (i) ->
@log "goto(#{i})"
@show(i)
@disable_panels()
@activate(i)
@load(i) unless @already_loaded(i)
@enable_panel(i) if @already_loaded(i)
# Hide all pages, show page identified by `i`
show: (i) ->
$("##{@options.id} .pages .page").hide()
$("##{@options.id} .pages .page:nth-child(#{i+1}), ##{@options.id} .pages .page:nth-child(#{i+1}) ul").show()
# Implement drag & droppable instances, with appropriate callbacks.
drag_drop: ->
base_url = @options.base_url
thumb_url = @options.thumb_url
ref = this
# Note the use of liveDraggable here. See extended plugin at the bottom of this file.
$(".draggable").liveDraggable
appendTo: "body"
helper: "clone"
start: ->
$('div.ui-droppable, textarea.ui-droppable').addClass('asset-drop-target')
stop: ->
$('div.ui-droppable, textarea.ui-droppable').removeClass('asset-drop-target')
$("textarea").droppable
activeClass: "asset-notice"
hoverClass: "asset-success"
drop: (event, ui) ->
unless $(ui.helper).data('role') == 'gravity'
ref.target_textarea = this
ref.resize_modal(ui.draggable)
$(".droppable").droppable
activeClass: "asset-notice"
hoverClass: "asset-success"
drop: (event, ui) ->
unless $(ui.helper).data('role') == 'gravity'
$(this).html ui.draggable.clone()
asset_id = $(ui.draggable).attr("id").split("-")[1]
$(this).parent().children().first().val asset_id
$(this).parent().find('a.asset-remove').removeClass('hide').show()
# Build html for modal windows wherein users can resize the asset's dimensions & geometry.
# Executes when dropping a file into a textarea. The first argument is the response
# from the droppable callbacks defined above.
resize_modal: (el) ->
uid = $(el).attr("data-uid")
size = $(el).attr("data-size")
orientation = $(el).attr("data-orientation")
base_url = @options.base_url
thumb_url = @options.thumb_url
geometry = '100x>'
opts =
src: "#{base_url}#{thumb_url}/#{geometry}?uid=#{uid}"
orientation: orientation
dimensions: size
uid: uid
html = Mustache.to_html(@tpl('drop'), opts)
$.facebox("
#{html}
")
# Removes active state from all tabs, adds it back for tab identified by `i`
activate: (i) ->
@log "activate(#{i})"
@current = i
tabs = $("##{@options.id} a.tab")
tabs.removeClass('on')
tabs.eq(i).addClass('on')
# Highlight & load next tab (right) by incrementing `@current`
next: ->
if @current < @options.pages.length - 1
@log "next()"
@current += 1
@goto(@current)
# Highlight & load previous tab (left) by decrementing `@current`
previous: ->
unless @current == 0
@log "previous()"
@current -= 1
@goto(@current)
# Loops through all pages, generates HTML and returns concatenated string of everything.
get_pages: (tpl = 'page') ->
html = ''
$.each @options.pages, (idx,el) =>
el['classes'] = 'first-child' if idx == 0
html += Mustache.to_html @tpl(tpl), el
html
# Expands and collapses asset toolbar.
toggle: ->
el = $("##{@options.id}")
if @options.expanded
@options.expanded = false
$('body').animate {'padding-bottom': 0}, "fast"
el.animate {height: @options.collapsed_height}, "fast", =>
@collapse()
@options.onCollapse()
el.trigger('collapse')
else
$("##{@options.id}-handle").hide()
@options.expanded = true
$('body').animate {'padding-bottom': @options.expanded_height}, "fast"
el.animate {height: @options.expanded_height}, "fast", =>
@expand()
@options.onExpand()
el.trigger('expand')
# Loads contents of page identified by `i`
load: (i) ->
@log "load(#{i})"
ref = this
load_next_page = true
load_next_page = false if @options.pages[i]['last_request_empty']
load_next_page = true if @reloading
if @options.pages[i]['url'] && load_next_page
@loading.show()
url = @next_page_url(i)
data_type = @options.pages[i]['data_type'] if @options.pages[i]['data_type']
$.get url, (response, xhr) ->
ref.loading.hide()
ref.options.pages[i]['loaded'] = true
# If response is empty, let users know by loading an empty notification.
if $.trim(response) == '' || response.length == 0
ref.options.pages[i]['last_request_empty'] = true
ref.load_empty(i) if ref.reloading || !ref.options.pages[i]['panel_selector']
else
switch data_type
when "json"
# Parse json for requests of that type.
ref.load_json i, response
when "html"
else
# Parse html by default or for requests of that specific type.
ref.load_html i, response
, data_type
else
# Notify console if we couldn't load a page due to a missing URL.
@log "ERROR --> Couldn't load page because there was no url" unless @options.pages[i]['last_request_empty']
# For empty requests, insert notification text into page identified by `i`
load_empty: (i) ->
@log "load_empty(#{i})"
empty = Mustache.to_html(@tpl('empty'))
@load_html(i, empty)
@loading.hide()
$('li.empty').css('width',$('.ampn').first().width())
$('li.empty a').click =>
@goto(@options.pages.length-2)
# Load html content returned as `response` by XHR request into page identified by `i`
load_html: (i, response) ->
@log "load(#{i}) html"
selector = "##{@options.id} .pages .page:nth-child(#{(i+1)})"
selector += " ul" if @options.pages[i]['panels'] || @searching
$(selector).html(response).show()
@panels(i)
# Parse `response` as json data and build list-items for each element contained therein.
# This method assumes page identified by `i` contains an ample_panels instance.
load_json: (i, response) ->
@log "load(#{i}) json"
panels_loaded = if @options.pages[i]['panel_selector'] then true else false
ref = this
selector = "##{@options.id} .pages .page:nth-child(#{(i+1)}) ul"
$.each response, (j,el) ->
link = ref.build(el)
li = $('').append(link)
if panels_loaded
$(selector).amplePanels('append', li)
else
$(selector).append(li)
ref.load_img(li.find('a'), el.sizes.tn)
$(selector).show()
@panels(i) unless panels_loaded
if @reloading
@reloading = false
@controls()
# Parse `response` from search results as json data.
load_results: (response) ->
@log "load_results()"
i = @options.pages.length - 1
if response.length > 0
# Build list-item for each item returned from search query.
$.each response, (j,el) =>
link = @build(el)
li = $('').append(link)
$("#asset-results ul").amplePanels('append', li)
@load_img(link, el.sizes.tn)
else
# No results were returned, inject no-results verbiage.
no_results = Mustache.to_html(@tpl('no_results'))
@load_html(i, no_results)
@loading.hide()
$('li.empty').css('width',$('.ampn').first().width())
@options.pages[i]['panel_selector'] = "#asset-results ul"
@active_panel = $(@options.pages[i]['panel_selector'])
@searching = false
@loading.hide()
@controls()
# Builds each asset instance with proper attributes.
build: (el) ->
ref = this
show_url = Mustache.to_html @options.show_url, { id: el.id }
link = $("")
.attr('id',"file-#{el.id}")
.attr('data-uid',"#{el.uid}")
.attr('data-filename',"#{el.filename}")
.attr('data-gravity', el.gravity)
.addClass('draggable')
if el.document == 'true'
link.addClass('document')
else
link.attr('data-orientation',el.orientation)
link.attr('data-size',el.size)
link.click ->
# Open a modal window on any asset instance's click event.
ref.modal_open(el)
false
link
# Opens modal window instance for asset detail.
modal_open: (data) ->
@modal_active = true
if data.document == 'true'
# Asset is a document, so lets instantiate PDFObject for viewing inline.
html = Mustache.to_html @tpl('pdf'),
filename: data.uid,
id: data.id,
mime_type: data.mime_type
$.facebox("
#{html}
")
myPDF = new PDFObject(
url: data.url
pdfOpenParams:
view: "Fit"
).embed("pdf")
else
# Asset is an image, lets display it inline, according to its orientation.
geometry = if data.orientation == 'portrait' then 'x300>' else '480x>'
url = "#{@options.base_url}#{@options.thumb_url}/#{geometry}?uid=#{data.uid}"
delete_url = Mustache.to_html @options.show_url, { id: data.id }
gravity_url = Mustache.to_html @options.gravity_url, { id: data.id }
gravity = $("a[data-uid='#{data.uid}']").first().attr('data-gravity')
keywords = ""
html = Mustache.to_html @tpl('show'),
filename: data.filename,
size: data.size,
mime_type: data.mime_type,
keywords: keywords,
src: url,
orientation: data.orientation,
id: data.id,
uid: data.uid,
gravity: gravity,
delete_url: "#{@options.base_url}#{delete_url}",
gravity_url: "#{@options.base_url}#{gravity_url}"
$.facebox("
#{html}
")
# Update the asset timestamp.
@touch(data)
# Create new image element from `src`, insert into `el` and fadeIn opacity.
load_img: (el,src) ->
img = new Image()
$(img).load(->
$(this).hide()
$(el).html this
$(this).fadeIn()
).attr src: src
# Generates the next URL for paginated record sets.
next_page_url: (i) ->
@options.pages[i]['pages_loaded'] = 0 unless @options.pages[i]['pages_loaded']
@options.pages[i]['pages_loaded'] += 1
"#{@options.pages[i]['url']}?page=#{@options.pages[i]['pages_loaded']}"
# By touching asset records, we update the timestamp value which ensures our recently
# viewed tab contains accurate results. Called from `modal_open()`
touch: (el) ->
@log "touch()"
touch_url = Mustache.to_html @options.touch_url, { id: el.id }
$.post "#{@options.base_url}#{touch_url}"
# Instantiate an amplePanels instance within page `i` if `@options.pages[i]['panels']` is true.
panels: (i) ->
ref = this
if @options.pages[i]['panels']
@log "panels(#{i})"
el = "##{@options.id} .pages .page:nth-child(#{(i+1)}) ul"
@options.pages[i]['panel_selector'] = el
@active_panel = el
@options.pages[i][''] = $(el).attr('id',"#{@options.pages[i]['id']}-panel")
$(el).parent().addClass('panels')
@controls()
$(el).amplePanels(@options.pages_options)
.bind 'slide_horizontal', (e,d,dir) ->
ref.load(i) if dir == 'next'
# Disable all panels, preventing any loading or key-driven actions from taking place within any amplePanels instance.
disable_panels: ->
@log "disable_panels()"
ref = this
@controls(false)
$.each @options.pages, (i,el) ->
$(ref.options.pages[i]['panel_selector']).amplePanels('disable') if ref.options.pages[i]['panel_selector']
# Enable panels instance contained with page identified by `i`.
# This allows key-events and previous/next actions to be executed.
enable_panel: (i) ->
@log "enable_panel(#{i})"
if @options.pages[i]['panel_selector']
@active_panel = @options.pages[i]['panel_selector']
$(@options.pages[i]['panel_selector']).amplePanels('enable')
@controls()
# Toggles display of left/right arrows which control amplePanels paging event, determined by `display`.
controls: (display=true) ->
@log "controls(#{display})"
display = false if $(@active_panel).find('li').length < @options.pages_options.per_page
switch display
when true
$('nav.controls').show()
when false
$('nav.controls').hide()
# Evaluates whether URL attached to page `i` has been loaded yet. Returns `boolean`.
already_loaded: (i) ->
typeof @options.pages[i]['loaded'] == 'boolean' && @options.pages[i]['loaded']
# Removes the asset from drop-target identified by `el`.
remove: (el) ->
parent = $(el).parent()
parent.find('.droppable').empty().html('Drag Asset Here')
parent.find('input').val('')
$(el).hide()
# Called upon successful DELETE request for a specific asset. Removes any instances of
# asset identified by `id`, closes the modal window and reloads the first tab.
delete: (id) ->
@log "delete(#{id})"
$("a#file-#{id}").parent().remove()
$(document).trigger('close.facebox')
@reload(0)
false
# Upon collapse, we disable panels.
collapse: ->
$("##{@options.id}-handle").css('bottom',-35).show().animate({'bottom': 0},'fast')
@disable_panels()
# Expands the asset toolbar and reenables the currently loaded tab.
expand: ->
@goto(@current)
# Setup all associated events.
events: ->
@modal_events()
@global_events()
@field_events()
@drop_events()
@drag_events()
@reload_events()
@resize_events()
@key_events()
@tab_events()
ref = this
# Collapse toolbar
$("##{@options.id} a.collapse").live 'click', =>
@toggle()
# Reload the first tab following a successful upload.
$('body').bind 'ample_uploadify.complete', =>
@reload(0)
# Bind event to succesful deletion of an asset.
$("a.asset-delete").live 'ajax:success', ->
id = parseInt $(this).attr('data-id')
ref.delete(id)
# Bind live event to any asset-remove element.
$("a.asset-remove").live 'click', ->
ref.remove(this)
false
# Bind `toggle()` method to toolbar handle.
$("##{@options.id}-handle").live 'click', =>
@toggle()
false
# Bind events for global left/right arrows to currently active panel's `previous()` and `next()` methods.
global_events: ->
$('a.global.next').click =>
$(@active_panel).amplePanels('next')
$('a.global.previous').click =>
$(@active_panel).amplePanels('previous')
# TODO: kill key events during drag?
drag_events: ->
@log "drag_events()"
# Open modal window when clicking an asset contained with a drop-target.
drop_events: ->
ref = this
$('.asset-drop .droppable a').live 'click', ->
id = $(this).attr("href")
$.get $(this).attr("href"), (response) ->
ref.modal_open(response)
, 'json'
false
# Toggle the active state of key_events when user focuses / blurs on textareas or input fields.
field_events: ->
@log "field_events()"
$('textarea, input').live 'blur', =>
@keys_enabled = true
$('textarea, input').live 'focus', =>
@keys_enabled = false
# Bind `reload()` method to assets-reload button.
reload_events: ->
@log "reload_events()"
reload = $('')
reload.appendTo('.asset-refresh').click (e) =>
@reload(@current)
# Builds the markup for an asset dropped into a textarea.
resize_events: ->
$('.asset-resize').live 'click', =>
constraints = $('#asset-constraints').val()
uid = $('#asset-uid').val()
width = $('#asset-width').val()
height = $('#asset-height').val()
alt = $('#asset-alt').val()
geometry = "#{width}x#{height}#{constraints}"
if constraints == '#' && (width == '' || height == '')
alert 'Can\'t resize image using this geometry. Please select another option or supply a value for both width and height.'
else
url = encodeURI "#{@options.base_url}#{@options.thumb_url}/#{geometry}?uid=#{uid}"
url = url.replace('#','%23')
textile = "!#{url}(#{alt})!"
html = ""
$(@target_textarea).insertAtCaret (if $(@target_textarea).hasClass('textile') then textile else html)
$(document).trigger('close.facebox')
# Toggles params when modal window is opened or closed.
modal_events: ->
@modal_active = false
$(document).bind 'afterClose.facebox', =>
@keys_enabled = true
@modal_active = false
$(document).bind 'loading.facebox', =>
@keys_enabled = false
@modal_active = true
# Setup amplePanels instance for search results and bind search field to methods that
# execute request and parse response.
search: ->
@log 'search()'
search_url = "#{@options.base_url}#{@options.search_url}"
i = ($("##{@options.id} .pages .page").length - 1)
ref = this
$('#asset-results ul').attr('id','assets-result-list').amplePanels(@options.pages_options)
@options.pages[i] = { loaded: true }
$('#asset-search').bind 'change', ->
$("#asset-results ul").amplePanels('empty')
ref.loading.show()
ref.controls(false)
ref.show(i)
ref.activate(i)
ref.searching = true
$('.asset-results').show()
$.post search_url, $(this).serialize(), (response) ->
ref.load_results(response)
, 'json'
# Bind events to tabs.
tab_events: ->
tabs = $("##{@options.id} a.tab")
ref = this
$.each tabs, (idx, el) ->
$(this).addClass('on') if idx == 0
$(el).click ->
ref.goto(idx)
false
# Controls all user keyboard events. Binds as neccesary and
# prevents interaction when key functions are disabled.
key_events: ->
ref = this
previous = 37
next = 39
up = 38
down = 40
escape = 27
# Why does this need to be on keyup?
$(document).keyup (e) =>
return unless @keys_enabled
switch e.keyCode
when escape
@toggle() unless @modal_active
e.stopPropagation();
# Keydown events.
$(document).keydown (e) =>
return unless @keys_enabled
if @active_panel
switch e.keyCode
when previous
$(@active_panel).amplePanels('previous')
when next
$(@active_panel).amplePanels('next')
when up
@previous()
when down
@next()
e.stopPropagation();
# Returns Mustache template for template defined by `view`
tpl: (view) ->
@tpls()[view]
# Returns object containing all Mustache templates.
tpls: ->
# Layout returns the HTML structure of the main asset toolbar.
layout: '
'
# Handle returns HTML for the asset toolbar toggle handle.
handle: '{{ title }}'
# Tab returns HTML for each tab instance.
tab: '{{ title }}'
# Page returns generic HTML structure for each page.
page: '
'
# Show represents the HTML used within the modal window detail view for non-document assets.
show: '
'
# Your search query returned an empty result set...
no_results: '
Sorry. Your search returned zero results.
'
# Drop returns HTML used in the modal window resize view. This is used when dropping an asset onto a textarea.
drop: '
({{ dimensions }}, {{ orientation }})
x
'
# Extend draggable to elements added to the DOM after page load.
jQuery.fn.liveDraggable = (opts) ->
@live "mouseover", ->
$(this).data("init", true).draggable opts unless $(this).data("init")
# Insert `value` at the cursor position of the currently focused textarea or input field.
jQuery.fn.insertAtCaret = (value) ->
@each (i) ->
if document.selection
@focus()
sel = document.selection.createRange()
sel.text = value
@focus()
else if @selectionStart or @selectionStart is "0"
startPos = @selectionStart
endPos = @selectionEnd
scrollTop = @scrollTop
@value = @value.substring(0, startPos) + value + @value.substring(endPos, @value.length)
@focus()
@selectionStart = startPos + value.length
@selectionEnd = startPos + value.length
@scrollTop = scrollTop
else
@value += value
@focus()