Advanced Customization in ComfortableMexicanSofa CMS Admin on Rails 6

Billy Cheng
8 min readSep 8, 2020

--

Last time I briefly introduced the CMS engine for Ruby on Rails. I hope you have started building your site or planning your new project around this. It’s a powerful and fully customizable CMS. In my last article, I have talked about the features and how it could integrate with your existing application. Now I want to show you some more advanced customization that you may find it useful should you want more control.

If you have not read my last article, you should read it first.

There are several things that you can modify the CMS Admin and I want to show you here:

  • Add ordering in any list
  • Add new features to WYSIWYG Redactor editor
  • How to group different tags into the same namespaces

Add ordering in any list

If you have visited “Layouts” or “Pages”, you can see there is a “三” on the left side of the list that allows you to re-order. What if you also want to use this re-ordering feature in your has-many relationship?

In my travel plan website, I have a model called Itinerary and each itinerary has many destinations. In my form, I want to be able to reorder my destination within the itinerary. There are several things you need to do to get you started.

First in your Desination model, add a new column rails g migration AddPositionToDestinations position:integer . Then in your model, you create a method so when the model is created a default position is assigned.

app/models/itinerary.rbclass Itinerary < ApplicationRecord  has_many :destinations, dependent: :destroyendapp/models/destination.rbclass Destination < ApplicationRecord  belongs_to :destination
before_create :assign_position
protected def assign_position
max = Destination.maximum(:position)
self.position = max ? max + 1 : 0
end
end

Next, in your controller add the following code.

app/controllers/admin/destinations_controller.rbclass Admin::DestinationsController < Comfy::Admin::BaseController  include ::Comfy::ReorderAction
self.reorder_action_resource = ::Destination
end

Straight forward, right?! Now the most important part is how we actually do in the front end. Since we want to make the list sortable within the Itinerary form instead of the index page of Destination. There are more changes to be done.

There are three points to note here.

  1. You have to make your list destinations-ortable
  2. assign data-id to your model id.
  3. Set an UI element with css classdragger , so you can easily drag and drop to reorder
app/views/admin/itineraries/_form.html.erb<div class="form-row">
<div class="form-group col-md-10">
<%= form.text_field :name, class: "form-control" %>
</div>
<div class="form-group col-md-2">
<%= form.number_field :position, class: "form-control" %>
</div>

....
</div>
<hr sytle="border-bottom: 1px solid #d6dce5;">
<div class="form-row">
<div class="form-group col-md-5">
<label>Destinations</label>
<div class="row">
<% if @itinerary.destinations.present? %>
<ul class="list destinations-sortable">
<% @itinerary.destinations.each do |destination| %>
<li data-id="<%= destination.id %>">
<div class="row">
<div class="col-md-1 item">
<div class="item-controls d-none d-lg-block">
<div class="dragger">
<i class="fas fa-bars"></i>
</div>
</div>
</div>
<div class="col-md-8 item">
<span><%= destination.name %></span>
</div>
<div class="col-md-1 item">
<span><%= destination.position.present? ? destination.position + 1 : destination.position %></span>
</div>
<div class="col-md-3 d-flex align-items-center justify-content-md-end">
<%= link_to 'Delete', admin_itinerary_destination_path(@itinerary, destination), method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-sm btn-danger' %>
</div>
</div>
</li>
<% end %>
</ul>
<% else %>
No destination for this itinerary.
<% end %>
</div>
</div>
</div>

Once you have done all these, you can refresh the page and try.

You will notice that your list is still not able to drag and drop. You are missing the two more configuration.

