atomic_cms 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc +16 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +5 -0
  6. data/.ruby-version +1 -0
  7. data/Gemfile +2 -0
  8. data/Gemfile.lock +243 -0
  9. data/README.md +175 -0
  10. data/Rakefile +15 -0
  11. data/app/assets/images/icon_add_component.png +0 -0
  12. data/app/assets/images/icon_add_component@2x.png +0 -0
  13. data/app/assets/javascripts/atomic_cms.js +286 -0
  14. data/app/assets/stylesheets/atomic_cms.css.scss +135 -0
  15. data/app/components/array_component.rb +11 -0
  16. data/app/controllers/atomic_cms/components_controller.rb +7 -0
  17. data/app/controllers/atomic_cms/media_controller.rb +18 -0
  18. data/app/controllers/concerns/media_scrubber.rb +28 -0
  19. data/app/helpers/component_helper.rb +21 -0
  20. data/app/models/atomic_cms/image.rb +6 -0
  21. data/app/models/atomic_cms/media.rb +12 -0
  22. data/app/views/components/_children_field.html.slim +6 -0
  23. data/app/views/components/_edit.html.slim +2 -0
  24. data/app/views/components/_file_field.slim +5 -0
  25. data/app/views/components/_markdown_field.slim +5 -0
  26. data/app/views/components/_select_field.slim +5 -0
  27. data/app/views/components/_template_field.slim +1 -0
  28. data/app/views/components/_text_area_field.slim +4 -0
  29. data/app/views/components/_text_field.slim +3 -0
  30. data/atomic_cms.gemspec +30 -0
  31. data/bin/rails +12 -0
  32. data/config/routes.rb +4 -0
  33. data/db/migrate/20151013175254_create_media.rb +9 -0
  34. data/lib/generators/atomic_cms/assets/assets_generator.rb +33 -7
  35. data/lib/generators/atomic_cms/scaffold/scaffold_generator.rb +76 -0
  36. data/lib/generators/atomic_cms/templates/admin.erb +79 -0
  37. data/lib/generators/atomic_cms/templates/controller.erb +16 -0
  38. data/lib/generators/atomic_cms/templates/model.erb +4 -0
  39. data/lib/generators/atomic_cms/templates/show.html.slim +1 -0
  40. data/vendor/assets/javascripts/angular-markdown.js +19 -0
  41. data/vendor/assets/javascripts/showdown.min.js +2 -0
  42. metadata +40 -21
