ckeditor5 1.0.6 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad357ef6b45ba35fdea777e70dae559476bd57040b23f49cfefa3581b4b3aafc
4
- data.tar.gz: 89da75ef1325766ad4a2f0d81152822f8774b55efdf81bd76b8d9479332cb749
3
+ metadata.gz: ba0f56261f0c699d67fce659fe6cd4cbc3fc2f288ebedf0fd908f4f6d9555001
4
+ data.tar.gz: 9c5b41693f3858a94841b7e6392bbd573a3bc83c3a936c7dd2fbdcfff42fda63
5
5
  SHA512:
6
- metadata.gz: f4f599b202ef0c109e14ff12c25f09890808d53e2116b2d3b6e51dfbfc5f389ea1d62651aa1a493fb897838e56ca2f3345addee8eebd55f5c2b7126f55169ce3
7
- data.tar.gz: fc254a7f703defae55c901a444329772e177dfbfc8d0ee4ea3864c6c516b1c36f64e21166ec2e787d79ce368e09981ad524f4187842f31b53b8084ab690ed13b
6
+ metadata.gz: e12b59365223eeeee6cccef5e771ae2df9e17e9500275a871c8f7b41b6a39abe0cec725848f887ead1a3235d2d50447fdb8b99ead940d1dc50c5f795369d9354
7
+ data.tar.gz: b163824d28f9867b0bee31e273e5b0be6ad380891d43a1e48ed1fb4df0c38460a4cfddc5fcb7fdf1fa1430950b6cd3d47a6c4cbcf8c4067f5178660f6c991d15
data/README.md CHANGED
@@ -8,6 +8,10 @@
8
8
 
9
9
  Unofficial CKEditor 5 Ruby on Rails integration gem. Provides seamless integration of CKEditor 5 with Rails applications through web components and helper methods.
10
10
 
11
+ <p align="center">
12
+ <img src="docs/intro-classic-editor.png" alt="CKEditor 5 Classic Editor in Ruby on Rails application">
13
+ </p>
14
+
11
15
  ## Installation 🛠️
12
16
 
13
17
  Add this line to your application's Gemfile:
@@ -16,21 +20,33 @@ Add this line to your application's Gemfile:
16
20
  gem 'ckeditor5'
17
21
  ```
18
22
 
19
- Usage in your Rails application:
23
+ In your config:
24
+
25
+ ```rb
26
+ # config/initializers/ckeditor5.rb
27
+
28
+ CKEditor5::Rails::Engine.configure do |config|
29
+ config.presets.override :default do
30
+ version '43.3.0'
31
+ end
32
+ end
33
+ ```
34
+
35
+ In your view:
20
36
 
21
37
  ```erb
22
38
  <!-- app/views/demos/index.html.erb -->
23
39
 
24
40
  <% content_for :head do %>
25
- <%= ckeditor5_assets version: '43.2.0', translations: [:pl, :es] %>
41
+ <%= ckeditor5_assets %>
42
+ <!-- or using inline config: ckeditor5_assets version: '43.3.0', premium: false -->
26
43
  <% end %>
27
44
 
28
- <%= ckeditor5_editor style: 'width: 600px' %>
45
+ <%= ckeditor5_editor style: 'width: 600px', initial_data: '<YOUR DATA>' %>
46
+ <!-- or using inline config: ckeditor5_editor type: :classic, config: { toolbar: [:Bold, :Italic] }, ... -->
29
47
  ```
30
48
 
31
- Result:
32
-
33
- ![CKEditor 5 Classic Editor in Ruby on Rails application](docs/intro-classic-editor.png)
49
+ Voilà! You have CKEditor 5 integrated with your Rails application. 🎉
34
50
 
35
51
  ## Table of Contents 📚
36
52
 
@@ -41,17 +57,18 @@ Result:
41
57
  - [Available Configuration Methods ⚙️](#available-configuration-methods-️)
42
58
  - [`version(version)` method](#versionversion-method)
43
59
  - [`gpl` method](#gpl-method)
60
+ - [`license_key(key)` method](#license_keykey-method)
44
61
  - [`premium` method](#premium-method)
45
62
  - [`translations(*languages)` method](#translationslanguages-method)
46
- - [`license_key(key)` method](#license_keykey-method)
47
63
  - [`ckbox` method](#ckbox-method)
48
64
  - [`type(type)` method](#typetype-method)
49
- - [`plugins(*names, **kwargs)` method](#pluginsnames-kwargs-method)
50
- - [`toolbar(*items, should_group_when_full: true)` method](#toolbaritems-should_group_when_full-true-method)
65
+ - [`toolbar(*items, should_group_when_full: true, &block)` method](#toolbaritems-should_group_when_full-true-block-method)
51
66
  - [`menubar(visible: true)` method](#menubarvisible-true-method)
52
67
  - [`language(ui, content:)` method](#languageui-content-method)
53
68
  - [`configure(name, value)` method](#configurename-value-method)
54
69
  - [`plugin(name, premium:, import_name:)` method](#pluginname-premium-import_name-method)
70
+ - [`plugins(*names, **kwargs)` method](#pluginsnames-kwargs-method)
71
+ - [`inline_plugin(name, code)` method](#inline_pluginname-code-method)
55
72
  - [Including CKEditor 5 assets 📦](#including-ckeditor-5-assets-)
56
73
  - [Lazy loading 🚀](#lazy-loading-)
57
74
  - [GPL usage 🆓](#gpl-usage-)
@@ -62,6 +79,18 @@ Result:
62
79
  - [Inline editor 📝](#inline-editor-)
63
80
  - [Balloon editor 🎈](#balloon-editor-)
64
81
  - [Decoupled editor 🌐](#decoupled-editor-)
82
+ - [How to access editor instance? 🤔](#how-to-access-editor-instance-)
83
+ - [Events fired by the editor 🔊](#events-fired-by-the-editor-)
84
+ - [`editor-ready` event](#editor-ready-event)
85
+ - [`editor-error` event](#editor-error-event)
86
+ - [Common Tasks and Solutions 💡](#common-tasks-and-solutions-)
87
+ - [Setting Initial Content 📝](#setting-initial-content-)
88
+ - [Setting Editor Language 🌐](#setting-editor-language-)
89
+ - [Integrating with Forms 📋](#integrating-with-forms-)
90
+ - [Rails form builder integration](#rails-form-builder-integration)
91
+ - [Simple form integration](#simple-form-integration)
92
+ - [Custom Styling 🎨](#custom-styling-)
93
+ - [Custom plugins 🧩](#custom-plugins-)
65
94
  - [License 📜](#license-)
66
95
 
67
96
  ## Presets 🎨
@@ -75,18 +104,16 @@ You can create your own by defining it in the `config/initializers/ckeditor5.rb`
75
104
 
76
105
  CKEditor5::Rails::Engine.configure do |config|
77
106
  config.presets.define :custom
78
- gpl # Use GPL license
79
-
107
+ gpl
80
108
  type :classic
81
109
 
82
110
  menubar
83
-
84
111
  toolbar :undo, :redo, :|, :heading, :|, :bold, :italic, :underline, :|,
85
- :link, :insertImage, :ckbox, :mediaEmbed, :insertTable, :blockQuote, :|,
112
+ :link, :insertImage, :mediaEmbed, :insertTable, :blockQuote, :|,
86
113
  :bulletedList, :numberedList, :todoList, :outdent, :indent
87
114
 
88
115
  plugins :AccessibilityHelp, :Autoformat, :AutoImage, :Autosave,
89
- :BlockQuote, :Bold, :CKBox, :CKBoxImageEdit, :CloudServices,
116
+ :BlockQuote, :Bold, :CloudServices,
90
117
  :Essentials, :Heading, :ImageBlock, :ImageCaption, :ImageInline,
91
118
  :ImageInsert, :ImageInsertViaUrl, :ImageResize, :ImageStyle,
92
119
  :ImageTextAlternative, :ImageToolbar, :ImageUpload, :Indent,
@@ -95,6 +122,10 @@ CKEditor5::Rails::Engine.configure do |config|
95
122
  :SelectAll, :Table, :TableCaption, :TableCellProperties,
96
123
  :TableColumnResize, :TableProperties, :TableToolbar,
97
124
  :TextTransformation, :TodoList, :Underline, :Undo, :Base64UploadAdapter
125
+
126
+ configure :image, {
127
+ toolbar: ['imageTextAlternative', 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side']
128
+ }
98
129
  end
99
130
  end
100
131
  ```
