Files
jquery-notify/src/notify.coffee
T

545 lines
13 KiB
CoffeeScript
Raw Normal View History

2013-04-04 13:13:08 +11:00
'use strict'
#plugin constants
pluginName = 'notify'
2013-06-04 17:01:03 +10:00
pluginClassName = pluginName+'js'
2013-04-04 13:13:08 +11:00
2013-06-03 12:28:03 +10:00
# ================================
# POSITIONING
# ================================
2013-05-30 16:35:16 +10:00
2013-06-02 23:19:34 +10:00
positions =
2013-05-30 16:35:16 +10:00
t: 'top'
m: 'middle'
b: 'bottom'
l: 'left'
c: 'center'
r: 'right'
2013-06-03 09:15:58 +10:00
hAligns = ['l','c','r']
vAligns = ['t','m','b']
2013-05-31 13:18:43 +10:00
mainPositions = ['t','b','l','r']
2013-05-29 16:59:19 +10:00
#positions mapped to opposites
2013-05-30 16:35:16 +10:00
opposites =
2013-05-29 16:59:19 +10:00
t: 'b'
m: null
b: 't'
l: 'r'
c: null
r: 'l'
2013-04-04 13:13:08 +11:00
2013-05-29 16:59:19 +10:00
parsePosition = (str) ->
pos = []
$.each str.split(/\W+/), (i,word) ->
2013-05-30 16:35:16 +10:00
w = word.toLowerCase().charAt(0)
pos.push w if positions[w]
2013-05-29 16:59:19 +10:00
pos
2013-06-03 12:28:03 +10:00
# ================================
# STYLES
# ================================
styles = {}
coreStyle =
name: 'core'
html: """
2013-06-04 17:01:03 +10:00
<div class="#{pluginClassName}-wrapper">
<div class="#{pluginClassName}-arrow"></div>
<div class="#{pluginClassName}-container"></div>
2013-06-03 12:28:03 +10:00
</div>
"""
2013-06-06 14:03:15 +10:00
# <div class="#{pluginClassName}-debug"></div>
# .#{pluginClassName}-debug {
2013-06-03 12:28:03 +10:00
# position: absolute;
# border: 3px solid red;
# height: 0;
# width: 0;
# }
css: """
2013-06-04 17:01:03 +10:00
.#{pluginClassName}-corner {
2013-06-03 12:28:03 +10:00
position: fixed;
margin: 5px;
z-index: 1050;
}
2013-06-04 17:01:03 +10:00
.#{pluginClassName}-corner .#{pluginClassName}-wrapper,
.#{pluginClassName}-corner .#{pluginClassName}-container {
2013-06-03 12:28:03 +10:00
position: relative;
display: block;
height: inherit;
width: inherit;
2013-06-06 15:35:06 +10:00
margin: 3px;
2013-06-03 12:28:03 +10:00
}
2013-06-04 17:01:03 +10:00
.#{pluginClassName}-wrapper {
2013-06-03 12:28:03 +10:00
z-index: 1;
position: absolute;
display: inline-block;
height: 0;
width: 0;
}
2013-06-04 17:01:03 +10:00
.#{pluginClassName}-container {
2013-06-03 12:28:03 +10:00
display: none;
z-index: 1;
position: absolute;
cursor: pointer;
}
2013-06-04 17:01:03 +10:00
.#{pluginClassName}-text {
2013-06-03 12:28:03 +10:00
position: relative;
}
2013-06-04 17:01:03 +10:00
.#{pluginClassName}-arrow {
2013-06-03 12:28:03 +10:00
position: absolute;
z-index: 2;
width: 0;
height: 0;
}
"""
2013-06-04 17:01:03 +10:00
stylePrefixes =
"border-radius": ["-webkit-", "-moz-"]
2013-06-03 12:28:03 +10:00
2013-06-11 18:28:39 +10:00
getStyle = (name) ->
styles[name]
2013-06-03 12:28:03 +10:00
addStyle = (name, def) ->
2013-06-06 14:03:15 +10:00
2013-06-11 18:28:39 +10:00
unless name
throw "Missing Style name"
unless def
throw "Missing Style definition"
2013-06-06 14:03:15 +10:00
if styles[name]?.cssElem
2013-06-05 13:17:20 +10:00
if window.console
console.warn "#{pluginName}: overwriting style '#{name}'"
2013-06-06 14:03:15 +10:00
styles[name].cssElem.remove()
2013-06-05 13:17:20 +10:00
2013-06-03 12:28:03 +10:00
def.name = name
styles[name] = def
cssText = ""
if def.classes
2013-06-04 17:01:03 +10:00
$.each def.classes, (className, props) ->
cssText += ".#{pluginClassName}-#{def.name}-#{className} {\n"
$.each props, (name, val) ->
#vendor prefixes
if stylePrefixes[name]
$.each stylePrefixes[name], (i, prefix) ->
cssText += " #{prefix}#{name}: #{val};\n"
#add prop
cssText += " #{name}: #{val};\n"
cssText += "}\n"
2013-06-03 12:28:03 +10:00
if def.css
cssText += """
/* styles for #{def.name} */
#{def.css}
2013-04-04 13:48:42 +11:00
"""
2013-06-03 09:15:58 +10:00
2013-06-03 12:28:03 +10:00
return unless cssText
def.cssElem = insertCSS cssText
2013-06-05 13:17:20 +10:00
def.cssElem.attr('id', "notify-#{def.name}")
2013-06-03 09:15:58 +10:00
2013-06-03 12:28:03 +10:00
insertCSS = (cssText) ->
2013-06-04 17:01:03 +10:00
2013-06-03 12:28:03 +10:00
elem = createElem("style")
2013-06-12 13:28:47 +10:00
elem.attr 'type', 'text/css'
2013-06-03 12:28:03 +10:00
$("head").append elem
try
elem.html cssText
catch e #ie fix
elem[0].styleSheet.cssText = cssText
elem
2013-06-03 09:15:58 +10:00
2013-04-04 16:58:22 +11:00
2013-06-03 12:28:03 +10:00
# ================================
# OPTIONS
# ================================
2013-04-04 13:13:08 +11:00
#overridable options
pluginOptions =
2013-06-12 13:28:47 +10:00
clickToHide: true
2013-06-04 17:01:03 +10:00
autoHide: true
autoHideDelay: 5000
2013-05-31 13:18:43 +10:00
arrowShow: true
2013-04-04 13:13:08 +11:00
arrowSize: 5
breakNewLines: true
2013-06-05 13:17:20 +10:00
elementPosition: 'bottom'
globalPosition: 'top right'
2013-06-03 12:28:03 +10:00
style: 'bootstrap'
2013-06-04 17:01:03 +10:00
className: 'error'
2013-04-04 16:58:22 +11:00
showAnimation: 'slideDown'
2013-04-05 11:53:48 +11:00
showDuration: 400
2013-04-04 16:58:22 +11:00
hideAnimation: 'slideUp'
2013-04-05 11:53:48 +11:00
hideDuration: 200
2013-06-06 14:03:15 +10:00
gap: 5
2013-04-04 13:13:08 +11:00
2013-04-05 11:53:48 +11:00
inherit = (a, b) ->
F = () ->
F.prototype = a
$.extend true, new F(), b
2013-04-04 13:13:08 +11:00
2013-06-05 13:17:20 +10:00
defaults = (opts) ->
2013-06-03 12:28:03 +10:00
$.extend pluginOptions, opts
# ================================
# DOM MANIPULATION
# ================================
# plugin helpers
createElem = (tag) ->
$ "<#{tag}></#{tag}>"
2013-06-06 14:03:15 +10:00
# references to global anchor positions
globalAnchors = {}
2013-04-04 16:58:22 +11:00
#gets first on n radios, and gets the fancy stylised input for hidden inputs
2013-04-04 13:13:08 +11:00
getAnchorElement = (element) ->
#choose the first of n radios
if element.is('[type=radio]')
radios = element.parents('form:first').find('[type=radio]').filter (i, e) ->
$(e).attr('name') is element.attr('name')
element = radios.first()
#custom-styled inputs - find thier real element
element
2013-05-31 13:18:43 +10:00
incr = (obj, pos, val) ->
2013-05-30 16:35:16 +10:00
# console.log "incr ---- #{pos} #{val} (#{typeof val})"
if typeof val is 'string'
val = parseInt val, 10
else if typeof val isnt 'number'
return
return if isNaN val
2013-06-02 23:19:34 +10:00
opp = positions[opposites[pos.charAt(0)]]
2013-05-30 16:35:16 +10:00
temp = pos
#use the opposite if exists
if obj[opp] isnt `undefined`
pos = positions[opp.charAt(0)]
2013-06-06 14:03:15 +10:00
val = -val
2013-05-30 16:35:16 +10:00
if obj[pos] is `undefined`
obj[pos] = val
else
obj[pos] += val
null
2013-05-31 13:18:43 +10:00
realign = (alignment, inner, outer) ->
return if alignment in ['l','t']
0
else if alignment in ['c','m']
outer/2 - inner/2
else if alignment in ['r','b']
outer - inner
throw "Invalid alignment"
encode = (text) ->
encode.e = encode.e or createElem "div"
encode.e.text(text).html()
2013-04-05 11:53:48 +11:00
2013-06-03 12:28:03 +10:00
# ================================
# NOTIFY CLASS
# ================================
2013-04-16 15:57:38 +10:00
2013-04-04 13:13:08 +11:00
#define plugin
2013-04-04 16:58:22 +11:00
class Notification
2013-06-02 23:19:34 +10:00
2013-04-04 13:13:08 +11:00
#setup instance variables
2013-04-04 16:58:22 +11:00
constructor: (elem, data, options) ->
2013-06-04 17:01:03 +10:00
options = {className: options} if typeof options is 'string'
2013-04-05 11:53:48 +11:00
@options = inherit pluginOptions, if $.isPlainObject(options) then options else {}
2013-04-04 13:48:42 +11:00
2013-06-06 14:03:15 +10:00
#load style html into @userContainer
2013-04-04 13:48:42 +11:00
@loadHTML()
2013-06-03 12:28:03 +10:00
@wrapper = $(coreStyle.html)
2013-06-04 17:01:03 +10:00
@wrapper.data pluginClassName, @
@arrow = @wrapper.find ".#{pluginClassName}-arrow"
@container = @wrapper.find ".#{pluginClassName}-container"
2013-04-04 14:02:55 +11:00
@container.append @userContainer
2013-04-04 13:13:08 +11:00
2013-04-04 16:58:22 +11:00
if elem and elem.length
@elementType = elem.attr('type')
@originalElement = elem
@elem = getAnchorElement(elem)
2013-06-04 17:01:03 +10:00
@elem.data pluginClassName, @
2013-04-04 16:58:22 +11:00
# add into dom above elem
@elem.before @wrapper
2013-04-04 13:13:08 +11:00
2013-04-04 16:58:22 +11:00
@container.hide()
@run(data)
2013-04-04 13:13:08 +11:00
2013-04-11 17:22:27 +10:00
loadHTML: ->
style = @getStyle()
2013-04-04 14:02:55 +11:00
@userContainer = $(style.html)
2013-05-30 16:35:16 +10:00
@text = @userContainer.find '[data-notify-text]'
2013-04-04 13:48:42 +11:00
if @text.length is 0
2013-06-04 17:01:03 +10:00
@text = @userContainer.find '[data-notify-html]'
2013-06-05 13:17:20 +10:00
@rawHTML = true
2013-06-04 17:01:03 +10:00
if @text.length is 0
throw "style: '#{name}' HTML is missing a: 'data-notify-text' or 'data-notify-html' attribute"
@text.addClass "#{pluginClassName}-text"
2013-04-04 13:48:42 +11:00
2013-06-06 14:03:15 +10:00
show: (show, userCallback) ->
callback = =>
@destroy() if not show and not @elem
userCallback() if userCallback
2013-04-05 12:26:27 +11:00
2013-04-04 13:48:42 +11:00
hidden = @container.parent().parents(':hidden').length > 0
2013-06-02 23:19:34 +10:00
2013-04-05 12:26:27 +11:00
elems = @container.add @arrow
2013-04-05 15:34:55 +11:00
args = []
if hidden and show
fn = 'show'
else if hidden and not show
fn = 'hide'
else if not hidden and show
fn = @options.showAnimation
args.push @options.showDuration
else if not hidden and not show
fn = @options.hideAnimation
args.push @options.hideDuration
2013-05-31 13:18:43 +10:00
else
return callback()
2013-04-05 15:34:55 +11:00
args.push callback
2013-06-02 23:19:34 +10:00
elems[fn].apply elems, args
2013-04-04 13:13:08 +11:00
2013-06-06 14:03:15 +10:00
2013-06-06 15:35:06 +10:00
setGlobalPosition: () ->
position = @getPosition()
2013-06-06 14:03:15 +10:00
[pMain, pAlign] = position
main = positions[pMain]
align = positions[pAlign]
2013-06-06 15:35:06 +10:00
key = pMain+"|"+pAlign
2013-06-06 14:03:15 +10:00
anchor = globalAnchors[key]
unless anchor
anchor = globalAnchors[key] = createElem("div")
css = {}
css[main] = 0
if align is 'middle'
css.top = '45%'
else if align is 'center'
css.left = '45%'
else
css[align] = 0
anchor.css(css).addClass("#{pluginClassName}-corner")
$("body").append anchor
anchor.prepend @wrapper
2013-06-06 15:35:06 +10:00
setElementPosition: () ->
position = @getPosition()
2013-06-06 14:03:15 +10:00
[pMain, pAlign, pArrow] = position
2013-05-30 16:35:16 +10:00
2013-05-31 13:18:43 +10:00
#grab some dimensions
elemPos = @elem.position()
elemH = @elem.outerHeight()
elemW = @elem.outerWidth()
2013-06-03 09:15:58 +10:00
elemIH = @elem.innerHeight()
elemIW = @elem.innerWidth()
2013-05-31 13:18:43 +10:00
wrapPos = @wrapper.position()
contH = @container.height()
contW = @container.width()
2013-05-30 16:35:16 +10:00
2013-04-05 11:53:48 +11:00
#start calculations
2013-05-31 13:18:43 +10:00
mainFull = positions[pMain]
opp = opposites[pMain]
oppFull = positions[opp]
#initial positioning
css = {}
2013-06-04 17:01:03 +10:00
css[oppFull] = if pMain is 'b'
elemH
else if pMain is 'r'
elemW
else
0
2013-04-05 11:53:48 +11:00
2013-05-31 13:18:43 +10:00
#correct for elem-wrapper offset
# unless navigator.userAgent.match /MSIE/
2013-06-06 14:03:15 +10:00
incr css, 'top', elemPos.top - wrapPos.top
2013-05-31 13:18:43 +10:00
incr css, 'left', elemPos.left - wrapPos.left
2013-06-02 23:19:34 +10:00
2013-06-06 14:03:15 +10:00
#correct for margins
for pos in ['top', 'left']
margin = parseInt @elem.css("margin-#{pos}"), 10
incr css, pos, margin if margin
#correct for paddings (only for inline)
# if /^inline/.test @elem.css('display')
# padding = parseInt @elem.css("padding-#{pos}"), 10
# incr css, pos, -padding if padding
2013-04-05 11:53:48 +11:00
2013-06-06 14:03:15 +10:00
#add gap
gap = Math.max 0, @options.gap - if @options.arrowShow then arrowSize else 0
incr css, oppFull, gap
2013-06-03 12:28:03 +10:00
2013-05-31 13:18:43 +10:00
#calculate arrow
if not @options.arrowShow
2013-04-05 11:53:48 +11:00
@arrow.hide()
2013-05-31 13:18:43 +10:00
else
2013-06-03 09:15:58 +10:00
arrowSize = @options.arrowSize
2013-05-31 13:18:43 +10:00
arrowCss = $.extend {}, css
2013-06-04 17:01:03 +10:00
arrowColor = @userContainer.css("border-color") or
@userContainer.css("background-color") or
'white'
2013-05-31 13:18:43 +10:00
#build arrow
for pos in mainPositions
posFull = positions[pos]
continue if pos is opp
2013-06-04 17:01:03 +10:00
color = if posFull is mainFull then arrowColor else 'transparent'
2013-06-03 09:15:58 +10:00
arrowCss["border-#{posFull}"] = "#{arrowSize}px solid #{color}"
2013-05-31 13:18:43 +10:00
#add some room for the arrow
2013-06-03 09:15:58 +10:00
incr css, positions[opp], arrowSize
incr arrowCss, positions[pAlign], arrowSize*2 if pAlign in mainPositions
#calculate container alignment
if pMain in vAligns
2013-05-31 13:18:43 +10:00
incr css, 'left', realign(pAlign, contW, elemW)
2013-06-03 09:15:58 +10:00
incr arrowCss, 'left', realign(pAlign, arrowSize, elemIW) if arrowCss
else if pMain in hAligns
2013-05-31 13:18:43 +10:00
incr css, 'top', realign(pAlign, contH, elemH)
2013-06-03 09:15:58 +10:00
incr arrowCss, 'top', realign(pAlign, arrowSize, elemIH) if arrowCss
2013-04-05 11:53:48 +11:00
2013-05-31 13:18:43 +10:00
css.display = 'block' if @container.is(":visible")
2013-06-04 17:01:03 +10:00
#apply css
2013-05-31 13:18:43 +10:00
@container.removeAttr('style').css css
2013-06-03 09:15:58 +10:00
@arrow.removeAttr('style').css(arrowCss) if arrowCss
2013-04-05 11:53:48 +11:00
getPosition: ->
2013-06-05 13:17:20 +10:00
text = @options.position or if @elem then @options.elementPosition else @options.globalPosition
2013-05-29 16:59:19 +10:00
pos = parsePosition text
2013-06-03 09:15:58 +10:00
#validate position
2013-05-31 13:18:43 +10:00
pos[0] = 'b' if pos.length is 0
if pos[0] not in mainPositions
throw "Must be one of [#{mainPositions}]"
2013-06-03 12:28:03 +10:00
2013-06-03 09:15:58 +10:00
#validate alignment
2013-05-31 13:18:43 +10:00
if pos.length is 1 or
2013-06-03 09:15:58 +10:00
(pos[0] in vAligns and pos[1] not in hAligns) or
(pos[0] in hAligns and pos[1] not in vAligns)
pos[1] = if pos[0] in hAligns then 'm' else 'l'
2013-05-29 16:59:19 +10:00
2013-06-06 14:03:15 +10:00
if pos.length is 2
pos[2] = pos[1]
2013-05-29 16:59:19 +10:00
2013-06-06 14:03:15 +10:00
return pos
2013-04-04 13:13:08 +11:00
2013-04-04 13:48:42 +11:00
getStyle: (name) ->
name = @options.style unless name
2013-04-05 11:53:48 +11:00
name = 'default' unless name
2013-06-03 12:28:03 +10:00
style = styles[name]
2013-06-04 17:01:03 +10:00
throw "Missing style: #{name}"unless style
2013-04-04 13:48:42 +11:00
style
2013-06-05 13:17:20 +10:00
updateClasses: ->
2013-06-04 17:01:03 +10:00
classes = ['base']
2013-06-03 12:28:03 +10:00
2013-06-04 17:01:03 +10:00
if $.isArray @options.className
classes = classes.concat @options.className
else if @options.className
classes.push @options.className
2013-06-03 12:28:03 +10:00
style = @getStyle()
2013-06-04 17:01:03 +10:00
classes = $.map(classes, (n) -> "#{pluginClassName}-#{style.name}-#{n}").join ' '
2013-06-03 12:28:03 +10:00
2013-06-04 17:01:03 +10:00
@userContainer.attr 'class', classes
2013-04-05 11:53:48 +11:00
2013-04-04 13:13:08 +11:00
#run plugin
2013-04-04 16:58:22 +11:00
run: (data, options) ->
2013-04-04 13:13:08 +11:00
#update options
if $.isPlainObject(options)
2013-06-02 23:19:34 +10:00
$.extend @options, options
2013-04-04 13:13:08 +11:00
#shortcut special case
else if $.type(options) is 'string'
@options.color = options
2013-04-04 16:58:22 +11:00
if @container and not data
2013-06-05 13:17:20 +10:00
@show false
2013-04-04 13:13:08 +11:00
return
2013-04-04 16:58:22 +11:00
else if not @container and not data
2013-04-04 13:13:08 +11:00
return
#escape
unless @rawHTML
data = encode(data)
data = data.replace(/\n/,'<br/>') if @options.breakNewLines
2013-04-04 13:13:08 +11:00
#update content
@text.html(data)
2013-04-04 14:02:55 +11:00
2013-06-03 12:28:03 +10:00
#set styles
2013-06-05 13:17:20 +10:00
@updateClasses()
2013-06-03 12:28:03 +10:00
2013-06-06 14:03:15 +10:00
#positioning
if @elem
2013-06-06 15:35:06 +10:00
@setElementPosition()
2013-06-06 14:03:15 +10:00
else
2013-06-06 15:35:06 +10:00
@setGlobalPosition()
2013-04-04 13:13:08 +11:00
2013-04-04 16:58:22 +11:00
@show true
2013-04-04 13:13:08 +11:00
#autohide
2013-04-04 16:58:22 +11:00
if @options.autoHide
clearTimeout @autohideTimer
2013-05-31 13:18:43 +10:00
@autohideTimer = setTimeout =>
2013-04-04 16:58:22 +11:00
@show false
2013-04-04 13:13:08 +11:00
, @options.autoHideDelay
2013-04-05 15:34:55 +11:00
destroy: ->
2013-06-06 14:03:15 +10:00
@wrapper.remove()
2013-04-05 15:34:55 +11:00
2013-06-03 12:28:03 +10:00
# ================================
# API
# ================================
# $.pluginName( { ... } ) changes options for all instances
$[pluginName] = (elem, data, options) ->
if (elem and elem.nodeName) or elem.jquery
$(elem)[pluginName](data, options)
else
options = data
data = elem
new Notification null, data, options
elem
# $( ... ).pluginName( { .. } ) creates a cached instance on each
$.fn[pluginName] = (data, options) ->
$(@).each ->
2013-06-04 17:01:03 +10:00
inst = getAnchorElement($(@)).data(pluginClassName)
2013-06-03 12:28:03 +10:00
if inst
inst.run data, options
else
new Notification $(@), data, options
@
2013-06-06 14:03:15 +10:00
#extra methods
2013-06-12 13:28:47 +10:00
$.extend $[pluginName], { defaults, addStyle, pluginOptions, getStyle, insertCSS }
2013-06-03 12:28:03 +10:00
2013-04-05 12:26:27 +11:00
#when ready
2013-04-04 13:13:08 +11:00
$ ->
2013-06-03 12:28:03 +10:00
#insert default style
2013-06-12 13:28:47 +10:00
insertCSS(coreStyle.css).attr('id', 'core-notify')
2013-04-05 11:53:48 +11:00
2013-04-04 16:58:22 +11:00
#watch all notifications clicks
2013-06-12 13:28:47 +10:00
$(document).on 'click notify-hide', ".#{pluginClassName}-wrapper", (e) ->
2013-06-04 17:01:03 +10:00
inst = $(@).data pluginClassName
2013-06-12 13:28:47 +10:00
if inst and (inst.options.clickToHide or e.type is 'notify-hide')
2013-04-05 15:34:55 +11:00
inst.show false
2013-04-04 13:13:08 +11:00