This repository has been archived by the owner on Apr 26, 2018. It is now read-only.
/
space-pen.coffee
184 lines (148 loc) · 5.4 KB
/
space-pen.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
elements =
'a abbr address article aside audio b bdi bdo blockquote body button
canvas caption cite code colgroup datalist dd del details dfn div dl dt em
fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup
html i iframe ins kbd label legend li map mark menu meter nav noscript object
ol optgroup option output p pre progress q rp rt ruby s samp script section
select small span strong style sub summary sup table tbody td textarea tfoot
th thead time title tr u ul video area base br col command embed hr img input
keygen link meta param source track wbrk'.split /\s+/
voidElements =
'area base br col command embed hr img input keygen link meta param
source track wbr'.split /\s+/
events =
'blur change click dblclick error focus input keydown
keypress keyup load mousedown mousemove mouseout mouseover
mouseup resize scroll select submit unload'.split /\s+/
idCounter = 0
class View extends jQuery
@builderStack: []
elements.forEach (tagName) ->
View[tagName] = (args...) -> @currentBuilder().tag(tagName, args...)
@subview: (name, view) ->
@currentBuilder().subview(name, view)
@text: (string) -> @currentBuilder().text(string)
@raw: (string) -> @currentBuilder().raw(string)
@currentBuilder: ->
@builderStack[@builderStack.length - 1]
@pushBuilder: ->
@builderStack.push(new Builder)
@popBuilder: ->
@builderStack.pop()
@buildHtml: (fn) ->
@pushBuilder()
fn.call(this)
[html, postProcessingSteps] = @popBuilder().buildHtml()
@render: (fn) ->
[html, postProcessingSteps] = @buildHtml(fn)
fragment = $(html)
step(fragment) for step in postProcessingSteps
fragment
constructor: (params={}) ->
[html, postProcessingSteps] = @constructor.buildHtml -> @content(params)
jQuery.fn.init.call(this, html)
@constructor = jQuery # sadly, jQuery assumes this.constructor == jQuery in pushStack
@wireOutlets(this)
@bindEventHandlers(this)
@find('*').andSelf().data('view', this)
@attr('triggerAttachEvents', true)
step(this) for step in postProcessingSteps
@initialize?(params)
buildHtml: (params) ->
@constructor.builder = new Builder
@constructor.content(params)
[html, postProcessingSteps] = @constructor.builder.buildHtml()
@constructor.builder = null
postProcessingSteps
wireOutlets: (view) ->
@find('[outlet]').each ->
element = $(this)
outlet = element.attr('outlet')
view[outlet] = element
element.attr('outlet', null)
bindEventHandlers: (view) ->
for eventName in events
selector = "[#{eventName}]"
elements = view.find(selector).add(view.filter(selector))
elements.each ->
element = $(this)
methodName = element.attr(eventName)
element.on eventName, (event) -> view[methodName](event, element)
class Builder
constructor: ->
@document = []
@postProcessingSteps = []
buildHtml: ->
[@document.join(''), @postProcessingSteps]
tag: (name, args...) ->
options = @extractOptions(args)
@openTag(name, options.attributes)
if name in voidElements
if (options.text? or options.content?)
throw new Error("Self-closing tag #{name} cannot have text or content")
else
options.content?()
@text(options.text) if options.text
@closeTag(name)
openTag: (name, attributes) ->
attributePairs =
for attributeName, value of attributes
"#{attributeName}=\"#{value}\""
attributesString =
if attributePairs.length
" " + attributePairs.join(" ")
else
""
@document.push "<#{name}#{attributesString}>"
closeTag: (name) ->
@document.push "</#{name}>"
text: (string) ->
escapedString = string
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>')
@document.push escapedString
raw: (string) ->
@document.push string
subview: (outletName, subview) ->
subviewId = "subview-#{++idCounter}"
@tag 'div', id: subviewId
@postProcessingSteps.push (view) ->
view[outletName] = subview
subview.parentView = view
view.find("div##{subviewId}").replaceWith(subview)
extractOptions: (args) ->
options = {}
for arg in args
type = typeof(arg)
if type is "function"
options.content = arg
else if type is "string" or type is "number"
options.text = arg.toString()
else
options.attributes = arg
options
jQuery.fn.view = -> this.data('view')
# Trigger attach event when views are added to the DOM
triggerAttachEvent = (element) ->
if element?.attr?('triggerAttachEvents') and element.parents('html').length
element.find('[triggerAttachEvents]').add(element).trigger('attach')
for methodName in ['append', 'prepend', 'after', 'before']
do (methodName) ->
originalMethod = $.fn[methodName]
jQuery.fn[methodName] = (args...) ->
flatArgs = [].concat args...
result = originalMethod.apply(this, flatArgs)
triggerAttachEvent arg for arg in flatArgs
result
for methodName in ['prependTo', 'appendTo', 'insertAfter', 'insertBefore']
do (methodName) ->
originalMethod = $.fn[methodName]
jQuery.fn[methodName] = (args...) ->
result = originalMethod.apply(this, args)
triggerAttachEvent(this)
result
(exports ? this).View = View
(exports ? this).$$ = (fn) -> View.render.call(View, fn)