@@ -107,6 +138,13 @@ In order to override existing presets, you can use the `config.presets.override`
107
138
  CKEditor5::Rails::Engine.configure do |config|
108
139
  config.presets.override :default do
109
140
  menubar visible: false
141
+
142
+ toolbar do
143
+ remove :underline, :heading
144
+
145
+ # prepend :underline
146
+ # append :heading
147
+ end
110
148
  end
111
149
  end
112
150
  ```
@@ -146,9 +184,23 @@ config.presets.define :custom do
146
184
  end
147
185
  ```
148
186
 
187
+ #### `license_key(key)` method
188
+
189
+ Defines the license key of CKEditor 5. It calls `premium` method internally. The example below shows how to set the license key:
190
+
191
+ ```rb
192
+ # config/initializers/ckeditor5.rb
193
+
194
+ config.presets.define :custom do
195
+ # ... other configuration
196
+
197
+ license_key 'your-license-key'
198
+ end
199
+ ```
200
+
149
201
  #### `premium` method
150
202
 
151
- Defines if premium package (`ckeditor5-premium-features`) should be used.
203
+ Defines if premium package should be included in JS assets. The example below shows how to add `ckeditor5-premium-features` to import maps:
152
204
 
153
205
  ```rb
154
206
  # config/initializers/ckeditor5.rb
@@ -162,7 +214,7 @@ end
162
214
 
163
215
  #### `translations(*languages)` method
164
216
 
165
- Defines the translations of CKEditor 5. You can pass the language codes as arguments. The example below shows how to set the Polish and Spanish translations:
217
+ Defines the translations of CKEditor 5. You can pass the language codes as arguments. The example below shows how tell integration to fetch Polish and Spanish translations:
166
218
 
167
219
  ```rb
168
220
  # config/initializers/ckeditor5.rb
@@ -174,17 +226,15 @@ config.presets.define :custom do
174
226
  end
175
227
  ```
176
228
 
177
- #### `license_key(key)` method
178
-
179
- Defines the license key of CKEditor 5. It calls `premium` method internally. The example below shows how to set the license key:
229
+ ⚠️ You need to use `language` method to set the default language of the editor, as the `translations` only fetch the translations files and makes them available to later use.
180
230
 
181
231
  ```rb
182
232
  # config/initializers/ckeditor5.rb
183
233
 
184
234
  config.presets.define :custom do
185
- # ... other configuration
235
+ translations :pl
186
236
 
187
- license_key 'your-license-key'
237
+ language :pl
188
238
  end
189
239
  ```
190
240
 
@@ -224,9 +274,14 @@ config.presets.define :custom do
224
274
  end
225
275
  ```
226
276
 
227
- #### `plugins(*names, **kwargs)` method
277
+ #### `toolbar(*items, should_group_when_full: true, &block)` method
228
278
 
229
- Defines the plugins to be included in the editor. You can specify multiple plugins by passing their names as arguments. The keyword arguments are identical to the configuration of the `plugin` method defined below.
279
+ Defines the toolbar items. You can use predefined items like `:undo`, `:redo`, `:|` or specify custom items. There are a few special items:
280
+
281
+ - `:_` - breakpoint
282
+ - `:|` - separator
283
+
284
+ The `should_group_when_full` keyword argument determines whether the toolbar should group items when there is not enough space. It's set to `true` by default.
230
285
 
231
286
  ```rb
232
287
  # config/initializers/ckeditor5.rb
@@ -234,33 +289,43 @@ Defines the plugins to be included in the editor. You can specify multiple plugi
234
289
  config.presets.define :custom do
235
290
  # ... other configuration
236
291
 
237
- plugins :Bold, :Italic, :Underline, :Link
292
+ toolbar :undo, :redo, :|, :heading, :|, :bold, :italic, :underline, :|,
293
+ :link, :insertImage, :ckbox, :mediaEmbed, :insertTable, :blockQuote, :|,
294
+ :bulletedList, :numberedList, :todoList, :outdent, :indent
238
295
  end
239
296
  ```
