character 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|