ckeditor5 1.0.6 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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