config/routes.rb...namespace :admin do...resources :destinations do
collection do
put :reorder
end
end
end
...app/assets/javascripts/comfy/admin/cms/destination_sortable_list.js(() => {
const Rails = window.Rails;
const DATA_ID_ATTRIBUTE = 'data-id';
const sortableStore = {
get(sortable) {
return Array.from(sortable.el.children, (el) => el.getAttribute(DATA_ID_ATTRIBUTE));
},
set(sortable) {
fetch(`/admin/destinations/reorder`, {
body: JSON.stringify({order: sortable.toArray()}),
headers: {'Content-Type': 'application/json', 'X-CSRF-Token': Rails.csrfToken()},
credentials: 'same-origin',
method: 'PUT',
});
}
};
const sortableInstances = [];
window.CMS.sortableList = {
init(root = document) {
for (const sortableRoot of root.querySelectorAll('.destinations-sortable')) {
sortableInstances.push(Sortable.create(sortableRoot, {
handle: '.dragger',
draggable: 'li',
dataIdAttr: DATA_ID_ATTRIBUTE,
store: sortableStore,
onStart: (evt) => evt.from.classList.add('sortable-active'),
onEnd: (evt) => evt.from.classList.remove('sortable-active')
}));
}
},
dispose() {
for (const sortable of sortableInstances) {
sortable.destroy();
}
sortableInstances.length = 0;
}
}
})();
...app/assets/javascripts/comfy/admin/cms/custom.js...//= require comfy/admin/cms/destination_sortable_list

Add new features to WYSIWYG Redactor editor

By default, the Redactor editor that comes with ComfortableMexicanSofa has missed several features, e.g. the font color.

Default Redactor toolbar shipped with ComfortableMexicanSofa
After customization on Redactor setting

Now I am showing you how to override and what to add to make this happen.

First, override wysiwyg.js to add new icon to the toolbar. This js file comes with ComfortableMexicanSofa, you can actually look up this file in the original code. The plugins is what we are going to modify.

app/assets/javascripts/comfy/admin/cms/wysiwyg.js(() => {
const Rails = window.Rails;
const buildRedactorOptions = () => {
const fileUploadPath = document.querySelector('meta[name="cms-file-upload-path"]').content;
const pagesPath = document.querySelector('meta[name="cms-pages-path"]').content;
const csrfParam = Rails.csrfParam();
const csrfToken = Rails.csrfToken();
const imageUpload = new URL(fileUploadPath, document.location.href);
imageUpload.searchParams.set('source', 'redactor');
imageUpload.searchParams.set('type', 'image');
imageUpload.searchParams.set(csrfParam, csrfToken);
const imageManagerJson = new URL(fileUploadPath, document.location.href);
imageManagerJson.searchParams.set('source', 'redactor');
imageManagerJson.searchParams.set('type', 'image');
const fileUpload = new URL(fileUploadPath, document.location.href);
fileUpload.searchParams.set('source', 'redactor');
fileUpload.searchParams.set('type', 'file');
fileUpload.searchParams.set(csrfParam, csrfToken);
const fileManagerJson = new URL(fileUploadPath, document.location.href);
fileManagerJson.searchParams.set('source', 'redactor');
fileManagerJson.searchParams.set('type', 'file');
const definedLinks = new URL(pagesPath, document.location.href);
definedLinks.searchParams.set('source', 'redactor');
return {
minHeight: 160,
autoresize: true,
buttonSource: true,
formatting: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
plugins: ['fontcolor', 'imagemanager', 'filemanager', 'table', 'video', 'definedlinks'],
lang: CMS.getLocale(),
convertDivs: false,
imageUpload,
imageManagerJson,
fileUpload,
fileManagerJson,
definedLinks
};
};
const redactorInstances = [];
window.CMS.wysiwyg = {
init(root = document) {
const textareas = root.querySelectorAll('textarea.rich-text-editor, textarea[data-cms-rich-text]');
if (textareas.length === 0) return;
const redactorOptions = buildRedactorOptions();
for (const textarea of textareas) {
redactorInstances.push(new jQuery.Redactor(textarea, redactorOptions));
}
},
dispose() {
for (const redactor of redactorInstances) {
redactor.core.destroy();
}
redactorInstances.length = 0;
}
}
})();

Now we have specified we want to add “fontcolor” plugin to the redactor editor. Then we have to define the plugin fontcolor.js and you are all set. You can change the list of colors to whatever you want the palette to support. Other plugins you can add is font size and font family.

