character 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile +4 -0
- data/README.md +20 -0
- data/Rakefile +7 -0
- data/app/controllers/character/posts_controller.rb +27 -0
- data/app/models/character/post.rb +20 -0
- data/character.gemspec +24 -0
- data/lib/character.rb +3 -0
- data/lib/character/engine.rb +5 -0
- data/lib/character/routing.rb +11 -0
- data/lib/character/version.rb +3 -0
- data/lib/generators/character/install_generator.rb +42 -0
- data/lib/generators/character/templates/README +1 -0
- data/lib/generators/character/templates/admin/character.rb +3 -0
- data/vendor/assets/fonts/general_foundicons.eot +0 -0
- data/vendor/assets/fonts/general_foundicons.svg +15 -0
- data/vendor/assets/fonts/general_foundicons.ttf +0 -0
- data/vendor/assets/fonts/general_foundicons.woff +0 -0
- data/vendor/assets/javascripts/backbone.js +1431 -0
- data/vendor/assets/javascripts/character/index.js.coffee +53 -0
- data/vendor/assets/javascripts/character/models/post.js.coffee +39 -0
- data/vendor/assets/javascripts/character/views/app.js.coffee +81 -0
- data/vendor/assets/javascripts/character/views/editor.js.coffee +231 -0
- data/vendor/assets/javascripts/character/views/editor_settings.js.coffee +44 -0
- data/vendor/assets/javascripts/character/views/index.js.coffee +116 -0
- data/vendor/assets/javascripts/character/views/preview.js.coffee +49 -0
- data/vendor/assets/javascripts/jquery.smartresize.js +30 -0
- data/vendor/assets/javascripts/lodash.js +4258 -0
- data/vendor/assets/javascripts/showdown.js +62 -0
- data/vendor/assets/javascripts/underscore.string.js +600 -0
- data/vendor/assets/stylesheets/character/_base.css.scss +84 -0
- data/vendor/assets/stylesheets/character/_icons.css.scss.erb +96 -0
- data/vendor/assets/stylesheets/character/_view_editor.css.scss +115 -0
- data/vendor/assets/stylesheets/character/_view_index.css.scss +73 -0
- data/vendor/assets/stylesheets/character/_view_preview.css.scss +49 -0
- data/vendor/assets/stylesheets/character/index.css.scss +32 -0
- metadata +103 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
#= require ../lodash
|
2
|
+
#= require ../underscore.string
|
3
|
+
#= require ../backbone
|
4
|
+
#= require ../showdown
|
5
|
+
#= require ../jquery.smartresize
|
6
|
+
|
7
|
+
#= require_tree ./models
|
8
|
+
#= require_tree ./views
|
9
|
+
#= require_tree ./
|
10
|
+
|
11
|
+
|
12
|
+
window.set_routes = ->
|
13
|
+
Router = Backbone.Router.extend
|
14
|
+
routes:
|
15
|
+
'new': 'newPost'
|
16
|
+
'edit/:id': 'editPost'
|
17
|
+
'preview/:id': 'showPostPreview'
|
18
|
+
'': 'showIndex'
|
19
|
+
|
20
|
+
router = new Router
|
21
|
+
|
22
|
+
router.on 'route:newPost', ->
|
23
|
+
window.app.show_editor()
|
24
|
+
|
25
|
+
router.on 'route:editPost', (id) ->
|
26
|
+
window.app.show_editor(id)
|
27
|
+
|
28
|
+
router.on 'route:showPostPreview', (id) ->
|
29
|
+
window.app.show_preview(id)
|
30
|
+
|
31
|
+
router.on 'route:showIndex', (id) ->
|
32
|
+
if window.app.preview_view
|
33
|
+
window.app.close_preview()
|
34
|
+
else
|
35
|
+
window.app.show_index()
|
36
|
+
|
37
|
+
window.router = router
|
38
|
+
|
39
|
+
# Start Backbone history a necessary step for bookmarkable URL's
|
40
|
+
Backbone.history.start()
|
41
|
+
|
42
|
+
|
43
|
+
window.initialize_character = (blog_url = 'http://this-is-blog.com/', app_container = '#wrapper') ->
|
44
|
+
window.posts = new Posts()
|
45
|
+
|
46
|
+
window.posts.fetch success: ->
|
47
|
+
window.app = new AppView(blog_url, app_container)
|
48
|
+
window.app.show_index()
|
49
|
+
|
50
|
+
window.set_routes()
|
51
|
+
|
52
|
+
|
53
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Post extends Backbone.Model
|
2
|
+
#id
|
3
|
+
#title
|
4
|
+
#category
|
5
|
+
#featured_image
|
6
|
+
#html
|
7
|
+
#md
|
8
|
+
#date
|
9
|
+
#views
|
10
|
+
#published
|
11
|
+
|
12
|
+
idAttribute: '_id'
|
13
|
+
|
14
|
+
|
15
|
+
date_formatted: ->
|
16
|
+
date = @get('date')
|
17
|
+
if date then date.replace(/-/g, '/') else "Date Not Set"
|
18
|
+
|
19
|
+
|
20
|
+
featured_image: ->
|
21
|
+
@get('featured_image_url')
|
22
|
+
|
23
|
+
|
24
|
+
state: ->
|
25
|
+
if @get('published')
|
26
|
+
"Published"
|
27
|
+
else "Draft"
|
28
|
+
|
29
|
+
|
30
|
+
@slugify: (text) ->
|
31
|
+
_.string.slugify(text)
|
32
|
+
|
33
|
+
|
34
|
+
class Posts extends Backbone.Collection
|
35
|
+
model: Post
|
36
|
+
url: '/admin/character/posts'
|
37
|
+
|
38
|
+
window.Posts = Posts
|
39
|
+
window.Post = Post
|
@@ -0,0 +1,81 @@
|
|
1
|
+
############################
|
2
|
+
### CHARACTER :: AppView ###
|
3
|
+
############################
|
4
|
+
|
5
|
+
class AppView extends Backbone.View
|
6
|
+
|
7
|
+
initialize: (@blog_url, @el) ->
|
8
|
+
@render()
|
9
|
+
window.index_scroll_y = 0
|
10
|
+
@blog_url ||= 'http://default-blog-url.com/'
|
11
|
+
|
12
|
+
|
13
|
+
render: ->
|
14
|
+
html = """<div id='main'></div>"""
|
15
|
+
$(@el).append html
|
16
|
+
|
17
|
+
|
18
|
+
find_post: (id) ->
|
19
|
+
window.posts.get(id)
|
20
|
+
|
21
|
+
|
22
|
+
clear_view: ->
|
23
|
+
(@index_view.remove() ; delete @index_view) if @index_view
|
24
|
+
(@preview_view.remove() ; delete @preview_view) if @preview_view
|
25
|
+
(@editor_view.remove() ; delete @editor_view) if @editor_view
|
26
|
+
|
27
|
+
|
28
|
+
show_index: ->
|
29
|
+
@clear_view()
|
30
|
+
@index_view = new IndexView()
|
31
|
+
|
32
|
+
|
33
|
+
show_preview: (post_id) ->
|
34
|
+
if @editor_view
|
35
|
+
@clear_view()
|
36
|
+
@show_index()
|
37
|
+
|
38
|
+
@index_view.set_active(post_id)
|
39
|
+
|
40
|
+
if not @preview_view
|
41
|
+
@index_view.lock()
|
42
|
+
|
43
|
+
# render preview
|
44
|
+
@preview_view.remove() if @preview_view
|
45
|
+
|
46
|
+
post = @find_post(post_id)
|
47
|
+
@preview_view = new PostPreviewView model: post
|
48
|
+
|
49
|
+
html = @preview_view.render().el
|
50
|
+
|
51
|
+
$('#main').append(html)
|
52
|
+
|
53
|
+
window.scroll(0, 0)
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
close_preview: ->
|
58
|
+
@index_view.unlock()
|
59
|
+
|
60
|
+
# hide preview
|
61
|
+
if @preview_view
|
62
|
+
@preview_view.remove()
|
63
|
+
delete @preview_view
|
64
|
+
|
65
|
+
@index_view.unset_active()
|
66
|
+
window.index_scroll_y = 0
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
show_editor: (post_id) ->
|
71
|
+
@clear_view()
|
72
|
+
|
73
|
+
post = if post_id then @find_post(post_id) else null
|
74
|
+
|
75
|
+
@editor_view = new EditorView model: post
|
76
|
+
|
77
|
+
|
78
|
+
window.AppView = AppView
|
79
|
+
|
80
|
+
|
81
|
+
|
@@ -0,0 +1,231 @@
|
|
1
|
+
###############################
|
2
|
+
### CHARACTER :: EditorView ###
|
3
|
+
###############################
|
4
|
+
|
5
|
+
# Code is partly taken from:
|
6
|
+
# - http://www.showdown.im/showdown/example/showdown-gui.js
|
7
|
+
|
8
|
+
class EditorView extends Backbone.View
|
9
|
+
tagName: 'div'
|
10
|
+
className: 'editor'
|
11
|
+
|
12
|
+
|
13
|
+
initialize: ->
|
14
|
+
html = @render().el
|
15
|
+
$('#main').append(html)
|
16
|
+
|
17
|
+
@settings = new EditorSettingsView model: @model
|
18
|
+
|
19
|
+
@converter = new Showdown.converter()
|
20
|
+
@last_text = null
|
21
|
+
@title = document.getElementById('title')
|
22
|
+
@update_permalink()
|
23
|
+
|
24
|
+
@slug = document.getElementById('slug')
|
25
|
+
@markdown = document.getElementById('markdown')
|
26
|
+
@html = document.getElementById('html')
|
27
|
+
@max_delay = 3000
|
28
|
+
|
29
|
+
@resize_panels()
|
30
|
+
@typing_events()
|
31
|
+
@convert_text()
|
32
|
+
|
33
|
+
|
34
|
+
update_or_create_post: (extra_attributes, callback) ->
|
35
|
+
attributes =
|
36
|
+
title: $(@title).val()
|
37
|
+
md: $(@markdown).val()
|
38
|
+
html: @html.innerHTML
|
39
|
+
slug: @slug.innerHTML
|
40
|
+
date: @settings.date()
|
41
|
+
|
42
|
+
_.extend attributes, extra_attributes
|
43
|
+
|
44
|
+
if @model
|
45
|
+
@model.save(attributes, {success: callback})
|
46
|
+
else
|
47
|
+
window.posts.create(attributes, {wait: true, success: callback})
|
48
|
+
|
49
|
+
|
50
|
+
#if @model.isNew()
|
51
|
+
# @model.set
|
52
|
+
# id: @model.cid
|
53
|
+
# views: 0
|
54
|
+
# @model.save()
|
55
|
+
# window.posts.add(@model, {at: 0})
|
56
|
+
|
57
|
+
|
58
|
+
save_draft: ->
|
59
|
+
@update_or_create_post {published: false}, =>
|
60
|
+
@back_to_index()
|
61
|
+
|
62
|
+
|
63
|
+
publish: ->
|
64
|
+
@update_or_create_post {published: true}, =>
|
65
|
+
@back_to_index()
|
66
|
+
|
67
|
+
|
68
|
+
back_to_index: ->
|
69
|
+
path = if @model then "#/preview/#{@model.id}" else '#/'
|
70
|
+
router.navigate(path, {trigger: true})
|
71
|
+
|
72
|
+
|
73
|
+
events:
|
74
|
+
'click .save-draft': 'save_draft'
|
75
|
+
'click .publish': 'publish'
|
76
|
+
'click .cancel': 'back_to_index'
|
77
|
+
|
78
|
+
|
79
|
+
update_word_counter: ->
|
80
|
+
counter = 0
|
81
|
+
md_text = $(@markdown).val()
|
82
|
+
if md_text.length > 0
|
83
|
+
counter = md_text.match(/[^\s]+/g).length
|
84
|
+
$(document.getElementById('word_counter')).html "#{counter} words"
|
85
|
+
|
86
|
+
|
87
|
+
update_permalink: ->
|
88
|
+
blog_url = window.app.blog_url
|
89
|
+
slug = Post.slugify($(@title).val())
|
90
|
+
html = """<strong>Permalink:</strong> #{blog_url}<strong id='slug'>#{slug}</strong>"""
|
91
|
+
$('#permalink').html html
|
92
|
+
|
93
|
+
|
94
|
+
render: ->
|
95
|
+
post = if @model then @model.toJSON() else {title: 'Post Title', md: 'Post Text'}
|
96
|
+
|
97
|
+
html = """<header>
|
98
|
+
<div class='title'>
|
99
|
+
<input type='text' placeholder='Title' value='#{post.title}' id='title'/>
|
100
|
+
</div>
|
101
|
+
<div class='permalink' id='permalink'>
|
102
|
+
</div>
|
103
|
+
</header>
|
104
|
+
|
105
|
+
<div class='chr-panel left index fixed'>
|
106
|
+
<section class='container'>
|
107
|
+
<header>
|
108
|
+
<span class='title'>Markdown</span>
|
109
|
+
<span class='buttons'>
|
110
|
+
<a href='#' title='Markdown syntax' class='foundicon-help'></a>
|
111
|
+
</span>
|
112
|
+
</header>
|
113
|
+
<div>
|
114
|
+
<textarea id='markdown'>#{post.md}</textarea>
|
115
|
+
</div>
|
116
|
+
</section>
|
117
|
+
</div>
|
118
|
+
|
119
|
+
<div class='chr-panel right preview fixed'>
|
120
|
+
<section class='container'>
|
121
|
+
<header>
|
122
|
+
<span class='title'>Preview</span>
|
123
|
+
<span class='info' id='word_counter'>549 words</span>
|
124
|
+
</header>
|
125
|
+
|
126
|
+
<article>
|
127
|
+
<section class='content' id='html'></section>
|
128
|
+
</article>
|
129
|
+
</section>
|
130
|
+
</div>
|
131
|
+
|
132
|
+
<footer>
|
133
|
+
<button class='cancel'>Cancel</button>
|
134
|
+
<button class='publish'>Publish</button>
|
135
|
+
<button class='save-draft'>Save Draft</button>
|
136
|
+
</footer>"""
|
137
|
+
$(this.el).html html
|
138
|
+
return this
|
139
|
+
|
140
|
+
|
141
|
+
on_input: (callback) ->
|
142
|
+
# In "delayed" mode, we do the conversion at pauses in input.
|
143
|
+
# The pause is equal to the last runtime, so that slow
|
144
|
+
# updates happen less frequently.
|
145
|
+
#
|
146
|
+
# Use a timer to schedule updates. Each keystroke
|
147
|
+
# resets the timer.
|
148
|
+
|
149
|
+
# if we already have convertText scheduled, cancel it
|
150
|
+
if convert_text_timer
|
151
|
+
window.clearTimeout(convert_text_timer)
|
152
|
+
convert_text_timer = null
|
153
|
+
|
154
|
+
time_until_convert_text = 0
|
155
|
+
|
156
|
+
if time_until_convert_text > @max_delay
|
157
|
+
time_until_convert_text = @max_delay
|
158
|
+
|
159
|
+
# Schedule @convert_text().
|
160
|
+
# Even if we're updating every keystroke, use a timer at 0.
|
161
|
+
# This gives the browser time to handle other events.
|
162
|
+
convert_text_timer = window.setTimeout(callback, time_until_convert_text)
|
163
|
+
|
164
|
+
|
165
|
+
typing_events: ->
|
166
|
+
# First, try registering for keyup events
|
167
|
+
# (There's no harm in calling onInput() repeatedly)
|
168
|
+
@markdown.onkeyup = => @on_input(@convert_text)
|
169
|
+
@title.onkeyup = => @on_input(@update_permalink)
|
170
|
+
|
171
|
+
# In case we can't capture paste events, poll for them
|
172
|
+
polling_fallback = window.setInterval ( => @on_input() if not @markdown.value == @last_text ), 1000
|
173
|
+
|
174
|
+
# Try registering for paste events
|
175
|
+
@markdown.onpaste = =>
|
176
|
+
# It worked! Cancel paste polling.
|
177
|
+
if polling_fallback
|
178
|
+
window.clearInterval(polling_fallback)
|
179
|
+
polling_fallback = null
|
180
|
+
@on_input()
|
181
|
+
|
182
|
+
# Try registering for input events (the best solution)
|
183
|
+
if @markdown.addEventListener
|
184
|
+
# Let's assume input also fires on paste.
|
185
|
+
# No need to cancel our keyup handlers;
|
186
|
+
# they're basically free.
|
187
|
+
@markdown.addEventListener('input', @markdown.onpaste, false)
|
188
|
+
|
189
|
+
@markdown.focus()
|
190
|
+
|
191
|
+
|
192
|
+
convert_text: =>
|
193
|
+
text = @markdown.value
|
194
|
+
|
195
|
+
return if text and text == @last_text
|
196
|
+
|
197
|
+
@last_text = text
|
198
|
+
|
199
|
+
# do the conversion
|
200
|
+
@html.innerHTML = @converter.makeHtml(text)
|
201
|
+
@update_word_counter()
|
202
|
+
|
203
|
+
|
204
|
+
resize_panels: ->
|
205
|
+
markdown = $(@markdown)
|
206
|
+
html = $(@html).parent()
|
207
|
+
window_height = $(window).height()
|
208
|
+
footer_height = $('footer').outerHeight()
|
209
|
+
|
210
|
+
markdown.css 'height', window_height - markdown.offset().top - footer_height
|
211
|
+
html.css 'height', window_height - html.offset().top - footer_height
|
212
|
+
|
213
|
+
$(window).smartresize =>
|
214
|
+
@resize_panels()
|
215
|
+
|
216
|
+
|
217
|
+
destroy = ->
|
218
|
+
# clear smartresize event
|
219
|
+
@markdown.onkeyup = null
|
220
|
+
@title.onkeyup = null
|
221
|
+
|
222
|
+
|
223
|
+
window.EditorView = EditorView
|
224
|
+
|
225
|
+
|
226
|
+
|
227
|
+
|
228
|
+
|
229
|
+
|
230
|
+
|
231
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
class EditorSettingsView extends Backbone.View
|
3
|
+
tagName: 'div'
|
4
|
+
className: 'settings'
|
5
|
+
id: 'settings'
|
6
|
+
|
7
|
+
|
8
|
+
initialize: ->
|
9
|
+
html = @render().el
|
10
|
+
$('footer').append(html)
|
11
|
+
|
12
|
+
@settings_btn = document.getElementById('settings_btn')
|
13
|
+
|
14
|
+
@shown = false
|
15
|
+
|
16
|
+
|
17
|
+
render: ->
|
18
|
+
date = @model?.get('date')
|
19
|
+
html = """<div class='settings-box'>
|
20
|
+
Date: <input id='date' type='date' value='#{date}'>
|
21
|
+
</div>
|
22
|
+
<button class='foundicon-settings' id='settings_btn'></button>"""
|
23
|
+
$(this.el).html html
|
24
|
+
return this
|
25
|
+
|
26
|
+
|
27
|
+
date: ->
|
28
|
+
$('#date').val()
|
29
|
+
|
30
|
+
|
31
|
+
show_or_hide_settings_box: ->
|
32
|
+
if @shown
|
33
|
+
$(@el).removeClass('shown')
|
34
|
+
@shown = false
|
35
|
+
else
|
36
|
+
@shown = true
|
37
|
+
$(@el).addClass('shown')
|
38
|
+
|
39
|
+
|
40
|
+
events:
|
41
|
+
'click #settings_btn': 'show_or_hide_settings_box'
|
42
|
+
|
43
|
+
|
44
|
+
window.EditorSettingsView = EditorSettingsView
|