240
297
 
241
- #### `toolbar(*items, should_group_when_full: true)` method
298
+ Keep in mind that the order of items is important, and you should install the corresponding plugins. You can find the list of available plugins in the [CKEditor 5 documentation](https://ckeditor.com/docs/ckeditor5/latest/framework/architecture/plugins.html).
242
299
 
243
- Defines the toolbar items. You can use predefined items like `:undo`, `:redo`, `:|` or specify custom items. There are a few special items:
300
+ If you want to add or prepend items to the existing toolbar, you can use the block syntax:
244
301
 
245
- - `:_` - breakpoint
246
- - `:|` - separator
302
+ ```rb
303
+ # config/initializers/ckeditor5.rb
247
304
 
248
- The `should_group_when_full` keyword argument determines whether the toolbar should group items when there is not enough space. It's set to `true` by default.
305
+ config.presets.override :default do
306
+ # ... other configuration
307
+
308
+ toolbar do
309
+ append :selectAll, :|, :selectAll, :selectAll
310
+ # Or prepend: prepend :selectAll, :|, :selectAll, :selectAll
311
+ end
312
+ end
313
+ ```
314
+
315
+ If you want to remove items from the toolbar, you can use the `remove` method:
249
316
 
250
317
  ```rb
251
318
  # config/initializers/ckeditor5.rb
252
319
 
253
- config.presets.define :custom do
320
+ config.presets.override :default do
254
321
  # ... other configuration
255
322
 
256
- toolbar :undo, :redo, :|, :heading, :|, :bold, :italic, :underline, :|,
257
- :link, :insertImage, :ckbox, :mediaEmbed, :insertTable, :blockQuote, :|,
258
- :bulletedList, :numberedList, :todoList, :outdent, :indent
323
+ toolbar do
324
+ remove :selectAll, :heading #, ...
325
+ end
259
326
  end
260
327
  ```
261
328
 
262
- Keep in mind that the order of items is important, and you should install the corresponding plugins. You can find the list of available plugins in the [CKEditor 5 documentation](https://ckeditor.com/docs/ckeditor5/latest/framework/architecture/plugins.html).
263
-
264
329
  #### `menubar(visible: true)` method
265
330
 
266
331
  Defines the visibility of the menubar. By default, it's set to `true`.
@@ -335,7 +400,7 @@ config.presets.define :custom do
335
400
  end
336
401
  ```
337
402
 
338
- In order to import a plugin from a custom package, you can pass the `import_name` keyword argument:
403
+ In order to import a plugin from a custom ESM package, you can pass the `import_name` keyword argument:
339
404
 
340
405
  ```rb
341
406
  # config/initializers/ckeditor5.rb
@@ -347,6 +412,57 @@ config.presets.define :custom do
347
412
  end
348
413
  ```
349
414
 
415
+ In order to import a plugin from a custom Window entry, you can pass the `window_name` keyword argument:
416
+
417
+ ```rb
418
+ # config/initializers/ckeditor5.rb
419
+
420
+ config.presets.define :custom do
421
+ # ... other configuration
422
+
423
+ plugin :YourPlugin, window_name: 'YourPlugin'
424
+ end
425
+ ```
426
+
427
+ #### `plugins(*names, **kwargs)` method
428
+
429
+ Defines the plugins to be included in the editor. You can specify multiple plugins by passing their names as arguments. The keyword arguments are identical to the configuration of the `plugin` method defined below.
430
+
431
+ ```rb
432
+ # config/initializers/ckeditor5.rb
433
+
434
+ config.presets.define :custom do
435
+ # ... other configuration
436
+
437
+ plugins :Bold, :Italic, :Underline, :Link
438
+ end
439
+ ```
440
+
441
+ #### `inline_plugin(name, code)` method
442
+
443
+ Use with caution as this is an inline definition of the plugin code, and you can define a custom class or function for the plugin here. The example below shows how to define a custom plugin that highlights the text:
444
+
445
+ ```rb
446
+ # config/initializers/ckeditor5.rb
447
+
448
+ config.presets.define :custom do
449
+ # ... other configuration
450
+
451
+ inline_plugin :MyCustomPlugin, <<~JS
452
+ import { Plugin } from 'ckeditor5';
453
+
454
+ export default class MyCustomPlugin extends Plugin {
455
+ static get pluginName() {
456
+ return 'MyCustomPlugin';
457
+ }
458
+
459
+ init() {
460
+ // ... Your plugin code
461
+ }
462
+ }
463
+ JS
464
+ end
465
+ ```
350
466
  </details>
351
467
 
352
468
  ## Including CKEditor 5 assets 📦
@@ -368,7 +484,7 @@ It has been achieved by using web components, together with import maps, which a
368
484
 
369
485
  ### GPL usage 🆓
370
486
 
371
- If you want to use CKEditor 5 under the GPL license, you can include the assets using the `ckeditor5_assets` helper method with the `version` keyword argument. The example below shows how to include the assets for version `43.3.0`:
487
+ If you want to use CKEditor 5 under the GPL license, you can include the assets using the `ckeditor5_assets` without passing any arguments. However you can pass the `version` keyword argument with the version of CKEditor 5 you want to use:
372
488
 
373
489
  ```erb
374
490
  <!-- app/views/demos/index.html.erb -->
@@ -607,57 +723,301 @@ If you want to use a decoupled editor, you can pass the `type` keyword argument
607
723
  <%= ckeditor5_assets %>
608
724
  <% end %>
609
725
 
610
- <style>
611
- .menubar-container,
612
- .editable-container,
613
- .toolbar-container {
614
- position: relative;
615
- border: 1px solid red;
616
- }
726
+ <%= ckeditor5_editor type: :decoupled, style: 'width: 600px' do %>
727
+ <div class="menubar-container">
728
+ <%= ckeditor5_menubar %>
729
+ </div>
617
730
 
618
- .menubar-container::after,
619
- .editable-container::after,
620
- .toolbar-container::after {
621
- content: attr(class);
622
- position: absolute;
623
- background: red;
624
- color: #fff;
625
- top: 0;
626
- right: 0;
627
- font: 10px/2 monospace;
628
- padding: .1em .3em;
629
- }
731
+ <div class="toolbar-container">
732
+ <%= ckeditor5_toolbar %>
733
+ </div>
734
+
735
+ <div class="editable-container">
736
+ <%= ckeditor5_editable %>
737
+ </div>
738
+ <% end %>
739
+ ```
740
+
741
+ ## How to access editor instance? 🤔
742
+
743
+ You can access the editor instance using plain HTML and JavaScript, as CKEditor 5 is a web component with defined `instance`, `instancePromise` and `editables` properties.
744
+
745
+ For example:
746
+
747
+ ```erb
748
+ <!-- app/views/demos/index.html.erb -->
749
+
750
+ <% content_for :head do %>
751
+ <%= ckeditor5_assets %>
752
+ <% end %>
753
+
754
+ <%= ckeditor5_editor style: 'width: 600px', id: 'editor' %>
755
+ ```
756
+
757
+ ⚠️ Direct access of `instance` property of the web component. Keep in mind it's unsafe and may cause issues if the editor is not loaded yet.
758
+
759
+ ```js
760
+ document.getElementById('editor').instance
761
+ ```
762
+
763
+ 👌 Accessing the editor instance using `instancePromise` property. It's a promise that resolves to the editor instance when the editor is ready.
764
+
765
+ ```js
766
+ document.getElementById('editor').instancePromise.then(editor => {
767
+ console.log(editor);
768
+ });
769
+ ```
770
+
771
+ ✅ Accessing the editor through the `runAfterEditorReady` helper method. It's a safe way to access the editor instance when the editor is ready.
772
+
773
+ ```js
774
+ document.getElementById('editor').runAfterEditorReady(editor => {
775
+ console.log(editor);
776
+ });
777
+ ```
778
+
779
+ ## Events fired by the editor 🔊
780
+
781
+ ### `editor-ready` event
782
+
783
+ The event is fired when the initialization of the editor is completed. You can listen to it using the `editor-ready` event.
784
+
785
+ ```js
786
+ document.getElementById('editor').addEventListener('editor-ready', () => {
787
+ console.log('Editor is ready');
788
+ });
789
+ ```
790
+
791
+ ### `editor-error` event
792
+
793
+ The event is fired when the initialization of the editor fails. You can listen to it using the `editor-error` event.
794
+
795
+ ```js
796
+ document.getElementById('editor').addEventListener('editor-error', () => {
797
+ console.log('Editor has an error');
798
+ });
799
+ ```
800
+
801
+ ## Common Tasks and Solutions 💡
802
+
803
+ This section covers frequent questions and scenarios when working with CKEditor 5 in Rails applications.
804
+
805
+ ### Setting Initial Content 📝
806
+
807
+ ```erb
808
+ <%= ckeditor5_editor initial_data: "<p>Initial content</p>" %>
809
+ ```
810
+
811
+ ### Setting Editor Language 🌐
812
+
813
+ ```rb
814
+ config.presets.override :default do
815
+ translations :pl, :es
816
+ language :pl
817
+ end
818
+ ```
819
+
820
+ ### Integrating with Forms 📋
821
+
822
+ #### Rails form builder integration
823
+
824
+ ```erb
825
+ <%= form_for @post do |f| %>
826
+ <%= f.label :content %>
827
+ <%= f.ckeditor5 :content, required: true, style: 'width: 700px', initial_data: 'Hello World!' %>
828
+ <% end %>
829
+ ```
830
+
831
+ #### Simple form integration
832
+
833
+ ```erb
834
+ <%= simple_form_for :demo, url: '/demos', html: { novalidate: false } do |f| %>
835
+ <div class="form-group">
836
+ <%= f.input :content, as: :ckeditor5, initial_data: 'Hello, World 12!', input_html: { style: 'width: 600px' }, required: true %>
837
+ </div>
838
+
839
+ <div class="form-group mt-3">
840
+ <%= f.button :submit, 'Save', class: 'btn btn-primary' %>
841
+ </div>
842
+ <% end %>
843
+ ```
844
+
845
+ ### Custom Styling 🎨
846
+
847
+ ```erb
848
+ <%= ckeditor5_editor style: 'height: 400px; margin: 20px;' %>
849
+ ```
850
+
851
+ ### Custom plugins 🧩
852
+
853
+ You can create custom plugins for CKEditor 5 using the `inline_plugin` method. It allows you to define a custom class or function inside your preset configuration.
854
+
855
+ The example below shows how to define a custom plugin that allows toggling the highlight of the selected text:
856
+
857
+ ![CKEditor 5 Custom Highlight Plugin in Ruby on Rails application](docs/custom-highlight-plugin.png)
858
+
859
+ ```rb
860
+ # config/initializers/ckeditor5.rb
861
+
862
+ config.presets.define :custom do
863
+ # ... other configuration
630
864
 
631
- .menubar-container,
632
- .toolbar-container {
633
- padding: 1em;
865
+ # 1. You can define it inline like below or in a separate file.
866
+
867
+ # In case if plugin is located in external file (recommended), you can simply import it:
868
+
869
+ # inline_plugin :MyCustomPlugin, <<~JS
870
+ # import MyPlugin from 'app/javascript/custom_plugins/highlight.js';
871
+ # export default MyPlugin;
872
+ # JS
873
+
874
+ # 2. You can also use "window_name" option to import plugin from window object:
875
+
876
+ # plugin :MyPlugin, window_name: 'MyPlugin'
877
+
878
+ # 3. Create JavaScript file in app/javascript/custom_plugins/highlight.js:
879
+ # You can also use "plugin" to import plugin from file using 'import_name' option.
880
+ # Your `my-custom-plugin` must be present in import map.
881
+
882
+ # plugin :MyCustomPlugin, import_name: 'my-custom-plugin'
883
+
884
+ # 4 Create JavaScript file in app/javascript/custom_plugins/highlight.js:
885
+
886
+ # In Ruby initializer you can also load plugin code directly from file:
887
+ plugin :MyCustomPlugin, File.read(
888
+ Rails.root.join('app/javascript/custom_plugins/highlight.js')
889
+ )
890
+
891
+ # 5. Or even define it inline:
892
+ # plugin :MyCustomPlugin, <<~JS
893
+ # import { Plugin } from 'ckeditor5';
894
+ #
895
+ # export default class MyCustomPlugin extends Plugin {
896
+ # // ...
897
+ # }
898
+ # JS
899
+
900
+ # Add item to beginning of the toolbar.
901
+ toolbar do
902
+ prepend :highlight
903
+ end
904
+ end
905
+ ```
906
+
907
+ <details>
908
+ <summary>Example of Custom Highlight Plugin 🎨</summary>
909
+
910
+ ```js
911
+ // app/javascript/custom_plugins/highlight.js
912
+ import { Plugin, Command, ButtonView } from 'ckeditor5';
913
+
914
+ export default class MyCustomPlugin extends Plugin {
915
+ static get pluginName() {
916
+ return 'MyCustomPlugin';
634
917
  }
635
918
 
636
- .editable-container {
637
- padding: 3em;
638
- overflow-y: scroll;
639
- max-height: 300px;
919
+ init() {
920
+ const editor = this.editor;
921
+
922
+ // Define schema for highlight attribute
923
+ editor.model.schema.extend('$text', { allowAttributes: 'highlight' });
924
+
925
+ // Define conversion between model and view
926
+ editor.conversion.attributeToElement({
927
+ model: 'highlight',
928
+ view: {
929
+ name: 'span',
930
+ styles: {
931
+ 'background-color': 'yellow'
932
+ }
933
+ }
934
+ });
935
+
936
+ // Create command that handles highlighting logic
937
+ // Command pattern is used to encapsulate all the logic related to executing an action
938
+ const command = new HighlightCommand(editor);
939
+
940
+ // Register command in editor
941
+ editor.commands.add('highlight', command);
942
+
943
+ // Add UI button
944
+ editor.ui.componentFactory.add('highlight', locale => {
945
+ const view = new ButtonView(locale);
946
+
947
+ // Bind button state to command state using bind method
948
+ // bind() allows to sync button state with command state automatically
949
+ view.bind('isOn').to(command, 'value');
950
+
951
+ view.set({
952
+ label: 'Highlight',
953
+ withText: true,
954
+ tooltip: true
955
+ });
956
+
957
+ view.on('execute', () => {
958
+ editor.execute('highlight');
959
+ editor.editing.view.focus();
960
+ });
961
+
962
+ return view;
963
+ });
640
964
  }
965
+ }
966
+
967
+ // Command class that handles the highlight feature
968
+ // isEnabled property determines if command can be executed
969
+ class HighlightCommand extends Command {
970
+ execute() {
971
+ const model = this.editor.model;
972
+ const selection = model.document.selection;
973
+
974
+ model.change(writer => {
975
+ const ranges = model.schema.getValidRanges(selection.getRanges(), 'highlight');
976
+
977
+ for (const range of ranges) {
978
+ if (this.value) {
979
+ writer.removeAttribute('highlight', range);
980
+ } else {
981
+ writer.setAttribute('highlight', true, range);
982
+ }
983
+ }
984
+ });
985
+ }
986
+
987
+ refresh() {
988
+ const model = this.editor.model;
989
+ const selection = model.document.selection;
990
+ const isAllowed = model.schema.checkAttributeInSelection(selection, 'highlight');
641
991
 
642
- .editable-container .ck-editor__editable {
643
- min-height: 21cm;
644
- padding: 2em;
645
- border: 1px #D3D3D3 solid;
646
- border-radius: var(--ck-border-radius);
647
- background: white;
648
- box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
992
+ // Set if command is enabled based on schema
993
+ this.isEnabled = isAllowed;
994
+ this.value = this.#isHighlightedNodeSelected();
649
995
  }
650
- </style>
651
996
 
652
- <%= ckeditor5_editor type: :decoupled, style: 'width: 600px' do %>
653
- <div class="menubar-container"><%= ckeditor5_menubar %></div>
654
- <br>
655
- <div class="toolbar-container"><%= ckeditor5_toolbar %></div>
656
- <br>
657
- <div class="editable-container"><%= ckeditor5_editable %></div>
658
- <% end %>
997
+ // Check if the highlighted node is selected.
998
+ #isHighlightedNodeSelected() {
999
+ const { model } = this.editor
1000
+ const { schema } = model
1001
+ const selection = model.document.selection
1002
+
1003
+ if (selection.isCollapsed) {
1004
+ return selection.hasAttribute('highlight')
1005
+ }
1006
+
1007
+ return selection.getRanges().some(range =>
1008
+ Array
1009
+ .from(range.getItems())
1010
+ .some(item =>
1011
+ schema.checkAttribute(item, 'highlight') &&
1012
+ item.hasAttribute('highlight')
1013
+ )
1014
+ );
1015
+ }
1016
+ }
659
1017
  ```
660
1018
 
1019
+ </details>
1020
+
661
1021
  ## License 📜
662
1022
 
663
1023
  The MIT License (MIT)
@@ -41,7 +41,7 @@ module CKEditor5::Rails::Assets
41
41
  class JSExportsMeta
42
42
  attr_reader :url, :import_meta
43
43
 
44
- delegate :esm?, :window?, :import_name, :window_name, :import_as, to: :import_meta
44
+ delegate :esm?, :window?, :import_name, :window_name, :import_as, :to_h, to: :import_meta
45
45
 
46
46
  def initialize(url, translation: false, **import_options)
47
47
  @url = url
@@ -63,13 +63,14 @@ module CKEditor5::Rails::Assets
63
63
 
64
64
  def styles_tags
65
65
  @styles_tags ||= safe_join(bundle.stylesheets.map do |url|
66
- tag.link(href: url, rel: 'stylesheet')
66
+ tag.link(href: url, rel: 'stylesheet', crossorigin: 'anonymous')
67
67
  end)
68
68
  end
69
69
 
70
70
  def preload_tags
71
71
  @preload_tags ||= safe_join(bundle.preloads.map do |url|
72
- tag.link(href: url, rel: 'preload', as: self.class.url_resource_preload_type(url))
72
+ tag.link(href: url, rel: 'preload', as: self.class.url_resource_preload_type(url),
73
+ crossorigin: 'anonymous')
73
74
  end)
74
75
  end
75
76
  end
@@ -23,7 +23,8 @@
23
23
  */
24
24
  class CKEditorComponent extends HTMLElement {
25
25
  /**
26
- * List of attributes that trigger updates when changed
26
+ * List of attributes that trigger updates when changed.
27
+ *
27
28
  * @static
28
29
  * @returns {string[]} Array of attribute names to observe
29
30
  */
@@ -31,6 +32,16 @@ class CKEditorComponent extends HTMLElement {
31
32
  return ['config', 'plugins', 'translations', 'type'];
32
33
  }
33
34
 
35
+ /**
36
+ * List of input attributes that trigger updates when changed.
37
+ *
38
+ * @static
39
+ * @returns {string[]} Array of input attribute names to observe
40
+ */
41
+ static get inputAttributes() {
42
+ return ['name', 'required', 'value'];
43
+ }
44
+
34
45
  /** @type {Promise<import('ckeditor5').Editor>|null} Promise to initialize editor instance */
35
46
  instancePromise = Promise.withResolvers();
36
47
 
@@ -134,16 +145,17 @@ class CKEditorComponent extends HTMLElement {
134
145
  content = editablesOrContent.main;
135
146
  }
136
147
 
137
- const instance = await Editor.create(
138
- content,
139
- {
140
- ...this.#getConfig(),
141
- ...translations.length && {
142
- translations
143
- },
144
- plugins,
145
- }
146
- );
148
+ const config = {
149
+ ...this.#getConfig(),
150
+ ...translations.length && {
151
+ translations
152
+ },
153
+ plugins,
154
+ };
155
+
156
+ console.warn('Initializing CKEditor with config:', config);
157
+
158
+ const instance = await Editor.create(content, config);
147
159
 
148
160
  this.dispatchEvent(new CustomEvent('editor-ready', { detail: instance }));
149
161
 
@@ -168,6 +180,7 @@ class CKEditorComponent extends HTMLElement {
168
180
 
169
181
  if (!this.isMultiroot() && !this.isDecoupled()) {
170
182
  this.innerHTML = `<${this.#editorElementTag}></${this.#editorElementTag}>`;
183
+ this.#assignInputAttributes();
171
184
  }
172
185
 
173
186
  // Let's track changes in editables if it's a multiroot editor.
@@ -181,6 +194,8 @@ class CKEditorComponent extends HTMLElement {
181
194
 
182
195
  try {
183
196
  this.instance = await this.#initializeEditor(this.editables || this.#getConfig().initialData || '');
197
+ this.#setupContentSync();
198
+
184
199
  this.instancePromise.resolve(this.instance);
185
200
  } catch (err) {
186
201
  this.instancePromise.reject(err);
@@ -251,6 +266,74 @@ class CKEditorComponent extends HTMLElement {
251
266
  return { main: mainEditable };
252
267
  }
253
268
 
269
+ /**
270
+ * Copies input-related attributes from component to the main editable element
271
+ *
272
+ * @private
273
+ */
274
+ #assignInputAttributes() {
275
+ const textarea = this.querySelector('textarea');
276
+
277
+ if (!textarea) {
278
+ return;
279
+ }
280
+
281
+ for (const attr of CKEditorComponent.inputAttributes) {
282
+ if (this.hasAttribute(attr)) {
283
+ textarea.setAttribute(attr, this.getAttribute(attr));
284
+ }
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Sets up content sync between editor and textarea element.
290
+ *
291
+ * @private
292
+ */
293
+ #setupContentSync() {
294
+ if (!this.instance) {
295
+ return;
296
+ }
297
+
298
+ const textarea = this.querySelector('textarea');
299
+
300
+ if (!textarea) {
301
+ return;
302
+ }
303
+
304
+ // Initial sync
305
+ const syncInput = () => {
306
+ this.style.position = 'relative';
307
+
308
+ textarea.value = this.instance.getData();
309
+ textarea.tabIndex = -1;
310
+
311
+ Object.assign(textarea.style, {
312
+ display: 'flex',
313
+ position: 'absolute',
314
+ bottom: '0',
315
+ left: '50%',
316
+ width: '1px',
317
+ height: '1px',
318
+ opacity: '0',
319
+ pointerEvents: 'none',
320
+ margin: '0',
321
+ padding: '0',
322
+ border: 'none'
323
+ });
324
+ };
325
+
326
+ syncInput();
327
+
328
+ // Listen for changes
329
+ this.instance.model.document.on('change:data', () => {
330
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
331
+ textarea.dispatchEvent(new Event('change', { bubbles: true }));
332
+
333
+ syncInput();
334
+ });
335
+ }
336
+
254
337
  /**
255
338
  * Loads translation modules
256
339
  *
@@ -624,16 +707,48 @@ function execIfDOMReady(callback) {
624
707
  * @returns {Promise<Array<any>>} Loaded modules
625
708
  */
626
709
  function loadAsyncImports(imports = []) {
627
- return Promise.all(
628
- imports.map(async ({ import_name, import_as, window_name }) => {
629
- if (window_name && Object.prototype.hasOwnProperty.call(window, window_name)) {
630
- return window[window_name];
710
+ const loadInlinePlugin = async ({ name, code }) => {
711
+ const module = await import(`data:text/javascript,${encodeURIComponent(code)}`);
712
+
713
+ if (!module.default) {
714
+ throw new Error(`Inline plugin "${name}" must export a default class/function!`);
715
+ }
716
+
717
+ return module.default;
718
+ };
719
+
720
+ const loadExternalPlugin = async ({ import_name, import_as, window_name }) => {
721
+ if (window_name) {
722
+ if (!Object.prototype.hasOwnProperty.call(window, window_name)) {
723
+ throw new Error(
724
+ `Plugin window['${window_name}'] not found in global scope. ` +
725
+ 'Please ensure the plugin is loaded before CKEditor initialization.'
726
+ );
631
727
  }
632
728
 
633
- const module = await import(import_name);
634
- return import_as ? module[import_as] : module.default;
635
- })
636
- );
729
+ return window[window_name];
730
+ }
731
+
732
+ const module = await import(import_name);
733
+ const imported = module[import_as || 'default'];
734
+
735
+ if (!imported) {
736
+ throw new Error(`Plugin "${import_as}" not found in the ESM module "${import_name}"!`);
737
+ }
738
+
739
+ return imported;
740
+ };
741
+
742
+ return Promise.all(imports.map(item => {
743
+ switch(item.type) {
744
+ case 'inline':
745
+ return loadInlinePlugin(item);
746
+
747
+ case 'external':
748
+ default:
749
+ return loadExternalPlugin(item);
750
+ }
751
+ }));
637
752
  }
638
753
 
639
754
  customElements.define('ckeditor-component', CKEditorComponent);
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'props_plugin'
4
+ require_relative 'props_inline_plugin'
4
5
  require_relative 'props'
5
6
 
6
7
  module CKEditor5::Rails
@@ -11,6 +12,7 @@ module CKEditor5::Rails
11
12
  def ckeditor5_editor(
12
13
  config: nil, extra_config: {},
13
14
  type: nil, preset: :default,
15
+ initial_data: nil,
14
16
  **html_attributes, &block
15
17
  )
16
18
  context = validate_and_get_editor_context!
@@ -19,8 +21,11 @@ module CKEditor5::Rails
19
21
  config ||= preset.config
20
22
  type ||= preset.type
21
23
 
24
+ config = config.deep_merge(extra_config)
25
+ config[:initialData] = initial_data if initial_data
26
+
22
27
  editor_props = build_editor_props(
23
- config: config.deep_merge(extra_config),
28
+ config: config,
24
29
  type: type,
25
30
  context: context
26
31
  )
@@ -44,7 +44,7 @@ module CKEditor5::Rails::Editor
44
44
  end
45
45
 
46
46
  def serialize_translations
47
- context[:bundle].translations_scripts.map { |script| script.import_meta.to_h }.to_json
47
+ context[:bundle].translations_scripts.map(&:to_h).to_json
48
48
  end
49
49
 
50
50
  def serialize_plugins
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CKEditor5::Rails::Editor
4
+ class PropsInlinePlugin
5
+ def initialize(name, code)
6
+ @name = name
7
+ @code = code
8
+ validate_code!
9
+ end
10
+
11
+ def to_h
12
+ {
13
+ type: :inline,
14
+ name: name,
15
+ code: code
16
+ }
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :name, :code
22
+
23
+ def validate_code!
24
+ raise ArgumentError, 'Code must be a String' unless code.is_a?(String)
25
+
26
+ return if code.include?('export default')
27
+
28
+ raise ArgumentError,
29
+ 'Code must include `export default` that exports plugin definition!'
30
+ end
31
+ end
32
+ end
@@ -4,30 +4,35 @@ module CKEditor5::Rails::Editor
4
4
  class PropsPlugin
5
5
  delegate :to_h, to: :import_meta
6
6
 
7
- def initialize(name, premium: false, import_name: nil)
7
+ def initialize(name, premium: false, **js_import_meta)
8
8
  @name = name
9
- @premium = premium
10
- @import_name = import_name
11
- @import_name ||= premium ? 'ckeditor5-premium-features' : 'ckeditor5'
9
+ @js_import_meta = if js_import_meta.empty?
10
+ { import_name: premium ? 'ckeditor5-premium-features' : 'ckeditor5' }
11
+ else
12
+ js_import_meta
13
+ end
12
14
  end
13
15
 
14
16
  def self.normalize(plugin)
15
17
  case plugin
16
18
  when String, Symbol then new(plugin)
17
- when PropsPlugin then plugin
19
+ when PropsPlugin, PropsInlinePlugin then plugin
18
20
  else raise ArgumentError, "Invalid plugin: #{plugin}"
19
21
  end
20
22
  end
21
23
 
22
- private
23
-
24
- attr_reader :name, :premium, :import_name
24
+ def to_h
25
+ meta = ::CKEditor5::Rails::Assets::JSImportMeta.new(
26
+ import_as: js_import_meta[:window_name] ? nil : name,
27
+ **js_import_meta
28
+ ).to_h
25
29
 
26
- def import_meta
27
- ::CKEditor5::Rails::Assets::JSImportMeta.new(
28
- import_as: name,
29
- import_name: import_name
30
- )
30
+ meta.merge!({ type: :external })
31
+ meta
31
32
  end
33
+
34
+ private
35
+
36
+ attr_reader :name, :js_import_meta
32
37
  end
33
38
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'rails/engine'
4
4
  require_relative 'presets'
5
+ require_relative 'hooks/form'
5
6
 
6
7
  module CKEditor5::Rails
7
8
  class Engine < ::Rails::Engine
@@ -16,6 +17,22 @@ module CKEditor5::Rails
16
17
  end
17
18
  end
18
19
 
20
+ initializer 'ckeditor5.simple_form' do
21
+ next unless defined?(::SimpleForm)
22
+
23
+ require_relative 'hooks/simple_form'
24
+
25
+ ::SimpleForm::FormBuilder.map_type :ckeditor5, to: Hooks::SimpleForm::CKEditor5Input
26
+ end
27
+
28
+ initializer 'ckeditor5.form_builder' do
29
+ require_relative 'hooks/form'
30
+
31
+ ActionView::Helpers::FormBuilder.include(
32
+ Hooks::Form::FormBuilderExtension
33
+ )
34
+ end
35
+
19
36
  def self.base
20
37
  config.ckeditor5
21
38
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CKEditor5::Rails::Hooks
4
+ module Form
5
+ module FormBuilderExtension
6
+ def ckeditor5(method, options = {})
7
+ value = if object.respond_to?(method)
8
+ object.send(method)
9
+ else
10
+ options[:initial_data]
11
+ end
12
+
13
+ html_options = options.merge(
14
+ name: object_name,
15
+ required: options.delete(:required),
16
+ initial_data: value
17
+ )
18
+
19
+ @template.ckeditor5_editor(**html_options)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CKEditor5::Rails::Hooks
4
+ module SimpleForm
5
+ class CKEditor5Input < ::SimpleForm::Inputs::Base
6
+ def input(wrapper_options = nil)
7
+ merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
8
+ @builder.template.ckeditor5_editor(**editor_options(merged_input_options))
9
+ end
10
+
11
+ private
12
+
13
+ def editor_options(merged_input_options)
14
+ {
15
+ preset: input_options.fetch(:preset, :default),
16
+ type: input_options.fetch(:type, :classic),
17
+ config: input_options[:config],
18
+ initial_data: object.try(attribute_name) || input_options[:initial_data],
19
+ name: "#{object_name}[#{attribute_name}]",
20
+ **merged_input_options
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -29,7 +29,7 @@ module CKEditor5::Rails
29
29
 
30
30
  private
31
31
 
32
- def define_default_preset
32
+ def define_default_preset # rubocop:disable Metrics/MethodLength
33
33
  define :default do
34
34
  gpl
35
35
 
@@ -38,11 +38,11 @@ module CKEditor5::Rails
38
38
  menubar
39
39
 
40
40
  toolbar :undo, :redo, :|, :heading, :|, :bold, :italic, :underline, :|,
41
- :link, :insertImage, :ckbox, :mediaEmbed, :insertTable, :blockQuote, :|,
41
+ :link, :insertImage, :mediaEmbed, :insertTable, :blockQuote, :|,
42
42
  :bulletedList, :numberedList, :todoList, :outdent, :indent
43
43
 
44
44
  plugins :AccessibilityHelp, :Autoformat, :AutoImage, :Autosave,
45
- :BlockQuote, :Bold, :CKBox, :CKBoxImageEdit, :CloudServices,
45
+ :BlockQuote, :Bold, :CloudServices,
46
46
  :Essentials, :Heading, :ImageBlock, :ImageCaption, :ImageInline,
47
47
  :ImageInsert, :ImageInsertViaUrl, :ImageResize, :ImageStyle,
48
48
  :ImageTextAlternative, :ImageToolbar, :ImageUpload, :Indent,
@@ -51,6 +51,10 @@ module CKEditor5::Rails
51
51
  :SelectAll, :Table, :TableCaption, :TableCellProperties,
52
52
  :TableColumnResize, :TableProperties, :TableToolbar,
53
53
  :TextTransformation, :TodoList, :Underline, :Undo, :Base64UploadAdapter
54
+
55
+ configure :image, {
56
+ toolbar: ['imageTextAlternative', 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side']
57
+ }
54
58
  end
55
59
  end
56
60
  end
@@ -144,11 +148,22 @@ module CKEditor5::Rails
144
148
  }
145
149
  end
146
150
 
147
- def toolbar(*items, should_group_when_full: true)
148
- @config[:toolbar] = {
149
- items: items,
150
- shouldNotGroupWhenFull: !should_group_when_full
151
- }
151
+ def toolbar(*items, should_group_when_full: true, &block)
152
+ if @config[:toolbar].blank? || !items.empty?
153
+ @config[:toolbar] = {
154
+ items: items,
155
+ shouldNotGroupWhenFull: !should_group_when_full
156
+ }
157
+ end
158
+
159
+ return unless block
160
+
161
+ builder = ToolbarBuilder.new(@config[:toolbar])
162
+ builder.instance_eval(&block)
163
+ end
164
+
165
+ def inline_plugin(name, code)
166
+ @config[:plugins] << Editor::PropsInlinePlugin.new(name, code)
152
167
  end
153
168
 
154
169
  def plugin(name, **kwargs)
@@ -166,4 +181,40 @@ module CKEditor5::Rails
166
181
  }
167
182
  end
168
183
  end
184
+
185
+ class ToolbarBuilder
186
+ def initialize(toolbar_config)
187
+ @toolbar_config = toolbar_config
188
+ end
189
+
190
+ def items
191
+ @toolbar_config[:items]
192
+ end
193
+
194
+ def remove(*removed_items)
195
+ removed_items.each { |item| items.delete(item) }
196
+ end
197
+
198
+ def prepend(*prepended_items, before: nil)
199
+ if before
200
+ index = items.index(before)
201
+ raise ArgumentError, "Item '#{before}' not found in toolbar" unless index
202
+
203
+ items.insert(index, *prepended_items)
204
+ else
205
+ items.insert(0, *prepended_items)
206
+ end
207
+ end
208
+
209
+ def append(*appended_items, after: nil)
210
+ if after
211
+ index = items.index(after)
212
+ raise ArgumentError, "Item '#{after}' not found in toolbar" unless index
213
+
214
+ items.insert(index + 1, *appended_items)
215
+ else
216
+ items.push(*appended_items)
217
+ end
218
+ end
219
+ end
169
220
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CKEditor5::Rails
4
- VERSION = '1.0.6'
4
+ VERSION = '1.1.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ckeditor5
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mateusz Bagiński
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-10-31 00:00:00.000000000 Z
12
+ date: 2024-11-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -53,9 +53,12 @@ files:
53
53
  - lib/ckeditor5/rails/cloud/helpers.rb
54
54
  - lib/ckeditor5/rails/editor/helpers.rb
55
55
  - lib/ckeditor5/rails/editor/props.rb
56
+ - lib/ckeditor5/rails/editor/props_inline_plugin.rb
56
57
  - lib/ckeditor5/rails/editor/props_plugin.rb
57
58
  - lib/ckeditor5/rails/engine.rb
58
59
  - lib/ckeditor5/rails/helpers.rb
60
+ - lib/ckeditor5/rails/hooks/form.rb
61
+ - lib/ckeditor5/rails/hooks/simple_form.rb
59
62
  - lib/ckeditor5/rails/presets.rb
60
63
  - lib/ckeditor5/rails/semver.rb
61
64
  - lib/ckeditor5/rails/version.rb