composite_content 1.0.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.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/.rubocop.yml +43 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +239 -0
- data/LICENSE +13 -0
- data/README.md +112 -0
- data/Rakefile +17 -0
- data/composite_content.gemspec +47 -0
- data/config/locales/en.yml +10 -0
- data/config/locales/fr.yml +10 -0
- data/db/migrate/20220807184334_create_composite_content_slots.rb +17 -0
- data/db/migrate/20220807185808_create_composite_content_blocks.rb +14 -0
- data/db/migrate/20220807191353_create_composite_content_headings.rb +12 -0
- data/db/migrate/20220807191402_create_composite_content_quotes.rb +12 -0
- data/db/migrate/20220807191410_create_composite_content_texts.rb +11 -0
- data/gemfiles/rails-6.1.x.gemfile +14 -0
- data/lib/composite_content/action_view.rb +84 -0
- data/lib/composite_content/active_record.rb +45 -0
- data/lib/composite_content/block.rb +11 -0
- data/lib/composite_content/blocks/heading.rb +21 -0
- data/lib/composite_content/blocks/quote.rb +12 -0
- data/lib/composite_content/blocks/text.rb +12 -0
- data/lib/composite_content/blocks.rb +11 -0
- data/lib/composite_content/engine.rb +50 -0
- data/lib/composite_content/model/base.rb +13 -0
- data/lib/composite_content/model/blockable.rb +21 -0
- data/lib/composite_content/model/builder/base.rb +47 -0
- data/lib/composite_content/model/builder/block.rb +36 -0
- data/lib/composite_content/model/builder/slot.rb +31 -0
- data/lib/composite_content/model/builder.rb +13 -0
- data/lib/composite_content/model.rb +11 -0
- data/lib/composite_content/slot.rb +55 -0
- data/lib/composite_content/version.rb +5 -0
- data/lib/composite_content.rb +14 -0
- data/lib/generators/composite_content/install_generator.rb +20 -0
- data/lib/generators/composite_content/templates/block/_form.html.erb +13 -0
- data/lib/generators/composite_content/templates/blocks/heading/_form.html.erb +9 -0
- data/lib/generators/composite_content/templates/blocks/heading/_show.html.erb +1 -0
- data/lib/generators/composite_content/templates/blocks/quote/_form.html.erb +9 -0
- data/lib/generators/composite_content/templates/blocks/quote/_show.html.erb +7 -0
- data/lib/generators/composite_content/templates/blocks/text/_form.html.erb +4 -0
- data/lib/generators/composite_content/templates/blocks/text/_show.html.erb +1 -0
- data/lib/generators/composite_content/templates/slot/_form.html.erb +13 -0
- metadata +275 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CompositeContent
|
4
|
+
# Additional views helpers to manipulate composite content.
|
5
|
+
module ActionView
|
6
|
+
# Render the content of a CompositeContent::Slot.
|
7
|
+
def composite_content_render(slot)
|
8
|
+
renders = slot.blocks.collect do |block|
|
9
|
+
render "composite_content/blocks/#{block.blockable.block_type}/show", block: block
|
10
|
+
end
|
11
|
+
|
12
|
+
safe_join(renders)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Output an action link to add a block to a slot.
|
16
|
+
#
|
17
|
+
# ==== Signatures
|
18
|
+
#
|
19
|
+
# composite_content_add_block_link(label, form, block_type, options = {})
|
20
|
+
# # Explicit name
|
21
|
+
#
|
22
|
+
# composite_content_add_block_link(form, block_type, options = {}) do
|
23
|
+
# # Name as a block
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# composite_content_add_block_link(form, block_type, options = {})
|
27
|
+
# # Use default name
|
28
|
+
#
|
29
|
+
# ==== Parameters
|
30
|
+
#
|
31
|
+
# `label` is the text to be used as the link label.
|
32
|
+
# Just as when you use the Rails builtin helper +link_to+, you can give an explicit
|
33
|
+
# label to your link or use a block to build it. If you provide neither an explicit
|
34
|
+
# label nor a block, the default label will be used, looking for an I18n key named
|
35
|
+
# `composite_content.blocks.{block type}.add`.
|
36
|
+
#
|
37
|
+
# `form` is your form builder. Can be a SimpleForm::Builder, Formtastic::Builder or
|
38
|
+
# a standard Rails FormBuilder.
|
39
|
+
#
|
40
|
+
# `options` are passed to the cocooned_add_item_link helper, which some adjustements:
|
41
|
+
# :count, :form_name, :force_non_association_create are ignored, :form_name, :partial
|
42
|
+
# and :wrap_object are forced on purpose. Any other option will be passed.
|
43
|
+
# See the documentation of cocooned_add_item_link for details.
|
44
|
+
def composite_content_add_block_link(*args, &block)
|
45
|
+
return composite_content_add_block_link(capture(&block), *args) if block
|
46
|
+
return composite_content_add_block_link(nil, *args) if args.first.respond_to?(:object)
|
47
|
+
|
48
|
+
label, form, block_type, options = *args
|
49
|
+
label ||= composite_content_default_label(block_type)
|
50
|
+
opts = (options || {}).except(:count, :form_name, :force_non_association_create)
|
51
|
+
.merge!(form_name: :form,
|
52
|
+
partial: 'composite_content/block/form',
|
53
|
+
wrap_object: ->(b) { composite_content_wrap_block(b, block_type) })
|
54
|
+
|
55
|
+
cocooned_add_item_link(label, form, :blocks, opts)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Alias to cocooned_move_item_up_link
|
59
|
+
def composite_content_move_block_up_link(*args, &block)
|
60
|
+
cocooned_move_item_up_link(*args, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Alias to cocooned_move_item_down_link
|
64
|
+
def composite_content_move_block_down_link(*args, &block)
|
65
|
+
cocooned_move_item_down_link(*args, &block)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Alias to cocooned_remove_item_link
|
69
|
+
def composite_content_remove_block_link(*args, &block)
|
70
|
+
cocooned_remove_item_link(*args, &block)
|
71
|
+
end
|
72
|
+
|
73
|
+
protected
|
74
|
+
|
75
|
+
def composite_content_default_label(block_type)
|
76
|
+
I18n.t("composite_content.blocks.#{block_type}.add")
|
77
|
+
end
|
78
|
+
|
79
|
+
def composite_content_wrap_block(block, block_type)
|
80
|
+
block.blockable = CompositeContent::Blocks.const_get(block_type.to_s.classify).new
|
81
|
+
block
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CompositeContent
|
4
|
+
module ActiveRecord # :nodoc:
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Integration methods with ActiveRecord models
|
8
|
+
# These methods are available on any ActiveRecord model once CompositeContent is loaded.
|
9
|
+
#
|
10
|
+
# rubocop:disable Naming/PredicateName, Rails/ReflectionClassName
|
11
|
+
module ClassMethods
|
12
|
+
def has_composite_content(name = :composite_content, types: [])
|
13
|
+
CompositeContent::Model::Builder::Block.build(self, name, types)
|
14
|
+
slot_class = CompositeContent::Model::Builder::Slot.build(self, name, types)
|
15
|
+
|
16
|
+
has_one name, class_name: slot_class.name, as: :parent, dependent: :destroy
|
17
|
+
accepts_nested_attributes_for name, reject_if: :all_blank
|
18
|
+
|
19
|
+
include Mixins.instance_mixin(name)
|
20
|
+
extend Mixins.class_mixin(name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
# rubocop:enable Naming/PredicateName, Rails/ReflectionClassName
|
24
|
+
|
25
|
+
module Mixins # :nodoc:
|
26
|
+
class << self
|
27
|
+
def instance_mixin(name)
|
28
|
+
Module.new do
|
29
|
+
define_method name do
|
30
|
+
super() || send(:"build_#{name}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def class_mixin(name)
|
36
|
+
Module.new do
|
37
|
+
define_method "strong_parameters_for_#{name}" do
|
38
|
+
reflect_on_association(name).class_name.constantize.strong_parameters
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CompositeContent
|
4
|
+
# Base class for all types of blocks.
|
5
|
+
class Block < ::CompositeContent::Model::Base
|
6
|
+
acts_as_list scope: :slot_id
|
7
|
+
|
8
|
+
# Delegation to block types is dynamically declared when building dedicated slot and
|
9
|
+
# block classes. See CompositeContent::Model::Builder.
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CompositeContent
|
4
|
+
module Blocks
|
5
|
+
class Heading < ::CompositeContent::Model::Base
|
6
|
+
include ::CompositeContent::Model::Blockable
|
7
|
+
|
8
|
+
validates :level,
|
9
|
+
presence: true,
|
10
|
+
numericality: {
|
11
|
+
allow_blank: true,
|
12
|
+
only_integer: true,
|
13
|
+
greater_than_or_equal_to: 1,
|
14
|
+
less_than_or_equal_to: 6
|
15
|
+
}
|
16
|
+
|
17
|
+
validates :content,
|
18
|
+
presence: true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails'
|
4
|
+
require 'active_model_validations_reflection'
|
5
|
+
require 'acts_as_list'
|
6
|
+
require 'cocooned'
|
7
|
+
|
8
|
+
module CompositeContent
|
9
|
+
class Engine < ::Rails::Engine # :nodoc:
|
10
|
+
# Configurable engine options
|
11
|
+
#
|
12
|
+
# Feel free to adjust these settings in a initializer in your application.
|
13
|
+
# Ex:
|
14
|
+
#
|
15
|
+
# # In config/initializers/composite_content.rb
|
16
|
+
# require 'composite_content'
|
17
|
+
# CompositeContent::Engine.config.block_types += %w[CompositeContent::Blocks::Image]
|
18
|
+
|
19
|
+
# Allowed block types
|
20
|
+
config.block_types = %w[
|
21
|
+
CompositeContent::Blocks::Heading
|
22
|
+
CompositeContent::Blocks::Quote
|
23
|
+
CompositeContent::Blocks::Text
|
24
|
+
]
|
25
|
+
|
26
|
+
# Engine internals
|
27
|
+
#
|
28
|
+
# Following configurations are here for engine initialization and internal
|
29
|
+
# configurations and are not meant to be changed by your application.
|
30
|
+
|
31
|
+
# Isolate models (and their tables) in their own namespace.
|
32
|
+
isolate_namespace CompositeContent
|
33
|
+
|
34
|
+
config.i18n.load_path += Dir[root.join('config', 'locales', '**', '*.{rb,yml}')]
|
35
|
+
|
36
|
+
initializer 'composite_content.active_record' do |_app|
|
37
|
+
::ActiveSupport.on_load :active_record do
|
38
|
+
require 'composite_content/active_record'
|
39
|
+
include CompositeContent::ActiveRecord
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
initializer 'composite_content.action_view' do |_app|
|
44
|
+
ActiveSupport.on_load :action_view do
|
45
|
+
require 'composite_content/action_view'
|
46
|
+
include CompositeContent::ActionView
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CompositeContent
|
4
|
+
module Model
|
5
|
+
# Base class for engine's models.
|
6
|
+
# Equivalent to ApplicationRecord in a Rails app.
|
7
|
+
class Base < ::ActiveRecord::Base
|
8
|
+
self.abstract_class = true
|
9
|
+
|
10
|
+
include ActiveModel::Validations::Reflection
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CompositeContent
|
4
|
+
module Model
|
5
|
+
# Shared behaviors for concrete block implementations
|
6
|
+
module Blockable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
has_one :block,
|
11
|
+
as: :blockable,
|
12
|
+
touch: true,
|
13
|
+
dependent: :destroy
|
14
|
+
end
|
15
|
+
|
16
|
+
def block_type
|
17
|
+
self.class.name.demodulize.underscore
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CompositeContent
|
4
|
+
module Model
|
5
|
+
module Builder
|
6
|
+
class Base # :nodoc:
|
7
|
+
BLOCK_SUFFIX = 'Block'
|
8
|
+
SLOT_SUFFIX = 'Slot'
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def build(parent, association, types = [])
|
12
|
+
new.build(parent, association, types)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def build(parent, association, types = [])
|
17
|
+
demodulized = build_classname(parent, association).demodulize
|
18
|
+
|
19
|
+
parent.const_set(demodulized, build_class(parent, association, types))
|
20
|
+
parent.const_get(demodulized)
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_class(_parent, _association, _types = [])
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_classname(_parent, _association)
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def classname_for_slot(parent, association)
|
34
|
+
classname_for(parent, association, SLOT_SUFFIX)
|
35
|
+
end
|
36
|
+
|
37
|
+
def classname_for_block(parent, association)
|
38
|
+
classname_for(parent, association, BLOCK_SUFFIX)
|
39
|
+
end
|
40
|
+
|
41
|
+
def classname_for(parent, association, suffix)
|
42
|
+
[parent.model_name.to_s, [association.to_s.singularize.classify, suffix].join].join('::')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CompositeContent
|
4
|
+
module Model
|
5
|
+
module Builder
|
6
|
+
# Class builder for Block models
|
7
|
+
#
|
8
|
+
# Every composite content slot as its own block class, subclass of CompositeContent::Block.
|
9
|
+
# This allows to set distinct validations on allowed block types used in each composite
|
10
|
+
# content.
|
11
|
+
class Block < Base
|
12
|
+
alias build_classname classname_for_block
|
13
|
+
|
14
|
+
def build_class(parent, association, types = [])
|
15
|
+
Class.new(::CompositeContent::Block).tap do |klass|
|
16
|
+
klass.belongs_to :slot, class_name: classname_for_slot(parent, association), inverse_of: :blocks
|
17
|
+
|
18
|
+
klass.delegated_type :blockable, types: blockable_types(types), dependent: :destroy
|
19
|
+
klass.accepts_nested_attributes_for :blockable, reject_if: :all_blank
|
20
|
+
|
21
|
+
klass.validates_associated :blockable
|
22
|
+
klass.validates :blockable_type, inclusion: { in: blockable_types(types) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def blockable_types(types)
|
29
|
+
return CompositeContent::Engine.config.block_types if types.empty?
|
30
|
+
|
31
|
+
CompositeContent::Engine.config.block_types & types
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CompositeContent
|
4
|
+
module Model
|
5
|
+
module Builder
|
6
|
+
# Class builder for Slot models
|
7
|
+
#
|
8
|
+
# Every composite content slot is defined by its own class, subclass of CompositeContent::Slot.
|
9
|
+
# This allows to use of a dedicated block class where we can set validations on block types and
|
10
|
+
# to rely on ActiveRecord default behaviors for Single Table Inheritance models (as adding a
|
11
|
+
# constraint on type to queries).
|
12
|
+
class Slot < Base
|
13
|
+
alias build_classname classname_for_slot
|
14
|
+
|
15
|
+
def build_class(parent, association, _types = [])
|
16
|
+
Class.new(CompositeContent::Slot).tap do |klass|
|
17
|
+
klass.has_many :blocks,
|
18
|
+
-> { order(position: :asc) },
|
19
|
+
class_name: classname_for_block(parent, association),
|
20
|
+
foreign_key: :slot_id,
|
21
|
+
inverse_of: :slot,
|
22
|
+
dependent: :destroy
|
23
|
+
|
24
|
+
klass.accepts_nested_attributes_for :blocks, allow_destroy: true, reject_if: :all_blank
|
25
|
+
klass.validates_associated :blocks
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CompositeContent
|
4
|
+
# Slots are used as container for blocks collection.
|
5
|
+
#
|
6
|
+
# Whether a model as one or more composite content slots does not matter: blocks are always
|
7
|
+
# contained in a slot. This allows to keep programming interface consistent between uses and
|
8
|
+
# ease future integration of multiple slots on model where only one was expected at first.
|
9
|
+
class Slot < ::CompositeContent::Model::Base
|
10
|
+
belongs_to :parent, polymorphic: true
|
11
|
+
|
12
|
+
# Compatibility with parent models using Single Table Inheritance.
|
13
|
+
# See ActiveRecord::Associations::ClassMethods's overview for more detail.
|
14
|
+
def parent_type=(class_name)
|
15
|
+
super(class_name.constantize.base_class.to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Association with blocks is dynamically declared when building dedicated slot and
|
19
|
+
# block classes. See CompositeContent::Model::Builder.
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def block_class
|
23
|
+
@block_class ||= reflect_on_association(:blocks).class_name.constantize
|
24
|
+
end
|
25
|
+
|
26
|
+
def blockable_classes
|
27
|
+
@blockable_classes ||= begin
|
28
|
+
validators = block_class.validators_on_of_kinds(:blockable_type, :inclusion)
|
29
|
+
types = validators.collect(&:options).collect { |opts| opts.fetch(:in) }.flatten.compact.uniq
|
30
|
+
types.collect(&:constantize)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def strong_parameters
|
35
|
+
[:id, { blocks_attributes: block_column_names + [{ blockable_attributes: blockable_column_names }, :_destroy] }]
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def block_column_names
|
41
|
+
@block_column_names ||= begin
|
42
|
+
columns = block_class.column_names.collect(&:to_sym)
|
43
|
+
columns - %i[slot_id blockable_id created_at updated_at]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def blockable_column_names
|
48
|
+
@blockable_column_names ||= begin
|
49
|
+
columns = blockable_classes.collect(&:column_names).flatten.collect(&:to_sym).compact.uniq
|
50
|
+
columns - %i[created_at updated_at]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'composite_content/version'
|
4
|
+
require 'composite_content/engine'
|
5
|
+
require 'composite_content/model'
|
6
|
+
require 'composite_content/active_record'
|
7
|
+
|
8
|
+
module CompositeContent # :nodoc:
|
9
|
+
extend ActiveSupport::Autoload
|
10
|
+
|
11
|
+
autoload :Block
|
12
|
+
autoload :Blocks
|
13
|
+
autoload :Slot
|
14
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CompositeContent
|
4
|
+
module Generators
|
5
|
+
# Generator to copy CompositeContent views into an application.
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
desc 'Copies composite_content partial templates to your application.'
|
8
|
+
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
10
|
+
|
11
|
+
def copy_views
|
12
|
+
directory File.join(self.class.source_root), 'app/views/composite_content'
|
13
|
+
end
|
14
|
+
|
15
|
+
def copy_migrations
|
16
|
+
rake 'composite_content:install:migrations'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<div class="cocooned-item">
|
2
|
+
<%= form.hidden_field :id %>
|
3
|
+
<%= form.hidden_field :position %>
|
4
|
+
<%= form.hidden_field :blockable_type %>
|
5
|
+
|
6
|
+
<%= form.fields_for :blockable, form.object.blockable do |blockable_form| %>
|
7
|
+
<%= render "composite_content/blocks/#{form.object.blockable.block_type}/form", form: blockable_form %>
|
8
|
+
<% end %>
|
9
|
+
|
10
|
+
<%= composite_content_move_block_up_link form %>
|
11
|
+
<%= composite_content_move_block_down_link form %>
|
12
|
+
<%= composite_content_remove_block_link form %>
|
13
|
+
</div>
|
@@ -0,0 +1 @@
|
|
1
|
+
<h<%= block.blockable.level %>><%= block.blockable.content %></h<%= block.blockable.level %>>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= block.blockable.content %>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<%= form.hidden_field :id %>
|
2
|
+
|
3
|
+
<div data-cocooned-options="<%= { reorderable: true }.to_json %>">
|
4
|
+
<%= form.fields_for :blocks do |block_form| %>
|
5
|
+
<%= render 'composite_content/block/form', form: block_form %>
|
6
|
+
<% end %>
|
7
|
+
|
8
|
+
<div class="links">
|
9
|
+
<%= composite_content_add_block_link form, :heading %>
|
10
|
+
<%= composite_content_add_block_link form, :quote %>
|
11
|
+
<%= composite_content_add_block_link form, :text %>
|
12
|
+
</div>
|
13
|
+
</div>
|