app/assets/javascripts/comfy/admin/cms/redactor/plugins/fontcolor.js(function($)
{
$.Redactor.prototype.fontcolor = function()
{
return {
init: function()
{
var colors = [
'#ffffff', '#000000', '#eeece1', '#1f497d', '#4f81bd', '#c0504d', '#9bbb59', '#8064a2', '#4bacc6', '#f79646', '#ffff00',
'#f2f2f2', '#7f7f7f', '#ddd9c3', '#c6d9f0', '#dbe5f1', '#f2dcdb', '#ebf1dd', '#e5e0ec', '#dbeef3', '#fdeada', '#fff2ca',
'#d8d8d8', '#595959', '#c4bd97', '#8db3e2', '#b8cce4', '#e5b9b7', '#d7e3bc', '#ccc1d9', '#b7dde8', '#fbd5b5', '#ffe694',
'#bfbfbf', '#3f3f3f', '#938953', '#548dd4', '#95b3d7', '#d99694', '#c3d69b', '#b2a2c7', '#b7dde8', '#fac08f', '#f2c314',
'#a5a5a5', '#262626', '#494429', '#17365d', '#366092', '#953734', '#76923c', '#5f497a', '#92cddc', '#e36c09', '#c09100',
'#7f7f7f', '#0c0c0c', '#1d1b10', '#0f243e', '#244061', '#632423', '#4f6128', '#3f3151', '#31859b', '#974806', '#7f6000'
];
var $button = this.button.add('fontcolor', 'Text Color');
// this.button.setIcon($button, '<i class="re-icon-fontcolor"></i>');
var $dropdown = this.button.addDropdown($button);
$dropdown.attr('rel', 'fontcolor');
$dropdown.width(242);
var $selector = $('<div style="overflow: hidden; text-align: center;">');
var $selectorText = $('<span rel="text" class="re-dropdown-box-selector-font" style="background: #eee; float: left; padding: 8px 0; cursor: pointer; font-size: 12px; width: 50%;">Text</span>');
var $selectorBack = $('<span rel="back" class="re-dropdown-box-selector-font" style="float: left; padding: 8px 0; cursor: pointer; font-size: 12px; width: 50%;">Highlight</span>');
$selector.append($selectorText);
$selector.append($selectorBack);
$dropdown.append($selector);this.fontcolor.buildPicker($dropdown, 'textcolor', colors);
this.fontcolor.buildPicker($dropdown, 'backcolor', colors);
$selectorText.on('mousedown', function(e)
{
e.preventDefault();
$dropdown.find('.re-dropdown-box-selector-font').css('background', 'none');
$dropdown.find('.re-dropdown-box-backcolor').hide();
$dropdown.find('.re-dropdown-box-textcolor').show();
$(this).css('background', '#eee');
});
$selectorBack.on('mousedown', function(e)
{
e.preventDefault();
$dropdown.find('.re-dropdown-box-selector-font').css('background', 'none');
$dropdown.find('.re-dropdown-box-textcolor').hide();
$dropdown.find('.re-dropdown-box-backcolor').show();
$(this).css('background', '#eee');
});
},
buildPicker: function($dropdown, name, colors)
{
var $box = $('<div class="re-dropdown-box-' + name + '">');
var rule = (name == 'backcolor') ? 'background-color' : 'color';
var len = colors.length;
var self = this;
var func = function(e)
{
e.preventDefault();
self.fontcolor.set($(this).data('rule'), $(this).attr('rel'));
};
for (var z = 0; z < len; z++)
{
var color = colors[z];
var $swatch = $('<a rel="' + color + '" data-rule="' + rule +'" href="#" style="float: left; box-sizing: border-box; font-size: 0; border: 2px solid #fff; padding: 0; margin: 0; width: 22px; height: 22px;"></a>');
$swatch.css('background-color', color);
$swatch.on('mousedown', func);
$box.append($swatch);
}
var $elNone = $('<a href="#" style="display: block; clear: both; padding: 8px 5px; box-sizing: border-box; font-size: 12px; line-height: 1;"></a>').html(this.lang.get('none'));
$elNone.on('mousedown', $.proxy(function(e)
{
e.preventDefault();
this.fontcolor.remove(rule);
}, this));$box.append($elNone);
$dropdown.append($box);
if (name == 'backcolor')
{
$box.hide();
}
},
set: function(rule, type)
{
this.inline.format('span', 'style', rule + ': ' + type + ';');
this.dropdown.hide();
},
remove: function(rule)
{
this.inline.removeStyleRule(rule);
this.dropdown.hide();
}
};
};
})(jQuery);
...app/assets/javascripts/comfy/admin/cms/custom.js...//= require comfy/admin/cms/redactor/plugins/fontcolor

