├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── block_editor_manifest.js │ ├── images │ │ └── block_editor │ │ │ ├── .keep │ │ │ └── contact-form-block.svg │ └── stylesheets │ │ └── block_editor │ │ ├── _utilities.scss │ │ ├── backend.scss │ │ ├── blocks │ │ ├── _backend.scss │ │ ├── _frontend.scss │ │ ├── be-accordion │ │ │ └── _frontend.scss │ │ ├── be-alert │ │ │ └── _frontend.scss │ │ ├── be-card │ │ │ ├── _backend.scss │ │ │ └── _frontend.scss │ │ ├── be-contact-form │ │ │ └── _backend.scss │ │ ├── be-cover │ │ │ ├── _backend.scss │ │ │ └── _frontend.scss │ │ ├── block │ │ │ └── _backend.scss │ │ ├── button │ │ │ └── _frontend.scss │ │ ├── buttons │ │ │ ├── _backend.scss │ │ │ └── _frontend.scss │ │ ├── column │ │ │ └── _frontend.scss │ │ ├── columns │ │ │ ├── _backend.scss │ │ │ └── _frontend.scss │ │ ├── image │ │ │ ├── _backend.scss │ │ │ └── _frontend.scss │ │ ├── seperator │ │ │ └── _frontend.scss │ │ └── table │ │ │ └── _frontend.scss │ │ └── host_app │ │ ├── _variables.scss │ │ └── blocks │ │ ├── _backend.scss │ │ └── _frontend.scss ├── controllers │ └── block_editor │ │ └── application_controller.rb ├── helpers │ └── block_editor │ │ └── application_helper.rb ├── javascript │ ├── block_editor │ │ ├── blocks │ │ │ ├── be-accordion │ │ │ │ └── index.js │ │ │ ├── be-alert │ │ │ │ └── index.js │ │ │ ├── be-card │ │ │ │ └── index.js │ │ │ ├── be-contact-form │ │ │ │ └── index.js │ │ │ ├── be-cover │ │ │ │ └── index.js │ │ │ ├── block │ │ │ │ ├── edit-panel │ │ │ │ │ └── index.js │ │ │ │ └── edit.js │ │ │ ├── button │ │ │ │ └── edit.js │ │ │ ├── column │ │ │ │ └── edit.js │ │ │ └── index.js │ │ ├── components │ │ │ ├── block-editor │ │ │ │ ├── index.js │ │ │ │ ├── popover-wrapper.js │ │ │ │ └── styles.scss │ │ │ ├── header │ │ │ │ ├── index.js │ │ │ │ ├── redo.js │ │ │ │ ├── styles.scss │ │ │ │ └── undo.js │ │ │ ├── media-upload │ │ │ │ └── index.js │ │ │ ├── notices │ │ │ │ ├── index.js │ │ │ │ └── styles.scss │ │ │ └── sidebar │ │ │ │ ├── index.js │ │ │ │ └── styles.scss │ │ └── stores │ │ │ ├── action-types.js │ │ │ ├── actions.js │ │ │ ├── controls.js │ │ │ ├── index.js │ │ │ ├── reducer.js │ │ │ ├── resolvers.js │ │ │ └── selectors.js │ ├── controllers │ │ ├── block_editor_controller.jsx │ │ └── index.js │ └── packs │ │ └── block_editor │ │ ├── application.js │ │ └── application.scss ├── jobs │ └── block_editor │ │ └── application_job.rb ├── mailers │ └── block_editor │ │ └── application_mailer.rb ├── models │ ├── block_editor │ │ ├── application_record.rb │ │ ├── block_list.rb │ │ └── block_list_connection.rb │ └── concerns │ │ └── block_editor │ │ └── listable.rb └── views │ ├── block_editor │ └── blocks │ │ └── be │ │ └── contact-form │ │ └── _block.html │ └── layouts │ └── block_editor │ └── application.html.erb ├── bin ├── rails ├── webpack └── webpack-dev-server ├── block_editor.gemspec ├── config ├── initializers │ └── webpacker_extension.rb ├── routes.rb ├── webpack │ ├── development.js │ ├── environment.js │ ├── production.js │ └── test.js └── webpacker.yml ├── db └── migrate │ ├── 20210312032114_create_block_lists.rb │ └── 20210506220328_create_block_list_connections.rb ├── lib ├── block_editor.rb ├── block_editor │ ├── block_list_renderer.rb │ ├── blocks │ │ ├── base.rb │ │ ├── contact_form.rb │ │ └── reusable.rb │ ├── engine.rb │ ├── instance.rb │ └── version.rb └── tasks │ └── block_editor_tasks.rake ├── package.json ├── postcss.config.js ├── test ├── dummy │ ├── Rakefile │ ├── app │ │ ├── assets │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ ├── images │ │ │ │ └── .keep │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── channels │ │ │ └── application_cable │ │ │ │ ├── channel.rb │ │ │ │ └── connection.rb │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ └── concerns │ │ │ │ └── .keep │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── javascript │ │ │ └── packs │ │ │ │ └── application.js │ │ ├── jobs │ │ │ └── application_job.rb │ │ ├── mailers │ │ │ └── application_mailer.rb │ │ ├── models │ │ │ ├── application_record.rb │ │ │ └── concerns │ │ │ │ └── .keep │ │ └── views │ │ │ └── layouts │ │ │ ├── application.html.erb │ │ │ ├── mailer.html.erb │ │ │ └── mailer.text.erb │ ├── bin │ │ ├── rails │ │ ├── rake │ │ └── setup │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── cable.yml │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── application_controller_renderer.rb │ │ │ ├── assets.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── content_security_policy.rb │ │ │ ├── cookies_serializer.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── inflections.rb │ │ │ ├── mime_types.rb │ │ │ ├── permissions_policy.rb │ │ │ └── wrap_parameters.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── puma.rb │ │ ├── routes.rb │ │ └── storage.yml │ ├── lib │ │ └── assets │ │ │ └── .keep │ ├── log │ │ └── .keep │ └── public │ │ ├── 404.html │ │ ├── 422.html │ │ ├── 500.html │ │ ├── apple-touch-icon-precomposed.png │ │ ├── apple-touch-icon.png │ │ └── favicon.ico └── test_helper.rb ├── webpack ├── development.js ├── environment.js ├── production.js └── test.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /doc/ 3 | /log/*.log 4 | /pkg/ 5 | /tmp/ 6 | /test/dummy/db/*.sqlite3 7 | /test/dummy/db/*.sqlite3-* 8 | /test/dummy/log/*.log 9 | /test/dummy/storage/ 10 | /test/dummy/tmp/ 11 | .byebug_history 12 | /node_modules 13 | /public/packs/* 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This project follows [semver 2.0.0](http://semver.org/spec/v2.0.0.html) and the 4 | recommendations of [keepachangelog.com](http://keepachangelog.com/). 5 | 6 | ## Unreleased 7 | 8 | ### Breaking Changes 9 | - 10 | 11 | ### Added 12 | - 13 | 14 | ### Fixed 15 | - 16 | 17 | ## v1.0.0 - 2021-06-15 18 | 19 | ### Breaking Changes 20 | - 21 | 22 | ### Added 23 | - Reusable Blocks 24 | - Block Inserter Library 25 | - Base styles for all blocks - this is using Bootstrap styles which adds a soft dependancy on the framework 26 | - Accordion block 27 | - Alert (callout) block 28 | - Card block 29 | - Cover block 30 | - Contact Form block 31 | - Keyboard shortcuts 32 | 33 | ### Fixed 34 | - Added margin-bottom to sidebar to account for footer 35 | 36 | ## v0.1.3 - 2021-03-16 37 | 38 | ### Added 39 | - Initial release 40 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | # Specify your gem's dependencies in block_editor.gemspec. 5 | gemspec 6 | 7 | group :development do 8 | gem 'sqlite3' 9 | end 10 | 11 | # To use a debugger 12 | # gem 'byebug', group: [:development, :test] 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | block_editor (1.0.0) 5 | rails (~> 6.0) 6 | webpacker (~> 5.1) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (6.1.3.2) 12 | actionpack (= 6.1.3.2) 13 | activesupport (= 6.1.3.2) 14 | nio4r (~> 2.0) 15 | websocket-driver (>= 0.6.1) 16 | actionmailbox (6.1.3.2) 17 | actionpack (= 6.1.3.2) 18 | activejob (= 6.1.3.2) 19 | activerecord (= 6.1.3.2) 20 | activestorage (= 6.1.3.2) 21 | activesupport (= 6.1.3.2) 22 | mail (>= 2.7.1) 23 | actionmailer (6.1.3.2) 24 | actionpack (= 6.1.3.2) 25 | actionview (= 6.1.3.2) 26 | activejob (= 6.1.3.2) 27 | activesupport (= 6.1.3.2) 28 | mail (~> 2.5, >= 2.5.4) 29 | rails-dom-testing (~> 2.0) 30 | actionpack (6.1.3.2) 31 | actionview (= 6.1.3.2) 32 | activesupport (= 6.1.3.2) 33 | rack (~> 2.0, >= 2.0.9) 34 | rack-test (>= 0.6.3) 35 | rails-dom-testing (~> 2.0) 36 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 37 | actiontext (6.1.3.2) 38 | actionpack (= 6.1.3.2) 39 | activerecord (= 6.1.3.2) 40 | activestorage (= 6.1.3.2) 41 | activesupport (= 6.1.3.2) 42 | nokogiri (>= 1.8.5) 43 | actionview (6.1.3.2) 44 | activesupport (= 6.1.3.2) 45 | builder (~> 3.1) 46 | erubi (~> 1.4) 47 | rails-dom-testing (~> 2.0) 48 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 49 | activejob (6.1.3.2) 50 | activesupport (= 6.1.3.2) 51 | globalid (>= 0.3.6) 52 | activemodel (6.1.3.2) 53 | activesupport (= 6.1.3.2) 54 | activerecord (6.1.3.2) 55 | activemodel (= 6.1.3.2) 56 | activesupport (= 6.1.3.2) 57 | activestorage (6.1.3.2) 58 | actionpack (= 6.1.3.2) 59 | activejob (= 6.1.3.2) 60 | activerecord (= 6.1.3.2) 61 | activesupport (= 6.1.3.2) 62 | marcel (~> 1.0.0) 63 | mini_mime (~> 1.0.2) 64 | activesupport (6.1.3.2) 65 | concurrent-ruby (~> 1.0, >= 1.0.2) 66 | i18n (>= 1.6, < 2) 67 | minitest (>= 5.1) 68 | tzinfo (~> 2.0) 69 | zeitwerk (~> 2.3) 70 | builder (3.2.4) 71 | concurrent-ruby (1.1.9) 72 | crass (1.0.6) 73 | erubi (1.10.0) 74 | globalid (0.4.2) 75 | activesupport (>= 4.2.0) 76 | i18n (1.8.10) 77 | concurrent-ruby (~> 1.0) 78 | loofah (2.10.0) 79 | crass (~> 1.0.2) 80 | nokogiri (>= 1.5.9) 81 | mail (2.7.1) 82 | mini_mime (>= 0.1.1) 83 | marcel (1.0.1) 84 | method_source (1.0.0) 85 | mini_mime (1.0.3) 86 | minitest (5.14.4) 87 | nio4r (2.5.7) 88 | nokogiri (1.11.7-x86_64-darwin) 89 | racc (~> 1.4) 90 | racc (1.5.2) 91 | rack (2.2.3) 92 | rack-proxy (0.7.0) 93 | rack 94 | rack-test (1.1.0) 95 | rack (>= 1.0, < 3) 96 | rails (6.1.3.2) 97 | actioncable (= 6.1.3.2) 98 | actionmailbox (= 6.1.3.2) 99 | actionmailer (= 6.1.3.2) 100 | actionpack (= 6.1.3.2) 101 | actiontext (= 6.1.3.2) 102 | actionview (= 6.1.3.2) 103 | activejob (= 6.1.3.2) 104 | activemodel (= 6.1.3.2) 105 | activerecord (= 6.1.3.2) 106 | activestorage (= 6.1.3.2) 107 | activesupport (= 6.1.3.2) 108 | bundler (>= 1.15.0) 109 | railties (= 6.1.3.2) 110 | sprockets-rails (>= 2.0.0) 111 | rails-dom-testing (2.0.3) 112 | activesupport (>= 4.2.0) 113 | nokogiri (>= 1.6) 114 | rails-html-sanitizer (1.3.0) 115 | loofah (~> 2.3) 116 | railties (6.1.3.2) 117 | actionpack (= 6.1.3.2) 118 | activesupport (= 6.1.3.2) 119 | method_source 120 | rake (>= 0.8.7) 121 | thor (~> 1.0) 122 | rake (13.0.3) 123 | semantic_range (3.0.0) 124 | sprockets (4.0.2) 125 | concurrent-ruby (~> 1.0) 126 | rack (> 1, < 3) 127 | sprockets-rails (3.2.2) 128 | actionpack (>= 4.0) 129 | activesupport (>= 4.0) 130 | sprockets (>= 3.0.0) 131 | sqlite3 (1.4.2) 132 | thor (1.1.0) 133 | tzinfo (2.0.4) 134 | concurrent-ruby (~> 1.0) 135 | webpacker (5.4.0) 136 | activesupport (>= 5.2) 137 | rack-proxy (>= 0.6.1) 138 | railties (>= 5.2) 139 | semantic_range (>= 2.3.0) 140 | websocket-driver (0.7.5) 141 | websocket-extensions (>= 0.1.0) 142 | websocket-extensions (0.1.5) 143 | zeitwerk (2.4.2) 144 | 145 | PLATFORMS 146 | x86_64-darwin-19 147 | 148 | DEPENDENCIES 149 | block_editor! 150 | sqlite3 151 | 152 | BUNDLED WITH 153 | 2.2.11 154 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Patrick Lindsay 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Block Editor for Ruby on Rails 2 | This editor uses packages from the [Wordpress Gutenberg project](https://github.com/WordPress/gutenberg) to build a standalone block editor for Rails. This editor has been extracted from [Integral CMS](https://github.com/yamasolutions/integral) where it was built following the [Wordpress custom block editor tutorial](https://developer.wordpress.org/block-editor/how-to-guides/platform/custom-block-editor/). 3 | 4 | The editor currently uses the v9.2.1 Gutenberg release packages which were part of the Wordpress v5.6 release. 5 | 6 | Looking for a demo? Checkout this [simple host application example.](https://block-editor-rails.herokuapp.com/) 7 | 8 | More information; 9 | 10 | * [Simple host application example source repository](https://github.com/yamasolutions/block-editor-sample) 11 | * [Current Wordpress Editor Demo](https://wordpress.org/gutenberg/) 12 | * [Gutenberg Block Editor Developer Documentation](https://developer.wordpress.org/block-editor/) 13 | 14 | ## Installation 15 | Add this line to your application's Gemfile: 16 | 17 | ```ruby 18 | gem 'block_editor' 19 | ``` 20 | 21 | And then execute: 22 | ```bash 23 | $ bundle 24 | ``` 25 | 26 | Or install it yourself as: 27 | ```bash 28 | $ gem install block_editor 29 | ``` 30 | 31 | ## Usage 32 | 33 | * Grab the required migrations and migrate; 34 | ``` 35 | rails block_editor:install:migrations 36 | rails db:migrate 37 | ``` 38 | * Add `include BlockEditor::Listable` to any model you wish to associate the block editor with, i.e. 39 | 40 | ``` 41 | class Post < ApplicationRecord 42 | include BlockEditor::Listable 43 | end 44 | ``` 45 | * Add the block editor to your model form; 46 | ``` 47 | <%= form.fields_for :active_block_list do |block_list| %> 48 | <%= BlockEditor::Instance.render(block_list) %> 49 | <% end %> 50 | ``` 51 | * Add the block editor Javascript and styles within your `HEAD` tag 52 | ``` 53 | <%= javascript_pack_tag 'block_editor/application', 'data-turbolinks-track': 'reload', webpacker: 'BlockEditor' %> 54 | <%= stylesheet_pack_tag 'block_editor/application', 'data-turbolinks-track': 'reload', webpacker: 'BlockEditor' %> 55 | ``` 56 | * Boom! You have a Block Editor linked to your model 57 | 58 | Note: If the error `Webpacker::Manifest::MissingEntryError` appears you need to run the following command to precompile the BlockEditor assets; 59 | ``` 60 | rails block_editor:webpacker:compile 61 | ``` 62 | 63 | ### Strong Parameters 64 | Remember to permit the block list attributes if you're using Strong Parameters (you should be), i.e; 65 | ``` 66 | # Only allow a list of trusted parameters through. 67 | def post_params 68 | params.require(:post).permit(:title, active_block_list_attributes: [ :id, :content ]) 69 | end 70 | ``` 71 | 72 | Note: You may also need to specifically set the listable attribute within your controller before saving, i.e. - 73 | ``` 74 | @post.active_block_list.listable = @post 75 | ``` 76 | 77 | ### Styling 78 | 79 | BlockEditor has a soft dependency on Bootstrap. You will need to add this to your application in order for the default styles to be compiled. If you do not want to use the default styles do not include them in the your application stylesheet and override BlockEditor's backend stylesheet (`block_editor/frontend.scss`) with whatever custom styles you want to include. 80 | 81 | #### Frontend 82 | Include the default styles into your application stylesheet; 83 | ``` 84 | @import "block_editor/blocks/frontend"; 85 | @import "block_editor/utilities"; 86 | ``` 87 | 88 | ### Backend 89 | Add the backend stylesheet where you are rendering the block editor, for example admin dashboard; 90 | ``` 91 | <%= stylesheet_link_tag 'block_editor/backend', media: 'all', 'data-turbolinks-track': 'reload' %> 92 | ``` 93 | 94 | ### Overriding and/or adding custom styles 95 | The below files are provided by BlockEditor as entry points. Override them if you want to provide custom styling to your blocks; 96 | * `app/assets/stylesheets/block_editor/host_app/blocks/_frontend.scss` - Any styles that should be displayed within the frontend and backend 97 | * `app/assets/stylesheets/block_editor/host_app/blocks/_backend.scss` - Any styles that should _only_ be displayed within the block editor itself, i.e when creating or editing the blocks 98 | 99 | ### Media Uploader & Images 100 | There is no built in MediaUploader or media gallery, it is up to the host application to implement this. 101 | 102 | When the media uploader is requested the Block Editor checks if `window.BlockEditorMediaUploader` is defined. If it is defined the block editor will call `window.BlockEditorMediaUploader.open(callback)`, otherwise it will randomly select an image from [Unsplash](https://unsplash.com) 103 | 104 | When an image is successfully uploaded or selected the BlockEditorMediaUploader instance should call the callback which was passed into the `open` function; 105 | ``` 106 | callback({url: imageUrl}) 107 | ``` 108 | 109 | ### Turbolinks 110 | Currently Block Editor is not compatible with Turbolinks as history is only being reset on full page loads. To disable Turbolinks per page add the following in your layout view file within your ``;; 111 | ``` 112 | 113 | ``` 114 | 115 | ### Reusable Blocks 116 | 117 | BlockEditor will check the following endpoints for any available reusable blocks, if any are found they will appear in the inserter menus 118 | ``` 119 | get '/wp/v2/types', to: 'backend/block_lists#wp_types' 120 | get '/wp/v2/types/wp_block', to: 'backend/block_lists#wp_type' 121 | get '/wp/v2/block_lists', to: 'backend/block_lists#block_lists' 122 | get '/wp/v2/block_list/:id', to: 'backend/block_lists#show' 123 | get '/wp/v2/blocks/:id', to: 'backend/block_lists#show' 124 | ``` 125 | 126 | For an example of what the BlockEditor is expecting from these endpoints checkout how [Integral CMS has implemented this](https://github.com/yamasolutions/integral/blob/master/app/controllers/integral/backend/block_lists_controller.rb) 127 | 128 | ### Adding/Removing blocks 129 | *Currently there isn't a way of adding or removing blocks without forking the gem.* 130 | 131 | * Fork the gem 132 | * Edit `app/javascript/block_editor/blocks/index.js` where all blocks are registered 133 | 134 | 135 | ### Dynamic blocks 136 | Dynamic blocks are useful for non-static content such as a recent posts block or more complex blocks like a contact form block. 137 | 138 | *Currently the gem needs to be forked in order to create a dynamic block* 139 | 140 | A dynamic block is made up of 4 components; 141 | * Ruby class to handle frontend rendering 142 | * Partial to be rendered on the frontend 143 | * Frontend and backend styling 144 | * JS file to manage how the block behaves within the block editor itself (This part currently is not possible without forking the gem) [Read More](https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/writing-your-first-block-type) 145 | 146 | 1. Create block class which can be as simple as; 147 | ``` 148 | class RecentPosts < BlockEditor::Base 149 | def self.name 150 | 'block-editor/recent-posts' 151 | end 152 | end 153 | ``` 154 | 2. Add block class to ```BlockEditor.dynamic_blocks``` config; 155 | ``` 156 | // application.rb 157 | 158 | BlockEditor.dynamic_blocks = [RecentPosts] 159 | ``` 160 | 3. Create the view partial you want to be rendered as the block; 161 | ``` 162 | // app/views/block_editor/blocks/block-editor/recent-posts/_block.html.erb 163 | 164 | My Recent Posts 165 | <%= @posts %> 166 | ``` 167 | 4. Add any required styling to the frontend and backend stylesheets; 168 | 169 | ``` 170 | app/assets/stylesheets/block_editor/host_app/blocks/_frontend.scss 171 | ``` 172 | 173 | ``` 174 | app/assets/stylesheets/block_editor/host_app/blocks/_backend.scss 175 | ``` 176 | 5. Add the block to the block editor 177 | * Fork the gem 178 | * Create the block JS file i.e. `app/javascript/block_editor/blocks/contact_form/index.js` 179 | * Edit `app/javascript/block_editor/blocks/index.js` to register the block 180 | 181 | ## Contributing 182 | Contribution are very welcome! Currently the biggest issue that needs to be solved is extensibility. There is no way to modify and configure the editor behaviour (such as blocks to display, block output etc) without forking the engine. 183 | 184 | ## License 185 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 186 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | 3 | APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__) 4 | load "rails/tasks/engine.rake" 5 | 6 | load "rails/tasks/statistics.rake" 7 | 8 | require "bundler/gem_tasks" 9 | 10 | require "rake/testtask" 11 | 12 | Rake::TestTask.new(:test) do |t| 13 | t.libs << 'test' 14 | t.pattern = 'test/**/*_test.rb' 15 | t.verbose = false 16 | end 17 | 18 | task default: :test 19 | -------------------------------------------------------------------------------- /app/assets/config/block_editor_manifest.js: -------------------------------------------------------------------------------- 1 | //= link_directory ../stylesheets/block_editor .css 2 | -------------------------------------------------------------------------------- /app/assets/images/block_editor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamasolutions/block-editor/3454f449b22a041191a6eee53c645e0d764841d3/app/assets/images/block_editor/.keep -------------------------------------------------------------------------------- /app/assets/images/block_editor/contact-form-block.svg: -------------------------------------------------------------------------------- 1 | contact us -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/_utilities.scss: -------------------------------------------------------------------------------- 1 | .has-text-align-center { 2 | text-align: center; 3 | } 4 | 5 | .has-text-align-left { 6 | text-align: left; 7 | } 8 | 9 | .has-text-align-right { 10 | text-align: right; 11 | } 12 | 13 | .wp-block[data-align='center'] { 14 | text-align: center; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/backend.scss: -------------------------------------------------------------------------------- 1 | // Bootstrap functions (used within variables) 2 | @import "bootstrap/scss/functions"; 3 | 4 | // Custom variables 5 | @import "block_editor/host_app/variables"; 6 | 7 | // Bootstrap default variables, mixins & utilities 8 | @import "bootstrap/scss/variables"; 9 | @import "bootstrap/scss/mixins"; 10 | @import "bootstrap/scss/utilities"; 11 | 12 | .editor-styles-wrapper { 13 | background-color: $body-bg; 14 | color: $body-color; 15 | 16 | textarea { 17 | min-height: unset; 18 | width: 100%; 19 | background-color: transparent; 20 | } 21 | 22 | .block-editor-image-placeholder { 23 | display: flex; 24 | height: 13rem; 25 | background-color: $gray-300; 26 | justify-content: center; 27 | align-items: center; 28 | } 29 | 30 | // Bootstrap components 31 | @import "bootstrap/scss/root"; 32 | @import "bootstrap/scss/reboot"; 33 | @import "bootstrap/scss/type"; 34 | @import "bootstrap/scss/images"; 35 | @import "bootstrap/scss/containers"; 36 | @import "bootstrap/scss/grid"; 37 | @import "bootstrap/scss/tables"; 38 | @import "bootstrap/scss/forms"; 39 | @import "bootstrap/scss/buttons"; 40 | @import "bootstrap/scss/transitions"; 41 | @import "bootstrap/scss/dropdown"; 42 | @import "bootstrap/scss/button-group"; 43 | @import "bootstrap/scss/nav"; 44 | @import "bootstrap/scss/navbar"; 45 | @import "bootstrap/scss/card"; 46 | @import "bootstrap/scss/accordion"; 47 | @import "bootstrap/scss/breadcrumb"; 48 | @import "bootstrap/scss/pagination"; 49 | @import "bootstrap/scss/badge"; 50 | @import "bootstrap/scss/alert"; 51 | @import "bootstrap/scss/progress"; 52 | @import "bootstrap/scss/list-group"; 53 | @import "bootstrap/scss/close"; 54 | @import "bootstrap/scss/toasts"; 55 | @import "bootstrap/scss/modal"; 56 | @import "bootstrap/scss/tooltip"; 57 | @import "bootstrap/scss/popover"; 58 | @import "bootstrap/scss/carousel"; 59 | @import "bootstrap/scss/spinners"; 60 | @import "bootstrap/scss/offcanvas"; 61 | @import "bootstrap/scss/helpers"; // Helpers 62 | @import "bootstrap/scss/utilities/api"; // Utilities 63 | 64 | // Block editor blocks 65 | @import "block_editor/blocks/frontend"; 66 | @import "block_editor/blocks/backend"; 67 | @import "block_editor/utilities"; 68 | 69 | // Custom blocks & extensions 70 | @import "block_editor/host_app/blocks/frontend"; 71 | @import "block_editor/host_app/blocks/backend"; 72 | } 73 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/_backend.scss: -------------------------------------------------------------------------------- 1 | @import "block_editor/blocks/be-card/backend"; 2 | @import "block_editor/blocks/be-contact-form/backend"; 3 | @import "block_editor/blocks/be-cover/backend"; 4 | @import "block_editor/blocks/block/backend"; 5 | @import "block_editor/blocks/buttons/backend"; 6 | @import "block_editor/blocks/columns/backend"; 7 | @import "block_editor/blocks/image/backend"; 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/_frontend.scss: -------------------------------------------------------------------------------- 1 | @import "block_editor/blocks/be-accordion/frontend"; 2 | @import "block_editor/blocks/be-alert/frontend"; 3 | @import "block_editor/blocks/be-card/frontend"; 4 | @import "block_editor/blocks/be-cover/frontend"; 5 | @import "block_editor/blocks/button/frontend"; 6 | @import "block_editor/blocks/buttons/frontend"; 7 | @import "block_editor/blocks/column/frontend"; 8 | @import "block_editor/blocks/columns/frontend"; 9 | @import "block_editor/blocks/image/frontend"; 10 | @import "block_editor/blocks/seperator/frontend"; 11 | @import "block_editor/blocks/table/frontend"; 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/be-accordion/_frontend.scss: -------------------------------------------------------------------------------- 1 | .wp-block-be-accordion { 2 | margin-bottom: $paragraph-margin-bottom; 3 | } 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/be-alert/_frontend.scss: -------------------------------------------------------------------------------- 1 | .wp-block-be-alert { 2 | @each $state, $value in $theme-colors { 3 | $alert-background: shift-color($value, $alert-bg-scale); 4 | $alert-border: shift-color($value, $alert-border-scale); 5 | $alert-color: shift-color($value, $alert-color-scale); 6 | @if (contrast-ratio($alert-background, $alert-color) < $min-contrast-ratio) { 7 | $alert-color: mix($value, color-contrast($alert-background), abs($alert-color-scale)); 8 | } 9 | &.is-style-#{$state} { 10 | @include alert-variant($alert-background, $alert-border, $alert-color); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/be-card/_backend.scss: -------------------------------------------------------------------------------- 1 | .block-editor-block-list__block[data-type="be/card"] { 2 | .components-button { 3 | position: absolute; 4 | background-color: $primary; 5 | color: $white; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/be-card/_frontend.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamasolutions/block-editor/3454f449b22a041191a6eee53c645e0d764841d3/app/assets/stylesheets/block_editor/blocks/be-card/_frontend.scss -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/be-contact-form/_backend.scss: -------------------------------------------------------------------------------- 1 | .block-editor-block-list__block[data-type="be/contact-form"] > div { 2 | display: flex; 3 | height: 300px; 4 | padding: 3rem; 5 | margin: 1rem 0; 6 | align-items: center; 7 | justify-content: center; 8 | background-color: $white; 9 | p { 10 | font-size: 1.25rem; 11 | margin-right: 3rem; 12 | } 13 | .be-contact-form-outline { 14 | background-image: image_url('block_editor/contact-form-block.svg'); 15 | height: 100%; 16 | width: 25%; 17 | background-repeat: no-repeat; 18 | background-size: contain; 19 | background-position: center; 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/be-cover/_backend.scss: -------------------------------------------------------------------------------- 1 | .block-editor-block-list__block[data-type="be/cover"] { 2 | .components-button { 3 | position: absolute; 4 | background-color: $primary; 5 | color: $white; 6 | z-index: 1; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/be-cover/_frontend.scss: -------------------------------------------------------------------------------- 1 | .wp-block-be-cover { 2 | position: relative; 3 | padding: 5rem 1rem; 4 | margin-bottom: $paragraph-margin-bottom; 5 | background-color: $primary; 6 | 7 | @include media-breakpoint-up(md) { 8 | padding: 5*$spacer; 9 | } 10 | 11 | &-image { 12 | position: absolute; 13 | width: 100%; 14 | max-width: 100%; 15 | object-fit: cover; 16 | width: 100%; 17 | height: 100%; 18 | top: 0; 19 | left: 0; 20 | filter: brightness(80%); 21 | } 22 | 23 | &-content { 24 | padding: $spacer; 25 | background: $body-bg; 26 | position: relative; 27 | min-height: 10rem; 28 | width: 100%; 29 | background: none; 30 | color: $white; 31 | text-align: center; 32 | } 33 | 34 | &.is-style-secondary { 35 | background-color: $secondary; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/block/_backend.scss: -------------------------------------------------------------------------------- 1 | .block-editor-block-list__block[data-type="core/block"] { 2 | .block-editor-block-list__layout.is-root-container { 3 | padding: 0; 4 | } 5 | .reusable-block-edit-panel__button { 6 | display: none; 7 | } 8 | .block-list-appender { 9 | display: none; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/button/_frontend.scss: -------------------------------------------------------------------------------- 1 | .wp-block-button { 2 | display: inline-block; 3 | 4 | &__link { 5 | display: inline-block; 6 | font-family: $btn-font-family; 7 | font-weight: $btn-font-weight; 8 | line-height: $btn-line-height; 9 | color: $body-color; 10 | text-align: center; 11 | text-decoration: if($link-decoration == none, null, none); 12 | white-space: $btn-white-space; 13 | vertical-align: middle; 14 | cursor: if($enable-button-pointers, pointer, null); 15 | user-select: none; 16 | background-color: transparent; 17 | border: $btn-border-width solid transparent; 18 | @include button-size($btn-padding-y, $btn-padding-x, $btn-font-size, $btn-border-radius); 19 | @include transition($btn-transition); 20 | // @include button-variant($primary, $white); 21 | &:hover { 22 | color: $body-color; 23 | text-decoration: if($link-hover-decoration == underline, none, null); 24 | } 25 | } 26 | &.large { 27 | .wp-block-button__link{ 28 | @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg); 29 | } 30 | } 31 | 32 | @each $color, $value in $theme-colors { 33 | &.is-style-#{$color} { 34 | .wp-block-button__link{ 35 | @include button-variant($value, $value); 36 | } 37 | } 38 | } 39 | @each $color, $value in $theme-colors { 40 | &.is-style-outline-#{$color} { 41 | .wp-block-button__link{ 42 | @include button-outline-variant($value); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/buttons/_backend.scss: -------------------------------------------------------------------------------- 1 | .block-editor-block-list__block[data-type="core/buttons"] { 2 | &.is-selected { 3 | .block-list-appender { 4 | display: initial; 5 | } 6 | } 7 | .block-list-appender { 8 | display: none; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/buttons/_frontend.scss: -------------------------------------------------------------------------------- 1 | .wp-block-buttons { 2 | margin-bottom: $paragraph-margin-bottom; 3 | } 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/column/_frontend.scss: -------------------------------------------------------------------------------- 1 | .wp-block-column { 2 | flex: 1; 3 | padding-right: calc(var(--#{$variable-prefix}gutter-x) / 2); // stylelint-disable-line function-disallowed-list 4 | padding-left: calc(var(--#{$variable-prefix}gutter-x) / 2); // stylelint-disable-line function-disallowed-list 5 | margin-top: var(--#{$variable-prefix}gutter-y); 6 | 7 | &.is-vertically-aligned-top { 8 | align-self: flex-start; 9 | } 10 | 11 | &.is-vertically-aligned-center { 12 | align-self: center; 13 | } 14 | 15 | &.is-vertically-aligned-bottom { 16 | align-self: flex-end; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/columns/_backend.scss: -------------------------------------------------------------------------------- 1 | .block-editor-block-list__block[data-type="core/columns"] { 2 | > .block-editor-inner-blocks { 3 | flex: 1; 4 | padding-right: calc(var(--#{$variable-prefix}gutter-x) / 2); // stylelint-disable-line function-disallowed-list 5 | padding-left: calc(var(--#{$variable-prefix}gutter-x) / 2); // stylelint-disable-line function-disallowed-list 6 | margin-top: var(--#{$variable-prefix}gutter-y); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/columns/_frontend.scss: -------------------------------------------------------------------------------- 1 | .wp-block-columns { 2 | --#{$variable-prefix}gutter-x: #{$grid-gutter-width}; 3 | --#{$variable-prefix}gutter-y: 0; 4 | display: flex; 5 | margin-top: calc(var(--#{$variable-prefix}gutter-y) * -1); // stylelint-disable-line function-disallowed-list 6 | margin-right: calc(var(--#{$variable-prefix}gutter-x) / -2); // stylelint-disable-line function-disallowed-list 7 | margin-left: calc(var(--#{$variable-prefix}gutter-x) / -2); // stylelint-disable-line function-disallowed-list 8 | 9 | &:not(.is-style-no-stack) { 10 | @include media-breakpoint-down(lg) { 11 | display: block; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/image/_backend.scss: -------------------------------------------------------------------------------- 1 | .block-editor-block-list__block[data-type="core/image"] { 2 | .block-editor-media-placeholder__url-input-container, 3 | .components-form-file-upload { 4 | display: none; 5 | } 6 | } 7 | .components-dropdown-menu__toggle[aria-label="Change alignment"] { 8 | display: none !important; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/image/_frontend.scss: -------------------------------------------------------------------------------- 1 | .wp-block-image { 2 | margin-bottom: $paragraph-margin-bottom; 3 | 4 | img { 5 | max-width: 100%; 6 | } 7 | figcaption { 8 | margin-top: 5px; 9 | font-size: 0.9rem; 10 | } 11 | &.is-style-padded { 12 | max-width: 75%; 13 | margin-left: auto; 14 | margin-right: auto; 15 | } 16 | &.is-style-rounded { 17 | img { 18 | border-radius: $border-radius; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/seperator/_frontend.scss: -------------------------------------------------------------------------------- 1 | .wp-block-separator { 2 | &.is-style-dots { 3 | background: none !important; 4 | border: none; 5 | text-align: center; 6 | max-width: none; 7 | line-height: 1; 8 | height: 1.2*$spacer !important; 9 | opacity: 1; 10 | &:before{ 11 | content: "\00b7 \00b7 \00b7"; 12 | font-size: 1.2*$spacer; 13 | letter-spacing: 2em; 14 | padding-left: 2em; 15 | font-family: serif; 16 | color: $black; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/blocks/table/_frontend.scss: -------------------------------------------------------------------------------- 1 | .wp-block-table{ 2 | margin-bottom: $paragraph-margin-bottom; 3 | overflow-x: auto; 4 | table { 5 | --#{$variable-prefix}table-bg: #{$table-bg}; 6 | --#{$variable-prefix}table-striped-color: #{$table-striped-color}; 7 | --#{$variable-prefix}table-striped-bg: #{$table-striped-bg}; 8 | --#{$variable-prefix}table-active-color: #{$table-active-color}; 9 | --#{$variable-prefix}table-active-bg: #{$table-active-bg}; 10 | --#{$variable-prefix}table-hover-color: #{$table-hover-color}; 11 | --#{$variable-prefix}table-hover-bg: #{$table-hover-bg}; 12 | 13 | width: 100%; 14 | color: $table-color; 15 | vertical-align: $table-cell-vertical-align; 16 | border-color: $table-border-color; 17 | overflow-x: auto; 18 | -webkit-overflow-scrolling: touch; 19 | > :not(caption) > * > * { 20 | padding: $table-cell-padding-y $table-cell-padding-x; 21 | background-color: var(--#{$variable-prefix}table-bg); 22 | border-bottom-width: $table-border-width; 23 | box-shadow: inset 0 0 0 9999px var(--#{$variable-prefix}table-accent-bg); 24 | } 25 | 26 | > tbody { 27 | vertical-align: inherit; 28 | } 29 | > tbody > tr > th { 30 | text-align: center; 31 | } 32 | 33 | > thead { 34 | vertical-align: bottom; 35 | background: $secondary; 36 | color: $white; 37 | } 38 | > thead > tr > th:first-child{ 39 | text-align: center; 40 | } 41 | 42 | // Highlight border color between thead, tbody and tfoot. 43 | > :not(:last-child) > :last-child > * { 44 | border-bottom-color: $table-group-separator-color; 45 | } 46 | //border 47 | > :not(caption) > * { 48 | border-width: $table-border-width 0; 49 | 50 | // stylelint-disable-next-line selector-max-universal 51 | > * { 52 | border-width: 0 $table-border-width; 53 | } 54 | } 55 | } 56 | 57 | > figcaption { 58 | margin-top: $paragraph-margin-bottom / 4; 59 | } 60 | 61 | &.is-style-striped { 62 | table > tbody > tr:nth-of-type(#{$table-striped-order}) { 63 | --#{$variable-prefix}table-accent-bg: var(--#{$variable-prefix}table-striped-bg); 64 | color: var(--#{$variable-prefix}table-striped-color); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/host_app/_variables.scss: -------------------------------------------------------------------------------- 1 | // Override this file if you want to include any styling before default Block Editor styles are loaded. This would be a good place to add variables to customise Bootstrap 2 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/host_app/blocks/_backend.scss: -------------------------------------------------------------------------------- 1 | // Override this file if you want to include backend block styles to the Block Editor and not have to worry about losing any base styles 2 | -------------------------------------------------------------------------------- /app/assets/stylesheets/block_editor/host_app/blocks/_frontend.scss: -------------------------------------------------------------------------------- 1 | // Override this file if you want to include frontend block styles to the Block Editor and not have to worry about losing any base styles 2 | -------------------------------------------------------------------------------- /app/controllers/block_editor/application_controller.rb: -------------------------------------------------------------------------------- 1 | module BlockEditor 2 | class ApplicationController < ActionController::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/block_editor/application_helper.rb: -------------------------------------------------------------------------------- 1 | require "webpacker/helper" 2 | 3 | module BlockEditor 4 | module ApplicationHelper 5 | include ::Webpacker::Helper 6 | 7 | def current_webpacker_instance 8 | BlockEditor.webpacker 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/javascript/block_editor/blocks/be-accordion/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import classnames from 'classnames'; 4 | 5 | import { PanelBody, ToggleControl } from '@wordpress/components'; 6 | import { InnerBlocks, InspectorControls, PlainText } from '@wordpress/block-editor' 7 | import { box as icon } from '@wordpress/icons'; 8 | 9 | const name = 'be/accordion'; 10 | 11 | export { name }; 12 | 13 | export const settings = { 14 | title: 'Accordion', 15 | description: 'Accordions are elements that help you organize and navigate multiple documents in a single container.', 16 | icon, 17 | category: 'layout', 18 | attributes: { 19 | blockId: { 20 | type: 'string' 21 | }, 22 | title: { 23 | source: 'text', 24 | selector: '.accordion-button' 25 | }, 26 | isOpenByDefault: { 27 | type: 'boolean', 28 | default: true 29 | } 30 | }, 31 | example: { 32 | attributes: { 33 | title: 'Open by default accordion' 34 | }, 35 | innerBlocks: [ 36 | { 37 | name: 'core/paragraph', 38 | attributes: { 39 | content: 'Use an accordion to structure and optionally collapse content' 40 | } 41 | } 42 | ] 43 | }, 44 | edit({clientId, attributes, className, setAttributes, isSelected}) { 45 | const { blockId } = attributes; 46 | if ( ! blockId ) { 47 | setAttributes( { blockId: `id-${clientId}` } ); 48 | } else if ( blockId != clientId ) { 49 | setAttributes( { blockId: `id-${clientId}` } ); 50 | } 51 | 52 | // TODO: Update this to check if this OR any of the innerblocks are selected 53 | // let inlineStyle = (isSelected || attributes.isOpenByDefault) ? { display: 'block' } : {}; 54 | let inlineStyle = { display: 'block' }; 55 | 56 | return [ 57 | 58 | 59 | setAttributes({ isOpenByDefault: content }) } 62 | checked={ attributes.isOpenByDefault } 63 | /> 64 | 65 | 66 | , 67 | 68 |
69 |
70 |