@@ -0,0 +1,286 @@
1
+ //= require angular-markdown
2
+ //= require showdown.min
3
+
4
+ (function() {
5
+ 'use strict';
6
+ var findElementIndex, page, positionEditBox, scrollTo, cmsType;
7
+
8
+ findElementIndex = function(node) {
9
+ var i = 0;
10
+ while (node.previousSibling) {
11
+ node = node.previousSibling;
12
+ if (node.nodeType === 1) {
13
+ i += 1;
14
+ }
15
+ }
16
+ return i;
17
+ };
18
+
19
+ scrollTo = function(node) {
20
+ return $('html, body').animate({
21
+ scrollTop: $(node).offset().top
22
+ }, 0);
23
+ };
24
+
25
+ positionEditBox = function(node) {
26
+ var minTop, top;
27
+ minTop = $('#edit-' + cmsType()).offset().top + $('#edit-' + cmsType()).height() + 25;
28
+ top = Math.max(minTop, $(node).offset().top);
29
+ top -= $('#edit-node-column').offset().top;
30
+ return $('#edit-node').css({
31
+ top: top
32
+ }).show();
33
+ };
34
+
35
+ cmsType = function(node) {
36
+ var cms_type = angular.element(document.querySelector('#cms_type')).val();
37
+ return (cms_type !== undefined) ? cms_type : 'page';
38
+ };
39
+
40
+ page = angular.module('page', ['markdown', 'ngSanitize']);
41
+
42
+ page.filter('vimeo_url', [
43
+ '$sce', function($sce) {
44
+ return function(id) {
45
+ if (id.match(/^\d+$/)) {
46
+ return $sce.trustAsResourceUrl('https://player.vimeo.com/video/' + id);
47
+ }
48
+ };
49
+ }
50
+ ]);
51
+
52
+ page.controller('CmsCtrl', [
53
+ '$scope', function($scope) {
54
+ $scope.prefix = cmsType() + '[content_object]';
55
+ return $scope.preview = {};
56
+ }
57
+ ]);
58
+
59
+ page.directive('cmsNode', function() {
60
+ return {
61
+ restrict: 'A',
62
+ scope: true,
63
+ controller: [
64
+ '$scope', '$compile', function($scope, $compile) {
65
+ $scope.$compile = $compile;
66
+ $scope.preview = {};
67
+ return $scope.dragControlListeners = {
68
+ itemMoved: function(event) {
69
+ return alert('moved');
70
+ },
71
+ orderChanged: function(event) {
72
+ return alert('changed');
73
+ }
74
+ };
75
+ }
76
+ ],
77
+ link: function($scope, element, attrs, controller) {
78
+ var appendNode, prefixName, topLevelArray;
79
+ prefixName = $(element).data('cmsNode');
80
+ if (element.hasClass('cms-array-node')) {
81
+ prefixName = findElementIndex(element.get(0));
82
+ $scope.$on('renumber', function(event, args) {
83
+ prefixName = findElementIndex(element.get(0));
84
+ return $scope.prefix = $scope.$parent.prefix + '[' + prefixName + ']';
85
+ });
86
+ }
87
+ $scope.$parent.$watch('prefix', function(prefix) {
88
+ return $scope.prefix = prefix + '[' + prefixName + ']';
89
+ });
90
+ if ($(element).hasClass('cms-array')) {
91
+ topLevelArray = $(element).parent().closest('.cms-array').size() === 0;
92
+ appendNode = function(source) {
93
+ var newEl;
94
+ newEl = angular.element(source);
95
+ element.append(newEl);
96
+ $scope.$compile(newEl)($scope);
97
+ $('a', newEl).click(function(e) {
98
+ return e.preventDefault();
99
+ });
100
+ $(newEl).click();
101
+ if (topLevelArray) {
102
+ return scrollTo(newEl);
103
+ }
104
+ };
105
+ $scope.$on('append', function(event, args) {
106
+ if (!event.defaultPrevented) {
107
+ $.get(args.href, function(data) {
108
+ return appendNode(data);
109
+ });
110
+ return event.preventDefault();
111
+ }
112
+ });
113
+ if (topLevelArray) {
114
+ $('ol.edit-buttons li a.button').unbind('click').bind('click', function() {
115
+ $scope.$broadcast('append', {
116
+ href: $(this).attr('href')
117
+ });
118
+ return false;
119
+ });
120
+ }
121
+ }
122
+ return $scope.edit = function() {
123
+ var $delete, $draft, $editor, $element, $move, $sidebarButtons, $sidebarFields, domNode, parentScope, template;
124
+ $element = $(element);
125
+ $editor = $('#edit-node-fields');
126
+ $sidebarFields = $('<ol>');
127
+ $sidebarButtons = $('#edit-node fieldset.actions ol');
128
+ $sidebarButtons.find('li.button-field').remove();
129
+ $element.find('> .cms-fields > span.li').each(function() {
130
+ var li;
131
+ li = $('<li>').addClass($(this).attr('class')).html($(this).html());
132
+ if (li.hasClass('button-field')) {
133
+ return $sidebarButtons.prepend(li);
134
+ } else {
135
+ return $sidebarFields.append(li);
136
+ }
137
+ });
138
+
139
+ $editor.empty().append($sidebarFields);
140
+
141
+ $editor.find(':file').each(function() {
142
+ var $input = $(this);
143
+ var $next = $($input.siblings('input'));
144
+ $input.attr('name', null).val('');
145
+
146
+ $input.on('change', function(event) {
147
+ var formData = new FormData(),
148
+ fileData = event.target.files[0];
149
+ formData.append('file', fileData);
150
+
151
+ $.ajax({
152
+ url: '/atomic_cms/media',
153
+ type: 'POST',
154
+ dataType: 'text',
155
+ data: formData,
156
+ contentType: false,
157
+ processData: false,
158
+ success: function (data) {
159
+ var parsed = JSON.parse(data);
160
+ $next.val(parsed.url);
161
+ $next.change();
162
+ }
163
+ });
164
+ });
165
+ });
166
+
167
+ $editor.find('.add-children-sublist-item').each(function() {
168
+ var $input = $(this);
169
+ $input.attr('name', null).val('');
170
+ $input.click(function(e) {
171
+ e.preventDefault();
172
+ var component = $(this).siblings('.children-sublist').val();
173
+ if (component !== ''){
174
+ $scope.$broadcast('append', {
175
+ href: component
176
+ });
177
+ }
178
+ });
179
+ });
180
+
181
+ $editor.find(':input').each(function() {
182
+ //guard clause for handling alternatly handled inputs
183
+ if($(this).prop('type') === 'file') { return; }
184
+ if($(this).hasClass('children-sublist')) { return; }
185
+
186
+ var $input = $(this);
187
+ var fieldName = $input.attr('name').replace($scope.prefix, '').replace(/\[|\]/g, '');
188
+
189
+ $input.attr('name', null).val($scope.preview[fieldName]);
190
+
191
+ $input.on('keyup change', function() {
192
+ $scope.$apply(function() {
193
+ $scope.preview[fieldName] = $input.val();
194
+ });
195
+ });
196
+ });
197
+
198
+ $sidebarButtons.find('li.button-field a').unbind('click').click(function() {
199
+ if ($(this).hasClass('emit')) {
200
+ $scope.$emit('append', {
201
+ href: $(this).attr('href')
202
+ });
203
+ } else {
204
+ $scope.$broadcast('append', {
205
+ href: $(this).attr('href')
206
+ });
207
+ }
208
+ return false;
209
+ });
210
+ $draft = $('#draft-panel');
211
+ $draft.find('[data-cms-node]').removeClass('active');
212
+ $draft.add(element).addClass('active');
213
+ template = $element.find('> input[name*=template]').val();
214
+ $('#edit-node legend span').html('Edit ' + (template.replace(/([A-Z]+)/g, ' $1')));
215
+ positionEditBox($element);
216
+ $editor.find(':input').first().focus();
217
+ $delete = $('#edit-node li.delete').hide();
218
+ $move = $('#edit-node li.move').hide();
219
+ parentScope = $scope.$parent;
220
+ if (element.hasClass('cms-array-node')) {
221
+ $move.show();
222
+ $delete.show();
223
+ $delete.find('a.button').unbind('click').click(function() {
224
+ $('#done-edit-node').click();
225
+ element.remove();
226
+ $scope.$destroy();
227
+ return parentScope.$broadcast('renumber');
228
+ });
229
+ domNode = element.get(0);
230
+ $move.find('a#move-node-up').unbind('click').click(function() {
231
+ if (domNode.previousSibling) {
232
+ domNode.parentNode.insertBefore(domNode, domNode.previousSibling);
233
+ positionEditBox(domNode);
234
+ scrollTo(domNode);
235
+ parentScope.$broadcast('renumber');
236
+ }
237
+ return false;
238
+ });
239
+ $move.find('a#move-node-down').unbind('click').click(function() {
240
+ if (domNode.nextSibling) {
241
+ domNode.parentNode.insertBefore(domNode.nextSibling, domNode);
242
+ positionEditBox(domNode);
243
+ scrollTo(domNode);
244
+ parentScope.$broadcast('renumber');
245
+ }
246
+ return false;
247
+ });
248
+ }
249
+ return false;
250
+ };
251
+ }
252
+ };
253
+ });
254
+
255
+ page.directive('cmsField', function() {
256
+ return {
257
+ restrict: 'C',
258
+ scope: false,
259
+ link: function($scope, element, attrs) {
260
+ var $element, fieldName;
261
+ $element = $(element);
262
+ fieldName = $element.attr('name');
263
+ $scope.preview[fieldName] = $element.val();
264
+ return $scope.$watch('prefix', function(prefix) {
265
+ return $element.attr('name', prefix + '[' + fieldName + ']');
266
+ });
267
+ }
268
+ };
269
+ });
270
+
271
+ $(document).ready(function() {
272
+ $('#done-edit-node').click(function() {
273
+ var $draft = $('#draft-panel');
274
+
275
+ $draft.removeClass('active');
276
+ $draft.find('[data-cms-node]').removeClass('active');
277
+
278
+ $('#edit-node').hide();
279
+ return false;
280
+ });
281
+ return $('.preview a').click(function(e) {
282
+ return e.preventDefault();
283
+ });
284
+ });
285
+
286
+ }).call(this);
@@ -0,0 +1,135 @@
1
+ @mixin component_cms_preview {
2
+ #component_preview {
3
+ @content;
4
+ }
5
+ }
6
+
7
+ #component_preview {
8
+ a.button.append-inline, a.button.append-block {
9
+ opacity: 0.5;
10
+
11
+ &:hover {
12
+ opacity: 1;
13
+ }
14
+ }
15
+
16
+ a.button.append-block {
17
+ @include outer-container;
18
+ display: block;
19
+ }
20
+
21
+ .cms-fields {
22
+ display: none;
23
+ }
24
+ }
25
+
26
+ #wrapper #active_admin_content #main_content_wrapper #main_content {
27
+ form#edit-page, form.edit-atomic-content {
28
+ div.buttons {
29
+ @include clearfix;
30
+ margin: 0 0 20px;
31
+
32
+ ol.edit-buttons {
33
+ @include clearfix;
34
+ float: left;
35
+ width: 75%;
36
+
37
+ li {
38
+ float: left;
39
+ font-size: 12px;
40
+ margin: 0 10px 10px 0;
41
+
42
+ a.button {
43
+ @include linear-gradient(to bottom, #ffffff, #f1f1f1, $fallback: #f8f8f8);
44
+ @include shadow(1px, 1px, 3px, rgba(138, 138, 138, 0.22));
45
+ border-color: #dcdcdc;
46
+ color: #666666;
47
+ font-weight: normal;
48
+ line-height: 20px;
49
+ padding-left: 35px;
50
+ position: relative;
51
+ text-shadow: none;
52
+
53
+ &::after {
54
+ @include retina-image("icon_add_component", 16px 15px, "png", null, "@2x", true);
55
+ background-position: left center;
56
+ background-repeat: no-repeat;
57
+ content: '';
58
+ height: 30px;
59
+ position: absolute;
60
+ left: 10px;
61
+ top: 0;
62
+ width: 16px;
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ fieldset.actions {
69
+ float: right;
70
+ margin: 0;
71
+ padding: 0;
72
+ }
73
+ }
74
+ }
75
+
76
+ #draft-panel.active {
77
+ [data-cms-node] {
78
+ h1, h2, h3, h4, h5, h6, p, li, img, a.button, iframe {
79
+ opacity: 0.35;
80
+ }
81
+
82
+ li li {
83
+ opacity: 1;
84
+ }
85
+
86
+ &.active {
87
+ h1, h2, h3, h4, h5, h6, p, li, img, a.button, iframe {
88
+ opacity: 1;
89
+ }
90
+
91
+ [data-cms-node] {
92
+ h1, h2, h3, h4, h5, h6, p, li, img, a.button, iframe {
93
+ opacity: 0.35;
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ #edit-node-column {
102
+ position: relative;
103
+ }
104
+
105
+ #edit-node {
106
+ display: none;
107
+ position: absolute;
108
+ left: 0;
109
+ top: 0;
110
+ width: 100%;
111
+
112
+ li {
113
+ margin: 0 10px 10px 0;
114
+ }
115
+
116
+ li.delete {
117
+ a.button {
118
+ background-color: #d44040;
119
+ border-color: #cc2525;
120
+ color: #ffffff;
121
+ }
122
+ }
123
+ }
124
+
125
+ #edit-node-fields {
126
+ input[type=file] {
127
+ width: calc(80% - 22px);
128
+ font-size: 0.95em;
129
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
130
+ padding: 8px 10px 7px;
131
+ }
132
+ label.filler {
133
+ color: transparent;
134
+ }
135
+ }
@@ -0,0 +1,11 @@
1
+ class ArrayComponent < AtomicAssets::Component
2
+ def render
3
+ children.map(&:render).reduce(:+)
4
+ end
5
+
6
+ def edit
7
+ rtn = cms_fields
8
+ rtn << render_child_array
9
+ rtn.html_safe
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module AtomicCms
2
+ class ComponentsController < ApplicationController
3
+ def edit
4
+ render text: component(params[:id]).edit_array(!!params[:inline])
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ module AtomicCms
2
+ class MediaController < ApplicationController
3
+ def create
4
+ asset = MediaScrubber.new(file: media_params)
5
+ if asset.save
6
+ render json: { url: asset.url }.to_json, status: :created
7
+ else
8
+ render json: {}.to_json, status: :unprocessable_entity
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def media_params
15
+ params.require(:file)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ class MediaScrubber
2
+ attr_accessor :original, :filtered
3
+
4
+ def initialize(args)
5
+ @original = args.fetch(:file, nil)
6
+ @filtered = infer_media_type
7
+ end
8
+
9
+ def infer_media_type
10
+ return nil unless original.respond_to?(:content_type)
11
+ AtomicCms::Image.new(file: original) if original.content_type.match(/image/)
12
+ end
13
+
14
+ def valid?
15
+ return false unless filtered
16
+ filtered.valid?
17
+ end
18
+
19
+ def save
20
+ return false unless valid?
21
+ filtered.save
22
+ end
23
+
24
+ def url
25
+ return nil unless filtered
26
+ filtered.file.url
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ module ComponentHelper
2
+ def add_option(option, output = nil)
3
+ return unless option
4
+ return output if output
5
+ option
6
+ end
7
+
8
+ def markdown(text)
9
+ return unless text
10
+ Redcarpet::Markdown.new(Redcarpet::Render::HTML).render(text).html_safe
11
+ end
12
+
13
+ def markdown_help_url
14
+ "http://nestacms.com/docs/creating-content/markdown-cheat-sheet"
15
+ end
16
+
17
+ def render_children(children)
18
+ return children unless children.present? && children.is_a?(Array)
19
+ children.map(&:render).reduce(:+)
20
+ end
21
+ end
@@ -0,0 +1,6 @@
1
+ module AtomicCms
2
+ class Image < Media
3
+ validates_attachment :file, presence: true,
4
+ content_type: { content_type: /\Aimage/ }
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ module AtomicCms
2
+ class Media < ActiveRecord::Base
3
+ self.inheritance_column = "media_type"
4
+
5
+ has_attached_file :file
6
+ do_not_validate_attachment_file_type :file
7
+
8
+ def self.available_types
9
+ descendants.map { |desc| desc.name.demodulize }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ span.li.string.input
2
+ label.label #{name.to_s.humanize}
3
+ select.cms-field class="children-sublist"
4
+ option
5
+ = options_for_select(options[:collection], value)
6
+ a class="button add-children-sublist-item" Add
@@ -0,0 +1,2 @@
1
+ div#component_preview data-ng-app="page" data-ng-controller="CmsCtrl"
2
+ = f.object.content_edit
@@ -0,0 +1,5 @@
1
+ span.li.string.input
2
+ label.label #{name.to_s.humanize}
3
+ input.cms-field type="file"
4
+ label.label.filler #{name.to_s.humanize}
5
+ input.cms-field type="text" name="#{name}" data-ng-model="preview.#{name}" value="#{value}"
@@ -0,0 +1,5 @@
1
+ span.li.string.input
2
+ label.label #{name.to_s.humanize}
3
+ textarea.cms-field type="text" name="#{name}" data-ng-model="preview.#{name}" rows="10"
4
+ = value
5
+ = link_to "Need help with Markdown?", markdown_help_url, target: '_new'
@@ -0,0 +1,5 @@
1
+ span.li.string.input
2
+ label.label #{name.to_s.humanize}
3
+ select.cms-field name="#{name}" data-ng-model="preview.#{name}"
4
+ option
5
+ = options_for_select(options[:collection], value)
@@ -0,0 +1 @@
1
+ input.cms-field type="hidden" name="template_name" value="#{value}"
@@ -0,0 +1,4 @@
1
+ span.li.string.input
2
+ label.label #{name.to_s.humanize}
3
+ textarea.cms-field type="text" name="#{name}" data-ng-model="preview.#{name}" rows="10"
4
+ = value
@@ -0,0 +1,3 @@
1
+ span.li.string.input
2
+ label.label #{name.to_s.humanize}
3
+ input.cms-field type="text" name="#{name}" value="#{value}" data-ng-model="preview.#{name}"
@@ -0,0 +1,30 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'atomic_cms'
3
+ s.version = '0.2.2'
4
+ s.date = '2015-06-19'
5
+ s.summary = 'Atomic CMS'
6
+ s.description = 'Live CMS powered by atomic assets.'
7
+ s.authors = ['Don Humphreys']
8
+ s.email = 'dhumphreys88@gmail.com'
9
+ s.files = `git ls-files`.split(/\n/)
10
+ s.test_files = Dir['spec/**/*']
11
+ # s.homepage = 'http://rubygems.org/gems/atomic_cms'
12
+ # s.license = 'MIT'
13
+
14
+ s.add_dependency 'rails', '~> 4.2'
15
+ s.add_dependency 'activeadmin', '1.0.0.pre2'
16
+ s.add_dependency 'atomic_assets', '~> 0.1.0'
17
+ s.add_dependency 'jquery-rails', '~> 4.0', '>= 4.0.3'
18
+ s.add_dependency 'redcarpet', '~> 3.3'
19
+ s.add_dependency 'slim-rails', '~> 3.0'
20
+ s.add_dependency 'paperclip', '~> 4.3'
21
+
22
+ s.add_development_dependency 'rspec-core', '~> 3.3'
23
+ s.add_development_dependency 'rspec-expectations', '~> 3.3'
24
+ s.add_development_dependency 'rspec-mocks', '~> 3.3'
25
+ s.add_development_dependency 'rspec-support', '~> 3.3'
26
+ s.add_development_dependency 'rspec-rails'
27
+ s.add_development_dependency 'factory_girl_rails'
28
+ s.add_development_dependency 'shoulda-matchers'
29
+ s.add_development_dependency 'sqlite3'
30
+ end
data/bin/rails ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
3
+
4
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
5
+ ENGINE_PATH = File.expand_path('../../lib/atomic_cms/engine', __FILE__)
6
+
7
+ # Set up gems listed in the Gemfile.
8
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
9
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
10
+
11
+ require 'rails/all'
12
+ require 'rails/engine/commands'
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ AtomicCms::Engine.routes.draw do
2
+ resources :components, only: [:edit]
3
+ resources :media, only: [:create]
4
+ end
@@ -0,0 +1,9 @@
1
+ class CreateMedia < ActiveRecord::Migration
2
+ def change
3
+ create_table :atomic_cms_media do |t|
4
+ t.string :media_type
5
+ t.attachment :file
6
+ t.timestamps
7
+ end
8
+ end
9
+ end