Commits

Konstantin Semenov committed 51d7ad4

Source commit

  • Participants
  • Parent commits 643f952

Comments (0)

Files changed (153)

+# Bootsy Changelog
+
+## 1.2.0
+
+* Merged the [SimpleForm](https://github.com/plataformatec/simple_form) support into master.
+  You no longer need to use the [bootsy-simple_form](https://github.com/volmer/bootsy-simple_form)
+  gem in your project.
+
+## 1.1.0
+
+* Design and interaction improvements. Bootsy is a lot more 'ajaxy' now, and loads images
+  without flashing. This also changed the way deleting an image works. It no longer
+  'refreshes the gallery', but instead finds the element to delete, fades it out,
+  and removes it (thanks @anthonycollini).
+* Fixed the indent and outdent icons as the icons were reversed from what they
+  should be (thanks @anthonycollini).
+* `z-index` fix to allow the contextual drop-down menus to properly appear over the footer and
+  modal if it includes a longer list (thanks @anthonycollini).
+* A default message if there are no uploaded images (thanks @anthonycollini).
+* Bootsy now uses [Bootstrap File Input](https://github.com/grevory/bootstrap-file-input) with
+  auto submission on file input change (thanks @anthonycollini).
+
+## 1.0.0
+
+* Now Bootsy is compatible with Rails `4.0` - *Backwards incompatibility*: Bootsy does not support
+  Rails `3.2` anymore. We strongly recomend to move on and upgrade outdated projects to Rails `4.0`.
+  If you didn't upgrade your project yet, you can use
+  [our branch with *temporary* support for Rails `3.2`](https://github.com/volmer/bootsy/tree/rails-3.2).
+* Bootsy now accepts file storage in cloud services like Amazon S3 and others.
+  In `config/intializers/bootsy.rb`, just set a new attribute called `storage`
+  as :fog. If you do that, please add 'fog' to your Gemfile and create and
+  configure your service's credentials in an   initializer file, as described in [Carrierwave's docs](https://github.com/carrierwaveuploader/carrierwave/blob/master/README.md#using-amazon-s3).
+* It is possobile now to delete orphan image galleries. Just call `.destroy_orphans` with a time limit:
+
+```ruby
+Bootsy::ImageGallery.destroy_orphans(1.day.ago)
+```
+
+* Now it's possible to fully clear the editor in the client side, by calling the function `clear()` in your Bootsy area:
+
+```javascript
+Bootsy.areas[0].clear();
+```
+source "http://rubygems.org"
+
+# Gems used by the dummy application
+gem 'rails', '~> 4.0.0'
+gem 'jquery-rails', '~> 3.0'
+gem 'twitter-bootstrap-rails', '~> 2.2'
+gem 'sqlite3', '~> 1.3'
+gem 'simple_form', '~> 3.0.0.rc'
+
+gemspec
+
+# Coveralls
+gem 'coveralls', require: false
+PATH
+  remote: .
+  specs:
+    bootsy (1.2.1)
+      carrierwave (~> 0.9.0)
+      mini_magick (~> 3.6.0)
+      remotipart (~> 1.2.1)
+
+GEM
+  remote: http://rubygems.org/
+  specs:
+    actionmailer (4.0.0)
+      actionpack (= 4.0.0)
+      mail (~> 2.5.3)
+    actionpack (4.0.0)
+      activesupport (= 4.0.0)
+      builder (~> 3.1.0)
+      erubis (~> 2.7.0)
+      rack (~> 1.5.2)
+      rack-test (~> 0.6.2)
+    activemodel (4.0.0)
+      activesupport (= 4.0.0)
+      builder (~> 3.1.0)
+    activerecord (4.0.0)
+      activemodel (= 4.0.0)
+      activerecord-deprecated_finders (~> 1.0.2)
+      activesupport (= 4.0.0)
+      arel (~> 4.0.0)
+    activerecord-deprecated_finders (1.0.3)
+    activesupport (4.0.0)
+      i18n (~> 0.6, >= 0.6.4)
+      minitest (~> 4.2)
+      multi_json (~> 1.3)
+      thread_safe (~> 0.1)
+      tzinfo (~> 0.3.37)
+    arel (4.0.0)
+    atomic (1.1.13)
+    builder (3.1.4)
+    capybara (2.1.0)
+      mime-types (>= 1.16)
+      nokogiri (>= 1.3.3)
+      rack (>= 1.0.0)
+      rack-test (>= 0.5.4)
+      xpath (~> 2.0)
+    carrierwave (0.9.0)
+      activemodel (>= 3.2.0)
+      activesupport (>= 3.2.0)
+      json (>= 1.7)
+    childprocess (0.3.9)
+      ffi (~> 1.0, >= 1.0.11)
+    colorize (0.5.8)
+    coveralls (0.6.7)
+      colorize
+      multi_json (~> 1.3)
+      rest-client
+      simplecov (>= 0.7)
+      thor
+    cucumber (1.3.6)
+      builder (>= 2.1.2)
+      diff-lcs (>= 1.1.3)
+      gherkin (~> 2.12.0)
+      multi_json (~> 1.7.5)
+      multi_test (>= 0.0.2)
+    cucumber-rails (1.4.0)
+      capybara (>= 1.1.2)
+      cucumber (>= 1.2.0)
+      nokogiri (>= 1.5.0)
+      rails (>= 3.0.0)
+    database_cleaner (1.0.1)
+    diff-lcs (1.2.4)
+    erubis (2.7.0)
+    execjs (2.0.1)
+    factory_girl (4.2.0)
+      activesupport (>= 3.0.0)
+    factory_girl_rails (4.2.1)
+      factory_girl (~> 4.2.0)
+      railties (>= 3.0.0)
+    ffi (1.9.0)
+    gherkin (2.12.1)
+      multi_json (~> 1.3)
+    hike (1.2.3)
+    i18n (0.6.5)
+    jquery-rails (3.0.4)
+      railties (>= 3.0, < 5.0)
+      thor (>= 0.14, < 2.0)
+    json (1.8.0)
+    mail (2.5.4)
+      mime-types (~> 1.16)
+      treetop (~> 1.4.8)
+    mime-types (1.25)
+    mini_magick (3.6.0)
+      subexec (~> 0.2.1)
+    mini_portile (0.5.1)
+    minitest (4.7.5)
+    multi_json (1.7.9)
+    multi_test (0.0.2)
+    nokogiri (1.6.0)
+      mini_portile (~> 0.5.0)
+    polyglot (0.3.3)
+    rack (1.5.2)
+    rack-test (0.6.2)
+      rack (>= 1.0)
+    rails (4.0.0)
+      actionmailer (= 4.0.0)
+      actionpack (= 4.0.0)
+      activerecord (= 4.0.0)
+      activesupport (= 4.0.0)
+      bundler (>= 1.3.0, < 2.0)
+      railties (= 4.0.0)
+      sprockets-rails (~> 2.0.0)
+    railties (4.0.0)
+      actionpack (= 4.0.0)
+      activesupport (= 4.0.0)
+      rake (>= 0.8.7)
+      thor (>= 0.18.1, < 2.0)
+    rake (10.1.0)
+    remotipart (1.2.1)
+    rest-client (1.6.7)
+      mime-types (>= 1.16)
+    rspec-core (2.14.5)
+    rspec-expectations (2.14.2)
+      diff-lcs (>= 1.1.3, < 2.0)
+    rspec-mocks (2.14.3)
+    rspec-rails (2.14.0)
+      actionpack (>= 3.0)
+      activesupport (>= 3.0)
+      railties (>= 3.0)
+      rspec-core (~> 2.14.0)
+      rspec-expectations (~> 2.14.0)
+      rspec-mocks (~> 2.14.0)
+    rubyzip (0.9.9)
+    selenium-webdriver (2.35.1)
+      childprocess (>= 0.2.5)
+      multi_json (~> 1.0)
+      rubyzip (< 1.0.0)
+      websocket (~> 1.0.4)
+    shoulda-matchers (2.3.0)
+      activesupport (>= 3.0.0)
+    simple_form (3.0.0.rc)
+      actionpack (>= 4.0.0.rc1, < 4.1)
+      activemodel (>= 4.0.0.rc1, < 4.1)
+    simplecov (0.7.1)
+      multi_json (~> 1.0)
+      simplecov-html (~> 0.7.1)
+    simplecov-html (0.7.1)
+    sprockets (2.10.0)
+      hike (~> 1.2)
+      multi_json (~> 1.0)
+      rack (~> 1.0)
+      tilt (~> 1.1, != 1.3.0)
+    sprockets-rails (2.0.0)
+      actionpack (>= 3.0)
+      activesupport (>= 3.0)
+      sprockets (~> 2.8)
+    sqlite3 (1.3.8)
+    subexec (0.2.3)
+    thor (0.18.1)
+    thread_safe (0.1.2)
+      atomic
+    tilt (1.4.1)
+    treetop (1.4.15)
+      polyglot
+      polyglot (>= 0.3.1)
+    twitter-bootstrap-rails (2.2.8)
+      actionpack (>= 3.1)
+      execjs
+      rails (>= 3.1)
+      railties (>= 3.1)
+    tzinfo (0.3.37)
+    websocket (1.0.7)
+    xpath (2.0.0)
+      nokogiri (~> 1.3)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  bootsy!
+  coveralls
+  cucumber-rails (~> 1.4)
+  database_cleaner (~> 1.0.1)
+  factory_girl_rails (~> 4.2)
+  jquery-rails (~> 3.0)
+  rails (~> 4.0.0)
+  rspec-rails (~> 2.14)
+  selenium-webdriver (~> 2.35)
+  shoulda-matchers (~> 2.3)
+  simple_form (~> 3.0.0.rc)
+  sqlite3 (~> 1.3)
+  twitter-bootstrap-rails (~> 2.2)
+Copyright 2013 Volmer Soares
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+# Bootsy
+
+[![Gem Version](https://badge.fury.io/rb/bootsy.png)](http://badge.fury.io/rb/bootsy)
+[![Build Status](https://secure.travis-ci.org/volmer/bootsy.png?branch=master)](http://travis-ci.org/volmer/bootsy)
+[![Dependency Status](https://gemnasium.com/volmer/bootsy.png)](https://gemnasium.com/volmer/bootsy)
+[![Code Climate](https://codeclimate.com/github/volmer/bootsy.png)](https://codeclimate.com/github/volmer/bootsy)
+[![Coverage Status](https://coveralls.io/repos/volmer/bootsy/badge.png?branch=master)](https://coveralls.io/r/volmer/bootsy)
+
+*Bootsy* is a WYSIWYG solution for Rails based on [Bootstrap-wysihtml5](https://github.com/jhollingworth/bootstrap-wysihtml5) which includes image uploads via [CarrierWave](https://github.com/carrierwaveuploader/carrierwave).
+
+![image](https://f.cloud.github.com/assets/301187/1069163/d3d9de0a-140c-11e3-842a-040484df2933.png)
+
+
+## Requirements
+
+* Ruby `2.0` or `1.9.3`;
+* ImageMagick or GraphicsMagick (for MiniMagick);
+* Rails `4.0`;
+* [Bootstrap `2.3`](http://getbootstrap.com/2.3.2/) properly added to your application.
+
+
+## Installation
+
+1. Add Bootsy to your Gemfile:
+```ruby
+gem 'bootsy'
+```
+
+2. Run the bundle command to install it:
+```console
+bundle install
+```
+
+3. Run the install generator:
+```console
+rails generate bootsy:install
+```
+  It will include the javascripts and stylesheets in the assets pipeline, 
+  create the `bootsy.rb` initializer and add a copy of the english translations.
+
+4. Add and run migrations (if you're using ActiveRecord):
+```console
+rake bootsy:install:migrations
+rake db:migrate
+```
+
+
+## Usage
+
+Just call the brand new method `bootsy_area` in your `FormBuilder` instances, the 
+same way you'd call the basic `textarea` method. Example:
+```erb
+<%= form_for(@post) do |f| %>
+  <%= f.label :title %>
+  <%= f.text_field :title %>
+
+  <%= f.label :content %>
+  <%= f.bootsy_area :content %>
+
+  <%= f.submit %>
+<% end %>
+```
+
+Bootsy will group the uploaded images as galleries and associate them to one of 
+your models. For example, if you have a `Post` model and you want to use `bootsy_area` 
+with it, then you should include the `Bootsy::Container` module:
+```ruby
+class Post < ActiveRecord::Base
+  include Bootsy::Container
+
+  attr_accessible :content, :title
+end
+```
+
+Don't forget to ensure the association of new instances of your model with Bootsy 
+image galleries. For `strong_parameters`, you must allow the parameter `bootsy_image_gallery_id` 
+in your controllers. Example:
+```ruby
+private
+# Never trust parameters from the scary internet, only allow the white list through.
+def post_params
+  params.require(:post).permit(:title, :content, :bootsy_image_gallery_id)
+end
+```
+
+
+## Bootsy with [Simple Form](https://github.com/plataformatec/simple_form) builders
+
+Just use the brand new input type `bootsy` in your `SimpleForm::FormBuilder` instances, 
+in the same way you would use the basic `text` input. Example:
+```erb
+<%= simple_form_for @post do |f| %>
+  <%= f.input :title %>
+
+  <%= f.input :content, as: :bootsy %>
+
+  <%= f.button :submit %>
+<% end %>
+```
+
+
+## Editor options
+
+It is possible to customize how the editor is displayed and its behavior by passing 
+a hash `editor_options` to your `bootsy_area`.
+
+
+### Buttons
+
+You can enable/disable the buttons available on the editor. For example, if you 
+want to disable the link and color buttons:
+```erb
+<%= f.bootsy_area :my_attribute, editor_options: {link: false, color: false} %>
+```
+Available options are: `:font_styles`, `:emphasis`, `:lists`, `:html`, `:link`, `:image` and `:color`.
+
+
+### Alert for usaved changes
+
+By default, Bootsy alerts for unsaved changes if the user attempts to unload 
+the window. You can disable this behaviour by doing:
+```erb
+<%= f.bootsy_area :my_attribute, editor_options: {alert_unsaved: false} %>
+```
+
+## Uploader
+
+It's also possible to use Bootsy without the image upload feature. Just call 
+`bootsy_area` in a form builder not associated to a `Bootsy::Container` model. 
+This way users can insert images in their texts by providing an image url.
+
+
+## Configuration
+
+You can set the default editor options, image sizes available (small, medium, 
+large and/or its original), its dimensions and more. Take a look at the initalizer 
+file, `/config/initializers/bootsy.rb`.
+
+
+## I18n
+
+Bootsy defines some i18n keys. The english translation is automatically added 
+to your `config/locales` directory as `bootsy.en.yml`. You can follow that template 
+in order to translate Bootsy to your language. You can find some examples 
+[here](https://github.com/volmer/bootsy/tree/master/config/locales). It is also 
+necessary to add a translation for Bootstrap-wysihtml5, the javascript editor, in 
+your assets pipeline. Instructions [here](https://github.com/jhollingworth/bootstrap-wysihtml5#i18n). 
+If you are using the alert for unsaved changes, you have to define a translation 
+for it as well. Just follow [this example](https://github.com/volmer/bootsy/tree/master/app/assets/bootsy/locales/bootsy.pt-BR.js).
+
+
+## License
+
+MIT License. Copyright 2013 Volmer Soares
+#!/usr/bin/env rake
+begin
+  require 'bundler/setup'
+rescue LoadError
+  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
+end
+begin
+  require 'rdoc/task'
+rescue LoadError
+  require 'rdoc/rdoc'
+  require 'rake/rdoctask'
+  RDoc::Task = Rake::RDocTask
+end
+
+RDoc::Task.new(:rdoc) do |rdoc|
+  rdoc.rdoc_dir = 'rdoc'
+  rdoc.title    = 'Bootsy'
+  rdoc.options << '--line-numbers'
+  rdoc.rdoc_files.include('README.rdoc')
+  rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+Bundler::GemHelper.install_tasks
+
+require 'cucumber/rake/task'
+require 'rspec/core/rake_task'
+require 'coveralls/rake/task'
+
+Coveralls::RakeTask.new
+
+task default: [:spec, :cucumber, 'coveralls:push']
+
+RSpec::Core::RakeTask.new(:spec)
+
+Cucumber::Rake::Task.new do |t|
+  # Uncomment this line when cucumber/multi_test work with minitest.
+  # t.cucumber_opts = %w{--format pretty -s}
+end

app/assets/images/bootsy/ajax-loader.gif

Added
New image

app/assets/javascripts/bootsy.js

+//= require jquery.remotipart
+//= require bootsy/wysihtml5
+//= require bootsy/bootstrap-wysihtml5
+//= require bootsy/bootsy
+//= require bootsy/bootstrap.file-input.js
+//= require bootsy/init
+//= require bootsy/editor_options
+//= require bootsy/translations

app/assets/javascripts/bootsy/bootstrap-wysihtml5.js

+!function($, wysi) {
+    "use strict";
+
+    var tpl = {
+        "font-styles": function(locale, options) {
+            var size = (options && options.size) ? ' btn-'+options.size : '';
+            return "<li class='dropdown'>" +
+              "<a class='btn dropdown-toggle" + size + "' data-toggle='dropdown' href='#' title='" + locale.font_styles.title + "'>" +
+              "<i class='icon-font'></i>&nbsp;<span class='current-font'>" + locale.font_styles.normal + "</span>&nbsp;<b class='caret'></b>" +
+              "</a>" +
+              "<ul class='dropdown-menu'>" +
+                "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='div' tabindex='-1'>" + locale.font_styles.normal + "</a></li>" +
+                "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h1' tabindex='-1'>" + locale.font_styles.h1 + "</a></li>" +
+                "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h2' tabindex='-1'>" + locale.font_styles.h2 + "</a></li>" +
+                "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h3' tabindex='-1'>" + locale.font_styles.h3 + "</a></li>" +
+              "</ul>" +
+            "</li>";
+        },
+
+        "emphasis": function(locale, options) {
+            var size = (options && options.size) ? ' btn-'+options.size : '';
+            return "<li>" +
+              "<div class='btn-group'>" +
+                "<a class='btn" + size + "' data-wysihtml5-command='bold' title='CTRL+B' tabindex='-1'>" + locale.emphasis.bold + "</a>" +
+                "<a class='btn" + size + "' data-wysihtml5-command='italic' title='CTRL+I' tabindex='-1'>" + locale.emphasis.italic + "</a>" +
+                "<a class='btn" + size + "' data-wysihtml5-command='underline' title='CTRL+U' tabindex='-1'>" + locale.emphasis.underline + "</a>" +
+              "</div>" +
+            "</li>";
+        },
+
+        "lists": function(locale, options) {
+            var size = (options && options.size) ? ' btn-'+options.size : '';
+            return "<li>" +
+              "<div class='btn-group'>" +
+                "<a class='btn" + size + "' data-wysihtml5-command='insertUnorderedList' title='" + locale.lists.unordered + "' tabindex='-1'><i class='icon-list'></i></a>" +
+                "<a class='btn" + size + "' data-wysihtml5-command='insertOrderedList' title='" + locale.lists.ordered + "' tabindex='-1'><i class='icon-th-list'></i></a>" +
+                "<a class='btn" + size + "' data-wysihtml5-command='Outdent' title='" + locale.lists.outdent + "' tabindex='-1'><i class='icon-indent-left'></i></a>" +
+                "<a class='btn" + size + "' data-wysihtml5-command='Indent' title='" + locale.lists.indent + "' tabindex='-1'><i class='icon-indent-right'></i></a>" +
+              "</div>" +
+            "</li>";
+        },
+
+        "link": function(locale, options) {
+            var size = (options && options.size) ? ' btn-'+options.size : '';
+            return "<li>" +
+              "<div class='bootstrap-wysihtml5-insert-link-modal modal hide fade'>" +
+                "<div class='modal-header'>" +
+                  "<a class='close' data-dismiss='modal'>&times;</a>" +
+                  "<h3>" + locale.link.insert + "</h3>" +
+                "</div>" +
+                "<div class='modal-body'>" +
+                  "<input value='http://' class='bootstrap-wysihtml5-insert-link-url input-xlarge'>" +
+                "</div>" +
+                "<div class='modal-footer'>" +
+                  "<a href='#' class='btn' data-dismiss='modal'>" + locale.link.cancel + "</a>" +
+                  "<a href='#' class='btn btn-primary' data-dismiss='modal'>" + locale.link.insert + "</a>" +
+                "</div>" +
+              "</div>" +
+              "<a class='btn" + size + "' data-wysihtml5-command='createLink' title='" + locale.link.insert + "' tabindex='-1'><i class='icon-share'></i></a>" +
+            "</li>";
+        },
+
+        "image": function(locale, options) {
+            var size = (options && options.size) ? ' btn-'+options.size : '';
+            return "<li>" +
+              "<div class='bootstrap-wysihtml5-insert-image-modal modal hide fade'>" +
+                "<div class='modal-header'>" +
+                  "<a class='close' data-dismiss='modal'>&times;</a>" +
+                  "<h3>" + locale.image.insert + "</h3>" +
+                "</div>" +
+                "<div class='modal-body'>" +
+                  "<input value='http://' class='bootstrap-wysihtml5-insert-image-url input-xlarge'>" +
+                "</div>" +
+                "<div class='modal-footer'>" +
+                  "<a href='#' class='btn' data-dismiss='modal'>" + locale.image.cancel + "</a>" +
+                  "<a href='#' class='btn btn-primary' data-dismiss='modal'>" + locale.image.insert + "</a>" +
+                "</div>" +
+              "</div>" +
+              "<a class='btn" + size + "' data-wysihtml5-command='insertImage' title='" + locale.image.insert + "' tabindex='-1'><i class='icon-picture'></i></a>" +
+            "</li>";
+        },
+
+        "html": function(locale, options) {
+            var size = (options && options.size) ? ' btn-'+options.size : '';
+            return "<li>" +
+              "<div class='btn-group'>" +
+                "<a class='btn" + size + "' data-wysihtml5-action='change_view' title='" + locale.html.edit + "' tabindex='-1'><i class='icon-pencil'></i></a>" +
+              "</div>" +
+            "</li>";
+        },
+
+        "color": function(locale, options) {
+            var size = (options && options.size) ? ' btn-'+options.size : '';
+            return "<li class='dropdown'>" +
+              "<a class='btn dropdown-toggle" + size + "' data-toggle='dropdown' href='#' tabindex='-1' title='" + locale.colours.title + "'>" +
+                "<span class='current-color'>" + locale.colours.black + "</span>&nbsp;<b class='caret'></b>" +
+              "</a>" +
+              "<ul class='dropdown-menu'>" +
+                "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='black'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='black'>" + locale.colours.black + "</a></li>" +
+                "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='silver'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='silver'>" + locale.colours.silver + "</a></li>" +
+                "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='gray'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='gray'>" + locale.colours.gray + "</a></li>" +
+                "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='maroon'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='maroon'>" + locale.colours.maroon + "</a></li>" +
+                "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='red'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='red'>" + locale.colours.red + "</a></li>" +
+                "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='purple'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='purple'>" + locale.colours.purple + "</a></li>" +
+                "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='green'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='green'>" + locale.colours.green + "</a></li>" +
+                "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='olive'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='olive'>" + locale.colours.olive + "</a></li>" +
+                "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='navy'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='navy'>" + locale.colours.navy + "</a></li>" +
+                "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='blue'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='blue'>" + locale.colours.blue + "</a></li>" +
+                "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='orange'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='orange'>" + locale.colours.orange + "</a></li>" +
+              "</ul>" +
+            "</li>";
+        }
+    };
+
+    var templates = function(key, locale, options) {
+        return tpl[key](locale, options);
+    };
+
+
+    var Wysihtml5 = function(el, options) {
+        this.el = el;
+        var toolbarOpts = options || defaultOptions;
+        for(var t in toolbarOpts.customTemplates) {
+          tpl[t] = toolbarOpts.customTemplates[t];
+        }
+        this.toolbar = this.createToolbar(el, toolbarOpts);
+        this.editor =  this.createEditor(options);
+
+        window.editor = this.editor;
+
+        $('iframe.wysihtml5-sandbox').each(function(i, el){
+            $(el.contentWindow).off('focus.wysihtml5').on({
+                'focus.wysihtml5' : function(){
+                    $('li.dropdown').removeClass('open');
+                }
+            });
+        });
+    };
+
+    Wysihtml5.prototype = {
+
+        constructor: Wysihtml5,
+
+        createEditor: function(options) {
+            options = options || {};
+            
+            // Add the toolbar to a clone of the options object so multiple instances
+            // of the WYISYWG don't break because "toolbar" is already defined
+            options = $.extend(true, {}, options);
+            options.toolbar = this.toolbar[0];
+
+            var editor = new wysi.Editor(this.el[0], options);
+
+            if(options && options.events) {
+                for(var eventName in options.events) {
+                    editor.on(eventName, options.events[eventName]);
+                }
+            }
+            return editor;
+        },
+
+        createToolbar: function(el, options) {
+            var self = this;
+            var toolbar = $("<ul/>", {
+                'class' : "wysihtml5-toolbar",
+                'style': "display:none"
+            });
+            var culture = options.locale || defaultOptions.locale || "en";
+            for(var key in defaultOptions) {
+                var value = false;
+
+                if(options[key] !== undefined) {
+                    if(options[key] === true) {
+                        value = true;
+                    }
+                } else {
+                    value = defaultOptions[key];
+                }
+
+                if(value === true) {
+                    toolbar.append(templates(key, locale[culture], options));
+
+                    if(key === "html") {
+                        this.initHtml(toolbar);
+                    }
+
+                    if(key === "link") {
+                        this.initInsertLink(toolbar);
+                    }
+
+                    if(key === "image") {
+                        this.initInsertImage(toolbar);
+                    }
+
+                    if(key == "customCommand") {
+                        this.initCustomCommand(toolbar, options.customCommandCallback);
+                    }
+                }
+            }
+
+            if(options.toolbar) {
+                for(key in options.toolbar) {
+                    toolbar.append(options.toolbar[key]);
+                }
+            }
+
+            toolbar.find("a[data-wysihtml5-command='formatBlock']").click(function(e) {
+                var target = e.target || e.srcElement;
+                var el = $(target);
+                self.toolbar.find('.current-font').text(el.html());
+            });
+
+            toolbar.find("a[data-wysihtml5-command='foreColor']").click(function(e) {
+                var target = e.target || e.srcElement;
+                var el = $(target);
+                self.toolbar.find('.current-color').text(el.html());
+            });
+
+            this.el.before(toolbar);
+
+            return toolbar;
+        },
+
+        initHtml: function(toolbar) {
+            var changeViewSelector = "a[data-wysihtml5-action='change_view']";
+            toolbar.find(changeViewSelector).click(function(e) {
+                toolbar.find('a.btn').not(changeViewSelector).toggleClass('disabled');
+            });
+        },
+
+        initInsertImage: function(toolbar) {
+            var self = this;
+            var insertImageModal = toolbar.find('.bootstrap-wysihtml5-insert-image-modal');
+            var urlInput = insertImageModal.find('.bootstrap-wysihtml5-insert-image-url');
+            var insertButton = insertImageModal.find('a.btn-primary');
+            var initialValue = urlInput.val();
+            var caretBookmark;
+
+            var insertImage = function() {
+                var url = urlInput.val();
+                urlInput.val(initialValue);
+                self.editor.currentView.element.focus();
+                if (caretBookmark) {
+                  self.editor.composer.selection.setBookmark(caretBookmark);
+                  caretBookmark = null;
+                }
+                self.editor.composer.commands.exec("insertImage", url);
+            };
+
+            urlInput.keypress(function(e) {
+                if(e.which == 13) {
+                    insertImage();
+                    insertImageModal.modal('hide');
+                }
+            });
+
+            insertButton.click(insertImage);
+
+            insertImageModal.on('shown', function() {
+                urlInput.focus();
+            });
+
+            insertImageModal.on('hide', function() {
+                self.editor.currentView.element.focus();
+            });
+
+            toolbar.find('a[data-wysihtml5-command=insertImage]').click(function() {
+                var activeButton = $(this).hasClass("wysihtml5-command-active");
+
+                if (!activeButton) {
+                    self.editor.currentView.element.focus(false);
+                    caretBookmark = self.editor.composer.selection.getBookmark();
+                    insertImageModal.appendTo('body').modal('show');
+                    insertImageModal.on('click.dismiss.modal', '[data-dismiss="modal"]', function(e) {
+                        e.stopPropagation();
+                    });
+                    return false;
+                }
+                else {
+                    return true;
+                }
+            });
+        },
+
+        initCustomCommand: function(toolbar, callback) {
+            var self = this;
+
+            toolbar.find('a[data-wysihtml5-command=customCommand]').click(function() {
+                var activeButton = $(this).hasClass("wysihtml5-command-active");
+
+                if (!activeButton) {
+                    callback(self.editor);
+                    return false;
+                }
+                else {
+                    return true;
+                }
+            });
+        },
+
+        initInsertLink: function(toolbar) {
+            var self = this;
+            var insertLinkModal = toolbar.find('.bootstrap-wysihtml5-insert-link-modal');
+            var urlInput = insertLinkModal.find('.bootstrap-wysihtml5-insert-link-url');
+            var insertButton = insertLinkModal.find('a.btn-primary');
+            var initialValue = urlInput.val();
+            var caretBookmark;
+
+            var insertLink = function() {
+                var url = urlInput.val();
+                urlInput.val(initialValue);
+                self.editor.currentView.element.focus();
+                if (caretBookmark) {
+                  self.editor.composer.selection.setBookmark(caretBookmark);
+                  caretBookmark = null;
+                }
+                self.editor.composer.commands.exec("createLink", {
+                    href: url,
+                    target: "_blank",
+                    rel: "nofollow"
+                });
+            };
+            var pressedEnter = false;
+
+            urlInput.keypress(function(e) {
+                if(e.which == 13) {
+                    insertLink();
+                    insertLinkModal.modal('hide');
+                }
+            });
+
+            insertButton.click(insertLink);
+
+            insertLinkModal.on('shown', function() {
+                urlInput.focus();
+            });
+
+            insertLinkModal.on('hide', function() {
+                self.editor.currentView.element.focus();
+            });
+
+            toolbar.find('a[data-wysihtml5-command=createLink]').click(function() {
+                var activeButton = $(this).hasClass("wysihtml5-command-active");
+
+                if (!activeButton) {
+                    self.editor.currentView.element.focus(false);
+                    caretBookmark = self.editor.composer.selection.getBookmark();
+                    insertLinkModal.appendTo('body').modal('show');
+                    insertLinkModal.on('click.dismiss.modal', '[data-dismiss="modal"]', function(e) {
+                        e.stopPropagation();
+                    });
+                    return false;
+                }
+                else {
+                    return true;
+                }
+            });
+        }
+    };
+
+    // these define our public api
+    var methods = {
+        resetDefaults: function() {
+            $.fn.wysihtml5.defaultOptions = $.extend(true, {}, $.fn.wysihtml5.defaultOptionsCache);
+        },
+        bypassDefaults: function(options) {
+            return this.each(function () {
+                var $this = $(this);
+                $this.data('wysihtml5', new Wysihtml5($this, options));
+            });
+        },
+        shallowExtend: function (options) {
+            var settings = $.extend({}, $.fn.wysihtml5.defaultOptions, options || {});
+            var that = this;
+            return methods.bypassDefaults.apply(that, [settings]);
+        },
+        deepExtend: function(options) {
+            var settings = $.extend(true, {}, $.fn.wysihtml5.defaultOptions, options || {});
+            var that = this;
+            return methods.bypassDefaults.apply(that, [settings]);
+        },
+        init: function(options) {
+            var that = this;
+            return methods.shallowExtend.apply(that, [options]);
+        }
+    };
+
+    $.fn.wysihtml5 = function ( method ) {
+        if ( methods[method] ) {
+            return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
+        } else if ( typeof method === 'object' || ! method ) {
+            return methods.init.apply( this, arguments );
+        } else {
+            $.error( 'Method ' +  method + ' does not exist on jQuery.wysihtml5' );
+        }    
+    };
+
+    $.fn.wysihtml5.Constructor = Wysihtml5;
+
+    var defaultOptions = $.fn.wysihtml5.defaultOptions = {
+        "font-styles": true,
+        "color": false,
+        "emphasis": true,
+        "lists": true,
+        "html": false,
+        "link": true,
+        "image": true,
+        customCommand: false,
+        events: {},
+        parserRules: {
+            classes: {
+                // (path_to_project/lib/css/wysiwyg-color.css)
+                "wysiwyg-color-silver" : 1,
+                "wysiwyg-color-gray" : 1,
+                "wysiwyg-color-white" : 1,
+                "wysiwyg-color-maroon" : 1,
+                "wysiwyg-color-red" : 1,
+                "wysiwyg-color-purple" : 1,
+                "wysiwyg-color-fuchsia" : 1,
+                "wysiwyg-color-green" : 1,
+                "wysiwyg-color-lime" : 1,
+                "wysiwyg-color-olive" : 1,
+                "wysiwyg-color-yellow" : 1,
+                "wysiwyg-color-navy" : 1,
+                "wysiwyg-color-blue" : 1,
+                "wysiwyg-color-teal" : 1,
+                "wysiwyg-color-aqua" : 1,
+                "wysiwyg-color-orange" : 1
+            },
+            tags: {
+                "b":  {},
+                "i":  {},
+                "br": {},
+                "ol": {},
+                "ul": {},
+                "li": {},
+                "h1": {},
+                "h2": {},
+                "h3": {},
+                "blockquote": {},
+                "u": 1,
+                "img": {
+                    "check_attributes": {
+                        "width": "numbers",
+                        "alt": "alt",
+                        "src": "url",
+                        "height": "numbers"
+                    }
+                },
+                "a":  {
+                    set_attributes: {
+                        target: "_blank",
+                        rel:    "nofollow"
+                    },
+                    check_attributes: {
+                        href:   "url" // important to avoid XSS
+                    }
+                },
+                "span": 1,
+                "div": 1,
+                // to allow save and edit files with code tag hacks
+                "code": 1,
+                "pre": 1
+            }
+        },
+        stylesheets: ["./lib/css/wysiwyg-color.css"], // (path_to_project/lib/css/wysiwyg-color.css)
+        locale: "en"
+    };
+
+    if (typeof $.fn.wysihtml5.defaultOptionsCache === 'undefined') {
+        $.fn.wysihtml5.defaultOptionsCache = $.extend(true, {}, $.fn.wysihtml5.defaultOptions);
+    }
+
+    var locale = $.fn.wysihtml5.locale = {
+        en: {
+            font_styles: {
+                title: "Font style",
+                normal: "Normal text",
+                h1: "Heading 1",
+                h2: "Heading 2",
+                h3: "Heading 3"
+            },
+            emphasis: {
+                bold: "Bold",
+                italic: "Italic",
+                underline: "Underline"
+            },
+            lists: {
+                unordered: "Unordered list",
+                ordered: "Ordered list",
+                outdent: "Outdent",
+                indent: "Indent"
+            },
+            link: {
+                insert: "Insert link",
+                cancel: "Cancel"
+            },
+            image: {
+                insert: "Insert image",
+                cancel: "Cancel"
+            },
+            html: {
+                edit: "Edit HTML"
+            },
+            colours: {
+                title: "Text color",
+                black: "Black",
+                silver: "Silver",
+                gray: "Grey",
+                maroon: "Maroon",
+                red: "Red",
+                purple: "Purple",
+                green: "Green",
+                olive: "Olive",
+                navy: "Navy",
+                blue: "Blue",
+                orange: "Orange"
+            }
+        }
+    };
+
+}(window.jQuery, window.wysihtml5);

app/assets/javascripts/bootsy/bootstrap.file-input.js

+/*
+  Bootstrap - File Input
+  ======================
+
+  This is meant to convert all file input tags into a set of elements that displays consistently in all browsers.
+
+  Converts all
+  <input type="file">
+  into Bootstrap buttons
+  <a class="btn">Browse</a>
+
+*/
+$(function() {
+
+$.fn.bootstrapFileInput = function() {
+
+  this.each(function(i,elem){
+
+    var $elem = $(elem);
+
+    // Maybe some fields don't need to be standardized.
+    if (typeof $elem.attr('data-bfi-disabled') != 'undefined') {
+      return;
+    }
+
+    // Set the word to be displayed on the button
+    var buttonWord = 'Browse';
+
+    if (typeof $elem.attr('title') != 'undefined') {
+      buttonWord = $elem.attr('title');
+    }
+
+    // Start by getting the HTML of the input element.
+    // Thanks for the tip http://stackoverflow.com/a/1299069
+    var input = $('<div>').append( $elem.eq(0).clone() ).html();
+    var className = '';
+
+    if (!!$elem.attr('class')) {
+      className = ' ' + $elem.attr('class');
+    }
+
+    // Now we're going to replace that input field with a Bootstrap button.
+    // The input will actually still be there, it will just be float above and transparent (done with the CSS).
+    $elem.replaceWith('<a class="file-input-wrapper btn' + className + '">'+buttonWord+input+'</a>');
+  })
+
+  // After we have found all of the file inputs let's apply a listener for tracking the mouse movement.
+  // This is important because the in order to give the illusion that this is a button in FF we actually need to move the button from the file input under the cursor. Ugh.
+  .promise().done( function(){
+
+    // As the cursor moves over our new Bootstrap button we need to adjust the position of the invisible file input Browse button to be under the cursor.
+    // This gives us the pointer cursor that FF denies us
+    $('.file-input-wrapper').mousemove(function(cursor) {
+
+      var input, wrapper,
+        wrapperX, wrapperY,
+        inputWidth, inputHeight,
+        cursorX, cursorY;
+
+      // This wrapper element (the button surround this file input)
+      wrapper = $(this);
+      // The invisible file input element
+      input = wrapper.find("input");
+      // The left-most position of the wrapper
+      wrapperX = wrapper.offset().left;
+      // The top-most position of the wrapper
+      wrapperY = wrapper.offset().top;
+      // The with of the browsers input field
+      inputWidth= input.width();
+      // The height of the browsers input field
+      inputHeight= input.height();
+      //The position of the cursor in the wrapper
+      cursorX = cursor.pageX;
+      cursorY = cursor.pageY;
+
+      //The positions we are to move the invisible file input
+      // The 20 at the end is an arbitrary number of pixels that we can shift the input such that cursor is not pointing at the end of the Browse button but somewhere nearer the middle
+      moveInputX = cursorX - wrapperX - inputWidth + 20;
+      // Slides the invisible input Browse button to be positioned middle under the cursor
+      moveInputY = cursorY- wrapperY - (inputHeight/2);
+
+      // Apply the positioning styles to actually move the invisible file input
+      input.css({
+        left:moveInputX,
+        top:moveInputY
+      });
+    });
+
+    $('.file-input-wrapper input[type=file]').change(function(){
+
+      var fileName;
+      fileName = $(this).val();
+
+      // Remove any previous file names
+      $(this).parent().next('.file-input-name').remove();
+      if (!!$(this).prop('files') && $(this).prop('files').length > 1) {
+        fileName = $(this)[0].files.length+' files';
+        //$(this).parent().after('<span class="file-input-name">'+$(this)[0].files.length+' files</span>');
+      }
+      else {
+        // var fakepath = 'C:\\fakepath\\';
+        // fileName = $(this).val().replace('C:\\fakepath\\','');
+        fileName = fileName.substring(fileName.lastIndexOf('\\')+1,fileName.length);
+      }
+
+      $(this).parent().after('<span class="file-input-name">'+fileName+'</span>');
+    });
+
+  });
+
+};
+
+// Add the styles before the first stylesheet
+// This ensures they can be easily overridden with developer styles
+var cssHtml = '<style>'+
+  '.file-input-wrapper { overflow: hidden; position: relative; cursor: pointer; z-index: 1; }'+
+  '.file-input-wrapper input[type=file], .file-input-wrapper input[type=file]:focus, .file-input-wrapper input[type=file]:hover { position: absolute; top: 0; left: 0; cursor: pointer; opacity: 0; filter: alpha(opacity=0); z-index: 99; outline: 0; }'+
+  '.file-input-name { margin-left: 8px; }'+
+  '</style>';
+$('link[rel=stylesheet]').eq(0).before(cssHtml);
+
+});

app/assets/javascripts/bootsy/bootsy.js

+window.Bootsy = window.Bootsy || {};
+
+window.Bootsy.Area = function ($el) {
+  var self = this;
+  self.bootsyUploadInit = false; // this flag tells the refreshGallery method whether there was a new upload or not
+
+  this.progressBar = function () {
+    // Show loading spinner
+    $('.bootsy-spinner img').fadeIn(200);
+  };
+
+  this.setImageGalleryId = function (id) {
+    self.imageGalleryModal.data('bootsy-gallery-id', id)
+    $('input.bootsy_image_gallery_id').val(id);
+  };
+
+  this.deleteImage = function (id) {
+    self.imageGalleryModal.find("ul.thumbnails").find("[data-id='" + id + "']").hide(200, function(){
+      $(this).remove();
+      // Put message back if 0 images
+      if ( self.imageGalleryModal.find('.thumbnails li').length == 0 ) {
+        self.imageGalleryModal.find('.alert').fadeIn(200);
+      }
+    });
+  };
+
+  this.refreshGallery = function () {
+    self.progressBar();
+    $.ajax({
+      url: '/bootsy/images',
+      type: 'GET',
+      cache: false,
+      data: {
+        image_gallery_id: self.imageGalleryModal.data('bootsy-gallery-id')
+      },
+      dataType: 'json',
+      success: function (data) {
+        // Hide loading spinner
+        $('.bootsy-spinner img').fadeOut(200);
+        
+        // Cache the returned data
+        var $data = $(data.partial);
+
+        // Retrieve the last added li from the cached data
+        img = $data.find('ul.thumbnails > li').last();
+
+        if ( img.length ) {
+          // Thumbnails currently exist in the retrieved data, so hide the message
+          $('.alert').hide();
+        } else {
+          // Thumbnails do not exist in the retrieved data, so show the message
+          $('.thumbnails li').hide();
+          $('.alert').fadeIn(100);
+        }
+        
+        if ( self.imageGalleryModal.find('.modal-body').children().length == 0 ) {
+          // Init the modal content (only loads first time)
+          self.imageGalleryModal.find('.modal-content').html($data).hide().fadeIn(200);
+          // Nicer file input
+          $('.modal-footer #image_image_file').bootstrapFileInput();
+        } else if ( self.bootsyUploadInit == true ) {
+          self.bootsyUploadInit = false;
+          $(img).hide().appendTo(self.imageGalleryModal.find('.modal-body .thumbnails')).fadeIn(200);
+        } else {
+          // do nothing
+        }
+
+        self.imageGalleryModal.find('a.refresh-btn').hide();
+        self.imageGalleryModal.find('#refresh-gallery').hide();
+        self.imageGalleryModal.find('input#upload_submit').hide();
+
+
+        // Autosubmit on image selection
+        $('.modal-footer #image_image_file').on('change', function(){
+          self.progressBar();
+          self.bootsyUploadInit = true;
+          $(this).closest('form').submit();
+        });
+
+      },
+      error: function (e) {
+        alert(Bootsy.translations[self.locale].error);
+        self.imageGalleryModal.find('a.refresh-btn').show();
+      }
+    });
+  };
+
+  this.openImageGallery = function (editor) {
+    editor.currentView.element.focus(false);
+    self.caretBookmark = editor.composer.selection.getBookmark();
+    $('#bootsy_image_gallery').modal('show');
+  };
+
+  this.insertImage = function (image) {
+    $('#bootsy_image_gallery').modal('hide');
+    self.editor.currentView.element.focus();
+    if (self.caretBookmark) {
+      self.editor.composer.selection.setBookmark(self.caretBookmark);
+      self.caretBookmark = null;
+    }
+    self.editor.composer.commands.exec('insertImage', image);
+  };
+
+  this.on = function (eventName, callback) {
+    self.eventCallbacks[eventName].push(callback);
+  };
+
+  this.trigger = function (eventName) {
+    var callbacks = self.eventCallbacks[eventName];
+    for(i in callbacks) {
+      callbacks[i]();
+    }
+    self.triggeredEvents.push(eventName);
+  };
+
+  this.after = function (eventName, callback) {
+    if(self.triggeredEvents.indexOf(eventName) != -1) {
+      callback();
+    }else{
+      self.on(eventName, callback);
+    }
+  };
+
+  this.alertUnsavedChanges = function () {
+    if (self.unsavedChanges) {
+      return Bootsy.translations[self.locale].alert_unsaved;
+    }
+  };
+
+  this.clear = function () {
+    self.editor.clear();
+    self.setImageGalleryId('');
+  };
+
+  this.locale = $el.data('bootsy-locale') || $('html').attr('lang') || 'en';
+  this.caretBookmark = false;
+  this.unsavedChanges = false;
+  this.editor = false;
+  this.eventCallbacks = {'loaded': []};
+  this.triggeredEvents = [];
+  this.editorOptions = {locale: this.locale};
+
+  if ($el.data('bootsy-font-styles') === false) this.editorOptions['font-styles'] = false;
+  if ($el.data('bootsy-emphasis') === false) this.editorOptions.emphasis = false;
+  if ($el.data('bootsy-lists') === false) this.editorOptions.lists = false;
+  if ($el.data('bootsy-html') === true) this.editorOptions.html = true;
+  if ($el.data('bootsy-link') === false) this.editorOptions.link = false;
+  if ($el.data('bootsy-color') === false) this.editorOptions.color = false;
+
+  if ($el.data('bootsy-alert-unsaved') !== false) {
+    window.onbeforeunload = this.alertUnsavedChanges;
+  }
+
+  $el.closest('form').submit(function (e) {
+    self.unsavedChanges = false;
+    return true;
+  });
+
+  if ($el.data('bootsy-image') !== false) {
+    if ($el.data('bootsy-uploader') !== false) {
+      this.editorOptions.image = false;
+      this.editorOptions.customCommand = true;
+      this.editorOptions.customCommandCallback = this.openImageGallery;
+      this.editorOptions.customTemplates = {
+        customCommand: function (locale, options) {
+          var size = (options && options.size) ? ' btn-'+options.size : '';
+          return "<li>" +
+            "<a class='btn" + size + "' data-wysihtml5-command='customCommand' title='" + locale.image.insert + "' tabindex='-1'><i class='icon-picture'></i></a>" +
+          "</li>";
+        }
+      };
+
+      this.imageGalleryModal = $('#bootsy_image_gallery');
+      this.imageGalleryModal.find('a.refresh-btn').hide();
+
+      this.imageGalleryModal.parents('form').after(this.imageGalleryModal);
+
+      this.imageGalleryModal.on('click', 'a[href="#refresh-gallery"]', this.refreshGallery);
+
+      this.imageGalleryModal.find('a.destroy_btn').click(this.progressBar);
+
+      this.imageGalleryModal.modal({show: false});
+      this.imageGalleryModal.on('shown', this.refreshGallery);
+
+      this.imageGalleryModal.on('hide', function () {
+        self.progressBar();
+        self.editor.currentView.element.focus();
+      });
+
+      this.imageGalleryModal.on('click.dismiss.modal', '[data-dismiss="modal"]', function (e) {
+        e.stopPropagation();
+      });
+
+      this.imageGalleryModal.on('click', 'ul.dropdown-menu a.insert', function (e) {
+        var imagePrefix = "/"+$(this).attr('data-image-size')+"_";
+        if($(this).data('image-size') == 'original') {
+          imagePrefix = '/';
+        }
+        var img = $(this).parents('li.dropdown').find('img');
+        var obj = {
+          src: img.attr('src').replace("/thumb_", imagePrefix),
+          alt: img.attr('alt').replace("Thumb_", "")
+        };
+
+        obj.align = $(this).data('position');
+
+        self.insertImage(obj);
+      });
+    }
+  } else {
+    this.editorOptions.image = false;
+  }
+
+  this.editor = $el.wysihtml5($.extend(Bootsy.editorOptions, this.editorOptions)).data('wysihtml5').editor;
+
+  this.editor.on('change', function () {
+    self.unsavedChanges = true;
+  });
+
+  this.trigger('loaded');
+};

app/assets/javascripts/bootsy/editor_options.js

+window.Bootsy = window.Bootsy || {};
+
+var page_stylesheets = [];
+$('link[rel="stylesheet"]').each(function () {
+  page_stylesheets.push($(this).attr('href'));
+});
+
+window.Bootsy.editorOptions = {
+  parserRules: {
+    classes: {
+      "wysiwyg-color-silver" : 1,
+      "wysiwyg-color-gray" : 1,
+      "wysiwyg-color-white" : 1,
+      "wysiwyg-color-maroon" : 1,
+      "wysiwyg-color-red" : 1,
+      "wysiwyg-color-purple" : 1,
+      "wysiwyg-color-fuchsia" : 1,
+      "wysiwyg-color-green" : 1,
+      "wysiwyg-color-lime" : 1,
+      "wysiwyg-color-olive" : 1,
+      "wysiwyg-color-yellow" : 1,
+      "wysiwyg-color-navy" : 1,
+      "wysiwyg-color-blue" : 1,
+      "wysiwyg-color-teal" : 1,
+      "wysiwyg-color-aqua" : 1,
+      "wysiwyg-color-orange" : 1,
+      "wysiwyg-float-left": 1,
+      "wysiwyg-float-right": 1
+    },
+
+    tags: {
+      "b":  {},
+      "i":  {},
+      "br": {},
+      "ol": {},
+      "ul": {},
+      "li": {},
+      "h1": {},
+      "h2": {},
+      "h3": {},
+      "small": {},
+      "p": {},
+      "blockquote": {},
+      "u": 1,
+      "cite": {
+        "check_attributes": {
+          "title": "alt"
+        }
+      },
+      "img": {
+        "check_attributes": {
+          "width": "numbers",
+          "alt": "alt",
+          "src": "src",
+          "height": "numbers"
+        },
+        "add_class": {
+          "align": "align_img"
+        }
+      },
+
+      "a":  {
+        set_attributes: {
+          target: "_blank",
+          rel:    "nofollow"
+        },
+        check_attributes: {
+          href:   "url" // important to avoid XSS
+        }
+      },
+      "span": 1,
+      "div": 1,
+      // to allow save and edit files with code tag hacks
+      "code": 1,
+      "pre": 1
+    }
+  },
+  color: true,
+  stylesheets: page_stylesheets
+};

app/assets/javascripts/bootsy/init.js

+window.Bootsy = window.Bootsy || {};
+
+Bootsy.init = function() {
+  Bootsy.areas = [];
+
+  $('textarea.bootsy_text_area').each(function() {
+    Bootsy.areas.push(new Bootsy.Area($(this)));
+  });
+};
+
+$(Bootsy.init);

app/assets/javascripts/bootsy/locales/bootstrap-wysihtml5.pt-BR.js

+/**
+ * Brazilian portuguese translation for bootstrap-wysihtml5
+ */
+(function($){
+    $.fn.wysihtml5.locale["pt-BR"] = {
+        font_styles: {
+            title: "Estilo de fonte",
+            normal: "Texto normal",
+            h1: "Título 1",
+            h2: "Título 2",
+            h3: "Título 3"
+        },
+        emphasis: {
+            bold: "Negrito",
+            italic: "Itálico",
+            underline: "Sublinhado"
+        },
+        lists: {
+            unordered: "Lista",
+            ordered: "Lista numerada",
+            outdent: "Remover indentação",
+            indent: "Indentar"
+        },
+        link: {
+            insert: "Inserir link",
+            cancel: "Cancelar"
+        },
+        image: {
+            insert: "Inserir imagem",
+            cancel: "Cancelar"
+        },
+        html: {
+            edit: "Editar HTML"
+        },
+        colours: {
+            title: "Cor do texto",
+            black: "Preto",
+            silver: "Prata",
+            gray: "Cinza",
+            maroon: "Marrom",
+            red: "Vermelho",
+            purple: "Roxo",
+            green: "Verde",
+            olive: "Oliva",
+            navy: "Marinho",
+            blue: "Azul",
+            orange: "Laranja"
+        }
+    };
+}(jQuery));

app/assets/javascripts/bootsy/locales/bootsy.pt-BR.js

+/**
+ * Brazilian portuguese translation for Bootsy
+ */
+jQuery(function(){
+  Bootsy.translations['pt-BR'] = {
+    alert_unsaved: 'As suas modificações ainda não foram gravadas.'
+  };
+});

app/assets/javascripts/bootsy/translations.js

+window.Bootsy = window.Bootsy || {};
+
+window.Bootsy.translations = {
+  en: {
+    alert_unsaved: 'You have unsaved changes.',
+    error: 'Something went very wrong. Please try again later.'
+  }
+};