71 | setAttributes({ title: content }) } 73 | value={ attributes.title } 74 | placeholder="Your accordion title" 75 | className="accordion-button" 76 | /> 77 | </h2> 78 | <div className="accordion-collapse collapse show"> 79 | <div className="accordion-body" style={ inlineStyle }> 80 | <InnerBlocks/> 81 | </div> 82 | </div> 83 | </div> 84 | </div> 85 | ]; 86 | }, 87 | save({ attributes }) { 88 | let buttonVisibilityClass = attributes.isOpenByDefault ? "collapsed" : ""; 89 | let accordionVisibilityClass = attributes.isOpenByDefault ? "show" : ""; 90 | 91 | return ( 92 | <div className="wp-block-be-accordion accordion"> 93 | <div className="accordion-item"> 94 | <h2 className="accordion-header"> 95 | <button className={ "accordion-button " + buttonVisibilityClass } type="button" data-bs-toggle="collapse" data-bs-target={ "#" + attributes.blockId }> 96 | { attributes.title } 97 | </button> 98 | </h2> 99 | <div id={attributes.blockId} className={ "accordion-collapse collapse " + accordionVisibilityClass }> 100 | <div className="accordion-body"> 101 | <InnerBlocks.Content /> 102 | </div> 103 | </div> 104 | </div> 105 | </div> 106 | ); 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /app/javascript/block_editor/blocks/be-alert/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import { 5 | InnerBlocks 6 | } from '@wordpress/block-editor'; 7 | import { registerBlockStyle } from '@wordpress/blocks'; 8 | import { createBlock } from '@wordpress/blocks'; 9 | import { rawHandler } from '@wordpress/blocks'; 10 | import { box as icon } from '@wordpress/icons'; 11 | 12 | const name = 'be/alert'; 13 | 14 | export { name }; 15 | 16 | export const settings = { 17 | title: 'Callout (Alert)', 18 | description: 'Container to help draw attention to content.', 19 | icon, 20 | category: 'formatting', 21 | styles: [ 22 | { name: 'primary', label: 'Primary', isDefault: true }, 23 | { name: 'secondary', label: 'Secondary' }, 24 | { name: 'success', label: 'Success' }, 25 | { name: 'danger', label: 'Danger' }, 26 | { name: 'warning', label: 'Warning' }, 27 | { name: 'info', label: 'Info' }, 28 | { name: 'light', label: 'Light' }, 29 | { name: 'dark', label: 'Dark' } 30 | ], 31 | example: { 32 | innerBlocks: [ 33 | { 34 | name: 'core/paragraph', 35 | attributes: { 36 | content: 'Use a callout to grab the users attention.' 37 | } 38 | } 39 | ] 40 | }, 41 | edit(props) { 42 | return ( 43 | <div role="alert" className={ 'alert ' + props.className }> 44 | <InnerBlocks/> 45 | </div> 46 | ); 47 | }, 48 | save() { 49 | return <div role="alert" className='alert'><InnerBlocks.Content /></div>; 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /app/javascript/block_editor/blocks/be-card/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { 5 | InnerBlocks 6 | } from '@wordpress/block-editor'; 7 | import { registerBlockStyle } from '@wordpress/blocks'; 8 | import { createBlock } from '@wordpress/blocks'; 9 | import { rawHandler } from '@wordpress/blocks'; 10 | 11 | import { TextControl, PanelBody, ToggleControl, Button } from '@wordpress/components'; 12 | import { InspectorControls, RichText, MediaUpload, PlainText } from '@wordpress/block-editor' 13 | import { box as icon } from '@wordpress/icons'; 14 | 15 | const name = 'be/card'; 16 | 17 | export { name }; 18 | 19 | export const settings = { 20 | title: 'Card', 21 | description: 'Group a piece of content in an eye catching container.', 22 | icon, 23 | category: 'formatting', 24 | example: { 25 | attributes: { 26 | title: 'Container example', 27 | imageUrl: 'https://s.w.org/images/core/5.3/MtBlanc1.jpg', 28 | } 29 | }, 30 | attributes: { 31 | title: { 32 | source: 'text', 33 | selector: '.card-title' 34 | }, 35 | body: { 36 | type: 'array', 37 | source: 'children', 38 | selector: '.card-content' 39 | }, 40 | imageAlt: { 41 | attribute: 'alt', 42 | selector: '.card-img-top' 43 | }, 44 | imageUrl: { 45 | attribute: 'src', 46 | selector: '.card-img-top' 47 | }, 48 | hasCallToAction: { 49 | type: 'boolean' 50 | }, 51 | callToAction: { 52 | type: 'text' 53 | }, 54 | url: { 55 | type: 'text' 56 | }, 57 | openInNewTab: { 58 | type: 'boolean' 59 | } 60 | }, 61 | edit({attributes, className, setAttributes, isSelected}) { 62 | const getImageButton = (openEvent) => { 63 | if(attributes.imageUrl) { 64 | return ( 65 | <> 66 | { isSelected && 67 | <Button 68 | onClick={ openEvent } 69 | className="button" 70 | > 71 | Edit 72 | </Button> 73 | } 74 | <img 75 | src={ attributes.imageUrl } 76 | className="card-img-top" 77 | /> 78 | </> 79 | ); 80 | } 81 | else { 82 | return ( 83 | <div className="block-editor-image-placeholder"> 84 | <Button 85 | onClick={ openEvent } 86 | className="button button-large" 87 | > 88 | Pick an image 89 | </Button> 90 | </div> 91 | ); 92 | } 93 | }; 94 | return ([ 95 | <InspectorControls> 96 | <PanelBody title='Card settings'> 97 | <TextControl 98 | label="URL" 99 | value={ attributes.url } 100 | onChange={ content => setAttributes({ url: content }) } 101 | /> 102 | { attributes.url && ( 103 | <ToggleControl 104 | label="Display Call To Action" 105 | checked={ attributes.hasCallToAction } 106 | onChange={ content => setAttributes({ hasCallToAction: content }) } 107 | /> 108 | )} 109 | { attributes.url && ( 110 | <ToggleControl 111 | label="Open in new Tab?" 112 | checked={ attributes.openInNewTab } 113 | onChange={ content => setAttributes({ openInNewTab: content }) } 114 | /> 115 | )} 116 | </PanelBody> 117 | </InspectorControls>, 118 | <div className={ 'card ' + className }> 119 | <MediaUpload 120 | onSelect={ media => { setAttributes({ imageAlt: media.alt, imageUrl: media.url }); } } 121 | type="image" 122 | value={ attributes.imageID } 123 | render={ ({ open }) => getImageButton(open) } 124 | /> 125 | <div className='card-body'> 126 | <PlainText 127 | onChange={ content => setAttributes({ title: content }) } 128 | value={ attributes.title } 129 | placeholder="Your card title" 130 | className="card-title h2" 131 | /> 132 | <RichText 133 | onChange={ content => setAttributes({ body: content }) } 134 | value={ attributes.body } 135 | multiline="p" 136 | placeholder="Your card text" 137 | /> 138 | </div> 139 | { attributes.hasCallToAction && attributes.url && 140 | <PlainText 141 | onChange={ content => setAttributes({ callToAction: content }) } 142 | value={ attributes.callToAction } 143 | placeholder="Your Call To Action" 144 | className="card-link" 145 | /> 146 | } 147 | </div> 148 | ]); 149 | }, 150 | save({ attributes }) { 151 | const linkTarget = (attributes.openInNewTab) ? '_blank' : '_self'; 152 | const cardImage = (src, alt) => { 153 | if(!src) return null; 154 | 155 | return ( 156 | <img 157 | className="card-img-top" 158 | src={ src } 159 | alt={ alt } 160 | /> 161 | ); 162 | } 163 | 164 | return ( 165 | <div className="card"> 166 | { attributes.url ? ( 167 | <a 168 | href={ attributes.url } 169 | target= { linkTarget } 170 | > 171 | { cardImage(attributes.imageUrl, attributes.imageAlt) } 172 | </a> 173 | ) : ( 174 | cardImage(attributes.imageUrl, attributes.imageAlt) 175 | )} 176 | <div className="card-body"> 177 | <h3 className="card-title"> 178 | { attributes.url ? ( 179 | <a 180 | href={ attributes.url } 181 | target= { linkTarget } 182 | > 183 | { attributes.title } 184 | </a> 185 | ) : ( 186 | attributes.title 187 | )} 188 | </h3> 189 | <div className='card-content'> 190 | { attributes.body } 191 | </div> 192 | </div> 193 | { attributes.hasCallToAction && attributes.url && 194 | <RichText.Content 195 | tagName="a" 196 | className='card-link' 197 | href={ attributes.url } 198 | target= { linkTarget } 199 | value={ attributes.callToAction } 200 | /> 201 | } 202 | </div> 203 | ); 204 | } 205 | }; 206 | -------------------------------------------------------------------------------- /app/javascript/block_editor/blocks/be-contact-form/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { widget as icon } from '@wordpress/icons'; 5 | 6 | const name = 'be/contact-form'; 7 | 8 | export { name }; 9 | 10 | export const settings = { 11 | title: 'Contact Form', 12 | description: 'Allow users to get in touch using a contact form.', 13 | icon, 14 | category: 'widgets', 15 | edit({attributes, className, setAttributes, isSelected}) { 16 | return ([ 17 | <div className={ className }> 18 | <p>Contact Form</p> 19 | <div className='be-contact-form-outline'> 20 | </div> 21 | </div> 22 | ]); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /app/javascript/block_editor/blocks/be-cover/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { 5 | InnerBlocks 6 | } from '@wordpress/block-editor'; 7 | import { registerBlockStyle } from '@wordpress/blocks'; 8 | import { createBlock } from '@wordpress/blocks'; 9 | import { rawHandler } from '@wordpress/blocks'; 10 | 11 | import { TextControl, PanelBody, ToggleControl, Button } from '@wordpress/components'; 12 | import { InspectorControls, RichText, MediaUpload, PlainText } from '@wordpress/block-editor' 13 | import { cover as icon } from '@wordpress/icons'; 14 | 15 | const BLOCKS_TEMPLATE = [ 16 | [ 'core/heading', { content: 'Example Cover Title' } ], 17 | [ 'core/paragraph', { content: 'Break up content and draw attention to something using a cover block' } ] 18 | ]; 19 | const ALLOWED_BLOCKS = [ 'core/buttons', 'core/heading', 'core/paragraph', 'core/list' ]; 20 | const name = 'be/cover'; 21 | 22 | export { name }; 23 | 24 | export const settings = { 25 | title: 'Cover', 26 | description: 'Add an image with a text overlay - great for breaking up content.', 27 | icon, 28 | category: 'formatting', 29 | example: { 30 | innerBlocks: [ 31 | { 32 | name: 'core/heading', 33 | attributes: { 34 | content: 'Example Cover Title' 35 | } 36 | }, 37 | { 38 | name: 'core/paragraph', 39 | attributes: { 40 | content: 'Break up content and draw attention to something using a cover block' 41 | } 42 | }, 43 | { 44 | name: 'core/button', 45 | attributes: { 46 | content: 'Example Call To Action' 47 | } 48 | } 49 | ] 50 | }, 51 | attributes: { 52 | imageAlt: { 53 | attribute: 'alt', 54 | selector: '.wp-block-be-cover-image' 55 | }, 56 | imageUrl: { 57 | attribute: 'src', 58 | selector: '.wp-block-be-cover-image' 59 | } 60 | }, 61 | edit({attributes, className, setAttributes, isSelected}) { 62 | const getImageButton = (openEvent) => { 63 | if(attributes.imageUrl) { 64 | return ( 65 | <> 66 | { isSelected && 67 | <Button 68 | onClick={ openEvent } 69 | className="button" 70 | > 71 | Edit image 72 | </Button> 73 | } 74 | <img 75 | src={ attributes.imageUrl } 76 | className="wp-block-be-cover-image" 77 | /> 78 | </> 79 | ); 80 | } 81 | else { 82 | return ( 83 | <> 84 | { isSelected && 85 | <Button 86 | onClick={ openEvent } 87 | className="button button-large" 88 | > 89 | Pick an image 90 | </Button> 91 | } 92 | </> 93 | ); 94 | } 95 | }; 96 | return ( 97 | <div className={ className }> 98 | <MediaUpload 99 | onSelect={ media => { setAttributes({ imageAlt: media.alt, imageUrl: media.url }); } } 100 | type="image" 101 | value={ attributes.imageID } 102 | render={ ({ open }) => getImageButton(open) } 103 | /> 104 | <div className='wp-block-be-cover-content'> 105 | <InnerBlocks 106 | allowedBlocks={ ALLOWED_BLOCKS } 107 | template={ BLOCKS_TEMPLATE } 108 | /> 109 | </div> 110 | </div> 111 | ); 112 | }, 113 | save({ attributes }) { 114 | const cardImage = (src, alt) => { 115 | if(!src) return null; 116 | 117 | return ( 118 | <img 119 | className="wp-block-be-cover-image" 120 | src={ src } 121 | alt={ alt } 122 | /> 123 | ); 124 | } 125 | 126 | return ( 127 | <div> 128 | { cardImage(attributes.imageUrl, attributes.imageAlt) } 129 | <div className='wp-block-be-cover-content'> 130 | <InnerBlocks.Content /> 131 | </div> 132 | </div> 133 | ); 134 | }, 135 | }; 136 | -------------------------------------------------------------------------------- /app/javascript/block_editor/blocks/block/edit-panel/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | /** 5 | * WordPress dependencies 6 | */ 7 | import { Button } from '@wordpress/components'; 8 | import { useInstanceId, usePrevious } from '@wordpress/compose'; 9 | import { useEffect, useRef } from '@wordpress/element'; 10 | import { __ } from '@wordpress/i18n'; 11 | 12 | /** @typedef {import('@wordpress/element').WPComponent} WPComponent */ 13 | 14 | /** 15 | * ReusableBlockEditPanel props. 16 | * 17 | * @typedef WPReusableBlockEditPanelProps 18 | * 19 | * @property {boolean} isEditDisabled Is editing the reusable 20 | * block disabled. 21 | * @property {boolean} isEditing Is the reusable block 22 | * being edited. 23 | * @property {boolean} isSaving Is the reusable block 24 | * being saved. 25 | * @property {()=>void} onCancel Callback to run when 26 | * editing is canceled. 27 | * @property {(newTitle:string)=>void} onChangeTitle Callback to run when the 28 | * title input value is 29 | * changed. 30 | * @property {()=>void} onEdit Callback to run when 31 | * editing begins. 32 | * @property {()=>void} onSave Callback to run when 33 | * saving. 34 | * @property {string} title Title of the reusable 35 | * block. 36 | */ 37 | 38 | /** 39 | * Panel for enabling the editing and saving of a reusable block. 40 | * 41 | * @param {WPReusableBlockEditPanelProps} props Component props. 42 | * 43 | * @return {WPComponent} The panel. 44 | */ 45 | export default function ReusableBlockEditPanel( { 46 | isEditDisabled, 47 | isEditing, 48 | isSaving, 49 | onChangeTitle, 50 | onEdit, 51 | onSave, 52 | title, 53 | } ) { 54 | const instanceId = useInstanceId( ReusableBlockEditPanel ); 55 | const titleField = useRef(); 56 | const editButton = useRef(); 57 | const wasEditing = usePrevious( isEditing ); 58 | const wasSaving = usePrevious( isSaving ); 59 | 60 | // Select the title input when the form opens. 61 | useEffect( () => { 62 | if ( ! wasEditing && isEditing ) { 63 | titleField.current.select(); 64 | } 65 | }, [ isEditing ] ); 66 | 67 | // Move focus back to the Edit button after pressing the Escape key or Save. 68 | useEffect( () => { 69 | if ( ( wasEditing || wasSaving ) && ! isEditing && ! isSaving ) { 70 | editButton.current.focus(); 71 | } 72 | }, [ isEditing, isSaving ] ); 73 | 74 | function handleFormSubmit( event ) { 75 | event.preventDefault(); 76 | onSave(); 77 | } 78 | 79 | function handleTitleChange( event ) { 80 | onChangeTitle( event.target.value ); 81 | } 82 | 83 | return ( 84 | <> 85 | { ! isEditing && ! isSaving && ( 86 | <div className="reusable-block-edit-panel"> 87 | <b className="reusable-block-edit-panel__info">{ title }</b> 88 | <Button 89 | ref={ editButton } 90 | isSecondary 91 | className="reusable-block-edit-panel__button" 92 | disabled={ isEditDisabled } 93 | onClick={ onEdit } 94 | > 95 | { __( 'Edit' ) } 96 | </Button> 97 | </div> 98 | ) } 99 | { ( isEditing || isSaving ) && ( 100 | <form 101 | className="reusable-block-edit-panel" 102 | onSubmit={ handleFormSubmit } 103 | > 104 | <label 105 | htmlFor={ `reusable-block-edit-panel__title-${ instanceId }` } 106 | className="reusable-block-edit-panel__label" 107 | > 108 | { __( 'Name:' ) } 109 | </label> 110 | <input 111 | ref={ titleField } 112 | type="text" 113 | disabled={ isSaving } 114 | className="reusable-block-edit-panel__title" 115 | value={ title } 116 | onChange={ handleTitleChange } 117 | id={ `reusable-block-edit-panel__title-${ instanceId }` } 118 | /> 119 | <Button 120 | type="submit" 121 | isSecondary 122 | isBusy={ isSaving } 123 | disabled={ ! title || isSaving } 124 | className="reusable-block-edit-panel__button" 125 | > 126 | { __( 'Save' ) } 127 | </Button> 128 | </form> 129 | ) } 130 | </> 131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /app/javascript/block_editor/blocks/block/edit.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | /** 5 | * WordPress dependencies 6 | */ 7 | import { useSelect, useDispatch } from '@wordpress/data'; 8 | import { useEntityBlockEditor } from '@wordpress/core-data'; 9 | import { useCallback } from '@wordpress/element'; 10 | import { 11 | Placeholder, 12 | Spinner, 13 | Disabled, 14 | ToolbarGroup, 15 | ToolbarButton, 16 | } from '@wordpress/components'; 17 | import { __ } from '@wordpress/i18n'; 18 | import { 19 | BlockEditorProvider, 20 | WritingFlow, 21 | BlockList, 22 | BlockControls, 23 | useBlockProps, 24 | } from '@wordpress/block-editor'; 25 | 26 | /** 27 | * Internal dependencies 28 | */ 29 | import ReusableBlockEditPanel from './edit-panel'; 30 | 31 | export default function ReusableBlockEdit( { 32 | attributes: { ref }, 33 | clientId, 34 | isSelected, 35 | } ) { 36 | const recordArgs = [ 'postType', 'wp_block', ref ]; 37 | 38 | const { 39 | reusableBlock, 40 | hasResolved, 41 | isEditing, 42 | isSaving, 43 | canUserUpdate, 44 | settings, 45 | } = useSelect( 46 | ( select ) => ( { 47 | reusableBlock: select( 'core' ).getEditedEntityRecord( 48 | ...recordArgs 49 | ), 50 | hasResolved: select( 'core' ).hasFinishedResolution( 51 | 'getEditedEntityRecord', 52 | recordArgs 53 | ), 54 | isSaving: select( 'core' ).isSavingEntityRecord( ...recordArgs ), 55 | canUserUpdate: select( 'core' ).canUser( 'update', 'blocks', ref ), 56 | isEditing: select( 57 | 'core/reusable-blocks' 58 | ).__experimentalIsEditingReusableBlock( clientId ), 59 | settings: select( 'core/block-editor' ).getSettings(), 60 | } ), 61 | [ ref, clientId ] 62 | ); 63 | 64 | const { editEntityRecord, saveEditedEntityRecord } = useDispatch( 'core' ); 65 | const { __experimentalSetEditingReusableBlock } = useDispatch( 66 | 'core/reusable-blocks' 67 | ); 68 | const setIsEditing = useCallback( 69 | ( value ) => { 70 | __experimentalSetEditingReusableBlock( clientId, value ); 71 | }, 72 | [ clientId ] 73 | ); 74 | 75 | const { 76 | __experimentalConvertBlockToStatic: convertBlockToStatic, 77 | } = useDispatch( 'core/reusable-blocks' ); 78 | 79 | const { createSuccessNotice, createErrorNotice } = useDispatch( 80 | 'core/notices' 81 | ); 82 | const save = useCallback( async function () { 83 | try { 84 | await saveEditedEntityRecord( ...recordArgs ); 85 | createSuccessNotice( __( 'Block updated.' ), { 86 | type: 'snackbar', 87 | } ); 88 | } catch ( error ) { 89 | createErrorNotice( error.message, { 90 | type: 'snackbar', 91 | } ); 92 | } 93 | }, recordArgs ); 94 | 95 | const [ blocks, onInput, onChange ] = useEntityBlockEditor( 96 | 'postType', 97 | 'wp_block', 98 | { id: ref } 99 | ); 100 | 101 | const blockProps = useBlockProps(); 102 | 103 | if ( ! hasResolved ) { 104 | return ( 105 | <div { ...blockProps }> 106 | <Placeholder> 107 | <Spinner /> 108 | </Placeholder> 109 | </div> 110 | ); 111 | } 112 | 113 | if ( ! reusableBlock ) { 114 | return ( 115 | <div { ...blockProps }> 116 | <Placeholder> 117 | { __( 'Block has been deleted or is unavailable.' ) } 118 | </Placeholder> 119 | </div> 120 | ); 121 | } 122 | 123 | let element = ( 124 | <BlockEditorProvider 125 | value={ blocks } 126 | onInput={ onInput } 127 | onChange={ onChange } 128 | settings={ settings } 129 | > 130 | <WritingFlow> 131 | <BlockList /> 132 | </WritingFlow> 133 | </BlockEditorProvider> 134 | ); 135 | 136 | if ( ! isEditing ) { 137 | element = <Disabled>{ element }</Disabled>; 138 | } 139 | 140 | return ( 141 | <div { ...blockProps }> 142 | <div className="block-library-block__reusable-block-container"> 143 | { ( isSelected || isEditing ) && ( 144 | <ReusableBlockEditPanel 145 | isEditing={ isEditing } 146 | title={ reusableBlock.title } 147 | isSaving={ isSaving } 148 | isEditDisabled={ ! canUserUpdate } 149 | onEdit={ () => setIsEditing( true ) } 150 | onChangeTitle={ ( title ) => 151 | editEntityRecord( ...recordArgs, { title } ) 152 | } 153 | onSave={ () => { 154 | save(); 155 | setIsEditing( false ); 156 | } } 157 | /> 158 | ) } 159 | { element } 160 | </div> 161 | </div> 162 | ); 163 | } 164 | -------------------------------------------------------------------------------- /app/javascript/block_editor/blocks/button/edit.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import PropTypes from 'prop-types' 4 | 5 | /** 6 | * External dependencies 7 | */ 8 | import classnames from 'classnames'; 9 | 10 | /** 11 | * WordPress dependencies 12 | */ 13 | import { __ } from '@wordpress/i18n'; 14 | import { useCallback, useState } from '@wordpress/element'; 15 | import { 16 | KeyboardShortcuts, 17 | PanelBody, 18 | RangeControl, 19 | TextControl, 20 | ToggleControl, 21 | ToolbarButton, 22 | ToolbarGroup, 23 | Popover, 24 | } from '@wordpress/components'; 25 | import { 26 | BlockControls, 27 | InspectorControls, 28 | RichText, 29 | __experimentalBlock as Block, 30 | __experimentalLinkControl as LinkControl, 31 | } from '@wordpress/block-editor'; 32 | import { rawShortcut, displayShortcut } from '@wordpress/keycodes'; 33 | import { link } from '@wordpress/icons'; 34 | import { createBlock } from '@wordpress/blocks'; 35 | 36 | // #<{(|* 37 | // * Internal dependencies 38 | // |)}># 39 | // import ColorEdit from './color-edit'; 40 | // import getColorAndStyleProps from './color-props'; 41 | 42 | const NEW_TAB_REL = 'noreferrer noopener'; 43 | const MIN_BORDER_RADIUS_VALUE = 0; 44 | const MAX_BORDER_RADIUS_VALUE = 50; 45 | const INITIAL_BORDER_RADIUS_POSITION = 5; 46 | 47 | function BorderPanel( { borderRadius = '', setAttributes } ) { 48 | const setBorderRadius = useCallback( 49 | ( newBorderRadius ) => { 50 | setAttributes( { borderRadius: newBorderRadius } ); 51 | }, 52 | [ setAttributes ] 53 | ); 54 | return ( 55 | <PanelBody title={ __( 'Border settings' ) }> 56 | <RangeControl 57 | value={ borderRadius } 58 | label={ __( 'Border radius' ) } 59 | min={ MIN_BORDER_RADIUS_VALUE } 60 | max={ MAX_BORDER_RADIUS_VALUE } 61 | initialPosition={ INITIAL_BORDER_RADIUS_POSITION } 62 | allowReset 63 | onChange={ setBorderRadius } 64 | /> 65 | </PanelBody> 66 | ); 67 | } 68 | 69 | function URLPicker( { 70 | isSelected, 71 | url, 72 | setAttributes, 73 | opensInNewTab, 74 | onToggleOpenInNewTab, 75 | } ) { 76 | const [ isURLPickerOpen, setIsURLPickerOpen ] = useState( false ); 77 | const openLinkControl = () => { 78 | setIsURLPickerOpen( true ); 79 | 80 | // prevents default behaviour for event 81 | return false; 82 | }; 83 | const linkControl = isURLPickerOpen && ( 84 | <Popover 85 | position="bottom center" 86 | onClose={ () => setIsURLPickerOpen( false ) } 87 | > 88 | <LinkControl 89 | className="wp-block-navigation-link__inline-link-input" 90 | value={ { url, opensInNewTab } } 91 | onChange={ ( { 92 | url: newURL = '', 93 | opensInNewTab: newOpensInNewTab, 94 | } ) => { 95 | setAttributes( { url: newURL } ); 96 | 97 | if ( opensInNewTab !== newOpensInNewTab ) { 98 | onToggleOpenInNewTab( newOpensInNewTab ); 99 | } 100 | } } 101 | /> 102 | </Popover> 103 | ); 104 | return ( 105 | <> 106 | <BlockControls> 107 | <ToolbarGroup> 108 | <ToolbarButton 109 | name="link" 110 | icon={ link } 111 | title={ __( 'Link' ) } 112 | shortcut={ displayShortcut.primary( 'k' ) } 113 | onClick={ openLinkControl } 114 | /> 115 | </ToolbarGroup> 116 | </BlockControls> 117 | { isSelected && ( 118 | <KeyboardShortcuts 119 | bindGlobal 120 | shortcuts={ { 121 | [ rawShortcut.primary( 'k' ) ]: openLinkControl, 122 | } } 123 | /> 124 | ) } 125 | { linkControl } 126 | </> 127 | ); 128 | } 129 | 130 | function ButtonEdit( props ) { 131 | const { 132 | attributes, 133 | setAttributes, 134 | className, 135 | isSelected, 136 | onReplace, 137 | mergeBlocks, 138 | } = props; 139 | const { 140 | borderRadius, 141 | linkTarget, 142 | placeholder, 143 | rel, 144 | text, 145 | url, 146 | } = attributes; 147 | const onSetLinkRel = useCallback( 148 | ( value ) => { 149 | setAttributes( { rel: value } ); 150 | }, 151 | [ setAttributes ] 152 | ); 153 | 154 | const onToggleOpenInNewTab = useCallback( 155 | ( value ) => { 156 | const newLinkTarget = value ? '_blank' : undefined; 157 | 158 | let updatedRel = rel; 159 | if ( newLinkTarget && ! rel ) { 160 | updatedRel = NEW_TAB_REL; 161 | } else if ( ! newLinkTarget && rel === NEW_TAB_REL ) { 162 | updatedRel = undefined; 163 | } 164 | 165 | setAttributes( { 166 | linkTarget: newLinkTarget, 167 | rel: updatedRel, 168 | } ); 169 | }, 170 | [ rel, setAttributes ] 171 | ); 172 | 173 | // const colorProps = getColorAndStyleProps( attributes ); 174 | 175 | return ( 176 | <> 177 | <Block.div> 178 | <RichText 179 | placeholder={ placeholder || __( 'Add text…' ) } 180 | value={ text } 181 | onChange={ ( value ) => setAttributes( { text: value } ) } 182 | withoutInteractiveFormatting 183 | className={ classnames( 184 | className, 185 | 'wp-block-button__link', 186 | // colorProps.className, 187 | // { 188 | // 'no-border-radius': borderRadius === 0, 189 | // } 190 | ) } 191 | onSplit={ ( value ) => 192 | createBlock( 'core/button', { 193 | ...attributes, 194 | text: value, 195 | } ) 196 | } 197 | onReplace={ onReplace } 198 | onMerge={ mergeBlocks } 199 | identifier="text" 200 | /> 201 | </Block.div> 202 | <URLPicker 203 | url={ url } 204 | setAttributes={ setAttributes } 205 | isSelected={ isSelected } 206 | opensInNewTab={ linkTarget === '_blank' } 207 | onToggleOpenInNewTab={ onToggleOpenInNewTab } 208 | /> 209 | <InspectorControls> 210 | <PanelBody title={ __( 'Link settings' ) }> 211 | <ToggleControl 212 | label={ __( 'Open in new tab' ) } 213 | onChange={ onToggleOpenInNewTab } 214 | checked={ linkTarget === '_blank' } 215 | /> 216 | <TextControl 217 | label={ __( 'Link rel' ) } 218 | value={ rel || '' } 219 | onChange={ onSetLinkRel } 220 | /> 221 | </PanelBody> 222 | </InspectorControls> 223 | </> 224 | ); 225 | } 226 | 227 | export default ButtonEdit; 228 | -------------------------------------------------------------------------------- /app/javascript/block_editor/blocks/column/edit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classnames from 'classnames'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | 8 | /** 9 | * WordPress dependencies 10 | */ 11 | import { 12 | InnerBlocks, 13 | BlockControls, 14 | BlockVerticalAlignmentToolbar, 15 | InspectorControls, 16 | __experimentalBlock as Block, 17 | } from '@wordpress/block-editor'; 18 | import { PanelBody, RangeControl } from '@wordpress/components'; 19 | import { withDispatch, withSelect } from '@wordpress/data'; 20 | import { compose } from '@wordpress/compose'; 21 | import { __ } from '@wordpress/i18n'; 22 | 23 | function ColumnEdit( { 24 | attributes, 25 | setAttributes, 26 | updateAlignment, 27 | hasChildBlocks, 28 | } ) { 29 | const { verticalAlignment, width } = attributes; 30 | 31 | const classes = classnames( 'block-core-columns', { 32 | [ `is-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment, 33 | } ); 34 | 35 | const hasWidth = Number.isFinite( width ); 36 | 37 | return ( 38 | <> 39 | <BlockControls> 40 | <BlockVerticalAlignmentToolbar 41 | onChange={ updateAlignment } 42 | value={ verticalAlignment } 43 | /> 44 | </BlockControls> 45 | <InnerBlocks 46 | templateLock={ false } 47 | renderAppender={ 48 | hasChildBlocks 49 | ? undefined 50 | : () => <InnerBlocks.ButtonBlockAppender /> 51 | } 52 | __experimentalTagName={ Block.div } 53 | __experimentalPassedProps={ { 54 | className: classes, 55 | style: hasWidth ? { flexBasis: width + '%' } : undefined, 56 | } } 57 | /> 58 | </> 59 | ); 60 | } 61 | 62 | export default compose( 63 | withSelect( ( select, ownProps ) => { 64 | const { clientId } = ownProps; 65 | const { getBlockOrder } = select( 'core/block-editor' ); 66 | 67 | return { 68 | hasChildBlocks: getBlockOrder( clientId ).length > 0, 69 | }; 70 | } ), 71 | withDispatch( ( dispatch, ownProps, registry ) => { 72 | return { 73 | updateAlignment( verticalAlignment ) { 74 | const { clientId, setAttributes } = ownProps; 75 | const { updateBlockAttributes } = dispatch( 76 | 'core/block-editor' 77 | ); 78 | const { getBlockRootClientId } = registry.select( 79 | 'core/block-editor' 80 | ); 81 | 82 | // Update own alignment. 83 | setAttributes( { verticalAlignment } ); 84 | 85 | // Reset Parent Columns Block 86 | const rootClientId = getBlockRootClientId( clientId ); 87 | updateBlockAttributes( rootClientId, { 88 | verticalAlignment: null, 89 | } ); 90 | }, 91 | }; 92 | } ) 93 | )( ColumnEdit ); 94 | -------------------------------------------------------------------------------- /app/javascript/block_editor/blocks/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import classnames from 'classnames'; 7 | import { assign } from 'lodash'; 8 | 9 | /** 10 | * WordPress dependencies 11 | */ 12 | import '@wordpress/core-data'; 13 | import { registerCoreBlocks } from '@wordpress/block-library' 14 | import '@wordpress/block-editor'; 15 | import { 16 | registerBlockType, 17 | unregisterBlockType, 18 | registerBlockStyle, 19 | unregisterBlockStyle, 20 | unregisterBlockVariation 21 | } from '@wordpress/blocks'; 22 | import { createHigherOrderComponent } from '@wordpress/compose' 23 | import { addFilter } from '@wordpress/hooks'; 24 | 25 | /** 26 | * Internal dependencies 27 | */ 28 | import ColumnEdit from './column/edit'; 29 | import ButtonEdit from './button/edit'; 30 | import BlockEdit from './block/edit'; 31 | import MediaUpload from '../components/media-upload'; 32 | 33 | import * as accordion from './be-accordion'; 34 | import * as callout from './be-alert'; 35 | import * as card from './be-card'; 36 | import * as cover from './be-cover'; 37 | // import * as recentPosts from './recent-posts'; 38 | import * as contactForm from './be-contact-form'; 39 | 40 | export const registerBlocks = () => { 41 | // TODO: Remove this when upgrading to 10.5 -> https://github.com/WordPress/gutenberg/pull/30194 42 | const replaceButtonBlockEdit = ( settings, name ) => { 43 | if ( name !== 'core/button' ) { 44 | return settings; 45 | } 46 | 47 | return assign( {}, settings, { 48 | edit: ButtonEdit // Removes border radius panel 49 | }) 50 | } 51 | 52 | const replaceBlockEdit = ( settings, name ) => { 53 | if ( name !== 'core/block' ) { 54 | return settings; 55 | } 56 | 57 | return assign( {}, settings, { 58 | edit: BlockEdit // Removes 'convert to regular blocks' toolbar button 59 | }) 60 | } 61 | 62 | const replaceColumnBlockEdit = ( settings, name ) => { 63 | if ( name !== 'core/column' ) { 64 | return settings; 65 | } 66 | 67 | return assign( {}, settings, { 68 | edit: ColumnEdit // Removes column width options 69 | } ); 70 | } 71 | 72 | // Set default alignment to 'full' for all images 73 | const setDefaultAlignment = createHigherOrderComponent( ( BlockListBlock ) => { 74 | return ( props ) => { 75 | if ( props.name !== 'core/image' ) { 76 | return <BlockListBlock { ...props } />; 77 | } 78 | 79 | props.attributes.align = 'full'; 80 | 81 | return <BlockListBlock { ...props } />; 82 | }; 83 | }, 'setDefaultAlignment' ); 84 | 85 | const replaceMediaUpload = () => MediaUpload; 86 | 87 | addFilter( 88 | 'editor.BlockListBlock', 89 | 'block-editor/filters/core-image-block-list', 90 | setDefaultAlignment 91 | ); 92 | 93 | addFilter( 94 | 'blocks.registerBlockType', 95 | 'block-editor/filters/core-block', 96 | replaceBlockEdit 97 | ); 98 | 99 | addFilter( 100 | 'blocks.registerBlockType', 101 | 'block-editor/filters/core-button', 102 | replaceButtonBlockEdit 103 | ); 104 | 105 | addFilter( 106 | 'blocks.registerBlockType', 107 | 'block-editor/filters/core-column', 108 | replaceColumnBlockEdit 109 | ); 110 | 111 | addFilter( 112 | 'editor.MediaUpload', 113 | 'block-editor/filters/media-upload', 114 | replaceMediaUpload 115 | ); 116 | 117 | // Register WP blocks 118 | registerCoreBlocks(); 119 | 120 | // Unregister WP blocks which are not supported 121 | unregisterBlockType('core/gallery'); 122 | unregisterBlockType('core/quote'); 123 | unregisterBlockType('core/shortcode'); 124 | unregisterBlockType('core/archives'); 125 | unregisterBlockType('core/audio'); 126 | unregisterBlockType('core/calendar'); 127 | unregisterBlockType('core/categories'); 128 | unregisterBlockType('core/code'); 129 | unregisterBlockType('core/cover'); 130 | unregisterBlockType('core/embed'); 131 | unregisterBlockType('core/file'); 132 | unregisterBlockType('core/media-text'); 133 | unregisterBlockType('core/latest-comments'); 134 | unregisterBlockType('core/latest-posts'); 135 | unregisterBlockType('core/more'); 136 | unregisterBlockType('core/nextpage'); 137 | unregisterBlockType('core/preformatted'); 138 | unregisterBlockType('core/pullquote'); 139 | unregisterBlockType('core/rss'); 140 | unregisterBlockType('core/search'); 141 | unregisterBlockType('core/social-links'); 142 | unregisterBlockType('core/social-link'); 143 | unregisterBlockType('core/spacer'); 144 | unregisterBlockType('core/subhead'); 145 | unregisterBlockType('core/tag-cloud'); 146 | unregisterBlockType('core/text-columns'); 147 | unregisterBlockType('core/verse'); 148 | unregisterBlockType('core/video'); 149 | 150 | // Unregister WP block styles 151 | unregisterBlockStyle('core/separator', 'wide'); 152 | unregisterBlockStyle('core/button', 'fill'); 153 | unregisterBlockStyle('core/button', 'outline'); 154 | unregisterBlockStyle('core/image', 'default'); 155 | unregisterBlockStyle('core/image', 'rounded'); 156 | unregisterBlockStyle('core/table', 'regular'); 157 | unregisterBlockStyle('core/table', 'stripes'); 158 | 159 | // Unregister WP block variations 160 | unregisterBlockVariation('core/columns', 'two-columns-one-third-two-thirds'); 161 | unregisterBlockVariation('core/columns', 'two-columns-two-thirds-one-third'); 162 | unregisterBlockVariation('core/columns', 'three-columns-wider-center'); 163 | 164 | // Register custom blocks 165 | registerBlockType(accordion.name, accordion.settings); 166 | registerBlockType(callout.name, callout.settings); 167 | registerBlockType(card.name, card.settings); 168 | registerBlockType(contactForm.name, contactForm.settings); 169 | registerBlockType(cover.name, cover.settings); 170 | // registerBlockType(recentPosts.name, recentPosts.settings); 171 | 172 | // Register custom block styles 173 | registerBlockStyle( 'core/button', { 174 | name: 'primary', 175 | label: 'Primary', 176 | isDefault: true 177 | } ); 178 | registerBlockStyle( 'core/button', { 179 | name: 'secondary', 180 | label: 'Secondary' 181 | } ); 182 | registerBlockStyle( 'core/button', { 183 | name: 'outline-primary', 184 | label: 'Primary (Outlined)' 185 | } ); 186 | registerBlockStyle( 'core/button', { 187 | name: 'outline-secondary', 188 | label: 'Secondary (Outlined)' 189 | } ); 190 | registerBlockStyle( 'core/table', { 191 | name: 'unstriped', 192 | label: 'Unstriped', 193 | isDefault: true 194 | } ); 195 | registerBlockStyle( 'core/table', { 196 | name: 'striped', 197 | label: 'Striped' 198 | } ); 199 | registerBlockStyle( 'core/image', { 200 | name: 'default', 201 | label: 'Default', 202 | isDefault: true 203 | } ); 204 | registerBlockStyle( 'core/image', { 205 | name: 'padded', 206 | label: 'Padded' 207 | } ); 208 | registerBlockStyle( 'core/columns', { 209 | name: 'no-stack', 210 | label: 'No Stacking' 211 | } ); 212 | }; 213 | -------------------------------------------------------------------------------- /app/javascript/block_editor/components/block-editor/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | /** 4 | * WordPress dependencies 5 | */ 6 | import '@wordpress/editor'; // This shouldn't be necessary - currently required otherwise notices fails to initialized, think the data store is being registered 7 | import '@wordpress/format-library'; 8 | import { useSelect, useDispatch } from '@wordpress/data'; 9 | import { useEffect, useState, useMemo } from '@wordpress/element'; 10 | import { serialize, parse } from '@wordpress/blocks'; 11 | import { InterfaceSkeleton, FullscreenMode } from '@wordpress/interface'; 12 | import { useShortcut } from '@wordpress/keyboard-shortcuts'; 13 | import { 14 | BlockEditorKeyboardShortcuts, 15 | BlockEditorProvider, 16 | BlockList, 17 | BlockInspector, 18 | WritingFlow, 19 | ObserveTyping, 20 | BlockBreadcrumb, 21 | __experimentalLibrary as Library, 22 | } from '@wordpress/block-editor'; 23 | import { 24 | Popover, 25 | SlotFillProvider, 26 | DropZoneProvider, 27 | FocusReturnProvider, 28 | Button 29 | } from '@wordpress/components'; 30 | 31 | /** 32 | * Internal dependencies 33 | */ 34 | import Sidebar from '../sidebar'; 35 | import Header from '../header'; 36 | import Notices from '../notices'; 37 | import PopoverWrapper from './popover-wrapper'; 38 | import '../../stores'; // TODO: Think this store registering needs to be moved somewhere else so that it happens everytime a BlockEditor is initialized 39 | 40 | function BlockEditor( { input, settings: _settings } ) { 41 | const blocks = useSelect((select) => select("block-editor").getBlocks()); 42 | const { updateBlocks } = useDispatch("block-editor"); 43 | const __experimentalReusableBlocks = useSelect((select) => select( 'core' ).getEntityRecords('postType', 'wp_block', { per_page: -1 })); 44 | const settings = { ..._settings, __experimentalReusableBlocks }; 45 | 46 | function handleInput(newBlocks, persist) { 47 | updateBlocks(newBlocks); 48 | input.value = serialize(newBlocks); 49 | } 50 | 51 | function handleChange(newBlocks) { 52 | updateBlocks(newBlocks, true); 53 | input.value = serialize(newBlocks); 54 | } 55 | 56 | // Registering the shortcuts 57 | const { registerShortcut } = useDispatch( 'core/keyboard-shortcuts' ); 58 | useEffect( () => { 59 | registerShortcut( { 60 | name: 'core/editor/undo', 61 | category: 'global', 62 | description: 'Undo your last changes.', 63 | keyCombination: { 64 | modifier: 'primary', 65 | character: 'z', 66 | }, 67 | } ); 68 | 69 | registerShortcut( { 70 | name: 'core/editor/redo', 71 | category: 'global', 72 | description: 'Redo your last undo.', 73 | keyCombination: { 74 | modifier: 'primaryShift', 75 | character: 'z', 76 | }, 77 | } ); 78 | }) 79 | 80 | const { redo, undo } = useDispatch( 'block-editor' ); 81 | 82 | useShortcut( 83 | 'core/editor/undo', 84 | ( event ) => { 85 | undo(); 86 | event.preventDefault(); 87 | }, 88 | { bindGlobal: true } 89 | ); 90 | 91 | useShortcut( 92 | 'core/editor/redo', 93 | ( event ) => { 94 | redo(); 95 | event.preventDefault(); 96 | }, 97 | { bindGlobal: true } 98 | ); 99 | 100 | const { setIsInserterOpened } = useDispatch( 'block-editor' ); 101 | const isInserterOpened = useSelect((select) => select("block-editor").isInserterOpened()); 102 | 103 | return ( 104 | <> 105 | <FullscreenMode isActive={false} /> 106 | <SlotFillProvider> 107 | <DropZoneProvider> 108 | <BlockEditorProvider 109 | value={ blocks } 110 | onInput={ handleInput } 111 | onChange={ handleChange } 112 | settings={ settings } 113 | > 114 | <FocusReturnProvider> 115 | <InterfaceSkeleton 116 | header={<Header />} 117 | footer={<BlockBreadcrumb />} 118 | sidebar={<Sidebar />} 119 | leftSidebar={ 120 | isInserterOpened && ( 121 | <PopoverWrapper 122 | onClose={ () => setIsInserterOpened( false ) } 123 | > 124 | <div> 125 | <div> 126 | <Library 127 | showInserterHelpPanel={true} 128 | /> 129 | </div> 130 | </div> 131 | </PopoverWrapper> 132 | ) 133 | } 134 | content={ 135 | <> 136 | <Notices /> 137 | <Sidebar.InspectorFill> 138 | <BlockInspector /> 139 | </Sidebar.InspectorFill> 140 | <BlockEditorKeyboardShortcuts.Register /> 141 | <BlockEditorKeyboardShortcuts /> 142 | <WritingFlow> 143 | <ObserveTyping> 144 | <BlockList className="editor-styles-wrapper" /> 145 | </ObserveTyping> 146 | </WritingFlow> 147 | </> 148 | } 149 | /> 150 | <Popover.Slot /> 151 | </FocusReturnProvider> 152 | </BlockEditorProvider> 153 | </DropZoneProvider> 154 | </SlotFillProvider> 155 | </> 156 | ); 157 | } 158 | 159 | export default BlockEditor; 160 | -------------------------------------------------------------------------------- /app/javascript/block_editor/components/block-editor/popover-wrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | /** 5 | * WordPress dependencies 6 | */ 7 | import { 8 | withConstrainedTabbing, 9 | withFocusReturn, 10 | withFocusOutside, 11 | } from '@wordpress/components'; 12 | import { Component } from '@wordpress/element'; 13 | import { ESCAPE } from '@wordpress/keycodes'; 14 | 15 | function stopPropagation( event ) { 16 | event.stopPropagation(); 17 | } 18 | 19 | const DetectOutside = withFocusOutside( 20 | class extends Component { 21 | handleFocusOutside( event ) { 22 | this.props.onFocusOutside( event ); 23 | } 24 | 25 | render() { 26 | return this.props.children; 27 | } 28 | } 29 | ); 30 | 31 | const FocusManaged = withConstrainedTabbing( 32 | withFocusReturn( ( { children } ) => children ) 33 | ); 34 | 35 | export default function PopoverWrapper( { onClose, children, className } ) { 36 | // Event handlers 37 | const maybeClose = ( event ) => { 38 | // Close on escape 39 | if ( event.keyCode === ESCAPE && onClose ) { 40 | event.stopPropagation(); 41 | onClose(); 42 | } 43 | }; 44 | 45 | // Disable reason: this stops certain events from propagating outside of the component. 46 | // - onMouseDown is disabled as this can cause interactions with other DOM elements 47 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 48 | return ( 49 | <div 50 | className={ className } 51 | onKeyDown={ maybeClose } 52 | onMouseDown={ stopPropagation } 53 | > 54 | <DetectOutside onFocusOutside={ onClose }> 55 | <FocusManaged>{ children }</FocusManaged> 56 | </DetectOutside> 57 | </div> 58 | ); 59 | /* eslint-enable jsx-a11y/no-static-element-interactions */ 60 | } 61 | -------------------------------------------------------------------------------- /app/javascript/block_editor/components/block-editor/styles.scss: -------------------------------------------------------------------------------- 1 | .block-editor__block-list { 2 | padding-bottom: $grid-unit-30; 3 | padding-top: $grid-unit-30 + 5; 4 | margin-left: auto; 5 | margin-right: auto; 6 | } 7 | 8 | .block-editor__inner-wrapper { 9 | -webkit-overflow-scrolling: touch; 10 | background: $white; 11 | height: calc(100% - #{$header-height}); 12 | left: 0; 13 | overflow: auto; 14 | position: absolute; 15 | top: $header-height; 16 | width: 100%; 17 | 18 | @include break-small { 19 | width: calc(100% - #{$sidebar-width}); 20 | } 21 | } 22 | 23 | // Grab the default Editor styles for visual consistency with WP 24 | .editor-styles-wrapper { 25 | // Important all other selectors scoped underneath 26 | // `div.editor-styles-wrapper` 27 | @import "~@wordpress/editor/src/editor-styles.scss"; 28 | } 29 | -------------------------------------------------------------------------------- /app/javascript/block_editor/components/header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | /** 5 | * WordPress dependencies 6 | */ 7 | import { NavigableToolbar } from '@wordpress/block-editor'; 8 | import { Button } from '@wordpress/components'; 9 | import { resizeCornerNE as minimizeIcon } from '@wordpress/icons'; 10 | import { close as closeIcon } from '@wordpress/icons'; 11 | import { plus as openIcon } from '@wordpress/icons'; 12 | import { fullscreen as maximizeIcon } from '@wordpress/icons'; 13 | import { useSelect, useDispatch } from '@wordpress/data'; 14 | 15 | import HistoryUndo from './undo'; 16 | import HistoryRedo from './redo'; 17 | 18 | export default function Header() { 19 | const { setIsInserterOpened } = useDispatch( 'block-editor' ); 20 | const isInserterOpened = useSelect((select) => select("block-editor").isInserterOpened()); 21 | 22 | return ( 23 | <div 24 | className="block-editor__header" 25 | role="region" 26 | tabIndex="-1" 27 | > 28 | <div> 29 | { !isInserterOpened && 30 | <Button isPrimary 31 | icon={ openIcon } 32 | label={ 'Open Library' } 33 | onClick={ () => 34 | setIsInserterOpened( true ) 35 | } 36 | /> 37 | } 38 | { isInserterOpened && 39 | <Button isSecondary 40 | icon={ closeIcon } 41 | label={ 'Close Library' } 42 | onClick={ () => 43 | setIsInserterOpened( false ) 44 | } 45 | /> 46 | } 47 | <HistoryUndo /> 48 | <HistoryRedo /> 49 | <Button 50 | icon={ maximizeIcon } 51 | label={ 'Fullscreen' } 52 | // shortcut={ displayShortcut.primary( 'x' ) } 53 | className="block-editor__size-toggle-button block-editor__size-toggle-button__maximize" 54 | onClick={() => document.querySelector('.block-editor').classList.add('block-editor__fullscreen')} 55 | /> 56 | <Button 57 | icon={ minimizeIcon } 58 | label={ 'Minimize' } 59 | // shortcut={ displayShortcut.primary( 'x' ) } 60 | className="block-editor__size-toggle-button block-editor__size-toggle-button__minimize" 61 | onClick={() => document.querySelector('.block-editor').classList.remove('block-editor__fullscreen')} 62 | /> 63 | 64 | </div> 65 | </div> 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /app/javascript/block_editor/components/header/redo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { __ } from '@wordpress/i18n'; 4 | import { Button } from '@wordpress/components'; 5 | import { withSelect, withDispatch } from '@wordpress/data'; 6 | import { compose } from '@wordpress/compose'; 7 | import { displayShortcut } from '@wordpress/keycodes'; 8 | import { redo as redoIcon } from '@wordpress/icons'; 9 | 10 | function HistoryRedo( { hasRedo, redo, ...props } ) { 11 | return ( 12 | <Button 13 | { ...props } 14 | icon={ redoIcon } 15 | label={ __( 'Redo' ) } 16 | shortcut={ displayShortcut.primaryShift( 'z' ) } 17 | // If there are no redo levels we don't want to actually disable this 18 | // button, because it will remove focus for keyboard users. 19 | // See: https://github.com/WordPress/gutenberg/issues/3486 20 | aria-disabled={ ! hasRedo } 21 | onClick={ hasRedo ? redo : undefined } 22 | className="editor-history__redo" 23 | /> 24 | ); 25 | } 26 | 27 | const EnhancedHistoryRedo = compose( [ 28 | withSelect( ( select ) => ( { 29 | hasRedo: select( 'block-editor' ).hasRedo(), 30 | } ) ), 31 | withDispatch( ( dispatch ) => ( { 32 | redo: dispatch( 'block-editor' ).redo, 33 | } ) ), 34 | ] )( HistoryRedo ); 35 | 36 | export default EnhancedHistoryRedo; 37 | -------------------------------------------------------------------------------- /app/javascript/block_editor/components/header/styles.scss: -------------------------------------------------------------------------------- 1 | .block-editor__header { 2 | padding: 0 .5rem; 3 | align-items: center; 4 | background: $white; 5 | border-bottom: 1px solid $gray-200; 6 | display: flex; 7 | height: $header-height; 8 | justify-content: space-between; 9 | left: 0; 10 | // Stick the toolbar to the top, because the admin bar is not fixed on mobile. 11 | position: sticky; 12 | right: 0; 13 | top: 0; 14 | z-index: 1; 15 | } 16 | -------------------------------------------------------------------------------- /app/javascript/block_editor/components/header/undo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { __ } from '@wordpress/i18n'; 4 | import { Button } from '@wordpress/components'; 5 | import { withSelect, withDispatch } from '@wordpress/data'; 6 | import { compose } from '@wordpress/compose'; 7 | import { displayShortcut } from '@wordpress/keycodes'; 8 | import { undo as undoIcon } from '@wordpress/icons'; 9 | 10 | function HistoryUndo( { hasUndo, undo, ...props } ) { 11 | return ( 12 | <Button 13 | { ...props } 14 | icon={ undoIcon } 15 | label={ __( 'Undo' ) } 16 | shortcut={ displayShortcut.primary( 'z' ) } 17 | // If there are no undo levels we don't want to actually disable this 18 | // button, because it will remove focus for keyboard users. 19 | // See: https://github.com/WordPress/gutenberg/issues/3486 20 | aria-disabled={ ! hasUndo } 21 | onClick={ hasUndo ? undo : undefined } 22 | className="editor-history__undo" 23 | /> 24 | ); 25 | } 26 | 27 | const EnhancedHistoryUndo = compose( [ 28 | withSelect( ( select ) => ( { 29 | hasUndo: select( 'block-editor' ).hasUndo(), 30 | } ) ), 31 | withDispatch( ( dispatch ) => ( { 32 | undo: dispatch( 'block-editor' ).undo, 33 | } ) ), 34 | ] )( HistoryUndo ); 35 | 36 | export default EnhancedHistoryUndo; 37 | -------------------------------------------------------------------------------- /app/javascript/block_editor/components/media-upload/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | /** 5 | * WordPress dependencies 6 | */ 7 | import { Component } from '@wordpress/element'; 8 | 9 | class MediaUpload extends Component { 10 | constructor( { 11 | allowedTypes, 12 | gallery = false, 13 | unstableFeaturedImageFlow = false, 14 | modalClass, 15 | multiple = false, 16 | title = 'Select or Upload Media', 17 | } ) { 18 | super( ...arguments ); 19 | this.openUploader = this.openUploader.bind( this ); 20 | } 21 | 22 | openUploader() { 23 | if (window.BlockEditorMediaUploader == undefined) { 24 | console.warn('window.BlockEditorMediaUploader undefined. Pulling in random image from Unsplash') 25 | 26 | this.props.onSelect( { url: 'https://source.unsplash.com/random/800x500' }); 27 | } else { 28 | window.BlockEditorMediaUploader.open(this.props.onSelect); 29 | } 30 | } 31 | 32 | render() { 33 | return this.props.render( { open: this.openUploader } ); 34 | } 35 | } 36 | 37 | export default MediaUpload; 38 | -------------------------------------------------------------------------------- /app/javascript/block_editor/components/notices/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | /** 5 | * WordPress dependencies 6 | */ 7 | import { useSelect, useDispatch } from '@wordpress/data'; 8 | import { SnackbarList } from '@wordpress/components'; 9 | 10 | export default function Notices() { 11 | const notices = useSelect( 12 | ( select ) => 13 | select( 'core/notices' ) 14 | .getNotices() 15 | .filter( ( notice ) => notice.type === 'snackbar' ), 16 | [] 17 | ); 18 | const { removeNotice } = useDispatch( 'core/notices' ); 19 | return ( 20 | <SnackbarList 21 | className="edit-site-notices" 22 | notices={ notices } 23 | onRemove={ removeNotice } 24 | /> 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/javascript/block_editor/components/notices/styles.scss: -------------------------------------------------------------------------------- 1 | .edit-site-notices { 2 | bottom: 20px; 3 | padding-left: 16px; 4 | padding-right: 16px; 5 | position: fixed; 6 | right: 0; 7 | } 8 | 9 | @include editor-left(".edit-site-notices"); -------------------------------------------------------------------------------- /app/javascript/block_editor/components/sidebar/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | /** 5 | * WordPress dependencies 6 | */ 7 | import { createSlotFill, Panel } from '@wordpress/components'; 8 | import { __ } from '@wordpress/i18n'; 9 | 10 | const { Slot: InspectorSlot, Fill: InspectorFill } = createSlotFill( 11 | 'StandAloneBlockEditorSidebarInspector' 12 | ); 13 | 14 | function Sidebar() { 15 | return ( 16 | <div 17 | className="block-editor__sidebar" 18 | role="region" 19 | aria-label={ __( 'Standalone Block Editor advanced settings.' ) } 20 | tabIndex="-1" 21 | > 22 | <InspectorSlot bubblesVirtually /> 23 | </div> 24 | ); 25 | } 26 | 27 | Sidebar.InspectorFill = InspectorFill; 28 | 29 | export default Sidebar; 30 | -------------------------------------------------------------------------------- /app/javascript/block_editor/components/sidebar/styles.scss: -------------------------------------------------------------------------------- 1 | .block-editor__sidebar { 2 | background: $white; 3 | border-left: $border-width solid $gray-200; 4 | bottom: 0; 5 | height: 100vh; 6 | overflow: hidden; 7 | position: absolute; 8 | right: 0; 9 | top: 0; 10 | width: $sidebar-width; 11 | margin-bottom: 33px; 12 | //z-index: z-index(".edit-site-sidebar"); 13 | 14 | @include break-small() { 15 | -webkit-overflow-scrolling: touch; 16 | height: auto; 17 | overflow: auto; 18 | // top: $admin-bar-height-big + $header-height; 19 | } 20 | 21 | // @include break-medium() { 22 | // top: $admin-bar-height + $header-height; 23 | // } 24 | 25 | @include break-small() { 26 | display: block; 27 | } 28 | 29 | > .components-panel { 30 | border-left: 0; 31 | border-right: 0; 32 | margin-bottom: -1px; 33 | margin-top: -1px; 34 | 35 | > .components-panel__header { 36 | background: $gray-200; 37 | } 38 | } 39 | 40 | .block-editor-block-inspector__card { 41 | margin: 0; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/javascript/block_editor/stores/action-types.js: -------------------------------------------------------------------------------- 1 | export const UPDATE_BLOCKS = "UPDATE_BLOCKS"; 2 | export const PERSIST_BLOCKS = "PERSIST_BLOCKS"; 3 | export const FETCH_BLOCKS_FROM_STORAGE = "FETCH_BLOCKS_FROM_STORAGE"; 4 | export const PERSIST_BLOCKS_TO_STORAGE = "PERSIST_BLOCKS_TO_STORAGE"; 5 | -------------------------------------------------------------------------------- /app/javascript/block_editor/stores/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | UPDATE_BLOCKS, 3 | PERSIST_BLOCKS, 4 | FETCH_BLOCKS_FROM_STORAGE, 5 | PERSIST_BLOCKS_TO_STORAGE, 6 | } from "./action-types"; 7 | import { ActionCreators as ReduxUndo } from "redux-undo"; 8 | 9 | /** 10 | * Returns an action object used to open/close the inserter. 11 | * 12 | * @param {boolean} value A boolean representing whether the inserter should be opened or closed. 13 | * @return {Object} Action object. 14 | */ 15 | export function setIsInserterOpened( value ) { 16 | return { 17 | type: 'SET_IS_INSERTER_OPENED', 18 | value, 19 | }; 20 | } 21 | 22 | export function undo() { 23 | return ReduxUndo.undo(); 24 | } 25 | 26 | export function redo() { 27 | return ReduxUndo.redo(); 28 | } 29 | 30 | export function *updateBlocks( blocks, persist = false ) { 31 | 32 | if( persist ) { 33 | yield persistBlocksToStorage(blocks); 34 | } 35 | 36 | return { 37 | type: persist ? PERSIST_BLOCKS : UPDATE_BLOCKS, 38 | blocks, 39 | }; 40 | } 41 | 42 | export function fetchBlocksFromStorage() { 43 | return { 44 | type: FETCH_BLOCKS_FROM_STORAGE, 45 | }; 46 | }; 47 | 48 | export function persistBlocksToStorage(blocks) { 49 | return { 50 | type: PERSIST_BLOCKS_TO_STORAGE, 51 | blocks, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /app/javascript/block_editor/stores/controls.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_BLOCKS_FROM_STORAGE, 3 | PERSIST_BLOCKS_TO_STORAGE, 4 | } from "./action-types"; 5 | import { serialize } from "@wordpress/blocks"; 6 | 7 | export default { 8 | [PERSIST_BLOCKS_TO_STORAGE](action) { 9 | return new Promise((resolve, reject) => { 10 | window.localStorage.setItem("blockEditorBlocks", serialize(action.blocks)); 11 | resolve(action.blocks); 12 | }); 13 | }, 14 | [FETCH_BLOCKS_FROM_STORAGE]() { 15 | return new Promise((resolve, reject) => { 16 | const storedBlocks = 17 | window.localStorage.getItem("blockEditorBlocks") || []; 18 | resolve(storedBlocks); 19 | }); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /app/javascript/block_editor/stores/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { registerStore } from '@wordpress/data'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import reducer from './reducer'; 10 | import * as selectors from './selectors'; 11 | import * as actions from './actions'; 12 | import * as resolvers from "./resolvers"; 13 | import controls from './controls'; 14 | 15 | /** 16 | * Module Constants 17 | */ 18 | const MODULE_KEY = 'block-editor'; 19 | 20 | const store = registerStore(MODULE_KEY, { 21 | reducer, 22 | selectors, 23 | actions, 24 | controls, 25 | resolvers, 26 | }); 27 | 28 | window.blockEditorStore = store; 29 | 30 | export default store; 31 | -------------------------------------------------------------------------------- /app/javascript/block_editor/stores/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from '@wordpress/data'; 2 | 3 | import undoable, { groupByActionTypes, includeAction } from "redux-undo"; 4 | import { UPDATE_BLOCKS, PERSIST_BLOCKS } from "./action-types"; 5 | 6 | function blocksReducer(state = [], action) { 7 | switch (action.type) { 8 | case UPDATE_BLOCKS: 9 | case PERSIST_BLOCKS: 10 | const { blocks } = action; 11 | 12 | return { 13 | blocks, 14 | }; 15 | } 16 | 17 | return state; 18 | } 19 | 20 | 21 | /** 22 | * Reducer tracking whether the inserter is open. 23 | * 24 | * @param {boolean} state 25 | * @param {Object} action 26 | */ 27 | function isInserterOpened( state = false, action ) { 28 | switch ( action.type ) { 29 | case 'SET_IS_INSERTER_OPENED': 30 | return action.value; 31 | } 32 | return state; 33 | } 34 | 35 | export default combineReducers( { 36 | isInserterOpened: isInserterOpened, 37 | history: undoable(blocksReducer, { 38 | filter: includeAction('PERSIST_BLOCKS') 39 | }) 40 | } ); 41 | -------------------------------------------------------------------------------- /app/javascript/block_editor/stores/resolvers.js: -------------------------------------------------------------------------------- 1 | import { parse } from "@wordpress/blocks"; 2 | import { fetchBlocksFromStorage, updateBlocks } from "./actions"; 3 | 4 | export function *getBlocks() { 5 | const rawBlocks = yield fetchBlocksFromStorage(); 6 | const persist = false; 7 | const blocks = parse(rawBlocks); 8 | yield updateBlocks(blocks, persist); 9 | return blocks; 10 | } 11 | -------------------------------------------------------------------------------- /app/javascript/block_editor/stores/selectors.js: -------------------------------------------------------------------------------- 1 | import { createRegistrySelector } from '@wordpress/data'; 2 | 3 | export const getBlocks = ( state ) => { 4 | return state.history.present.blocks || []; 5 | } 6 | 7 | /** 8 | * Returns true if the inserter is opened. 9 | * 10 | * @param {Object} state Global application state. 11 | * 12 | * @return {boolean} Whether the inserter is opened. 13 | */ 14 | export function isInserterOpened( state ) { 15 | return state.isInserterOpened; 16 | } 17 | 18 | export const hasUndo = (state) => { 19 | return state.history.past?.length; 20 | }; 21 | 22 | export const hasRedo = (state) => { 23 | return state.history.future?.length; 24 | }; 25 | -------------------------------------------------------------------------------- /app/javascript/controllers/block_editor_controller.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import PropTypes from 'prop-types' 4 | 5 | import { Controller } from "stimulus" 6 | 7 | import { render } from '@wordpress/element' 8 | 9 | import BlockEditor from '../block_editor/components/block-editor' 10 | import { registerBlocks } from '../block_editor/blocks' 11 | 12 | registerBlocks() 13 | 14 | export default class extends Controller { 15 | static targets = [ "output", "input" ] 16 | 17 | connect() { 18 | const settings = { 19 | alignWide: true, 20 | __experimentalFeatures: { 21 | global: { 22 | color: { 23 | custom: false, 24 | palette: [], 25 | gradients: [] 26 | }, 27 | typography: { 28 | dropCap: false, 29 | fontSizes: false, 30 | customLineHeight: false 31 | } 32 | } 33 | }, 34 | mediaUpload: function uploadMedia( { 35 | allowedTypes, 36 | additionalData = {}, 37 | filesList, 38 | maxUploadFileSize, 39 | onError = noop, 40 | onFileChange, 41 | wpAllowedMimeTypes = null, 42 | } ) { 43 | } 44 | } 45 | 46 | window.localStorage.setItem("blockEditorBlocks", this.inputTarget.value); 47 | this.editor = render( <BlockEditor input={ this.inputTarget } settings={ settings } />, this.outputTarget ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | window.wp = null 2 | 3 | import { Application } from "stimulus" 4 | import { definitionsFromContext } from "stimulus/webpack-helpers" 5 | 6 | const application = Application.start() 7 | const context = require.context("controllers", true, /_controller\.jsx$/) 8 | application.load(definitionsFromContext(context)) 9 | -------------------------------------------------------------------------------- /app/javascript/packs/block_editor/application.js: -------------------------------------------------------------------------------- 1 | import "controllers" 2 | import './application.scss' 3 | -------------------------------------------------------------------------------- /app/javascript/packs/block_editor/application.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Noto+Serif:wght@400;700&display=swap'); 2 | 3 | @import "~@wordpress/base-styles/colors"; 4 | @import "~@wordpress/base-styles/variables"; 5 | @import "~@wordpress/base-styles/mixins"; 6 | @import "~@wordpress/base-styles/breakpoints"; 7 | @import "~@wordpress/base-styles/animations"; 8 | @import "~@wordpress/base-styles/z-index"; 9 | 10 | @import "~@wordpress/components/src/style"; 11 | @import "~@wordpress/interface/src/style"; 12 | @import "~@wordpress/block-editor/src/style"; 13 | @import "~@wordpress/block-library/src/style"; 14 | 15 | $header-height: 50px; 16 | 17 | // Internal 18 | @import "../../block_editor/components/sidebar/styles"; 19 | @import "../../block_editor/components/header/styles"; 20 | @import "../../block_editor/components/block-editor/styles"; 21 | 22 | .block-editor-block-list__layout.is-root-container { 23 | padding-top: 1rem; 24 | padding-bottom: 1rem; 25 | } 26 | 27 | .block-editor { 28 | @include reset; 29 | position: relative; 30 | margin: 1rem 0; 31 | padding: 0; 32 | font-family: $editor-font; 33 | font-size: $editor-font-size; 34 | line-height: $editor-line-height; 35 | border: 1px solid #f3f4f5; 36 | height: 500px; 37 | overflow-y: hidden; 38 | 39 | a, 40 | div { 41 | outline: 0; 42 | } 43 | 44 | label { 45 | font-size: $default-font-size; 46 | } 47 | 48 | .editor-styles-wrapper { 49 | font-family: $default-font; 50 | font-size: $default-font-size; 51 | } 52 | 53 | [type='text'], [type='password'], [type='date'], [type='datetime'], [type='datetime-local'], [type='month'], [type='week'], [type='email'], [type='number'], [type='search'], [type='tel'], [type='time'], [type='url'], [type='color'], textarea { 54 | margin-bottom: 0; 55 | } 56 | 57 | @include admin-scheme(#1b8ecf); 58 | 59 | .block-editor-inserter__main-area { 60 | padding-bottom: 2rem; 61 | } 62 | 63 | .block-editor-url-popover__additional-controls, 64 | .block-editor-media-replace-flow__options .components-form-file-upload { 65 | display: none; 66 | } 67 | 68 | .components-dropdown-menu__toggle[aria-label="Change alignment"] { 69 | display: none !important; 70 | } 71 | 72 | .components-toolbar-button[aria-label="Crop"] { 73 | display: none !important; 74 | } 75 | 76 | .block-editor-block-mover__move-button-container { 77 | display: inline-flex; 78 | } 79 | 80 | .block-editor-inserter__manage-reusable-blocks { 81 | display: none; 82 | } 83 | 84 | &:not(.block-editor__fullscreen) { 85 | .block-editor__size-toggle-button__minimize { 86 | display: none; 87 | } 88 | } 89 | 90 | &.block-editor__fullscreen { 91 | .block-editor__size-toggle-button__maximize { 92 | display: none; 93 | } 94 | } 95 | 96 | &__size-toggle-button { 97 | position: absolute; 98 | right: 0; 99 | top: 7px; 100 | z-index: 1; 101 | } 102 | 103 | &__fullscreen { 104 | position: absolute; 105 | top: 0px; 106 | right: 0px; 107 | bottom: 0px; 108 | left: 0px; 109 | padding: 0; 110 | z-index: 99; 111 | margin: 0; 112 | height: unset; 113 | .block-editor { 114 | height: 100%; 115 | } 116 | } 117 | } 118 | 119 | // In order to use mix-blend-mode, this element needs to have an explicitly set background-color. 120 | // We scope it to .wp-toolbar to be wp-admin only, to prevent bleed into other implementations. 121 | html.wp-toolbar { 122 | background: $white; 123 | } 124 | 125 | // The modals are shown outside the .block-editor wrapper, they need these styles. 126 | .block-editor, .components-modal__frame { 127 | @include reset; 128 | } 129 | 130 | .interface-interface { 131 | &-skeleton { 132 | position: absolute; 133 | top: 0; 134 | left: 0; 135 | background-color: white; 136 | &__footer { 137 | position: inherit; 138 | .block-editor-block-breadcrumb { 139 | position: absolute; 140 | bottom: 0; 141 | z-index: 1; 142 | background-color: $white; 143 | width: 100%; 144 | border-top: 1px solid #f3f4f5; 145 | padding: 1rem 0; 146 | } 147 | } 148 | &__sidebar { 149 | width: 280px; 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /app/jobs/block_editor/application_job.rb: -------------------------------------------------------------------------------- 1 | module BlockEditor 2 | class ApplicationJob < ActiveJob::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/block_editor/application_mailer.rb: -------------------------------------------------------------------------------- 1 | module BlockEditor 2 | class ApplicationMailer < ActionMailer::Base 3 | default from: 'from@example.com' 4 | layout 'mailer' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/models/block_editor/application_record.rb: -------------------------------------------------------------------------------- 1 | module BlockEditor 2 | class ApplicationRecord < ActiveRecord::Base 3 | self.abstract_class = true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/block_editor/block_list.rb: -------------------------------------------------------------------------------- 1 | module BlockEditor 2 | # Represents a block list 3 | class BlockList < ApplicationRecord 4 | # Associations 5 | belongs_to :listable, polymorphic: true, touch: true, optional: true 6 | has_many :inverse_block_list_connections, class_name: "BlockListConnection", foreign_key: "child_id" 7 | has_many :block_list_connections, foreign_key: 'parent_id' 8 | has_many :parents, :through => :inverse_block_list_connections 9 | has_many :reusable_blocks, through: :block_list_connections, source: 'child' 10 | 11 | # Callbacks 12 | after_save_commit :touch_parents 13 | after_save_commit :set_reusable_blocks 14 | before_validation :set_defaults 15 | 16 | # Validations 17 | validates :name, presence: true 18 | 19 | # Scopes 20 | scope :reusable, -> { where(listable_id: nil, listable_type: nil) } 21 | scope :search, ->(query) { where('lower(name) LIKE ?', "%#{query.downcase}%") } 22 | 23 | def reusable? 24 | listable_type.nil? && listable_id.nil? 25 | end 26 | 27 | private 28 | 29 | def touch_parents 30 | parents.touch_all if reusable? 31 | end 32 | 33 | def set_reusable_blocks 34 | # Find all instances of a reusable block 35 | html = Nokogiri::HTML(content) 36 | ids = html.xpath('//comment()').select {|comment| comment.inner_text.starts_with?(" wp:#{BlockEditor::Blocks::Reusable.name}") }.map do |block_instance| 37 | block_attributes = block_instance.inner_text.split(" wp:#{BlockEditor::Blocks::Reusable.name}")[1][0...-1] 38 | block_attributes = block_attributes.blank? ? {} : JSON.parse(block_attributes) 39 | block_attributes['ref'] 40 | end 41 | 42 | self.reusable_block_ids = ids || [] 43 | end 44 | 45 | def set_defaults 46 | if self.name.blank? 47 | self.name = Time.now 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/models/block_editor/block_list_connection.rb: -------------------------------------------------------------------------------- 1 | module BlockEditor 2 | class BlockListConnection < ApplicationRecord 3 | belongs_to :parent, class_name: 'BlockEditor::BlockList' 4 | belongs_to :child, class_name: 'BlockEditor::BlockList' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/models/concerns/block_editor/listable.rb: -------------------------------------------------------------------------------- 1 | module BlockEditor 2 | module Listable 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | has_many :block_lists, as: :listable, class_name: 'BlockEditor::BlockList' 7 | has_one :active_block_list, -> { where active: true }, class_name: 'BlockEditor::BlockList', as: :listable 8 | 9 | validates :active_block_list, presence: true 10 | 11 | accepts_nested_attributes_for :active_block_list 12 | 13 | after_initialize :set_block_list_defaults 14 | end 15 | 16 | private 17 | 18 | def set_block_list_defaults 19 | return if self.persisted? 20 | 21 | self.active_block_list ||= self.build_active_block_list(listable: self) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/views/block_editor/blocks/be/contact-form/_block.html: -------------------------------------------------------------------------------- 1 | <div class="wp-block-be-contact-form"> 2 | <!-- Contact Form Placeholder --> 3 | </div> 4 | -------------------------------------------------------------------------------- /app/views/layouts/block_editor/application.html.erb: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <title>Block editor</title> 5 | <%= csrf_meta_tags %> 6 | <%= csp_meta_tag %> 7 | 8 | <%= stylesheet_link_tag "block_editor/application", media: "all" %> 9 | </head> 10 | <body> 11 | 12 | <%= yield %> 13 | 14 | </body> 15 | </html> 16 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path('..', __dir__) 6 | ENGINE_PATH = File.expand_path('../lib/block_editor/engine', __dir__) 7 | APP_PATH = File.expand_path('../test/dummy/config/application', __dir__) 8 | 9 | # Set up gems listed in the Gemfile. 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 11 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) 12 | 13 | require "rails/all" 14 | require "rails/engine/commands" 15 | -------------------------------------------------------------------------------- /bin/webpack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/webpack_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::WebpackRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /bin/webpack-dev-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/dev_server_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::DevServerRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /block_editor.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/block_editor/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "block_editor" 5 | spec.version = BlockEditor::VERSION 6 | spec.authors = ["Patrick Lindsay"] 7 | spec.email = ["patrick@yamasolutions.com"] 8 | spec.homepage = "https://github.com/yamasolutions/block-editor" 9 | spec.summary = "Ruby on Rails Block Editor" 10 | spec.description = "A block editor for Ruby on Rails built from the Wordpress Gutenberg project" 11 | spec.license = "MIT" 12 | 13 | spec.metadata["homepage_uri"] = spec.homepage 14 | spec.metadata["source_code_uri"] = "https://github.com/yamasolutions/block-editor" 15 | spec.metadata["changelog_uri"] = "https://github.com/yamasolutions/block-editor/blob/master/CHANGELOG.md" 16 | 17 | spec.files = Dir["{app,bin,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md", "package.json", "yarn.lock", "postcss.config.js"] 18 | 19 | spec.add_dependency "rails", "~> 6.0" 20 | spec.add_dependency 'webpacker', '~> 5.1' 21 | end 22 | -------------------------------------------------------------------------------- /config/initializers/webpacker_extension.rb: -------------------------------------------------------------------------------- 1 | module Webpacker::DynamicTag 2 | def javascript_pack_tag(*names, **options) 3 | return super unless options[:webpacker] 4 | new_helper = self.dup 5 | new_helper.define_singleton_method(:current_webpacker_instance) do 6 | options[:webpacker].constantize.webpacker 7 | end 8 | new_helper.javascript_pack_tag(*names, **options.except(:webpacker)) 9 | end 10 | 11 | def stylesheet_pack_tag(*names, **options) 12 | return super unless options[:webpacker] 13 | new_helper = self.dup 14 | new_helper.define_singleton_method(:current_webpacker_instance) do 15 | options[:webpacker].constantize.webpacker 16 | end 17 | new_helper.stylesheet_pack_tag(*names, **options.except(:webpacker)) 18 | end 19 | end 20 | 21 | Webpacker::Helper.prepend Webpacker::DynamicTag 22 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | BlockEditor::Engine.routes.draw do 2 | end 3 | -------------------------------------------------------------------------------- /config/webpack/development.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /config/webpack/environment.js: -------------------------------------------------------------------------------- 1 | const { environment } = require('@rails/webpacker') 2 | 3 | module.exports = environment 4 | -------------------------------------------------------------------------------- /config/webpack/production.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /config/webpack/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /config/webpacker.yml: -------------------------------------------------------------------------------- 1 | # Note: You must restart bin/webpack-dev-server for changes to take effect 2 | 3 | default: &default 4 | source_path: app/javascript 5 | source_entry_path: packs 6 | public_root_path: public 7 | public_output_path: packs 8 | cache_path: tmp/cache/webpacker 9 | webpack_compile_output: true 10 | 11 | # Additional paths webpack should lookup modules 12 | # ['app/assets', 'engine/foo/app/assets'] 13 | additional_paths: [] 14 | 15 | # Reload manifest.json on all requests so we reload latest compiled packs 16 | cache_manifest: false 17 | 18 | # Extract and emit a css file 19 | extract_css: false 20 | 21 | static_assets_extensions: 22 | - .jpg 23 | - .jpeg 24 | - .png 25 | - .gif 26 | - .tiff 27 | - .ico 28 | - .svg 29 | - .eot 30 | - .otf 31 | - .ttf 32 | - .woff 33 | - .woff2 34 | 35 | extensions: 36 | - .mjs 37 | - .js 38 | - .sass 39 | - .scss 40 | - .css 41 | - .module.sass 42 | - .module.scss 43 | - .module.css 44 | - .png 45 | - .svg 46 | - .gif 47 | - .jpeg 48 | - .jpg 49 | 50 | development: 51 | <<: *default 52 | compile: true 53 | 54 | # Reference: https://webpack.js.org/configuration/dev-server/ 55 | dev_server: 56 | https: false 57 | host: localhost 58 | port: 3035 59 | public: localhost:3035 60 | hmr: false 61 | # Inline should be set to true if using HMR 62 | inline: true 63 | overlay: true 64 | compress: true 65 | disable_host_check: true 66 | use_local_ip: false 67 | quiet: false 68 | pretty: false 69 | headers: 70 | 'Access-Control-Allow-Origin': '*' 71 | watch_options: 72 | ignored: '**/node_modules/**' 73 | 74 | 75 | test: 76 | <<: *default 77 | compile: true 78 | 79 | # Compile test packs to a separate directory 80 | public_output_path: packs-test 81 | 82 | production: 83 | <<: *default 84 | 85 | # Production depends on precompilation of packs prior to booting for performance. 86 | compile: false 87 | 88 | # Extract and emit a css file 89 | extract_css: true 90 | 91 | # Cache manifest.json for performance 92 | cache_manifest: true 93 | -------------------------------------------------------------------------------- /db/migrate/20210312032114_create_block_lists.rb: -------------------------------------------------------------------------------- 1 | class CreateBlockLists < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :block_editor_block_lists do |t| 4 | t.string :name 5 | t.text :content 6 | t.boolean :active, default: false 7 | t.references :listable, polymorphic: true 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20210506220328_create_block_list_connections.rb: -------------------------------------------------------------------------------- 1 | class CreateBlockListConnections < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :block_editor_block_list_connections do |t| 4 | t.belongs_to :parent 5 | t.belongs_to :child 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/block_editor.rb: -------------------------------------------------------------------------------- 1 | require "block_editor/version" 2 | require "block_editor/engine" 3 | 4 | require "block_editor/instance" 5 | require 'block_editor/blocks/base' 6 | require 'block_editor/blocks/contact_form' 7 | require 'block_editor/blocks/reusable' 8 | require 'block_editor/block_list_renderer' 9 | 10 | module BlockEditor 11 | ROOT_PATH = Pathname.new(File.join(__dir__, "..")) 12 | 13 | class << self 14 | def webpacker 15 | @webpacker ||= ::Webpacker::Instance.new( 16 | root_path: ROOT_PATH, 17 | config_path: ROOT_PATH.join("config/webpacker.yml") 18 | ) 19 | end 20 | end 21 | 22 | mattr_accessor :dynamic_blocks 23 | @@dynamic_blocks = ['BlockEditor::Blocks::Reusable', 'BlockEditor::Blocks::ContactForm'] 24 | 25 | mattr_accessor :frontend_parent_controller 26 | @@frontend_parent_controller = 'ApplicationController' 27 | end 28 | 29 | -------------------------------------------------------------------------------- /lib/block_editor/block_list_renderer.rb: -------------------------------------------------------------------------------- 1 | module BlockEditor 2 | # Handles the rendering of a block list including dynamic blocks and removing HTML comments 3 | class BlockListRenderer 4 | # Renders dynamic blocks within the HTML snippet then strips all HTML comments (including Gutenberg markup) 5 | # 6 | # @param raw_html [String] 7 | # 8 | # @return [String] Parsed content 9 | def self.render(raw_html) 10 | html = Nokogiri::HTML::DocumentFragment.parse(raw_html) 11 | 12 | # Find & render all instances of a dynamic block (including reusable blocks) 13 | BlockEditor.dynamic_blocks.each do |dynamic_block| 14 | dynamic_block = dynamic_block.constantize 15 | 16 | html.search('.//comment()').select {|comment| comment.inner_text.starts_with?(" wp:#{dynamic_block.name}") }.each do |block_instance| 17 | block_attributes = block_instance.inner_text.split(" wp:#{dynamic_block.name}")[1][0...-1] 18 | block_attributes = block_attributes.blank? ? {} : JSON.parse(block_attributes) 19 | block_content = render_block(dynamic_block, block_attributes) 20 | 21 | block_instance.replace(block_content) 22 | end 23 | end 24 | 25 | html.search('.//comment()').remove 26 | html.to_s.html_safe 27 | end 28 | 29 | # Renders a specific block using the provided options 30 | # 31 | # @param block [String] name of block 32 | # @param options [Hash] block options to use when rendering 33 | # 34 | # @return [String] block content (HTML) 35 | def self.render_block(block, options) 36 | block.render(options) 37 | rescue StandardError => e 38 | respond_with_block_error(e) 39 | end 40 | 41 | # Handles block errors 42 | def self.respond_with_block_error(error) 43 | Rails.logger.error("Error rendering block - #{error.message}") 44 | '' 45 | end 46 | end 47 | end 48 | 49 | 50 | -------------------------------------------------------------------------------- /lib/block_editor/blocks/base.rb: -------------------------------------------------------------------------------- 1 | module BlockEditor 2 | # Blocks used to render dynamic content 3 | module Blocks 4 | # Base for dynamic blocks 5 | class Base 6 | def self.name 7 | raise NotImplementedError, 'Must specify block name' 8 | end 9 | 10 | # Render the block 11 | def self.render(options = {}) 12 | options = options.reverse_merge(default_options.with_indifferent_access) 13 | 14 | controller.render( 15 | partial: "block_editor/blocks/#{name}/block", 16 | locals: { options: options }, 17 | layout: false 18 | ) 19 | end 20 | 21 | # Frontend controller used to render views 22 | def self.controller 23 | BlockEditor.frontend_parent_controller.constantize 24 | end 25 | 26 | # Default widget options 27 | def self.default_options 28 | {} 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/block_editor/blocks/contact_form.rb: -------------------------------------------------------------------------------- 1 | module BlockEditor 2 | # Blocks used to render dynamic content 3 | module Blocks 4 | # Outputs Contact Form block 5 | class ContactForm < Base 6 | def self.name 7 | 'be/contact-form' 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/block_editor/blocks/reusable.rb: -------------------------------------------------------------------------------- 1 | module BlockEditor 2 | # Blocks used to render dynamic content 3 | module Blocks 4 | # Outputs Reusable block 5 | class Reusable < Base 6 | def self.name 7 | 'block' 8 | end 9 | 10 | # Render the block 11 | def self.render(options = {}) 12 | BlockListRenderer.render(BlockEditor::BlockList.find(options['ref']).content) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/block_editor/engine.rb: -------------------------------------------------------------------------------- 1 | module BlockEditor 2 | class Engine < ::Rails::Engine 3 | isolate_namespace BlockEditor 4 | 5 | initializer "webpacker.proxy" do |app| 6 | insert_middleware = begin 7 | BlockEditor.webpacker.config.dev_server.present? 8 | rescue 9 | nil 10 | end 11 | next unless insert_middleware 12 | 13 | app.middleware.insert_before( 14 | 0, Webpacker::DevServerProxy, 15 | ssl_verify_none: true, 16 | webpacker: BlockEditor.webpacker 17 | ) 18 | end 19 | 20 | # Initializer to combine this engines static assets with the static assets of the host application 21 | initializer 'static assets' do |app| 22 | app.middleware.insert_before(::ActionDispatch::Static, ::ActionDispatch::Static, "#{root}/public") 23 | end 24 | 25 | initializer 'block_editor.assets.precompile' do |app| 26 | assets_for_precompile = [ 27 | 'block_editor/frontend.css', 28 | 'block_editor/backend.css' 29 | ] 30 | 31 | app.config.assets.precompile.concat assets_for_precompile 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/block_editor/instance.rb: -------------------------------------------------------------------------------- 1 | module BlockEditor 2 | class Instance 3 | include ActionView::Helpers::TagHelper 4 | include ActionView::Helpers::FormTagHelper 5 | 6 | attr_accessor :output_buffer 7 | 8 | def self.render(form_builder) 9 | self.new.render(form_builder) 10 | end 11 | 12 | def render(form_builder) 13 | content_tag(:div, data: { controller: 'block-editor' }) do 14 | form_builder.hidden_field(:content, data: { 'block-editor-target' => 'input' }) + 15 | content_tag('div', nil, { class: 'block-editor', data: { 'block-editor-target' => 'output' } }) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/block_editor/version.rb: -------------------------------------------------------------------------------- 1 | module BlockEditor 2 | VERSION = '1.0.0' 3 | end 4 | -------------------------------------------------------------------------------- /lib/tasks/block_editor_tasks.rake: -------------------------------------------------------------------------------- 1 | def ensure_log_goes_to_stdout 2 | old_logger = Webpacker.logger 3 | Webpacker.logger = ActiveSupport::Logger.new(STDOUT) 4 | yield 5 | ensure 6 | Webpacker.logger = old_logger 7 | end 8 | 9 | 10 | namespace :block_editor do 11 | namespace :webpacker do 12 | desc "Install deps with yarn" 13 | task :yarn_install do 14 | Dir.chdir(File.join(__dir__, "../..")) do 15 | system "yarn install --no-progress --production" 16 | end 17 | end 18 | 19 | desc "Compile JavaScript packs using webpack for production with digests" 20 | task compile: [:yarn_install, :environment] do 21 | Webpacker.with_node_env("production") do 22 | ensure_log_goes_to_stdout do 23 | if BlockEditor.webpacker.commands.compile 24 | # Successful compilation! 25 | else 26 | # Failed compilation 27 | exit! 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | 35 | def yarn_install_available? 36 | rails_major = Rails::VERSION::MAJOR 37 | rails_minor = Rails::VERSION::MINOR 38 | 39 | rails_major > 5 || (rails_major == 5 && rails_minor >= 1) 40 | end 41 | 42 | def enhance_assets_precompile 43 | # yarn:install was added in Rails 5.1 44 | deps = yarn_install_available? ? [] : ["block_editor:webpacker:yarn_install"] 45 | Rake::Task["assets:precompile"].enhance(deps) do 46 | Rake::Task["block_editor:webpacker:compile"].invoke 47 | end 48 | end 49 | 50 | # Compile packs after we've compiled all other assets during precompilation 51 | skip_webpacker_precompile = %w(no false n f).include?(ENV["WEBPACKER_PRECOMPILE"]) 52 | 53 | unless skip_webpacker_precompile 54 | if Rake::Task.task_defined?("assets:precompile") 55 | enhance_assets_precompile 56 | else 57 | Rake::Task.define_task("assets:precompile" => "block_editor:webpacker:compile") 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "block-editor", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@babel/core": "7.9.0", 7 | "@rails/webpacker": "^5.2.1", 8 | "@wordpress/base-styles": "3.2.0", 9 | "@wordpress/block-editor": "5.1.0", 10 | "@wordpress/block-library": "2.26.0", 11 | "@wordpress/components": "11.1.0", 12 | "@wordpress/editor": "9.24.0", 13 | "@wordpress/format-library": "1.25.0", 14 | "@wordpress/interface": "0.10.0", 15 | "@wordpress/reusable-blocks": "1.0.0", 16 | "@wordpress/server-side-render": "1.19.0", 17 | "babel-loader": "8.1.0", 18 | "babel-preset-react-app": "^9.1.2", 19 | "react": "^16.13.1", 20 | "react-dom": "^16.13.1", 21 | "redux-undo": "^1.0.1", 22 | "stimulus": "^2.0.0" 23 | }, 24 | "babel": { 25 | "presets": [ 26 | "react-app" 27 | ] 28 | }, 29 | "devDependencies": { 30 | "webpack-dev-server": "^3.11.0" 31 | }, 32 | "resolutions": { 33 | "**/@wordpress/block-editor": "5.1.0", 34 | "**/@wordpress/editor": "9.24.0", 35 | "**/@wordpress/reusable-blocks": "1.0.0", 36 | "**/@wordpress/components": "11.1.0", 37 | "**/@wordpress/server-side-render": "1.19.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | //= link block_editor_manifest.js 4 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamasolutions/block-editor/3454f449b22a041191a6eee53c645e0d764841d3/test/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /test/dummy/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamasolutions/block-editor/3454f449b22a041191a6eee53c645e0d764841d3/test/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require rails-ujs 14 | //= require activestorage 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /test/dummy/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamasolutions/block-editor/3454f449b22a041191a6eee53c645e0d764841d3/test/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <title>Dummy</title> 5 | <meta name="viewport" content="width=device-width,initial-scale=1"> 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= stylesheet_link_tag 'application', media: 'all' %> 10 | </head> 11 | 12 | <body> 13 | <%= yield %> 14 | </body> 15 | </html> 16 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 5 | <style> 6 | /* Email styles need to be inline */ 7 | </style> 8 | </head> 9 | 10 | <body> 11 | <%= yield %> 12 | </body> 13 | </html> 14 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /test/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /test/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /test/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?('config/database.yml') 22 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! 'bin/rails db:prepare' 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! 'bin/rails log:clear tmp:clear' 30 | 31 | puts "\n== Restarting application server ==" 32 | system! 'bin/rails restart' 33 | end 34 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | require "block_editor" 9 | 10 | module Dummy 11 | class Application < Rails::Application 12 | config.load_defaults Rails::VERSION::STRING.to_f 13 | 14 | # Configuration for the application, engines, and railties goes here. 15 | # 16 | # These settings can be overridden in specific environments using the files 17 | # in config/environments, which are processed later. 18 | # 19 | # config.time_zone = "Central Time (US & Canada)" 20 | # config.eager_load_paths << Rails.root.join("extras") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) 3 | 4 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) 5 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) 6 | -------------------------------------------------------------------------------- /test/dummy/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: dummy_production 11 | -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable/disable caching. By default caching is disabled. 18 | # Run rails dev:cache to toggle caching. 19 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 20 | config.action_controller.perform_caching = true 21 | config.action_controller.enable_fragment_cache_logging = true 22 | 23 | config.cache_store = :memory_store 24 | config.public_file_server.headers = { 25 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 26 | } 27 | else 28 | config.action_controller.perform_caching = false 29 | 30 | config.cache_store = :null_store 31 | end 32 | 33 | # Store uploaded files on the local file system (see config/storage.yml for options). 34 | config.active_storage.service = :local 35 | 36 | # Don't care if the mailer can't send. 37 | config.action_mailer.raise_delivery_errors = false 38 | 39 | config.action_mailer.perform_caching = false 40 | 41 | # Print deprecation notices to the Rails logger. 42 | config.active_support.deprecation = :log 43 | 44 | # Raise exceptions for disallowed deprecations. 45 | config.active_support.disallowed_deprecation = :raise 46 | 47 | # Tell Active Support which deprecation messages to disallow. 48 | config.active_support.disallowed_deprecation_warnings = [] 49 | 50 | # Raise an error on page load if there are pending migrations. 51 | config.active_record.migration_error = :page_load 52 | 53 | # Highlight code that triggered database queries in logs. 54 | config.active_record.verbose_query_logs = true 55 | 56 | # Debug mode disables concatenation and preprocessing of assets. 57 | # This option may cause significant delays in view rendering with a large 58 | # number of complex assets. 59 | config.assets.debug = true 60 | 61 | # Suppress logger output for asset requests. 62 | config.assets.quiet = true 63 | 64 | # Raises error for missing translations. 65 | # config.i18n.raise_on_missing_translations = true 66 | 67 | # Annotate rendered view with file names. 68 | # config.action_view.annotate_rendered_view_with_filenames = true 69 | 70 | # Use an evented file watcher to asynchronously detect changes in source code, 71 | # routes, locales, etc. This feature depends on the listen gem. 72 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker 73 | 74 | # Uncomment if you wish to allow Action Cable access from any origin. 75 | # config.action_cable.disable_request_forgery_protection = true 76 | end 77 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | # config.require_master_key = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress CSS using a preprocessor. 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 34 | # config.asset_host = 'http://assets.example.com' 35 | 36 | # Specifies the header that your server uses for sending files. 37 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 38 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 39 | 40 | # Store uploaded files on the local file system (see config/storage.yml for options). 41 | config.active_storage.service = :local 42 | 43 | # Mount Action Cable outside main process or domain. 44 | # config.action_cable.mount_path = nil 45 | # config.action_cable.url = 'wss://example.com/cable' 46 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 47 | 48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 49 | # config.force_ssl = true 50 | 51 | # Include generic and useful information about system operation, but avoid logging too much 52 | # information to avoid inadvertent exposure of personally identifiable information (PII). 53 | config.log_level = :info 54 | 55 | # Prepend all log lines with the following tags. 56 | config.log_tags = [ :request_id ] 57 | 58 | # Use a different cache store in production. 59 | # config.cache_store = :mem_cache_store 60 | 61 | # Use a real queuing backend for Active Job (and separate queues per environment). 62 | # config.active_job.queue_adapter = :resque 63 | # config.active_job.queue_name_prefix = "dummy_production" 64 | 65 | config.action_mailer.perform_caching = false 66 | 67 | # Ignore bad email addresses and do not raise email delivery errors. 68 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 69 | # config.action_mailer.raise_delivery_errors = false 70 | 71 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 72 | # the I18n.default_locale when a translation cannot be found). 73 | config.i18n.fallbacks = true 74 | 75 | # Send deprecation notices to registered listeners. 76 | config.active_support.deprecation = :notify 77 | 78 | # Log disallowed deprecations. 79 | config.active_support.disallowed_deprecation = :log 80 | 81 | # Tell Active Support which deprecation messages to disallow. 82 | config.active_support.disallowed_deprecation_warnings = [] 83 | 84 | # Use default logging formatter so that PID and timestamp are not suppressed. 85 | config.log_formatter = ::Logger::Formatter.new 86 | 87 | # Use a different logger for distributed setups. 88 | # require "syslog/logger" 89 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 90 | 91 | if ENV["RAILS_LOG_TO_STDOUT"].present? 92 | logger = ActiveSupport::Logger.new(STDOUT) 93 | logger.formatter = config.log_formatter 94 | config.logger = ActiveSupport::TaggedLogging.new(logger) 95 | end 96 | 97 | # Do not dump schema after migrations. 98 | config.active_record.dump_schema_after_migration = false 99 | 100 | # Inserts middleware to perform automatic connection switching. 101 | # The `database_selector` hash is used to pass options to the DatabaseSelector 102 | # middleware. The `delay` is used to determine how long to wait after a write 103 | # to send a subsequent read to the primary. 104 | # 105 | # The `database_resolver` class is used by the middleware to determine which 106 | # database is appropriate to use based on the time delay. 107 | # 108 | # The `database_resolver_context` class is used by the middleware to set 109 | # timestamps for the last write to the primary. The resolver uses the context 110 | # class timestamps to determine how long to wait before reading from the 111 | # replica. 112 | # 113 | # By default Rails will store a last write timestamp in the session. The 114 | # DatabaseSelector middleware is designed as such you can define your own 115 | # strategy for connection switching and pass that into the middleware through 116 | # these configuration options. 117 | # config.active_record.database_selector = { delay: 2.seconds } 118 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver 119 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session 120 | end 121 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | config.cache_classes = true 12 | 13 | # Do not eager load code on boot. This avoids loading your whole application 14 | # just for the purpose of running a single test. If you are using a tool that 15 | # preloads Rails for running tests, you may have to set it to true. 16 | config.eager_load = false 17 | 18 | # Configure public file server for tests with Cache-Control for performance. 19 | config.public_file_server.enabled = true 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 22 | } 23 | 24 | # Show full error reports and disable caching. 25 | config.consider_all_requests_local = true 26 | config.action_controller.perform_caching = false 27 | config.cache_store = :null_store 28 | 29 | # Raise exceptions instead of rendering exception templates. 30 | config.action_dispatch.show_exceptions = false 31 | 32 | # Disable request forgery protection in test environment. 33 | config.action_controller.allow_forgery_protection = false 34 | 35 | # Store uploaded files on the local file system in a temporary directory. 36 | config.active_storage.service = :test 37 | 38 | config.action_mailer.perform_caching = false 39 | 40 | # Tell Action Mailer not to deliver emails to the real world. 41 | # The :test delivery method accumulates sent emails in the 42 | # ActionMailer::Base.deliveries array. 43 | config.action_mailer.delivery_method = :test 44 | 45 | # Print deprecation notices to the stderr. 46 | config.active_support.deprecation = :stderr 47 | 48 | # Raise exceptions for disallowed deprecations. 49 | config.active_support.disallowed_deprecation = :raise 50 | 51 | # Tell Active Support which deprecation messages to disallow. 52 | config.active_support.disallowed_deprecation_warnings = [] 53 | 54 | # Raises error for missing translations. 55 | # config.i18n.raise_on_missing_translations = true 56 | 57 | # Annotate rendered view with file names. 58 | # config.action_view.annotate_rendered_view_with_filenames = true 59 | end 60 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 13 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] 9 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | 19 | # If you are using UJS then enable automatic nonce generation 20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 21 | 22 | # Set the nonce only to specific directives 23 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 24 | 25 | # Report CSP violations to a specified URI 26 | # For further information see the following documentation: 27 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 28 | # Rails.application.config.content_security_policy_report_only = true 29 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [ 5 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 6 | ] 7 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /test/dummy/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 12 | # terminating a worker in development environments. 13 | # 14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 15 | 16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 17 | # 18 | port ENV.fetch("PORT") { 3000 } 19 | 20 | # Specifies the `environment` that Puma will run in. 21 | # 22 | environment ENV.fetch("RAILS_ENV") { "development" } 23 | 24 | # Specifies the `pidfile` that Puma will use. 25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 26 | 27 | # Specifies the number of `workers` to boot in clustered mode. 28 | # Workers are forked web server processes. If using threads and workers together 29 | # the concurrency of the application would be max `threads` * `workers`. 30 | # Workers do not work on JRuby or Windows (both of which do not support 31 | # processes). 32 | # 33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 34 | 35 | # Use the `preload_app!` method when specifying a `workers` number. 36 | # This directive tells Puma to first boot the application and load code 37 | # before forking the application. This takes advantage of Copy On Write 38 | # process behavior so workers use less memory. 39 | # 40 | # preload_app! 41 | 42 | # Allow puma to be restarted by `rails restart` command. 43 | plugin :tmp_restart 44 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | mount BlockEditor::Engine => "/block_editor" 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /test/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamasolutions/block-editor/3454f449b22a041191a6eee53c645e0d764841d3/test/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /test/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamasolutions/block-editor/3454f449b22a041191a6eee53c645e0d764841d3/test/dummy/log/.keep -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <title>The page you were looking for doesn't exist (404)</title> 5 | <meta name="viewport" content="width=device-width,initial-scale=1"> 6 | <style> 7 | .rails-default-error-page { 8 | background-color: #EFEFEF; 9 | color: #2E2F30; 10 | text-align: center; 11 | font-family: arial, sans-serif; 12 | margin: 0; 13 | } 14 | 15 | .rails-default-error-page div.dialog { 16 | width: 95%; 17 | max-width: 33em; 18 | margin: 4em auto 0; 19 | } 20 | 21 | .rails-default-error-page div.dialog > div { 22 | border: 1px solid #CCC; 23 | border-right-color: #999; 24 | border-left-color: #999; 25 | border-bottom-color: #BBB; 26 | border-top: #B00100 solid 4px; 27 | border-top-left-radius: 9px; 28 | border-top-right-radius: 9px; 29 | background-color: white; 30 | padding: 7px 12% 0; 31 | box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); 32 | } 33 | 34 | .rails-default-error-page h1 { 35 | font-size: 100%; 36 | color: #730E15; 37 | line-height: 1.5em; 38 | } 39 | 40 | .rails-default-error-page div.dialog > p { 41 | margin: 0 0 1em; 42 | padding: 1em; 43 | background-color: #F7F7F7; 44 | border: 1px solid #CCC; 45 | border-right-color: #999; 46 | border-left-color: #999; 47 | border-bottom-color: #999; 48 | border-bottom-left-radius: 4px; 49 | border-bottom-right-radius: 4px; 50 | border-top-color: #DADADA; 51 | color: #666; 52 | box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); 53 | } 54 | </style> 55 | </head> 56 | 57 | <body class="rails-default-error-page"> 58 | <!-- This file lives in public/404.html --> 59 | <div class="dialog"> 60 | <div> 61 | <h1>The page you were looking for doesn't exist.</h1> 62 | <p>You may have mistyped the address or the page may have moved.</p> 63 | </div> 64 | <p>If you are the application owner check the logs for more information.</p> 65 | </div> 66 | </body> 67 | </html> 68 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <title>The change you wanted was rejected (422)</title> 5 | <meta name="viewport" content="width=device-width,initial-scale=1"> 6 | <style> 7 | .rails-default-error-page { 8 | background-color: #EFEFEF; 9 | color: #2E2F30; 10 | text-align: center; 11 | font-family: arial, sans-serif; 12 | margin: 0; 13 | } 14 | 15 | .rails-default-error-page div.dialog { 16 | width: 95%; 17 | max-width: 33em; 18 | margin: 4em auto 0; 19 | } 20 | 21 | .rails-default-error-page div.dialog > div { 22 | border: 1px solid #CCC; 23 | border-right-color: #999; 24 | border-left-color: #999; 25 | border-bottom-color: #BBB; 26 | border-top: #B00100 solid 4px; 27 | border-top-left-radius: 9px; 28 | border-top-right-radius: 9px; 29 | background-color: white; 30 | padding: 7px 12% 0; 31 | box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); 32 | } 33 | 34 | .rails-default-error-page h1 { 35 | font-size: 100%; 36 | color: #730E15; 37 | line-height: 1.5em; 38 | } 39 | 40 | .rails-default-error-page div.dialog > p { 41 | margin: 0 0 1em; 42 | padding: 1em; 43 | background-color: #F7F7F7; 44 | border: 1px solid #CCC; 45 | border-right-color: #999; 46 | border-left-color: #999; 47 | border-bottom-color: #999; 48 | border-bottom-left-radius: 4px; 49 | border-bottom-right-radius: 4px; 50 | border-top-color: #DADADA; 51 | color: #666; 52 | box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); 53 | } 54 | </style> 55 | </head> 56 | 57 | <body class="rails-default-error-page"> 58 | <!-- This file lives in public/422.html --> 59 | <div class="dialog"> 60 | <div> 61 | <h1>The change you wanted was rejected.</h1> 62 | <p>Maybe you tried to change something you didn't have access to.</p> 63 | </div> 64 | <p>If you are the application owner check the logs for more information.</p> 65 | </div> 66 | </body> 67 | </html> 68 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <title>We're sorry, but something went wrong (500)</title> 5 | <meta name="viewport" content="width=device-width,initial-scale=1"> 6 | <style> 7 | .rails-default-error-page { 8 | background-color: #EFEFEF; 9 | color: #2E2F30; 10 | text-align: center; 11 | font-family: arial, sans-serif; 12 | margin: 0; 13 | } 14 | 15 | .rails-default-error-page div.dialog { 16 | width: 95%; 17 | max-width: 33em; 18 | margin: 4em auto 0; 19 | } 20 | 21 | .rails-default-error-page div.dialog > div { 22 | border: 1px solid #CCC; 23 | border-right-color: #999; 24 | border-left-color: #999; 25 | border-bottom-color: #BBB; 26 | border-top: #B00100 solid 4px; 27 | border-top-left-radius: 9px; 28 | border-top-right-radius: 9px; 29 | background-color: white; 30 | padding: 7px 12% 0; 31 | box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); 32 | } 33 | 34 | .rails-default-error-page h1 { 35 | font-size: 100%; 36 | color: #730E15; 37 | line-height: 1.5em; 38 | } 39 | 40 | .rails-default-error-page div.dialog > p { 41 | margin: 0 0 1em; 42 | padding: 1em; 43 | background-color: #F7F7F7; 44 | border: 1px solid #CCC; 45 | border-right-color: #999; 46 | border-left-color: #999; 47 | border-bottom-color: #999; 48 | border-bottom-left-radius: 4px; 49 | border-bottom-right-radius: 4px; 50 | border-top-color: #DADADA; 51 | color: #666; 52 | box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); 53 | } 54 | </style> 55 | </head> 56 | 57 | <body class="rails-default-error-page"> 58 | <!-- This file lives in public/500.html --> 59 | <div class="dialog"> 60 | <div> 61 | <h1>We're sorry, but something went wrong.</h1> 62 | </div> 63 | <p>If you are the application owner check the logs for more information.</p> 64 | </div> 65 | </body> 66 | </html> 67 | -------------------------------------------------------------------------------- /test/dummy/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamasolutions/block-editor/3454f449b22a041191a6eee53c645e0d764841d3/test/dummy/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /test/dummy/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamasolutions/block-editor/3454f449b22a041191a6eee53c645e0d764841d3/test/dummy/public/apple-touch-icon.png -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamasolutions/block-editor/3454f449b22a041191a6eee53c645e0d764841d3/test/dummy/public/favicon.ico -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require_relative "../test/dummy/config/environment" 5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] 6 | ActiveRecord::Migrator.migrations_paths << File.expand_path('../db/migrate', __dir__) 7 | require "rails/test_help" 8 | 9 | 10 | # Load fixtures from the engine 11 | if ActiveSupport::TestCase.respond_to?(:fixture_path=) 12 | ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) 13 | ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path 14 | ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" 15 | ActiveSupport::TestCase.fixtures :all 16 | end 17 | -------------------------------------------------------------------------------- /webpack/development.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /webpack/environment.js: -------------------------------------------------------------------------------- 1 | const { environment } = require('@rails/webpacker') 2 | 3 | module.exports = environment 4 | -------------------------------------------------------------------------------- /webpack/production.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /webpack/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | --------------------------------------------------------------------------------