ckeditor5 1.0.6 → 1.1.0

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: b14779f0c4c2e4358c77ed004b6c5ef191b2ff766fc95ba6c8f0245f7314f206
4
+ data.tar.gz: 0a47a7b166356002ef4b25879e420b42693bc1851596da4b4db1382d704fc875
5
5
  SHA512:
6
- metadata.gz: f4f599b202ef0c109e14ff12c25f09890808d53e2116b2d3b6e51dfbfc5f389ea1d62651aa1a493fb897838e56ca2f3345addee8eebd55f5c2b7126f55169ce3
7
- data.tar.gz: fc254a7f703defae55c901a444329772e177dfbfc8d0ee4ea3864c6c516b1c36f64e21166ec2e787d79ce368e09981ad524f4187842f31b53b8084ab690ed13b
6
+ metadata.gz: c7d8727f46f14c759f181dc77e19d4778efdb34e19327667922df06a7dcaa65d2512a4106d9dc0cf2f8810c7cb03b51e8cf440bda521f2cd237f7535447ac644
7
+ data.tar.gz: f6b08e3fa6292af226cdc2e29867ec5ffaa52cdba2a3ca6843735e4672e6d9411a7a30520b007a4cd267fa7dc69e380de8135831767d396674b4f768efc19737
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
  ```
@@ -146,9 +177,23 @@ config.presets.define :custom do
146
177
  end
147
178
  ```
148
179
 
180
+ #### `license_key(key)` method
181
+
182
+ Defines the license key of CKEditor 5. It calls `premium` method internally. The example below shows how to set the license key:
183
+
184
+ ```rb
185
+ # config/initializers/ckeditor5.rb
186
+
187
+ config.presets.define :custom do
188
+ # ... other configuration
189
+
190
+ license_key 'your-license-key'
191
+ end
192
+ ```
193
+
149
194
  #### `premium` method
150
195
 
151
- Defines if premium package (`ckeditor5-premium-features`) should be used.
196
+ Defines if premium package should be included in JS assets. The example below shows how to add `ckeditor5-premium-features` to import maps:
152
197
 
153
198
  ```rb
154
199
  # config/initializers/ckeditor5.rb
@@ -162,7 +207,7 @@ end
162
207
 
163
208
  #### `translations(*languages)` method
164
209
 
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:
210
+ 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
211
 
167
212
  ```rb
168
213
  # config/initializers/ckeditor5.rb
@@ -174,17 +219,15 @@ config.presets.define :custom do
174
219
  end
175
220
  ```
176
221
 
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:
222
+ ⚠️ 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
223
 
181
224
  ```rb
182
225
  # config/initializers/ckeditor5.rb
183
226
 
184
227
  config.presets.define :custom do
185
- # ... other configuration
228
+ translations :pl
186
229
 
187
- license_key 'your-license-key'
230
+ language :pl
188
231
  end
189
232
  ```
190
233
 
@@ -224,21 +267,7 @@ config.presets.define :custom do
224
267
  end
225
268
  ```
226
269
 
227
- #### `plugins(*names, **kwargs)` method
228
-
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.
230
-
231
- ```rb
232
- # config/initializers/ckeditor5.rb
233
-
234
- config.presets.define :custom do
235
- # ... other configuration
236
-
237
- plugins :Bold, :Italic, :Underline, :Link
238
- end
239
- ```
240
-
241
- #### `toolbar(*items, should_group_when_full: true)` method
270
+ #### `toolbar(*items, should_group_when_full: true, &block)` method
242
271
 
243
272
  Defines the toolbar items. You can use predefined items like `:undo`, `:redo`, `:|` or specify custom items. There are a few special items:
244
273
 
@@ -261,6 +290,21 @@ end
261
290
 
262
291
  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
292
 
293
+ If you want to add or prepend items to the existing toolbar, you can use the block syntax:
294
+
295
+ ```rb
296
+ # config/initializers/ckeditor5.rb
297
+
298
+ config.presets.override :default do
299
+ # ... other configuration
300
+
301
+ toolbar do
302
+ append :selectAll, :|, :selectAll, :selectAll
303
+ # Or prepend: prepend :selectAll, :|, :selectAll, :selectAll
304
+ end
305
+ end
306
+ ```
307
+
264
308
  #### `menubar(visible: true)` method
265
309
 
266
310
  Defines the visibility of the menubar. By default, it's set to `true`.
@@ -335,7 +379,7 @@ config.presets.define :custom do
335
379
  end
