atomic_cms 0.2.1 → 0.2.2

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.
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