For more plugin information, you can refer to the Redactor documentation. https://imperavi.com/assets/pdf/redactor-documentation-10.pdf

How to group different tags into the same namespaces

As you define the layout of your site, you may want to organize the different section into a group, so they are meaningful and easier for the Website editor to manage the content.

ComfortableMexicanSofa comes with a namespace attribute where you could group different sections within the same layout in the same namespace.

Take the following snippet of code where I have 5 different namespace to organize the content.

<section class="section-aboutus page-aboutus bigger-padding">
<div class="container">
<div class="row">
<div class="col-lg-6">
<div class="aboutus-card box-shadow-md">
<div class="aboutus-card-content bigger">
<div class="aboutus-card-icon">
{{ cms:file about_us_image, as: image }}
<!-- <img src="img/aboutus-card-icon.png" alt=""> -->
</div>
<h2 class="aboutus-card-title">{{ cms:text title_1, namespace: title_1 }}</h2>
{{ cms:wysiwyg text_11, namespace: title_1 }}
{{ cms:wysiwyg text_12, namespace: title_1 }}
{{ cms:wysiwyg text_13, namespace: title_1 }}
</div>
<!--/aboutus-card-content-->
<div class="d-none d-lg-inline aboutus-card-image" style="background-image: url( {{ cms:file desktop, as: url, namespace: background_photo }} )"></div>
<div class="d-lg-none aboutus-card-image" style="background-image: url({{ cms:file mobile, as: url, namespace: background_photo }})"></div>
</div>
<!--/aboutus-card-->
</div>
<!--/col-->
<div class="col-lg-6">
<div class="aboutus-card-content box-shadow-light mw-100 mb-30">
<h3 class="aboutus-card-title mb-3">
{{ cms:text title_2, namespace: title_2 }}
</h3>
<h4 class="fw-400 mb-0">
{{ cms:wysiwyg text_21, namespace: title_2 }}
</h4>
</div>
<!--/aboutus-card-content-->
<div class="aboutus-card-content box-shadow-light mw-100">
<h3 class="aboutus-card-title mb-3">
{{ cms:text title_3, namespace: title_3 }}
</h3>
{{ cms:wysiwyg text_31, namespace: title_3 }}
<h3 class="aboutus-card-title mb-3">
{{ cms:text title_4, namespace: title_4 }}
</h3>
{{ cms:wysiwyg text_41, namespace: title_4 }}
<h3 class="aboutus-card-title mb-3">
{{ cms:text title_5, namespace: title_5 }}
</h3>
{{ cms:wysiwyg text_51, namespace: title_5 }}
</div>
<!--/aboutus-card-content-->
</div>
<!--/col-->
</div>
<!--/row-->
</div>
<!--/container-->
</section>
<!--/section-aboutus-->

Once you are done, you can see the page will be divided into different tabs.

Namespace

In this article, I have showed you how to use the default framework to add re-ordering to your list, add plugins to your Redactor editor and use namespace to organize your content.

If you enjoy it, leave me a response and see you next time!

--

--

Billy Cheng

Share my tips and codes on my work with Ruby on Rails