336
380
  ```
337
381
 
338
- In order to import a plugin from a custom package, you can pass the `import_name` keyword argument:
382
+ In order to import a plugin from a custom ESM package, you can pass the `import_name` keyword argument:
339
383
 
340
384
  ```rb
341
385
  # config/initializers/ckeditor5.rb
@@ -347,6 +391,57 @@ config.presets.define :custom do
347
391
  end
348
392
  ```
349
393
 
394
+ In order to import a plugin from a custom Window entry, you can pass the `window_name` keyword argument:
395
+
396
+ ```rb
397
+ # config/initializers/ckeditor5.rb
398
+
399
+ config.presets.define :custom do
400
+ # ... other configuration
401
+
402
+ plugin :YourPlugin, window_name: 'YourPlugin'
403
+ end
404
+ ```
405
+
406
+ #### `plugins(*names, **kwargs)` method
407
+
408
+ 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.
409
+
410
+ ```rb
411
+ # config/initializers/ckeditor5.rb
412
+
413
+ config.presets.define :custom do
414
+ # ... other configuration
415
+
416
+ plugins :Bold, :Italic, :Underline, :Link
417
+ end
418
+ ```
419
+
420
+ #### `inline_plugin(name, code)` method
421
+
422
+ 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:
423
+
424
+ ```rb
425
+ # config/initializers/ckeditor5.rb
426
+
427
+ config.presets.define :custom do
428
+ # ... other configuration
429
+
430
+ inline_plugin :MyCustomPlugin, <<~JS
431
+ import { Plugin } from 'ckeditor5';
432
+
433
+ export default class MyCustomPlugin extends Plugin {
434
+ static get pluginName() {
435
+ return 'MyCustomPlugin';
436
+ }
437
+
438
+ init() {
439
+ // ... Your plugin code
440
+ }
441
+ }
442
+ JS
443
+ end
444
+ ```
350
445
  </details>
351
446
 
352
447
  ## Including CKEditor 5 assets 📦
@@ -368,7 +463,7 @@ It has been achieved by using web components, together with import maps, which a
368
463
 
369
464
  ### GPL usage 🆓
370
465
 
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`:
466
+ 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
467
 
373
468
  ```erb
374
469
  <!-- app/views/demos/index.html.erb -->
@@ -607,57 +702,301 @@ If you want to use a decoupled editor, you can pass the `type` keyword argument
607
702
  <%= ckeditor5_assets %>
608
703
  <% end %>
609
704
 
610
- <style>
611
- .menubar-container,
612
- .editable-container,
613
- .toolbar-container {
614
- position: relative;
615
- border: 1px solid red;
616
- }
705
+ <%= ckeditor5_editor type: :decoupled, style: 'width: 600px' do %>
706
+ <div class="menubar-container">
707
+ <%= ckeditor5_menubar %>
708
+ </div>
617
709
 
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
- }
710
+ <div class="toolbar-container">
711
+ <%= ckeditor5_toolbar %>
712
+ </div>
713
+
714
+ <div class="editable-container">
715
+ <%= ckeditor5_editable %>
716
+ </div>
717
+ <% end %>
718
+ ```
719
+
720
+ ## How to access editor instance? 🤔
721
+
722
+ 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.
723
+
724
+ For example:
725
+
726
+ ```erb
727
+ <!-- app/views/demos/index.html.erb -->
728
+
729
+ <% content_for :head do %>
730
+ <%= ckeditor5_assets %>
731
+ <% end %>
732
+
733
+ <%= ckeditor5_editor style: 'width: 600px', id: 'editor' %>
734
+ ```
735
+
736
+ ⚠️ 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.
737
+
738
+ ```js
739
+ document.getElementById('editor').instance
740
+ ```
741
+
742
+ 👌 Accessing the editor instance using `instancePromise` property. It's a promise that resolves to the editor instance when the editor is ready.
743
+
744
+ ```js
745
+ document.getElementById('editor').instancePromise.then(editor => {
746
+ console.log(editor);
747
+ });
748
+ ```
749
+
750
+ ✅ Accessing the editor through the `runAfterEditorReady` helper method. It's a safe way to access the editor instance when the editor is ready.
751
+
752
+ ```js
753
+ document.getElementById('editor').runAfterEditorReady(editor => {
754
+ console.log(editor);
755
+ });
756
+ ```
757
+
758
+ ## Events fired by the editor 🔊
759
+
760
+ ### `editor-ready` event
761
+
762
+ The event is fired when the initialization of the editor is completed. You can listen to it using the `editor-ready` event.
763
+
764
+ ```js
765
+ document.getElementById('editor').addEventListener('editor-ready', () => {
766
+ console.log('Editor is ready');
767
+ });
768
+ ```
769
+
770
+ ### `editor-error` event
771
+
772
+ The event is fired when the initialization of the editor fails. You can listen to it using the `editor-error` event.
773
+
774
+ ```js
775
+ document.getElementById('editor').addEventListener('editor-error', () => {
776
+ console.log('Editor has an error');
777
+ });
778
+ ```
779
+
780
+ ## Common Tasks and Solutions 💡
781
+
782
+ This section covers frequent questions and scenarios when working with CKEditor 5 in Rails applications.
783
+
784
+ ### Setting Initial Content 📝
785
+
786
+ ```erb
787
+ <%= ckeditor5_editor initial_data: "<p>Initial content</p>" %>
788
+ ```
789
+
790
+ ### Setting Editor Language 🌐
791
+
792
+ ```rb
793
+ config.presets.override :default do
794
+ translations :pl, :es
795
+ language :pl
796
+ end
797
+ ```
798
+
799
+ ### Integrating with Forms 📋
630
800
 
631
- .menubar-container,
632
- .toolbar-container {
633
- padding: 1em;
801
+ #### Rails form builder integration
802
+
803
+ ```erb
804
+ <%= form_for @post do |f| %>
805
+ <%= f.label :content %>
806
+ <%= f.ckeditor5 :content, required: true, style: 'width: 700px', initial_data: 'Hello World!' %>
807
+ <% end %>
808
+ ```
809
+
810
+ #### Simple form integration
811
+
812
+ ```erb
813
+ <%= simple_form_for :demo, url: '/demos', html: { novalidate: false } do |f| %>
814
+ <div class="form-group">
815
+ <%= f.input :content, as: :ckeditor5, initial_data: 'Hello, World 12!', input_html: { style: 'width: 600px' }, required: true %>
816
+ </div>
817
+
818
+ <div class="form-group mt-3">
819
+ <%= f.button :submit, 'Save', class: 'btn btn-primary' %>
820
+ </div>
821
+ <% end %>
822
+ ```
823
+
824
+ ### Custom Styling 🎨
825
+
826
+ ```erb
827
+ <%= ckeditor5_editor style: 'height: 400px; margin: 20px;' %>
828
+ ```
829
+
830
+ ### Custom plugins 🧩
831
+
832
+ 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.
833
+
834
+ The example below shows how to define a custom plugin that allows toggling the highlight of the selected text:
835
+
836
+ ![CKEditor 5 Custom Highlight Plugin in Ruby on Rails application](docs/custom-highlight-plugin.png)
837
+
838
+ ```rb
839
+ # config/initializers/ckeditor5.rb
840
+
841
+ config.presets.define :custom do
842
+ # ... other configuration
843
+
844
+ # 1. You can define it inline like below or in a separate file.
845
+
846
+ # In case if plugin is located in external file (recommended), you can simply import it:
847
+
848
+ # inline_plugin :MyCustomPlugin, <<~JS
849
+ # import MyPlugin from 'app/javascript/custom_plugins/highlight.js';
850
+ # export default MyPlugin;
851
+ # JS
852
+
853
+ # 2. You can also use "window_name" option to import plugin from window object:
854
+
855
+ # plugin :MyPlugin, window_name: 'MyPlugin'
856
+
857
+ # 3. Create JavaScript file in app/javascript/custom_plugins/highlight.js:
858
+ # You can also use "plugin" to import plugin from file using 'import_name' option.
859
+ # Your `my-custom-plugin` must be present in import map.
860
+
861
+ # plugin :MyCustomPlugin, import_name: 'my-custom-plugin'
862
+
863
+ # 4 Create JavaScript file in app/javascript/custom_plugins/highlight.js:
864
+
865
+ # In Ruby initializer you can also load plugin code directly from file:
866
+ plugin :MyCustomPlugin, File.read(
867
+ Rails.root.join('app/javascript/custom_plugins/highlight.js')
868
+ )
869
+
870
+ # 5. Or even define it inline:
871
+ # plugin :MyCustomPlugin, <<~JS
872
+ # import { Plugin } from 'ckeditor5';
873
+ #
874
+ # export default class MyCustomPlugin extends Plugin {
875
+ # // ...
876
+ # }
877
+ # JS
878
+
879
+ # Add item to beginning of the toolbar.
880
+ toolbar do
881
+ prepend :highlight
882
+ end
883
+ end
884
+ ```
885
+
886
+ <details>
887
+ <summary>Example of Custom Highlight Plugin 🎨</summary>
888
+
889
+ ```js
890
+ // app/javascript/custom_plugins/highlight.js
891
+ import { Plugin, Command, ButtonView } from 'ckeditor5';
892
+
893
+ export default class MyCustomPlugin extends Plugin {
894
+ static get pluginName() {
895
+ return 'MyCustomPlugin';
634
896
  }
635
897
 
636
- .editable-container {
637
- padding: 3em;
638
- overflow-y: scroll;
639
- max-height: 300px;
898
+ init() {
899
+ const editor = this.editor;
900
+
901
+ // Define schema for highlight attribute
902
+ editor.model.schema.extend('$text', { allowAttributes: 'highlight' });
903
+
904
+ // Define conversion between model and view
905
+ editor.conversion.attributeToElement({
906
+ model: 'highlight',
907
+ view: {
908
+ name: 'span',
909
+ styles: {
910
+ 'background-color': 'yellow'
911
+ }
912
+ }
913
+ });
914
+
915
+ // Create command that handles highlighting logic
916
+ // Command pattern is used to encapsulate all the logic related to executing an action
917
+ const command = new HighlightCommand(editor);
918
+
919
+ // Register command in editor
920
+ editor.commands.add('highlight', command);
921
+
922
+ // Add UI button
923
+ editor.ui.componentFactory.add('highlight', locale => {
924
+ const view = new ButtonView(locale);
925
+
926
+ // Bind button state to command state using bind method
927
+ // bind() allows to sync button state with command state automatically
928
+ view.bind('isOn').to(command, 'value');
929
+
930
+ view.set({
931
+ label: 'Highlight',
932
+ withText: true,
933
+ tooltip: true
934
+ });
935
+
936
+ view.on('execute', () => {
937
+ editor.execute('highlight');
938
+ editor.editing.view.focus();
939
+ });
940
+
941
+ return view;
942
+ });
640
943
  }
944
+ }
945
+
946
+ // Command class that handles the highlight feature
947
+ // isEnabled property determines if command can be executed
948
+ class HighlightCommand extends Command {
949
+ execute() {
950
+ const model = this.editor.model;
951
+ const selection = model.document.selection;
952
+
953
+ model.change(writer => {
954
+ const ranges = model.schema.getValidRanges(selection.getRanges(), 'highlight');
955
+
956
+ for (const range of ranges) {
957
+ if (this.value) {
958
+ writer.removeAttribute('highlight', range);
959
+ } else {
960
+ writer.setAttribute('highlight', true, range);
961
+ }
962
+ }
963
+ });
964
+ }
965
+
966
+ refresh() {
967
+ const model = this.editor.model;
968
+ const selection = model.document.selection;
969
+ const isAllowed = model.schema.checkAttributeInSelection(selection, 'highlight');
641
970
 
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);
971
+ // Set if command is enabled based on schema
972
+ this.isEnabled = isAllowed;
973
+ this.value = this.#isHighlightedNodeSelected();
649
974
  }
650
- </style>
651
975
 
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 %>
976
+ // Check if the highlighted node is selected.
977
+ #isHighlightedNodeSelected() {
978
+ const { model } = this.editor
979
+ const { schema } = model
980
+ const selection = model.document.selection
981
+
982
+ if (selection.isCollapsed) {
983
+ return selection.hasAttribute('highlight')
984
+ }
985
+
986
+ return selection.getRanges().some(range =>
987
+ Array
988
+ .from(range.getItems())
989
+ .some(item =>
990
+ schema.checkAttribute(item, 'highlight') &&
991
+ item.hasAttribute('highlight')
992
+ )
993
+ );
994
+ }
995
+ }
659
996
  ```
660
997
 
998
+ </details>
999
+
661
1000
  ## License 📜
662
1001
 
663
1002
  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,36 @@ 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 prepend(*items, before: nil)
191
+ toolbar_items = @toolbar_config[:items]
192
+
193
+ if before
194
+ index = toolbar_items.index(before)
195
+ raise ArgumentError, "Item '#{before}' not found in toolbar" unless index
196
+
197
+ toolbar_items.insert(index, *items)
198
+ else
199
+ toolbar_items.insert(0, *items)
200
+ end
201
+ end
202
+
203
+ def append(*items, after: nil)
204
+ toolbar_items = @toolbar_config[:items]
205
+
206
+ if after
207
+ index = toolbar_items.index(after)
208
+ raise ArgumentError, "Item '#{after}' not found in toolbar" unless index
209
+
210
+ toolbar_items.insert(index + 1, *items)
211
+ else
212
+ toolbar_items.push(*items)
213
+ end
214
+ end
215
+ end
169
216
  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.0'
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.0
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