From 105a4d380af39ab02d527a0f2c55a302ca6af29e Mon Sep 17 00:00:00 2001
From: Snater <git@snater.com>
Date: Wed, 28 Feb 2024 18:18:37 +0100
Subject: [PATCH] Issue #3314446: CKEditor 5 compatibility

---
 css/ImageHandler.css                          |  13 +-
 insert.libraries.yml                          |  64 +-
 insert.module                                 |  73 +--
 js/EditorInterface.js                         | 123 ----
 js/EditorManager.js                           | 152 -----
 js/FileHandler.js                             |  50 +-
 js/FocusManager.js                            | 295 ++++-----
 js/Handler.js                                 | 569 +++++++++++-------
 js/ImageHandler.js                            | 444 ++++----------
 js/Inserter.js                                | 257 +++-----
 js/Rotator.js                                 | 234 -------
 js/editors/CKEditor.js                        | 184 ------
 js/editors/namespace.js                       |   6 -
 js/insert.api.js                              |  66 --
 js/insert.js                                  | 146 +----
 .../insert_responsive_image.module            |  10 +-
 templates/insert-button-widget.html.twig      |  36 +-
 templates/insert-image.html.twig              |   4 +-
 .../InsertImageCKEditorTest.php               |  54 +-
 .../InsertImageCKEditorTestBase.php           |   5 +-
 .../FunctionalJavaScript/InsertImageTest.php  |  51 +-
 21 files changed, 748 insertions(+), 2088 deletions(-)
 delete mode 100644 js/EditorInterface.js
 delete mode 100644 js/EditorManager.js
 delete mode 100644 js/Rotator.js
 delete mode 100644 js/editors/CKEditor.js
 delete mode 100644 js/editors/namespace.js
 delete mode 100644 js/insert.api.js

diff --git a/css/ImageHandler.css b/css/ImageHandler.css
index f0dff76..fd50d15 100644
--- a/css/ImageHandler.css
+++ b/css/ImageHandler.css
@@ -1,5 +1,4 @@
-.insert-rotate,
-.insert-align {
+.insert-rotate {
   margin-right: 1em;
 }
 
@@ -8,14 +7,6 @@
   position: relative;
 }
 
-.insert-button-wrapper .insert-button-overlay {
-  display: none;
-  height: 100%;
-  position: absolute;
-  width: 100%;
-  z-index: 10;
-}
-
 .insert-required {
   outline: 2px solid red;
-}
+}
\ No newline at end of file
diff --git a/insert.libraries.yml b/insert.libraries.yml
index bfdddc9..6607502 100644
--- a/insert.libraries.yml
+++ b/insert.libraries.yml
@@ -1,108 +1,60 @@
 insert:
-  version: 2.x
+  version: 3.x
   js:
     js/insert.js: {}
   dependencies:
     - core/drupalSettings
-    - core/jquery
     - insert/namespace
-    - insert/editors
-    - insert/EditorManager
     - insert/FileHandler
     - insert/FocusManager
     - insert/ImageHandler
     - insert/Inserter
 
 Inserter:
-  version: 2.x
+  version: 3.x
   js:
     js/Inserter.js: {}
   dependencies:
-    - core/jquery
     - insert/namespace
 
 Handler:
-  version: 2.x
+  version: 3.x
   js:
     js/Handler.js: {}
   dependencies:
-    - core/jquery
     - insert/namespace
 
 FileHandler:
-  version: 2.x
+  version: 3.x
   js:
     js/FileHandler.js: {}
   css:
     theme:
       css/FileHandler.css: {}
   dependencies:
-    - core/jquery
     - insert/Handler
     - insert/namespace
 
 ImageHandler:
-  version: 2.x
+  version: 3.x
   js:
     js/ImageHandler.js: {}
   css:
     theme:
       css/ImageHandler.css: {}
   dependencies:
-    - core/jquery
     - insert/FileHandler
     - insert/namespace
     - insert/Rotator
 
-editors:
-  version: 2.x
-  js:
-    js/editors/CKEditor.js: {}
-  dependencies:
-    - core/ckeditor
-    - insert/editors.namespace
-    - insert/EditorInterface
-
-editors.namespace:
-  version: 2.x
-  js:
-    js/editors/namespace.js: {}
-  dependencies:
-    - insert/namespace
-
-EditorInterface:
-  version: 2.x
-  js:
-    js/EditorInterface.js: {}
-  dependencies:
-    - core/jquery
-    - insert/namespace
-
-EditorManager:
-  version: 2.x
-  js:
-    js/EditorManager.js: {}
-  dependencies:
-    - core/jquery
-    - insert/namespace
-
 FocusManager:
-  version: 2.x
+  version: 3.x
   js:
     js/FocusManager.js: {}
   dependencies:
-    - core/jquery
-    - insert/namespace
-
-Rotator:
-  version: 2.x
-  js:
-    js/Rotator.js: {}
-  dependencies:
-    - core/jquery
     - insert/namespace
 
 namespace:
-  version: 2.x
+  version: 3.x
   js:
-    js/namespace.js: {}
+    js/namespace.js: {}
\ No newline at end of file
diff --git a/insert.module b/insert.module
index 3925d4b..2561b40 100644
--- a/insert.module
+++ b/insert.module
@@ -30,10 +30,7 @@ const INSERT_DEFAULT_SETTINGS = [
   'default' => 'insert__auto',
   'auto_image_style' => 'image',
   'link_image' => NULL,
-  'caption' => FALSE,
   'width' => '',
-  'align' => FALSE,
-  'rotate' => FALSE,
 ];
 
 /**
@@ -101,8 +98,14 @@ function insert_form_alter(array &$form, FormStateInterface $form_state, $form_i
 
     // Ensure attributes set by Insert are not stripped from the output:
     if (in_array($form['format']['#default_value'], $text_formats)) {
-      $form['filters']['settings']['filter_html']['allowed_html']['#element_validate'][]
-        = '_insert_allowed_html_validate';
+      if (isset($form['filters']['settings']['filter_html'])) {
+        $form['filters']['settings']['filter_html']['allowed_html']['#element_validate'][]
+          = '_insert_allowed_html_validate';
+      }
+      if (isset($form['editor']['settings']['subform']['plugins']['ckeditor5_sourceEditing'])) {
+        $form['editor']['settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags']['#element_validate'][]
+          = '_insert_allowed_html_validate';
+      }
     }
   }
 }
@@ -142,8 +145,7 @@ function _insert_field_process(array $element, FormStateInterface $form_state, a
   $insertType = $element['#insert']['type'];
 
   $element['insert'] = [
-    '#type' => $element['#insert']['settings']['align']
-    ? 'fieldset' : 'container',
+    '#type' => 'container',
     '#attributes' => [
       'class' => [
         'insert',
@@ -671,14 +673,6 @@ function _insert_settings_form(array $settings, $insertType) {
       '#weight' => 25,
     ];
 
-    $element['caption'] = [
-      '#type' => 'checkbox',
-      '#title' => t('Apply caption to images'),
-      '#default_value' => $settings['caption'],
-      '#description' => t('Applies a <code>data-caption</code> attribute to images. Transformation into an actual caption is performed by the caption filter that needs to be enabled for the text format in use. See text format configuration per @content_authoring and ensure <em>Caption images</em> in the <em>Enabled filters</em> section is checked. By default, the image title input field content will be used as caption.', ['@content_authoring' => Link::fromTextAndUrl(t('content authoring admin page'), Url::fromRoute('filter.admin_overview'))->toString()]),
-      '#weight' => 25,
-    ];
-
     $element['width'] = [
       '#title' => t('Maximum image insert width'),
       '#type' => 'textfield',
@@ -688,22 +682,6 @@ function _insert_settings_form(array $settings, $insertType) {
       '#description' => t('When inserting images, the height and width of images may be scaled down to fit within the specified width. Note that this does not resize the image, it only affects the HTML output.'),
       '#weight' => 26,
     ];
-
-    $element['align'] = [
-      '#type' => 'checkbox',
-      '#title' => t('Alignment controls'),
-      '#default_value' => $settings['align'],
-      '#description' => t('Alignment may be applied using radio buttons.'),
-      '#weight' => 27,
-    ];
-
-    $element['rotate'] = [
-      '#type' => 'checkbox',
-      '#title' => t('Rotation controls'),
-      '#default_value' => $settings['rotate'],
-      '#description' => t('The image may be rotated by using rotation controls.'),
-      '#weight' => 28,
-    ];
   }
 
   return $element;
@@ -819,27 +797,6 @@ function insert_editor_js_settings_alter(array &$settings) {
     'audio[contenteditable,controls,src,type,data-insert-attach]',
     'video[contenteditable,controls,src,type,data-insert-attach]',
   ]);
-
-  foreach (array_keys($settings['editor']['formats']) as $text_format_id) {
-    if (!isset($settings['editor']['formats'][$text_format_id]['editorSettings']['drupalExternalPlugins'])) {
-      continue;
-    }
-    // If drupalimage is disabled (the editor's image button is removed),
-    // still, CKEditor's naive image2 plugin wraps the image in a span tag
-    // blocking from applying a style to the image making it impossible to set
-    // alignment on the image. Therefore, the image2 plugin needs to be disabled
-    // in that case.
-    // With drupalimage enabled, the user has to set alignment using the
-    // editor's image button; With drupalimage disabled, the user has to set
-    // alignment using the styles drop-down which allows applying additional
-    // styles to images as well.
-    if (!in_array(
-      'drupalimage',
-      array_keys($settings['editor']['formats'][$text_format_id]['editorSettings']['drupalExternalPlugins'])
-    )) {
-      $settings['editor']['formats'][$text_format_id]['editorSettings']['removePlugins'] = 'image2';
-    }
-  }
 }
 
 /**
@@ -858,6 +815,8 @@ function _insert_allowed_html_validate(array $element, FormStateInterface &$form
   $attributes = [
     'img' => [
       'class' => NULL,
+      'data-insert-attach' => NULL,
+      'data-insert-type' => NULL,
       'src' => NULL,
       'width' => NULL,
       'height' => NULL,
@@ -866,6 +825,8 @@ function _insert_allowed_html_validate(array $element, FormStateInterface &$form
     ],
     'a' => [
       'class' => NULL,
+      'data-insert-attach' => NULL,
+      'data-insert-type' => NULL,
       'title' => NULL,
       'type' => NULL,
     ],
@@ -875,15 +836,21 @@ function _insert_allowed_html_validate(array $element, FormStateInterface &$form
       // white-listed:
       'contenteditable' => NULL,
       'controls' => NULL,
+      'data-insert-attach' => NULL,
+      'data-insert-type' => NULL,
       'src' => NULL,
       'type' => NULL,
     ],
     'span' => [
       'class' => NULL,
+      'data-insert-attach' => NULL,
+      'data-insert-type' => NULL,
     ],
     'video' => [
       'contenteditable' => NULL,
       'controls' => NULL,
+      'data-insert-attach' => NULL,
+      'data-insert-type' => NULL,
       'src' => NULL,
       'type' => NULL,
     ],
@@ -1073,4 +1040,4 @@ function insert_migrate_prepare_row(Row $row, MigrateSourceInterface $source, Mi
   }
   $insert_settings = $row->getSourceProperty('widget/settings');
   $row->setSourceProperty('insert_config', MigrateInsertWidgetSettings::getInsertWidgetSettings($insert_settings));
-}
+}
\ No newline at end of file
diff --git a/js/EditorInterface.js b/js/EditorInterface.js
deleted file mode 100644
index ba20221..0000000
--- a/js/EditorInterface.js
+++ /dev/null
@@ -1,123 +0,0 @@
-(function($, Drupal) {
-  'use strict';
-
-  /**
-   * @constructor
-   */
-  Drupal.insert.EditorInterface = Drupal.insert.EditorInterface || (function() {
-
-    /**
-     * @constructor
-     */
-    function EditorInterface() {
-      throw new Error('EditorInterface cannot be instantiated directly.');
-    }
-
-    $.extend(EditorInterface.prototype, {
-
-      /**
-       * @type {Function}
-       */
-      editorConstructor: undefined,
-
-      /**
-       * Checks whether this editor interface is to be used.
-       *
-       * @return {boolean}
-       */
-      check: function() {
-        throw new Error('Method not overridden.');
-      },
-
-      /**
-       * @param {*} editor
-       * @return {string}
-       */
-      getId: function(editor) {
-        throw new Error('Method not overridden.');
-      },
-
-      /**
-       * Checks whether the editor is fully initialized.
-       *
-       * @param {*} editor
-       * @return {boolean}
-       */
-      isReady: function(editor) {
-        throw new Error('Method not overridden.');
-      },
-
-      /**
-       *
-       * @return {*[]}
-       */
-      getInstances: function() {
-        throw new Error('Method not overridden.');
-      },
-
-      /**
-       * @return {*|undefined}
-       */
-      getCurrentInstance: function() {
-        throw new Error('Method not overridden.');
-      },
-
-      /**
-       * @param {*} editor
-       * @return {HTMLElement|undefined}
-       */
-      getElement: function(editor) {
-        throw new Error('Method not overridden.');
-      },
-
-      /**
-       * @param {*} editor
-       * @return {jQuery}
-       */
-      getDom: function(editor) {
-        throw new Error('Method not overridden.');
-      },
-
-      /**
-       * @param {*} editor
-       * @return {string}
-       */
-      getData: function(editor) {
-        throw new Error('Method not overridden.');
-      },
-
-      /**
-       * @param {*} editor
-       * @param {string} syncId
-       * @param {string} value
-       */
-      setCaption: function(editor, syncId, value) {
-        throw new Error('Method not overridden.');
-      },
-
-      /**
-       * @param {*} editor
-       * @param {string} uuid
-       * @return {string|null|undefined} Either an alignment value, null if
-       *   alignment is not set, or undefined if there is no inserted image.
-       */
-      getAlign: function(editor, uuid) {
-        throw new Error('Method not overridden.');
-      },
-
-      /**
-       * @param {*} editor
-       * @param {string} uuid
-       * @param {string} value
-       */
-      setAlign: function(editor, uuid, value) {
-        throw new Error('Method not overridden.');
-      }
-
-    });
-
-    return EditorInterface;
-
-  })();
-
-})(jQuery, Drupal);
diff --git a/js/EditorManager.js b/js/EditorManager.js
deleted file mode 100644
index e48b9cf..0000000
--- a/js/EditorManager.js
+++ /dev/null
@@ -1,152 +0,0 @@
-(function($, Drupal) {
-  'use strict';
-
-  /**
-   * Manages interaction with editor instances.
-   * @constructor
-   *
-   * @param {Drupal.insert.EditorInterface} [editorInterface]
-   */
-  Drupal.insert.EditorManager = Drupal.insert.EditorManager || (function() {
-
-    /**
-     * @type {Drupal.insert.EditorInterface}
-     */
-    var eInterface;
-
-    /**
-     * Storage for editor contents for checking whether contents actually
-     * changed when the "change" event is triggered.
-     * @type {Object}
-     */
-    var editorContents;
-
-    /**
-     * @constructor
-     *
-     * @param {Drupal.insert.EditorInterface} [editorInterface]
-     */
-    function EditorManager(editorInterface) {
-      if (editorInterface && typeof editorInterface !== 'object') {
-        throw new Error('editorInterface needs to be an instance of Drupal.insert.EditorInterface.');
-      }
-
-      eInterface = editorInterface;
-      editorContents = {};
-      this._editors = {};
-      this._classesToRetain = {};
-    }
-
-    $.extend(EditorManager.prototype, {
-
-      /**
-       * @type {Object}
-       */
-      _editors: undefined,
-
-      /**
-       * @type {Object}
-       */
-      _classesToRetain: undefined,
-
-      /**
-       * @param {eInterface.editorConstructor} editor
-       * @return {boolean}
-       *   FALSE if editor is already registered, TRUE if editor was added.
-       */
-      addEditor: function(editor) {
-        var editorId = eInterface.getId(editor);
-
-        if (this._editors[editorId]) {
-          return false;
-        }
-
-        var self = this;
-
-        this._evaluateEditorContent(editor);
-        editorContents[editorId] = eInterface.getData(editor);
-
-        editor.on('change', function(e) {
-          var editorContent = eInterface.getData(e.editor);
-
-          if (editorContent === editorContents[eInterface.getId(e.editor)]) {
-            return;
-          }
-          editorContents[eInterface.getId(e.editor)] = editorContent;
-
-          self._evaluateEditorContent(e.editor);
-        });
-
-        this._editors[editorId] = editor;
-
-        return true;
-      },
-
-      /**
-       * Parses the editor's content and stores CSS classes to retain at their
-       * corresponding nodes.
-       *
-       * @param {eInterface.editorConstructor} editor
-       */
-      _evaluateEditorContent: function(editor) {
-        var self = this;
-
-        $(eInterface.getDom(editor)).find('[data-insert-type]').each(function() {
-          var $element = $(this);
-          var classes = $element.data('insert-class');
-
-          if (typeof classes !== 'undefined' && classes !== '') {
-            // Element has already been evaluated: Make sure classes to retain
-            // are in place.
-            $.each(classes.split(' '), function() {
-              if (!$element.hasClass(this)) {
-                $element.addClass(this);
-              }
-            });
-            return true;
-          }
-
-          // Initialize element.
-          var retain = [];
-          classes = $(this).attr('class');
-          if (typeof classes !== 'undefined') {
-            $.each($(this).attr('class').split(' '), function() {
-              if (self._isClassToRetain(this, $element.data('insert-type'))) {
-                retain.push(this);
-              }
-            });
-          }
-
-          $(this).attr(
-            'data-insert-class',
-            retain.length ? retain.join(' ') : ''
-          );
-        });
-      },
-
-      /**
-       * Determines whether a CSS class is supposed to be retained.
-       *
-       * @param {string} className
-       * @param {string} fieldType
-       * @return {boolean}
-       */
-      _isClassToRetain: function(className, fieldType) {
-        return this._classesToRetain[fieldType]
-          && $.inArray(className, this._classesToRetain[fieldType]) !== -1;
-      },
-
-      /**
-       * @param {Object} classesToRetain
-       */
-      updateClassesToRetain: function(classesToRetain) {
-        this._classesToRetain = classesToRetain;
-      }
-
-    });
-
-    return EditorManager;
-
-  })();
-
-})(jQuery, Drupal);
diff --git a/js/FileHandler.js b/js/FileHandler.js
index 75c818a..1bdb3b5 100644
--- a/js/FileHandler.js
+++ b/js/FileHandler.js
@@ -1,43 +1,19 @@
-(function($, Drupal) {
+(function(Drupal) {
   'use strict';
 
-  var PARENT = Drupal.insert.Handler;
-
-  /**
-   * @type {Object}
-   */
-  var SELECTORS = {
+  const SELECTORS = {
     description: 'input[name$="[description]"]',
-    filename: 'input.insert-filename'
+    filename: 'input.insert-filename',
   };
 
   /**
-   * Builds content to be inserted on generic file fields.
-   * @constructor
-   *
-   * @param {Drupal.insert.Inserter} inserter
-   * @param {Object} [widgetSettings]
-   * @param {HTMLElement} [wrapper]
-   */
-  Drupal.insert.FileHandler = Drupal.insert.FileHandler || (function() {
-
-    /**
-     * @constructor
-     *
-     * @param {Drupal.insert.Inserter} inserter
-     * @param {Object} [widgetSettings]
-     * @param {HTMLElement} [wrapper]
-     */
-    function FileHandler(inserter, widgetSettings, wrapper) {
-      PARENT.prototype.constructor.apply(this, arguments);
-    }
-
-    $.extend(FileHandler.prototype, PARENT.prototype, {
-      _selectors: SELECTORS
-    });
-
-    return FileHandler;
-
-  })();
-
-})(jQuery, Drupal);
+   * The File Handler handles insertion of non-image files, e.g. provided by a
+   * File field.
+   **/
+  Drupal.insert.FileHandler = class extends Drupal.insert.Handler {
+    constructor(container, widgetSetting, selectors, wrapper) {
+    super(container, widgetSetting, selectors || SELECTORS, wrapper);
+  }
+};
+
+})(Drupal);
\ No newline at end of file
diff --git a/js/FocusManager.js b/js/FocusManager.js
index c62709f..081810b 100644
--- a/js/FocusManager.js
+++ b/js/FocusManager.js
@@ -1,199 +1,158 @@
-(function($, Drupal) {
+(function(Drupal) {
   'use strict';
 
   /**
-   * Keeps track of focusing elements that the Insert module interacts with.
-   * @constructor
-   *
-   * @param {Drupal.insert.EditorInterface} [editorInterface]
-   * @param {HTMLElement} [defaultTarget]
+   * The Focus Manager keeps track of the CKEditor instances and text areas on
+   * the page, including which of the elements is currently focused and was
+   * previously focused to determine the target for an insertion operation.
    */
-  Drupal.insert.FocusManager = Drupal.insert.FocusManager || (function() {
+  class FocusManager {
 
     /**
-     * @type {Drupal.insert.EditorInterface|undefined}
+     * Registry of the CKEditor instances tracked by the FocusManager
+     * @type {ckeditor.Editor[]}
      */
-    var eInterface;
+    #editors;
 
     /**
-     * @constructor
-     *
-     * @param {Drupal.insert.EditorInterface} [editorInterface]
-     * @param {HTMLElement} [defaultTarget]
+     * Registry of the textareas tracked by the FocusManager
+     * @type {HTMLTextAreaElement[]}
      */
-    function FocusManager(editorInterface, defaultTarget) {
-      if (editorInterface && typeof editorInterface !== 'object') {
-        throw new Error('editorInterface needs to be an instance of Drupal.insert.EditorInterface.');
-      }
+    #textareas;
+
+    /**
+     * The current target for inserting.
+     * @type {ckeditor.Editor|HTMLTextAreaElement|undefined}
+     */
+    #focusTarget;
+
+    /**
+     * The previous target used as a fallback when inserting while there is no
+     * current target.
+     * @type {ckeditor.Editor|HTMLTextAreaElement|undefined}
+     */
+    #previousFocusTarget;
 
-      eInterface = editorInterface;
-      this._$defaultTarget = $(defaultTarget);
-      this._editors = {};
-      this._$textareas = $();
+    /**
+     * The default target used when there has been no interaction with any
+     * editor or textarea yet.
+     * @type {ckeditor.Editor|HTMLTextAreaElement|undefined}
+     */
+    defaultTarget;
+
+    constructor() {
+      this.#editors = [];
+      this.#textareas = [];
+
+      const firstEditorElement
+        = document.querySelector('.ck-editor__editable');
+
+      this.defaultTarget = firstEditorElement
+        ? firstEditorElement.ckeditorInstance
+        : document.querySelector('textarea') || undefined;
     }
 
-    $.extend(FocusManager.prototype, {
-
-      /**
-       * Target for inserting when no textarea was focused yet.
-       * @type {jQuery}
-       */
-      _$defaultTarget: undefined,
-
-      /**
-       * @type {Object}
-       */
-      _editors: undefined,
-
-      /**
-       * @type {jQuery}
-       */
-      _$textareas: undefined,
-
-      /**
-       * @param {HTMLElement} [element]
-       */
-      setDefaultTarget: function(element) {
-        this._$defaultTarget = $(element);
-      },
-
-      /**
-       * @param {eInterface.editorConstructor} editor
-       */
-      addEditor: function(editor) {
-        if (eInterface && !this._editors[eInterface.getId(editor)]) {
-          this._attachEvents(editor);
-          this._editors[eInterface.getId(editor)] = editor;
-        }
-      },
+    /**
+     * @returns {ckeditor.Editor[]}
+     */
+    get editors() {
+      return this.#editors;
+    }
 
-      /**
-       * @param {jQuery} $textareas
-       */
-      addTextareas: function($textareas) {
-        var $unregistered = $textareas.not(this._$textareas);
+    /**
+     * @returns {HTMLTextAreaElement[]}
+     */
+    get textareas() {
+      return this.#textareas;
+    }
 
-        if ($unregistered.length) {
-          this._attachEvents($unregistered);
-          this._$textareas = this._$textareas.add($unregistered);
-        }
-      },
-
-      /**
-       * @return {*[]}
-       */
-      getEditors: function() {
-        var editors = [];
-        $.each(this._editors, function() {
-          editors.push(this);
-        });
-        return editors;
-      },
-
-      /**
-       * @return {jQuery}
-       */
-      getTextareas: function() {
-        return this._$textareas;
-      },
-
-      /**
-       * @param {eInterface.editorConstructor|jQuery} editorOrTextareas
-       */
-      _attachEvents: function(editorOrTextareas) {
-
-        // Beware: CKEditor neither supports chaining nor event namespaces!
-        editorOrTextareas.on('focus', function(event) {
-          $(':data(insertIsFocused)').removeData('insertIsFocused');
-          $(':data(insertLastFocused)').removeData('insertLastFocused');
-
-            var subject = (event.editor && event.editor instanceof eInterface.editorConstructor)
-              ? eInterface.getElement(event.editor)
-              : this;
-            $(subject).data('insertIsFocused', true).data('insertLastFocused', true);
-        });
-
-        editorOrTextareas.on('blur', function(event) {
-          var subject = (event.editor && event.editor instanceof eInterface.editorConstructor)
-            ? eInterface.getElement(event.editor)
-            : this;
-          // Delay removing focus marker, so, when instantly clicking on the
-          // Insert button, the focused subject will receive the input.
-          setTimeout(function() {
-            $(subject).removeData('insertIsFocused');
-          }, 1000);
-        });
-      },
-
-      /**
-       * @return {HTMLElement|null}
-       */
-      _getLastFocused: function() {
-        var $lastFocusedTextarea = $(':data(insertLastFocused)');
-        if (!$lastFocusedTextarea.length) {
-          $lastFocusedTextarea = this._$defaultTarget;
-        }
-        return $lastFocusedTextarea.length ? $lastFocusedTextarea.get(0) : null;
-      },
-
-      /**
-       * @return {jQuery}
-       */
-      _getCurrentlyFocusedTextarea: function() {
-        return this._$textareas.find(':data(insertIsFocused)');
-      },
-
-      /**
-       * @return {eInterface.editorConstructor|HTMLElement}
-       */
-      getActive: function() {
-        var subject = this._getCurrentlyFocusedTextarea();
-
-        if (subject.length !== 0) {
-          return subject.get(0);
-        }
+    /**
+     * @param {ckeditor.Editor} editor
+     */
+    addEditor(editor) {
+      if (!this.#editors.map(editor => editor.id).includes(editor.id)) {
+        this.#attachEditorEvents(editor);
+        this.#editors.push(editor);
+      }
+    }
 
-        var lastFocused = this._getLastFocused();
-        subject = undefined;
-
-        if (eInterface) {
-          $.each(this._editors, function(id, editor) {
-            // Editor element is undefined after switching to restricted HTML
-            // mode, as that mode detaches the WYSIWYG editor.
-            var editorElement = eInterface.getElement(editor);
-            if (editorElement && lastFocused && editorElement === lastFocused) {
-              subject = this;
-              return false;
-            }
-          });
+    /**
+     * @param {ckeditor.Editor} editor
+     */
+    #attachEditorEvents(editor) {
+      editor.editing.view.document.on('change:isFocused', (event, data, isFocused) => {
+        if (isFocused) {
+          this.#focusTarget = editor;
+        } else {
+          this.#previousFocusTarget = editor;
+
+          if (this.#focusTarget && this.#focusTarget.id === editor.id) {
+            this.#focusTarget = undefined;
+          }
         }
+      });
+    }
 
-        if (subject) {
-          return subject;
-        }
-        if (lastFocused) {
-          return lastFocused;
-        }
+    /**
+     * @param {HTMLTextAreaElement} textarea
+     */
+    addTextarea(textarea) {
+      if (!this.#textareas.includes(textarea)) {
+        this.#attachTextareaEvents(textarea);
+        this.#textareas.push(textarea);
+      }
+    }
+
+    /**
+     * @param {HTMLTextAreaElement} textarea
+     */
+    #attachTextareaEvents(textarea) {
+      textarea.addEventListener('focus', event => {
+        this.#focusTarget = event.target;
+      });
 
-        var currentEditorInstance = eInterface
-          ? eInterface.getCurrentInstance()
-          : undefined;
+      textarea.addEventListener('blur', event => {
+        this.#previousFocusTarget = event.target;
 
-        if (currentEditorInstance) {
-          return currentEditorInstance;
+        if (this.#focusTarget && this.#focusTarget === textarea) {
+          this.#focusTarget = undefined;
         }
+      });
+    }
+
+    /**
+     * Returns the best guessed target to insert content into.
+     * @returns {ckeditor.Editor|HTMLTextAreaElement|undefined}
+     */
+    getTarget() {
+      if (this.#focusTarget) {
+        return this.#focusTarget;
+      }
+
+      if (this.#previousFocusTarget) {
+        return this.#previousFocusTarget;
+      }
+
+      if (this.defaultTarget) {
+        return this.defaultTarget;
+      }
 
         if (this._$textareas.length) {
           return this._$textareas.get(0);
         }
 
-        return null;
+      if (this.#editors.length) {
+        return this.#editors[0];
       }
 
-    });
+      if (this.#textareas.length) {
+        return this.#textareas[0];
+      }
 
-    return FocusManager;
+      console.warn('Insert: Unable to determine the insertion target');
+    }
+  }
 
-  })();
+  Drupal.insert.Manager = FocusManager;
 
-})(jQuery, Drupal);
+})(Drupal);
\ No newline at end of file
diff --git a/js/Handler.js b/js/Handler.js
index 681908a..c9d9354 100644
--- a/js/Handler.js
+++ b/js/Handler.js
@@ -1,263 +1,380 @@
-(function($, Drupal) {
+(function (Drupal) {
   'use strict';
 
   /**
-   * Builds content to be inserted.
-   * @constructor
-   *
-   * @param {Drupal.insert.Inserter} inserter
-   * @param {Object} [widgetSettings]
-   * @param {HTMLElement} [wrapper]
+   * Finds a CKEditor element according to a predicate function looping through
+   * an element's descendants.
+   * @param {ckeditor.Element} modelElement
+   * @param {(child: ckeditor.Element) => boolean} predicate
+   * @returns {boolean}
    */
-  Drupal.insert.Handler = Drupal.insert.Handler || (function() {
+  function findDescendant(modelElement, predicate) {
+    if (predicate(modelElement)) {
+      return modelElement;
+    }
+    if (modelElement.getChildren) {
+      for (const child of modelElement.getChildren()) {
+        const found = findDescendant(child, predicate);
+        if (found) {
+          return found;
+        }
+      }
+    }
+  }
+
+  /**
+   * Returns whether a provided editor element is the one to be synced by
+   * Insert.
+   * @param {ckeditor.Element} element
+   * @param {string} syncId
+   * @returns {boolean}
+   */
+  function isSyncedElement(element, syncId) {
+    for (const key of element.getAttributeKeys()) {
+      const attribute = element.getAttribute(key);
+      const attributes = attribute.attributes;
+
+      if (!attributes || !attributes['data-insert-attach']) {
+        continue;
+      }
+
+      const attach = JSON.parse(attributes['data-insert-attach']);
+
+      if (attach.id === syncId) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * Base class for handling content types, that is building the content to be
+   * inserted as per content type, e.g. image or (non-image) file.
+   */
+  Drupal.insert.Handler = class {
 
     /**
-     * @constructor
-     *
-     * @param {Drupal.insert.Inserter} inserter
-     * @param {Object} [widgetSettings]
-     * @param {HTMLElement} [wrapper]
+     * Selectors for accessing input elements of the field Insert is attached
+     * to.
+     * @type {{[key: string]: string}}
+     * @protected
      */
-    function Handler(inserter, widgetSettings, wrapper) {
-      if (typeof inserter === 'undefined') {
-        throw new Error('inserter needs to be specified.');
-      }
+    _selectors;
+
+    /**
+     * The HTML element Insert is initialized on.
+     * @type {HTMLFieldSetElement}
+     * @protected
+     */
+    _container;
+
+    /**
+     * Insert widget settings.
+     * @type {object}
+     * @protected
+     */
+    _settings;
+
+    /**
+     * @type {HTMLElement}
+     */
+    #wrapper;
+
+    /**
+     * @type {HTMLButtonElement}
+     */
+    #button;
+
+    /**
+     * The button overlay allows hover events over the button in disabled
+     * state. The overlay is used only when the button is disabled and is used
+     * to highlight invalid components when hovering the button.
+     * @type {HTMLSpanElement|undefined}
+     * @protected
+     */
+    _buttonOverlay;
 
-      this._inserter = inserter;
+    /**
+     * @param {HTMLFieldSetElement} container
+     * @param {object} widgetSettings
+     * @param {{[key: string]: string}} selectors
+     * @param {HTMLElement} wrapper
+     */
+    constructor(container, widgetSettings, selectors, wrapper) {
+      this._container = container;
       this._settings = widgetSettings || {};
+      this._selectors = selectors;
+      this.#wrapper = wrapper || container.parentElement;
+      this.#button = container.querySelector('.insert-button');
 
-      var self = this;
-      var $wrapper = typeof wrapper === 'undefined'
-        ? this._inserter.$container.parent() : $(wrapper);
+      this.#connectSelectors();
+    }
 
-      $.each(this._selectors, function() {
-        $wrapper.find(this).on('input.insert', function() {
-          self._update();
+    /**
+     * Attaches the "input" event to this handler's selectors for updating
+     * attached values of inserted elements
+     */
+    #connectSelectors() {
+      Object.values(this._selectors).forEach(selector => {
+        this.#wrapper
+          .querySelector(selector)?.addEventListener('input', () => {
+          this.#update();
         });
       });
     }
 
-    $.extend(Handler.prototype, {
-
-      /**
-       * @type {Object}
-       */
-      _selectors: {},
-
-      /**
-       * @type {Drupal.insert.Inserter}
-       */
-      _inserter: undefined,
-
-      /**
-       * @type {Object}
-       */
-      _settings: null,
-
-      /**
-       * The button overlay allows hover events over the button in disabled
-       * state. The overlay is used only when the button is disabled and is used
-       * to highlight invalid components when hovering the button.
-       * @type {jQuery|undefined}
-       */
-      _$buttonOverlay: undefined,
-
-      /**
-       * @return {string}
-       */
-      buildContent: function() {
-        var template = this._inserter.getTemplate();
-        return this._attachValues(template);
-      },
-
-      /**
-       * Attaches attributes and content according to data-insert-attach
-       * definition.
-       *
-       * @param {string} template
-       * @return {string}
-       */
-      _attachValues: function(template) {
-        var self = this;
-        var values = this._aggregateValues();
-        var $tempContainer = $('<div>').html(template);
-
-        $tempContainer.find('[data-insert-attach]').each(function() {
-          self._setValues($(this), values);
-        });
+    /**
+     * @returns {string}
+     */
+    buildContent() {
+      return this._attachValues(this.#getTemplate());
+    }
 
-        return $tempContainer.html();
-      },
+    /**
+     * Returns the template for the currently selected insert style.
+     * @returns {string}
+     */
+    #getTemplate() {
+      const style = this._container.querySelector('.insert-style').value;
+      return this._container
+        .querySelector('input.insert-template[name$="[' + style + ']"]').value;
+    }
 
-      /**
-       * Updates all registered textareas and editors with the current values
-       * managed by this Handler instance.
-       */
-      _update: function() {
-        var self = this;
-        var syncId = this._inserter.$button.data('insert-id');
+    /**
+     * Attaches attributes and content according to data-insert-attach
+     * definition.
+     * @param {string} template
+     * @returns {string}
+     * @protected
+     */
+    _attachValues(template) {
+      const values = this.#aggregateValues();
+      const tempContainer = document.createElement('div');
 
-        if (typeof syncId === 'undefined') {
-          return;
-        }
+      tempContainer.innerHTML = template;
+      tempContainer.querySelectorAll('[data-insert-attach]').forEach(element => {
+        this.#setValues(element, values);
+      });
 
-        var values = this._aggregateValues();
+      return tempContainer.innerHTML;
+    }
 
-        this._inserter.getFocusManager().getTextareas().each(function() {
-          self._updateTextarea($(this), syncId, values);
-        });
+    /**
+     * Updates all registered textareas and editors with the current values
+     * managed by this Handler instance.
+     */
+    #update() {
+      const syncId = this.#button.dataset.insertId;
+
+      if (syncId === undefined) {
+        return;
+      }
+
+      const values = this.#aggregateValues();
+
+      Drupal.insert.FocusManager.textareas.forEach(textarea => {
+        this.#updateTextarea(textarea, syncId, values);
+      });
+
+      Drupal.insert.FocusManager.editors.forEach(editor => {
+        this.#updateEditor(editor, syncId);
+      });
+    }
+
+    /**
+     * Updates a particular textarea with a set of values.
+     * @param {HTMLTextAreaElement} textarea
+     * @param {string} syncId
+     * @param {{[key: string]: string}} values
+     */
+    #updateTextarea(textarea, syncId, values) {
+      const temp = document.createElement('div');
+      temp.innerHTML = textarea.value;
 
-        $.each(this._inserter.getFocusManager().getEditors(), function() {
-          self._updateEditor(this, syncId, values);
+      const elements = this.#findByAttachmentId(temp, syncId);
+
+      if (elements.length) {
+        elements.forEach(element => {
+          this.#setValues(element, values);
         });
-      },
-
-      /**
-       * Updates a particular textarea with a set of values.
-       *
-       * @param {jQuery} $textarea
-       * @param {string|int} syncId
-       * @param {Object} values
-       */
-      _updateTextarea: function($textarea, syncId, values) {
-        var self = this;
-        var $dom = $('<div>').html($textarea.val());
-        var $attachments = this._findByAttachmentId($dom, syncId);
-
-        if ($attachments.length) {
-          $attachments.each(function() {
-            self._setValues($(this), values);
-          });
-          $textarea.val($dom.html());
-        }
-      },
-
-      /**
-       * Updates a particular editor with a set of values.
-       *
-       * @param {Drupal.insert.EditorInterface} editor
-       * @param {string|int} syncId
-       * @param {Object} values
-       */
-      _updateEditor: function(editor, syncId, values) {
-        var self = this;
-        var editorInterface = this._inserter.getEditorInterface();
-        var $dom;
-
-        try {
-          $dom = editorInterface.getDom(editor);
+        textarea.value = temp.innerHTML;
+      }
+    }
+
+    /**
+     * Finds attachments for a specific syncId.
+     * @param {HTMLElement} dom
+     * @param {string} syncId
+     * @returns {HTMLElement[]}
+     */
+    #findByAttachmentId(dom, syncId) {
+      const attachments = [];
+
+      dom.querySelectorAll('[data-insert-attach]').forEach(element => {
+        const insertAttach = JSON.parse(element.dataset.insertAttach);
+
+        if (insertAttach.id === syncId) {
+          attachments.push(element);
         }
-        catch(error) {
-          // That editor has not been initialized yet.
+      });
+
+      return attachments;
+    }
+
+    /**
+     * Updates a particular editor with a set of values.
+     * @param {ckeditor.Editor} editor
+     * @param {string} syncId
+     */
+    #updateEditor(editor, syncId) {
+      editor.model.change(writer => {
+        // TODO: Support multiple elements
+
+        const element = findDescendant(
+          writer.model.document.getRoot(),
+          element => isSyncedElement(element, syncId)
+        );
+
+        if (!element) {
           return;
         }
 
-        this._findByAttachmentId($dom, syncId).each(function() {
-          self._setValues($(this), values);
-        });
-      },
-
-      /**
-       * Finds attachments for a specific syncId.
-       *
-       * @param {jQuery} $dom
-       * @param {string|int} syncId
-       * @return {jQuery}
-       */
-      _findByAttachmentId: function($dom, syncId) {
-        var $attachments = $();
-
-        $dom.find('[data-insert-attach]').each(function() {
-          if ($(this).data('insert-attach').id === syncId.toString()) {
-            $attachments = $attachments.add(this);
+        const elementToReplace = element.is('element')
+          ? element
+          : element.parent;
+
+        const position = writer.createPositionAfter(elementToReplace);
+        const viewFragment = editor.data.processor.toView(this.buildContent());
+        const modelFragment = editor.data.toModel(viewFragment);
+
+        writer.model.insertContent(modelFragment, position);
+        writer.remove(elementToReplace);
+      });
+    }
+
+    /**
+     * Sets attributes and/or content on a node according to its
+     * data-insert-attach definition.
+     * @param {HTMLElement} element
+     * @param {{[key: string]: string}} values
+     * @returns {HTMLElement}
+     */
+    #setValues(element, values) {
+      const attach = JSON.parse(element.dataset.insertAttach || null);
+
+      this.#setAttributes(element, values, attach);
+      this.#setContent(element, values, attach);
+
+      return element;
+    }
+
+    /**
+     * @param {HTMLElement} element
+     * @param {{[key: string]: string}} values
+     * @param {object} attach
+     */
+    #setAttributes(element, values, attach) {
+      if (!attach?.attributes) {
+        return;
+      }
+      
+      for (const [attributeName, keys] of Object.entries(attach.attributes)) {
+        for (const key of keys) {
+          if (!values[key]) {
+            continue;
           }
-        });
 
-        return $attachments;
-      },
-
-      /**
-       * Sets attributes and/or content on a node according to its
-       * data-insert-attach definition.
-       *
-       * @param {jQuery} $node
-       * @param {Object} values
-       */
-      _setValues: function($node, values) {
-        var attach = $node.data('insert-attach');
-
-        if (attach.attributes) {
-          $.each(attach.attributes, function(attributeName, keys) {
-            $.each(keys, function() {
-              if (values[this]) {
-                if (values[this] === '') {
-                  $node.removeAttr(attributeName);
-                }
-                else {
-                  $node.attr(attributeName, values[this]);
-                }
-                return false;
-              }
-            });
-          });
-        }
+          if (values[key] === '') {
+            element.removeAttribute(attributeName);
+          } else {
+            element.setAttribute(attributeName, values[key]);
+          }
 
-        if (attach.content) {
-          $.each(attach.content, function() {
-            if (values[this]) {
-              $node.text(values[this]);
-              return false;
-            }
-          });
+          break;
         }
-      },
-
-      /**
-       * Returns all values gathered using this._selectors.
-       *
-       * @return {Object}
-       */
-      _aggregateValues: function() {
-        var self = this;
-        var values = {};
-        var $fieldDataWrapper = this._inserter.$container.parent();
-
-        $.each(this._selectors, function(key, selector) {
-          var value = $(selector, $fieldDataWrapper).val();
-          values[key] = value ? self._htmlEntities(value) : value;
-        });
+      }
+    }
+
+    /**
+     * @param {HTMLElement} element
+     * @param {{[key: string]: string}} values
+     * @param {object} attach
+     */
+    #setContent(element, values, attach) {
+      if (!attach?.content) {
+        return;
+      }
 
-        return values;
-      },
-
-      /**
-       * @param string
-       * @return string
-       */
-      _htmlEntities: function(string) {
-        return string
-          .replace(/&/g, '&amp;')
-          .replace(/"/g, '&quot;')
-          .replace(/</g, '&lt;')
-          .replace(/>/g, '&gt;');
-      },
-
-      /**
-       * @param {boolean} disable
-       */
-      _disable: function(disable) {
-        if (!this._$buttonOverlay) {
-          var $container = this._inserter.$container;
-          this._$buttonOverlay = $container.find('.insert-button-overlay');
+      for (const key of attach.content) {
+        if (values[key]) {
+          element.innerText = values[key];
+          break;
         }
-        this._inserter.$button.prop('disabled', disable);
-        this._$buttonOverlay[disable ? 'show' : 'hide']();
       }
+    }
+
+    /**
+     * Returns all values gathered using this._selectors.
+     * @returns {{[key: string]: string}}
+     */
+    #aggregateValues() {
+      const values = {};
+      const fieldDataWrapper = this._container.parentNode;
+
+      Object.entries(this._selectors).forEach(([key, selector]) => {
+        var value = fieldDataWrapper.querySelector(selector)?.value;
+        values[key] = value ? this.#htmlEntities(value) : value;
+      });
 
-    });
+      return values;
+    }
+
+    /**
+     * @param {string} string
+     * @returns {string}
+     */
+    #htmlEntities(string) {
+      return string
+        .replace(/&/g, '&amp;')
+        .replace(/"/g, '&quot;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;');
+    }
+
+    /**
+     * Toggles disabled state of the insert button.
+     * @param {boolean} disable
+     * @protected
+     */
+    _disable(disable) {
+      const wrapper = this._container
+        .querySelector('.insert-button-wrapper');
+      const button = wrapper.querySelector('.insert-button');
 
-    return Handler;
+      if (disable) {
+        const overlay = document.createElement('span');
+        overlay.classList.add('insert-button-overlay');
+        wrapper.appendChild(overlay);
+        wrapper.removeChild(button);
+        overlay.appendChild(button);
 
-  })();
+        button.setAttribute('disabled', 'true');
+      } else {
+
+        const overlay = wrapper.querySelector('.insert-button-overlay');
+
+        if (!overlay) {
+          return;
+        }
+
+        overlay.removeChild(button);
+        wrapper.removeChild(overlay);
+        wrapper.appendChild(button);
+
+        button.removeAttribute('disabled');
+      }
+    }
+  }
 
-})(jQuery, Drupal);
+})(Drupal);
\ No newline at end of file
diff --git a/js/ImageHandler.js b/js/ImageHandler.js
index ea569ff..b597743 100644
--- a/js/ImageHandler.js
+++ b/js/ImageHandler.js
@@ -1,12 +1,7 @@
-(function($, Drupal) {
+(function (Drupal) {
   'use strict';
 
-  var PARENT = Drupal.insert.FileHandler;
-
-  /**
-   * @type {Object}
-   */
-  var SELECTORS = {
+  const SELECTORS = {
     alt: 'input[name$="[alt]"], textarea[name$="[alt]"]',
     title: 'input[name$="[title]"], textarea[name$="[title]"]',
     description: 'input[name$="[description]"], textarea[name$="[description]"]'
@@ -17,361 +12,134 @@
    * having to specify the alternative text (if set to required).
    * @type {string[]}
    */
-  var noAltTextInsertStyles = ['link', 'icon_link'];
+  const noAltTextInsertStyles = ['link', 'icon_link'];
 
-  /**
-   * Builds content to be inserted on image fields.
-   * @constructor
-   *
-   * @param {Drupal.insert.Inserter} inserter
-   * @param {Object} [widgetSettings]
-   * @param {HTMLElement} [wrapper]
-   */
-  Drupal.insert.ImageHandler = Drupal.insert.ImageHandler || (function() {
+  Drupal.insert.ImageHandler = class extends Drupal.insert.FileHandler {
 
     /**
-     * @constructor
-     *
-     * @param {Drupal.insert.Inserter} inserter
-     * @param {Object} [widgetSettings]
-     * @param {HTMLElement} [wrapper]
+     * @type {HTMLInputElement}
      */
-    function ImageHandler(inserter, widgetSettings, wrapper) {
-      PARENT.prototype.constructor.apply(this, arguments);
-
-      this._uuid = this._inserter.$container.data('uuid');
+    #altField;
 
-      var $rotator = this._inserter.$container.find('.insert-rotate');
+    /**
+     * @type {HTMLSelectElement}
+     */
+    #insertStyle;
 
-      if ($rotator.length) {
-        this._rotator = new Drupal.insert.Rotator(
-          $rotator.get(0),
-          this._inserter.$container.find('.insert-templates').get(0),
-          this._inserter.getFocusManager(),
-          this._inserter.getEditorInterface()
-        );
-      }
+    /**
+     * @inheritDoc
+     */
+    constructor(container, widgetSettings, wrapper) {
+      super(container, widgetSettings, SELECTORS, wrapper);
 
-      this._initAltField();
-      this._initAlign();
+      this.#insertStyle = this._container.querySelector('.insert-style');
 
-      this._disable(!this._checkAltField());
+      this.#initAltField();
+      this._disable(!this.#checkAltField());
     }
 
-    $.extend(ImageHandler.prototype, PARENT.prototype, {
-      constructor: ImageHandler,
-
-      /**
-       * @type {Object}
-       */
-      _selectors: $.extend({}, PARENT.prototype._selectors, SELECTORS),
-
-      /**
-       * @type {string}
-       */
-      _uuid: undefined,
-
-      /**
-       * @type {Drupal.insert.Rotator|undefined}
-       */
-      _rotator: undefined,
-
-      /**
-       * The alternative text field corresponding to the image, if any.
-       * @type {jQuery}
-       */
-      _$altField: undefined,
-
-      /**
-       * @type {jQuery}
-       */
-      _$align: undefined,
-
-      /**
-       * @inheritDoc
-       *
-       * @param {string} content
-       * @return {HTMLElement|undefined}
-       *
-       * @triggers insertIntoActive
-       */
-      _insertIntoActive: function(content) {
-        var activeElement = PARENT.prototype._insertIntoActive.call(this, content);
-
-        if (activeElement) {
-          this._contentWarning(activeElement, content);
-        }
-
-        return activeElement;
-      },
-
-      /**
-       * Warns users when attempting to insert an image into an unsupported
-       * field.
-       *
-       * This function is only a 90% use-case, as it does not support when the
-       * filter tips are hidden, themed, or when only one format is available.
-       * However, it should fail silently in these situations.
-       *
-       * @param {HTMLElement} editorElement
-       * @param {string} content
-       */
-      _contentWarning: function(editorElement, content) {
-        if (!content.match(/<img /)) {
-          return;
-        }
-
-        var $wrapper = $(editorElement).parents('div.text-format-wrapper:first');
-        if (!$wrapper.length) {
-          return;
-        }
-
-        $wrapper.find('.filter-guidelines-item:visible li').each(function(index, element) {
-          var expression = new RegExp(Drupal.t('Allowed HTML tags'));
-          if (expression.exec(element.textContent) && !element.textContent.match(/<img( |>)/)) {
-            alert(Drupal.t("The selected text format will not allow it to display images. The text format will need to be changed for this image to display properly when saved."));
-          }
-        });
-      },
-
-      /**
-       * @inheritDoc
-       *
-       * @param {string} template
-       * @return {string}
-       */
-      _attachValues: function(template) {
-        template = PARENT.prototype._attachValues.call(this, template);
-        return this._updateImageDimensions(template);
-      },
-
-      /**
-       * Checks for a maximum dimension and scales down the width if necessary.
-       *
-       * @param {string} template
-       * @return {string}
-       *   Updated template.
-       */
-      _updateImageDimensions: function(template) {
-        var widthMatches = template.match(/width[ ]*=[ ]*"(\d*)"/i);
-        var heightMatches = template.match(/height[ ]*=[ ]*"(\d*)"/i);
-        if (this._settings.maxWidth && widthMatches && parseInt(widthMatches[1]) > this._settings.maxWidth) {
-          var insertRatio = this._settings.maxWidth / widthMatches[1];
-          var width = this._settings.maxWidth;
-          template = template.replace(/width[ ]*=[ ]*"?(\d*)"?/i, 'width="' + width + '"');
-
-          if (heightMatches) {
-            var height = Math.round(heightMatches[1] * insertRatio);
-            template = template.replace(/height[ ]*=[ ]*"?(\d*)"?/i, 'height="' + height + '"');
-          }
-        }
-        return template;
-      },
-
-      /**
-       * Initializes the alternative text input element, if any.
-       */
-      _initAltField: function() {
-        this._$altField = this._inserter.$container
-          .parent()
-          .find(this._selectors['alt']);
-
-        // If no alt field is found, look for a description field as the
-        // ImageHandler may be used to insert an image per file field.
-        if (!this._$altField.length) {
-          this._$altField = this._inserter.$container
-            .parent()
-            .find(this._selectors['description']);
-        }
+    /**
+     * @inheritDoc
+     * @param {string} template
+     * @returns {string}
+     */
+    _attachValues(template) {
+      template = super._attachValues(template);
+      return this.#updateImageDimensions(template);
+    }
 
-        if (!this._$altField.length) {
-          return;
+    /**
+     * Checks for a maximum dimension and scales down the width if necessary.
+     * @param {string} template
+     * @returns {string} Updated template
+     */
+    #updateImageDimensions(template) {
+      const widthMatches = template.match(/width[ ]*=[ ]*"(\d*)"/i);
+      const heightMatches = template.match(/height[ ]*=[ ]*"(\d*)"/i);
+
+      if (
+        this._settings.maxWidth
+        && widthMatches && parseInt(widthMatches[1]) > this.settings.maxWidth
+      ) {
+        const insertRatio = this._settings.maxWidth / widthMatches[1];
+        const width = this._settings.maxWidth;
+        template = template
+          .replace(/width[ ]*=[ ]*"?(\d*)"?/i, `width="${width}"`);
+
+        if (heightMatches) {
+          const height = Math.round(heightMatches[1] * insertRatio);
+          template = template
+            .replace(/height[ ]*=[ ]*"?(\d*)"?/i, `height="${height}"`);
         }
+      }
 
-        var self = this;
-
-        this._$altField.on('input.insert_image_handler', function() {
-          self._disable(!self._checkAltField());
-        });
-
-        this._inserter.$insertStyle.on('change.insert_image_handler', function() {
-          self._disable(!self._checkAltField());
-        });
-      },
-
-      /**
-       * Checks whether the alternative text configuration, its input and the
-       * selected style allows the image to get inserted. For example, if the
-       * alternative text is required, it may not be empty to allow inserting an
-       * image, as long as the image shall not be inserted in the form of a
-       * plain text link.
-       *
-       * @return {boolean}
-       *   TRUE if alternative text configuration/input is valid, FALSE if not.
-       */
-      _checkAltField: function() {
-        return !this._$altField.length
-          || !this._$altField.prop('required')
-          || this._$altField.prop('required') && this._$altField.val().trim() !== ''
-          || $.inArray(this._inserter.$insertStyle.val(), noAltTextInsertStyles) !== -1
-      },
-
-      /**
-       * Initializes alignment setting, if available.
-       */
-      _initAlign: function() {
-        var self = this;
-        var editorInterface = this._inserter.getEditorInterface();
-
-        this._$align = this._inserter.$container.find('.insert-align');
-
-        this._$align.add(this._inserter.$button)
-          .on('click.insert_image_handler', function() {
-            var value = self._$align.find(':checked').val();
-
-            self._inserter.getFocusManager().getTextareas().each(function() {
-              var $textarea = $(this);
-              var textareaString = $textarea.val();
-              var $dom = $('<div>').html(textareaString);
-              var $instances = self._findByUUID($dom, self._uuid);
-
-              if ($instances.length) {
-                $instances.attr('data-align', value);
-                $dom.find('img').each(function(index) {
-                  var i = 0;
-                  textareaString.replace(
-                    /<img[^>]*>/g,
-                    match => i++ !== index ? match : $('<div>').append($(this)).html()
-                  );
-                });
-                $textarea.val(textareaString);
-              }
-            });
-
-            $.each(self._inserter.getFocusManager().getEditors(), function() {
-              editorInterface.setAlign(this, self._uuid, value);
-            });
-          });
-
-        var value = this._getAlign();
-
-        if (value) {
-          this._$align.find('[value="' + value + '"]').prop('checked', true);
-        }
-      },
+      return template;
+    }
 
-      /**
-       * Finds nodes by UUID.
-       *
-       * @param {jQuery} $dom
-       * @param {string} uuid
-       * @return {jQuery}
-       */
-      _findByUUID: function($dom, uuid) {
-        var regExp = new RegExp(uuid + '$');
-        var $instances = $();
+    /**
+     * Initializes the alternative text input element, if any.
+     */
+    #initAltField() {
+      this.#altField = this._container.parentNode
+        .querySelector(this._selectors.alt);
+
+      // If no alt field is found, look for a description field as the
+      // ImageHandler may be used to insert an image per file field.
+      if (!this.#altField) {
+        this.#altField = this.container.parentNode
+          .querySelector(this._selectors.description);
+      }
 
-        $dom.find('[data-entity-uuid]').each(function() {
-          var uuid = $(this).data('entity-uuid');
-          if (typeof uuid !== 'undefined' && regExp.test(uuid)) {
-            $instances = $instances.add(this);
-          }
-        });
+      if (!this.#altField) {
+        return;
+      }
 
-        return $instances;
-      },
+      this.#altField.addEventListener('input', () => {
+        this._disable(!this.#checkAltField());
+      });
 
-      /**
-       * Returns current alignment simply using the first image instance.
-       * (If there are different alignments for an image's instances, alignment
-       * messed up somehow anyway.)
-       *
-       * @return {string|null}
-       */
-      _getAlign: function() {
-        var self = this;
-        var value;
-        var hasInsertedImages = false;
+      this.#insertStyle.addEventListener('change', () => {
+        this._disable(!this.#checkAltField());
+      });
+    }
 
-        this._inserter.getFocusManager().getTextareas().each(function() {
-          var $dom = $('<div>').html($(this).val());
-          var $nodes = self._findByUUID($dom, self._uuid);
+    /**
+     * Checks whether the alternative text configuration, its input and the
+     * selected style allows the image to get inserted. For example, if the
+     * alternative text is required, it may not be empty to allow inserting an
+     * image, as long as the image shall not be inserted in the form of a
+     * plain text link.
+     * @returns {boolean}
+     *   TRUE if alternative text configuration/input is valid, FALSE if not.
+     */
+    #checkAltField() {
+      return !this.#altField
+        || !this.#altField.getAttribute('required')
+        || this.#altField.getAttribute('required')
+          && this.#altField.value.trim() !== ''
+        || noAltTextInsertStyles.includes(this.#insertStyle.value);
+    }
 
-          if ($nodes.length) {
-            hasInsertedImages = true;
-          }
+    /**
+     * @inheritDoc
+     */
+    _disable(disable) {
+      super._disable(disable);
 
-          $nodes.each(function() {
-            value = $(this).attr('data-align');
-            return false;
-          });
+      const overlay = this._container
+        .querySelector('.insert-button-overlay');
 
-          return !value;
+      if (overlay) {
+        overlay.addEventListener('mouseover', () => {
+          this.#altField.classList.add('insert-required');
         });
 
-        if (value) {
-          return value;
-        }
-
-        var editorInterface = this._inserter.getEditorInterface();
-
-        $.each(this._inserter.getFocusManager().getEditors(), function() {
-          value = editorInterface.getAlign(this, self._uuid);
-          hasInsertedImages = hasInsertedImages || value !== undefined;
-          return false;
+        overlay.addEventListener('mouseout', () => {
+          this.#altField.classList.remove('insert-required');
         });
-
-        return !hasInsertedImages ? null : value ? value : 'none';
-      },
-
-      /**
-       * @inheritDoc
-       */
-      _setValues: function($node, values) {
-        PARENT.prototype._setValues.apply(this, arguments);
-
-        var attach = $node.data('insert-attach');
-
-        if (attach.attributes && attach.attributes['data-caption']) {
-          var text = '';
-          $.each(attach.attributes['data-caption'], function() {
-            if (typeof values[this] !== 'undefined' && values[this] !== '') {
-              text = values[this];
-              return false;
-            }
-          });
-          var editorInterface = this._inserter.getEditorInterface();
-
-          $.each(this._inserter.getFocusManager().getEditors(), function() {
-            editorInterface.setCaption(this, attach.id, text);
-          });
-        }
-      },
-
-      /**
-       * @param {boolean} disable
-       */
-      _disable: function(disable) {
-        if (!this._$buttonOverlay) {
-          var self = this;
-          var $container = this._inserter.$container;
-
-          this._$buttonOverlay = $container.find('.insert-button-overlay')
-            .on('mouseover.insert', function() {
-              self._$altField.addClass('insert-required');
-            })
-            .on('mouseout.insert', function() {
-              self._$altField.removeClass('insert-required');
-            });
-        }
-
-        PARENT.prototype._disable.apply(this, arguments);
       }
+    }
 
-    });
-
-    return ImageHandler;
-
-  })();
+  }
 
-})(jQuery, Drupal);
+})(Drupal);
\ No newline at end of file
diff --git a/js/Inserter.js b/js/Inserter.js
index 1cb26c8..eee03f4 100644
--- a/js/Inserter.js
+++ b/js/Inserter.js
@@ -1,185 +1,108 @@
-(function($, Drupal) {
+(function(Drupal) {
   'use strict';
 
   /**
-   * Handles inserting content into text areas and editors.
-   * @constructor
-   *
-   * @param {HTMLElement} insertContainer
-   * @param {Drupal.insert.FocusManager} focusManager
-   * @param {Drupal.insert.EditorInterface} [editorInterface]
+   * The Inserter manages inserting content by interfacing to the Focus Manager
+   * and a Handler instance. The Inserter will retrieve the insertion target
+   * from the Focus Manager and use a Handler to create the content to insert.
    */
-  Drupal.insert.Inserter = Drupal.insert.Inserter || (function() {
+  Drupal.insert.Inserter = class {
 
     /**
-     * @constructor
-     *
-     * @param {HTMLElement} insertContainer
-     * @param {Drupal.insert.FocusManager} focusManager
-     * @param {Drupal.insert.EditorInterface|undefined} [editorInterface]
+     * The element Insert is initialized on.
+     * @type {HTMLFieldSetElement}
      */
-    function Inserter(insertContainer, focusManager, editorInterface) {
-      var self = this;
+    #container;
 
-      if (typeof insertContainer === 'undefined') {
-        throw new Error('insertContainer needs to be specified.');
-      }
-      if (typeof focusManager === 'undefined') {
-        throw new Error('focusManager needs to be specified.')
-      }
-      if (editorInterface && typeof editorInterface !== 'object') {
-        throw new Error('editorInterface needs to be an instance of Drupal.insert.EditorInterface.');
-      }
+    /**
+     * The handler specific to the insertion method.
+     * @type {Drupal.insert.Handler}
+     */
+    #handler;
 
-      this._focusManager = focusManager;
-      this._editorInterface = editorInterface;
+    /**
+     * The "style" selector.
+     * @type {HTMLSelectElement}
+     */
+    #insertStyle;
 
-      this.$container = $(insertContainer);
-      this.$insertStyle = this.$container.find('.insert-style');
-      this.$button = this.$container.find('.insert-button');
-      this._type = this.$container.data('insert-type');
+    /**
+     * The insert button.
+     * @type {HTMLButtonElement}
+     */
+    #button;
 
-      this.$button.on('click.insert', function() {
-        self._insert();
-      });
+    /**
+     * @param {HTMLFieldSetElement} insertContainer
+     * @param {Drupal.insert.Handler} handler
+     */
+    constructor(insertContainer, handler) {
+      this.#container = insertContainer;
+      this.#handler = handler;
+      this.#insertStyle = this.#container.querySelector('.insert-style');
+      this.#button = this.#container.querySelector('.insert-button');
+
+      this.#button.addEventListener('click', () => this.#insert());
     }
 
-    $.extend(Inserter.prototype, {
-
-      /**
-       * @type {Drupal.insert.FocusManager}
-       */
-      _focusManager: undefined,
-
-      /**
-       * @type {Drupal.insert.EditorInterface|undefined}
-       */
-      _editorInterface: undefined,
-
-      /**
-       * @type {jQuery}
-       */
-      $container: undefined,
-
-      /**
-       * The Insert style select box or the hidden style input, if just one
-       * style is enabled.
-       * @type {jQuery}
-       */
-      $insertStyle: undefined,
-
-      /**
-       * @type {jQuery}
-       */
-      $button: undefined,
-
-      /**
-       * The widget type or type of the field, the Inserter interacts with, i.e.
-       * "file" or "image".
-       * @type {string|undefined}
-       */
-      _type: undefined,
-
-      /**
-       * @return {Drupal.insert.FocusManager}
-       */
-      getFocusManager: function() {
-        return this._focusManager;
-      },
-
-      /**
-       * @return {Drupal.insert.EditorInterface|undefined}
-       */
-      getEditorInterface: function() {
-        return this._editorInterface;
-      },
-
-      /**
-       * Returns the insert type.
-       *
-       * @return {string}
-       */
-      getType: function() {
-        return this._type;
-      },
-
-      /**
-       * Returns the template for the currently selected insert style.
-       *
-       * @return {string}
-       */
-      getTemplate: function() {
-        var style = this.$insertStyle.val();
-        return $('input.insert-template[name$="[' + style + ']"]', this.$container).val();
-      },
-
-      /**
-       * Inserts content into the current (or last active) editor/textarea on
-       * the page.
-       *
-       * @return {HTMLElement|undefined}
-       *
-       * @triggers insert
-       */
-      _insert: function() {
-        var active = this._focusManager.getActive();
-        var activeElement;
-        var content = $(this).triggerHandler('insert');
-
-        if (active && active.insertHtml && this._editorInterface) {
-          active.insertHtml(content);
-          activeElement = this._editorInterface.getElement(active);
-        }
-        else if (active) {
-          this._insertAtCursor(active, content);
-          activeElement = active;
-        }
-
-        return activeElement;
-      },
-
-      /**
-       * Insert content into a textarea at the current cursor position.
-       *
-       * @param {HTMLElement} textarea
-       *   The DOM object of the textarea that will receive the text.
-       * @param {string} content
-       *   The string to be inserted.
-       */
-      _insertAtCursor: function(textarea, content) {
-        // Record the current scroll position.
-        var scroll = textarea.scrollTop;
-
-        // IE support.
-        if (document.selection) {
-          textarea.focus();
-          var sel = document.selection.createRange();
-          sel.text = content;
-        }
-
-        // Mozilla/Firefox/Netscape 7+ support.
-        else if (textarea.selectionStart || textarea.selectionStart == '0') {
-          var startPos = textarea.selectionStart;
-          var endPos = textarea.selectionEnd;
-          textarea.value = textarea.value.substring(0, startPos)
-            + content
-            + textarea.value.substring(endPos, textarea.value.length);
-          textarea.selectionStart = textarea.selectionEnd = startPos + content.length;
-        }
-
-        // Fallback, just add to the end of the content.
-        else {
-          textarea.value += content;
-        }
-
-        // Ensure the textarea does not scroll unexpectedly.
-        textarea.scrollTop = scroll;
+    /**
+     * @returns {HTMLFieldSetElement}
+     */
+    get container() {
+      return this.#container;
+    }
+
+    /**
+     * Entrypoint for inserting content into an editor or textarea.
+     */
+    #insert() {
+      const target = Drupal.insert.FocusManager.getTarget();
+      const content = this.#handler.buildContent();
+
+      if (CKEditor5 && target instanceof CKEditor5.core.Editor) {
+        this.#insertIntoEditor(target, content);
+      } else if (target) {
+        this.#insertAtCursor(target, content);
       }
+    }
+
+    /**
+     * @param {ckeditor.Editor} editor
+     * @param {string} content
+     */
+    #insertIntoEditor(editor, content) {
+      editor.model.change(writer => {
+        const viewFragment = editor.data.processor.toView(content);
+        const modelFragment = editor.data.toModel(viewFragment);
+        writer.model.insertContent(modelFragment);
+
+        // Insert an empty space to step put of the inserted HTML structure when
+        // focusing the editor. Using '\u2060' (word-joiner) is not sufficient.
+        writer.model.insertContent(
+          writer.createText(' '),
+          modelFragment.parent
+        );
+      });
+    }
 
-    });
+    /**
+     * @param {HTMLTextAreaElement} textarea
+     * @param {string} content
+     */
+    #insertAtCursor(textarea, content) {
+      const scroll = textarea.scrollTop;
+      const startPos = textarea.selectionStart;
+      const endPos = textarea.selectionEnd;
+
+      textarea.value = textarea.value.substring(0, startPos)
+        + content
+        + textarea.value.substring(endPos, textarea.value.length);
 
-    return Inserter;
+      textarea.selectionStart = textarea.selectionEnd = startPos + content.length;
 
-  })();
+      // Restore the initial scroll position
+      textarea.scrollTop = scroll;
+    }
+  }
 
-})(jQuery, Drupal);
+})(Drupal);
\ No newline at end of file
diff --git a/js/Rotator.js b/js/Rotator.js
deleted file mode 100644
index dc4ab01..0000000
--- a/js/Rotator.js
+++ /dev/null
@@ -1,234 +0,0 @@
-(function($, Drupal) {
-  'use strict';
-
-  /**
-   * Image Rotator
-   * Responsible for having images rotated and updates image derivatives already
-   * placed in editor.
-   * @constructor
-   *
-   * @param {HTMLElement} node
-   * @param {HTMLElement} templates
-   * @param {Drupal.insert.FocusManager} focusManager
-   * @param {Drupal.insert.EditorInterface} [editorInterface]
-   */
-  Drupal.insert.Rotator = Drupal.insert.Rotator || (function() {
-
-    /**
-     * @type {Drupal.insert.FocusManager}
-     */
-    var fManager;
-
-    /**
-     * @type {Drupal.insert.EditorInterface}
-     */
-    var eInterface;
-
-    /**
-     * @constructor
-     *
-     * @param {HTMLElement} node
-     * @param {HTMLElement} templates
-     * @param {Drupal.insert.FocusManager} focusManager
-     * @param {Drupal.insert.EditorInterface} [editorInterface]
-     */
-    function Rotator(node, templates, focusManager, editorInterface) {
-      var self = this;
-
-      if (typeof node === 'undefined') {
-        throw new Error('Rotator root node needs to be specified.')
-      }
-      if (typeof node === 'undefined') {
-        throw new Error('Templates node needs to be specified.')
-      }
-      if (typeof focusManager !== 'object') {
-        throw new Error('focusManager needs to be an instance of Drupal.insert.FocusManager.')
-      }
-      if (editorInterface && typeof editorInterface !== 'object') {
-        throw new Error('editorInterface needs to be an instance of Drupal.insert.EditorInterface.');
-      }
-
-      this._$node = $(node);
-      this._$templates = $(templates);
-      fManager = focusManager;
-      eInterface = editorInterface;
-
-      $('.insert-rotate-controls a', node).on('click.insert-rotator', function(event) {
-        event.preventDefault();
-
-        $.ajax($(this).attr('href'), {
-          dataType: 'json'
-        })
-          .done(function(response) {
-            $('input[name="changed"]').val(response.revision);
-            self._updateImageRotation(response.data);
-          });
-      });
-    }
-
-    $.extend(Rotator.prototype, {
-
-      /**
-       * @type {jQuery}
-       */
-      _$node: undefined,
-
-      /**
-       * @type {jQuery}
-       */
-      _$templates: undefined,
-
-      /**
-       * Updates the preview image, the insert templates as well as any images
-       * derivatives already placed.
-       *
-       * @param {Object} json
-       */
-      _updateImageRotation: function(json) {
-        $.each(json, function(style_name, url) {
-          json[style_name] += url.indexOf('?') === -1 ? '?' : '&';
-          json[style_name] += 'insert-refresh=' + Date.now();
-        });
-
-        this._updatePreviewImage(json);
-        this._updateTemplates(json);
-        this._updateInsertedImages(json);
-      },
-
-      /**
-       * @param {Object} json
-       */
-      _updatePreviewImage: function(json) {
-        var $previewImg = this._$node.parents('.image-widget').find('.image-preview img');
-
-        if (!$previewImg.length) {
-          return;
-        }
-
-        $.each($previewImg.attr('class').split(/\s+/), function() {
-          var styleClass = this.match('^image-style-(.+)');
-
-          if (styleClass !== null && typeof json[styleClass[1]] !== 'undefined') {
-            $previewImg
-              .attr('src', json[styleClass[1]])
-              .removeAttr('width')
-              .removeAttr('height');
-
-            return false;
-          }
-        });
-      },
-
-      /**
-       * @param {Object} json
-       */
-      _updateTemplates: function(json) {
-        var self = this;
-
-        $.each(json, function(styleName, url) {
-          self._$templates
-            .children('.insert-template[name*="[' + styleName + ']"]')
-            .each(function() {
-              var $template = $(this);
-              var template =  $template.val();
-              var widthMatches = template.match(/width[ ]*=[ ]*"(\d*)"/i);
-              var heightMatches = template.match(/height[ ]*=[ ]*"(\d*)"/i);
-
-              if (heightMatches && heightMatches.length === 2) {
-                template = template.replace(/(width[ ]*=[ ]*")(\d*)"/i, 'width="' + heightMatches[1] + '"');
-              }
-              if (widthMatches && widthMatches.length === 2) {
-                template = template.replace(/(height[ ]*=[ ]*")(\d*)"/i, 'height="' + widthMatches[1] + '"');
-              }
-
-              $template.val(
-                template.replace(/src="[^"]+"/, 'src="' + url + '"')
-              );
-            });
-        });
-      },
-
-      /**
-       * @param {Object} json
-       */
-      _updateInsertedImages: function(json) {
-        var self = this;
-
-        $.each(json, function(styleName, url) {
-          var updatedImageCleanUrl = url.split('?')[0];
-          fManager.getTextareas().each(function() {
-            var $textarea = $(this);
-            var textareaString = $textarea.val();
-            var $newDom = self._updateDom($('<div>').html($(this).val()), url, updatedImageCleanUrl);
-
-            if ($newDom !== null) {
-              $newDom.find('img').each(function(index) {
-                var i = 0;
-                textareaString.replace(
-                  /<img[^>]*>/g,
-                  match => i++ !== index ? match : $('<div>').append($(this)).html()
-                );
-              });
-
-              $textarea.val(textareaString);
-            }
-          });
-
-
-          if (eInterface) {
-            $.each(fManager.getEditors(), function() {
-              self._updateDom(eInterface.getDom(this), url, updatedImageCleanUrl);
-            });
-          }
-        });
-      },
-
-      /**
-       * @param {jQuery} $dom
-       * @param {string} url
-       * @param {string} updatedImageCleanUrl
-       * @return {jQuery|null}
-       *   null if no update was applied.
-       */
-      _updateDom: function($dom, url, updatedImageCleanUrl) {
-        var found = false;
-
-        $dom.find('img').each(function() {
-          var $img = $(this);
-          var imgCleanUrl = $img.attr('src').split('?')[0];
-
-          if (imgCleanUrl === updatedImageCleanUrl) {
-            var width = $img.attr('width');
-            var height = $img.attr('height');
-
-            if (width) {
-              $img.attr('height', width);
-            }
-            else {
-              $img.removeAttr('height');
-            }
-            if (height) {
-              $img.attr('width', height);
-            }
-            else {
-              $img.removeAttr('width');
-            }
-
-            // Editor is supposed to automatically take care of the cache
-            // breaker getting removed.
-            $img.attr('src', url);
-
-            found = true;
-          }
-        });
-
-        return found ? $dom : null;
-      }
-
-    });
-
-    return Rotator;
-
-  })();
-
-})(jQuery, Drupal);
diff --git a/js/editors/CKEditor.js b/js/editors/CKEditor.js
deleted file mode 100644
index 066ed4b..0000000
--- a/js/editors/CKEditor.js
+++ /dev/null
@@ -1,184 +0,0 @@
-(function($, Drupal) {
-  'use strict';
-
-  /**
-   * @constructor
-   */
-  Drupal.insert.editors.CKEditor = Drupal.insert.editors.CKEditor || (function() {
-
-    /**
-     * @constructor
-     */
-    function CKEditor() {
-      var self = this;
-
-      if (this.check()) {
-        CKEDITOR.on('instanceReady', function(e) {
-          $(self).trigger({
-            type: 'instanceReady',
-            editor: e.editor
-          });
-        });
-      }
-    }
-
-    $.extend(CKEditor.prototype, {
-      constructor: CKEditor,
-
-      /**
-       * @inheritDoc
-       */
-      editorConstructor: CKEDITOR.editor,
-
-      /**
-       * @inheritDoc
-       */
-      check: function() {
-        return typeof CKEDITOR !== 'undefined';
-      },
-
-      /**
-       * @inheritDoc
-       */
-      getId: function(editor) {
-        return editor.id;
-      },
-
-      /**
-       * @inheritDoc
-       */
-      isReady: function(editor) {
-        return editor.status === 'ready';
-      },
-
-      /**
-       * @inheritDoc
-       */
-      getInstances: function() {
-        return CKEDITOR.instances;
-      },
-
-      /**
-       * @inheritDoc
-       */
-      getCurrentInstance: function() {
-        return CKEDITOR?.currentInstance;
-      },
-
-      /**
-       * @inheritDoc
-       */
-      getElement: function(editor) {
-        return editor.element ? editor.element.$ : undefined;
-      },
-
-      /**
-       * @inheritDoc
-       */
-      getDom: function(editor) {
-        return $(editor.document.$).find('body');
-      },
-
-      /**
-       * @inheritDoc
-       */
-      getData: function(editor) {
-        return editor.getData();
-      },
-
-      /**
-       * @inheritDoc
-       */
-      setCaption: function(editor, syncId, text) {
-        if (!editor.widgets) {
-          // Since captions are managed by widgets, no caption to update is
-          // present if there are no widgets.
-          return;
-        }
-
-        $.each(editor.widgets.instances, function() {
-          var $element = $(this.element.$);
-          var attach = $element
-            .find('[data-insert-attach]')
-            .addBack('[data-insert-attach]')
-            .data('insert-attach');
-
-          if (!attach || syncId !== attach.id) {
-            return true;
-          }
-
-          // Since setData will trigger events, avoid calling if there is no
-          // reason to.
-          if (text === '' && this.data.hasCaption) {
-            this.setData('hasCaption', false);
-          }
-          else if (text !== '' && !this.data.hasCaption) {
-            this.setData('hasCaption', true);
-          }
-
-          if (text !== '') {
-            // Text will not be set when caption is just being added by setting
-            // hasCaption to true, because setData is running asynchronously.
-            // CKEDITOR.plugins.widget.setData does not support providing a
-            // callback like CKEDITOR.editor.setData and
-            // CKEDITOR.plugins.widget's data event is triggered before changes
-            // are applied.
-            $element.find('[data-caption]').attr('data-caption', text);
-            $element.closest('.caption-img').find('figcaption').text(text);
-          }
-        });
-      },
-
-      /**
-       * @inheritDoc
-       */
-      getAlign: function(editor, uuid) {
-        var align = null;
-        var hasInstances = false;
-
-        $.each(this._filterInstances(editor, uuid), function() {
-          align = this.data.align;
-          hasInstances = true;
-          return false;
-        });
-
-        return !hasInstances ? undefined : align;
-      },
-
-      /**
-       * @inheritDoc
-       */
-      setAlign: function(editor, uuid, value) {
-        $.each(this._filterInstances(editor, uuid), function() {
-          this.setData('align', value);
-        });
-      },
-
-      /**
-       * @param {CKEDITOR.editor} editor
-       * @param {string} uuid
-       * @return {CKEDITOR.plugins.widget[]}
-       */
-      _filterInstances: function(editor, uuid) {
-        var instances = [];
-        var regExp = new RegExp(uuid + '$');
-
-        $.each(editor.widgets.instances, function() {
-          var instanceUUID = this.data['data-entity-uuid'];
-          if (instanceUUID && instanceUUID.match(regExp)) {
-            instances.push(this);
-          }
-        });
-
-        return instances;
-      }
-
-    });
-
-    return CKEditor;
-
-  })();
-
-  Drupal.insert.editors.interfaces.CKEditor = new Drupal.insert.editors.CKEditor();
-
-})(jQuery, Drupal);
diff --git a/js/editors/namespace.js b/js/editors/namespace.js
deleted file mode 100644
index 013d55f..0000000
--- a/js/editors/namespace.js
+++ /dev/null
@@ -1,6 +0,0 @@
-(function(Drupal) {
-  'use strict';
-
-  Drupal.insert.editors = Drupal.insert.editors || {};
-  Drupal.insert.editors.interfaces = Drupal.insert.editors.interfaces || {};
-})(Drupal);
diff --git a/js/insert.api.js b/js/insert.api.js
deleted file mode 100644
index 9f14afd..0000000
--- a/js/insert.api.js
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * @file
- * This is an example on how to implement the JavaScript part of Insert handling
- * for custom insert types.
- */
-
-(function($, Drupal) {
-  'use strict';
-
-  // The custom insert type id matching the one defined in the PHP module code:
-  var INSERT_TYPE_TEXT = 'text';
-
-  // CSS selector definitions that will be used to retrieve values.
-  var SELECTORS = {
-    description: 'input[name$="[description]"]'
-  };
-
-  // "insert_text" would be the module's machine name.
-  Drupal.behaviors.insert_text = {};
-
-  Drupal.behaviors.insert_text.attach = function(context) {
-    $('.insert', context).each(function() {
-      var $insert = $(this);
-
-      // Prevent processing a different insert type:
-      if ($insert.data('insert-type') !== INSERT_TYPE_TEXT) {
-        return;
-      }
-
-      // $insert.data('insert') is the Drupal.insert.Inserter instance attached
-      // to a field's value.
-      var $inserter = $($insert.data('insert'));
-
-      // Be sure to have the event listener attached only once.
-      $inserter.off('.insert_text').on('insert.insert_text', function(e) {
-        var inserter = e.target;
-
-        // Inserter manages retrieving the correct template for the currently
-        // selected style:
-        var template = inserter.getTemplate();
-
-        // The "root" node of the field that replacement value selectors shall
-        // be triggered on:
-        var $field = inserter.$container.closest('td');
-
-        // Loop through the selectors:
-        $.each(SELECTORS, function(key, selector) {
-          // Process replacements.
-          var value = $field.find(selector).val();
-          // This demonstrates a simple placeholder replacement. For more
-          // sophisticated functionality, like synchronisation, it might make
-          // sense to implement a prototype inheriting from
-          // Drupal.insert.Handler, attach an instance to the field and call:
-          // $field.data('myHandlerInstance').buildContent();
-          var fieldRegExp = new RegExp('__' + key + '__', 'g');
-          template = template.replace(fieldRegExp, value);
-        });
-
-        // Return the processed template.
-        return template;
-      });
-
-    });
-  }
-
-})(jQuery, Drupal);
diff --git a/js/insert.js b/js/insert.js
index 2cf9603..9d286e8 100644
--- a/js/insert.js
+++ b/js/insert.js
@@ -1,15 +1,10 @@
-(function($, Drupal, drupalSettings) {
+(function(Drupal, drupalSettings) {
   'use strict';
 
   /**
-   * @type {Drupal.insert.FocusManager}
+   * @type {Drupal.insert.Inserter[]}
    */
-  var focusManager;
-
-  /**
-   * @type {Drupal.insert.EditorManager|undefined}
-   */
-  var editorManager;
+  const registry = [];
 
   /**
    * Behavior to add "Insert" buttons.
@@ -21,130 +16,37 @@
       return;
     }
 
-    var editorInterface = undefined;
+    Drupal.insert.FocusManager = new Drupal.insert.Manager();
 
-    $.each(Drupal.insert.editors.interfaces, function() {
-      if (this.check()) {
-        editorInterface = this;
-        return false;
-      }
+    // Populate the Focus Manager registry
+    document.querySelectorAll('.ck-editor__editable').forEach(element => {
+      Drupal.insert.FocusManager.addEditor(element.ckeditorInstance);
     });
-
-    focusManager = focusManager || new Drupal.insert.FocusManager(
-      editorInterface
-    );
-
-    focusManager.addTextareas($('textarea:not([name$="[data][title]"])'));
-
-    if (editorInterface) {
-      editorManager = editorManager || new Drupal.insert.EditorManager(
-        editorInterface
-      );
-
-      // Aggregate classes each time the behaviour is triggered as another Insert
-      // type ("image", "file"), that has not been loaded yet, might have been
-      // loaded now.
-      editorManager.updateClassesToRetain(aggregateClassesToRetain());
-    }
-
-    // insert.js is loaded on page load.
-    if (editorInterface) {
-      $(editorInterface).on('instanceReady', function(e) {
-        if (editorManager) {
-          editorManager.addEditor(e.editor);
-        }
-        focusManager.addEditor(e.editor);
-      });
-    }
-
-    // insert.js is loaded asynchronously.
-    $.each(editorInterface.getInstances(), function(id, editor) {
-      if (editorInterface.isReady(editor)) {
-        if (editorManager) {
-          editorManager.addEditor(editor);
-        }
-        focusManager.addEditor(editor);
-      }
+    document.querySelectorAll('textarea').forEach(textarea => {
+      Drupal.insert.FocusManager.addTextarea(textarea);
     });
 
-    $('.insert', context).each(function() {
-      var $insert = $(this);
-
-      if (!$insert.data('insert')) {
-        var inserter = new Drupal.insert.Inserter(this, focusManager, editorInterface);
-        $insert.data('insert', inserter);
-
-        focusManager.setDefaultTarget(determineDefaultTarget($insert).get(0));
-      }
-
-      var insertType = $insert.data('insert-type');
-
-      if (insertType !== 'file' && insertType !== 'image') {
-        return true;
+    // Initialize Inserter managing content insertion
+    context.querySelectorAll('.insert').forEach(element => {
+      if (registry.find(inserter => inserter.container === element)) {
+        return;
       }
 
-      // Handle default insert types; Custom (third-party) insert types are to
-      // be handled by the modules supplying such custom types.
+      const insertType = element.dataset.insertType;
 
-      if (!$insert.data('insert-handler')) {
-        $insert.data('insert-handler',
+      registry.push(
+        new Drupal.insert.Inserter(
+          element,
           new Drupal.insert[insertType === 'image'
             ? 'ImageHandler'
             : 'FileHandler'
-            ](inserter, drupalSettings.insert.widgets[insertType])
-        )
-      }
-
-      $(inserter).off('.insert').on('insert.insert', function() {
-        return $insert.data('insert-handler').buildContent();
-      });
+          ](
+            element,
+            drupalSettings.insert.widgets[insertType]
+          )
+        ),
+      );
     });
-
   };
 
-  /**
-   * CKEditor removes all other classes when setting a style defined in
-   * CKEditor. Since it is impossible to inject solid code into CKEditor, CSS
-   * classes that should be retained are gathered for checking against those
-   * actually applied to individual images.
-   *
-   * @return {Object}
-   */
-  function aggregateClassesToRetain() {
-    var classesToRetain = {};
-
-    $.each(drupalSettings.insert.classes, function(type, typeClasses) {
-      classesToRetain[type] = [];
-
-      var classesToRetainString = typeClasses.insertClass
-        + ' ' + typeClasses.styleClass;
-
-      $.each(classesToRetainString.split(' '), function() {
-        classesToRetain[type].push(this.trim());
-      });
-    });
-
-    return classesToRetain;
-  }
-
-  /**
-   * Determines the default target objects shall be inserted in. The default
-   * target is used when no text area was focused yet.
-   *
-   * @param {jQuery} $insert
-   * @return {jQuery}
-   */
-  function determineDefaultTarget($insert) {
-    var $commentBody = $insert
-      .parents('.comment-form')
-      .find('#edit-comment-body-wrapper')
-      .find('textarea.text-full');
-
-    if ($commentBody.length) {
-      return $commentBody;
-    }
-
-    return $('#edit-body-wrapper').find('textarea.text-full');
-  }
-
-})(jQuery, Drupal, drupalSettings);
+})(Drupal, drupalSettings);
\ No newline at end of file
diff --git a/modules/insert_responsive_image/insert_responsive_image.module b/modules/insert_responsive_image/insert_responsive_image.module
index 21c65bb..a7357a9 100644
--- a/modules/insert_responsive_image/insert_responsive_image.module
+++ b/modules/insert_responsive_image/insert_responsive_image.module
@@ -48,11 +48,19 @@ function insert_responsive_image_insert_variables($insertType, array &$element,
     return;
   }
 
+  $image = \Drupal::service('image.factory')->get($file->getFileUri());
+
+  if (!$image->isValid()) {
+    return;
+  }
+
   $responsiveImageVars = [
+    'height' => $image->getHeight(),
     'item' => NULL,
     'item_attributes' => NULL,
     'responsive_image_style_id' => $styleName,
     'uri' => $file->getFileUri(),
+    'width' => $image->getWidth(),
   ];
 
   $style = ImageStyle::load($responsiveStyle->getFallbackImageStyle());
@@ -106,4 +114,4 @@ function insert_responsive_image_module_implements_alter(array &$implementations
     unset($implementations['insert_responsive_image']);
     $implementations['insert_responsive_image'] = $group;
   }
-}
+}
\ No newline at end of file
diff --git a/templates/insert-button-widget.html.twig b/templates/insert-button-widget.html.twig
index 69d3f2c..6c61d80 100644
--- a/templates/insert-button-widget.html.twig
+++ b/templates/insert-button-widget.html.twig
@@ -4,40 +4,6 @@
  * Template file for the insert button.
  */
 #}
-{% if insert.settings.rotate %}
-  <div class="insert-rotate">
-    <label>{{ 'Rotate'|trans }}</label>
-    <span class="insert-rotate-controls">
-      <span class="insert-rotate-controls-left"><a href="{{ url('insert.rotate', {'fid': insert.settings.fid, 'degree': 270, 'nid': nid}) }}">↺</a></span>
-      <span class="insert-rotate-controls-right"><a href="{{ url('insert.rotate', {'fid': insert.settings.fid, 'degree': 90, 'nid': nid}) }}">↻</a></span>
-    </span>
-  </div>
-{% endif %}
-
-{% if insert.settings.align %}
-  <div class="insert-align form-type-radios">
-    <label>{{ 'Align'|trans }}</label>
-    <span class="insert-align-controls container-inline">
-      <span class="insert-align-controls-none form-type-radio">
-        <input type="radio" id="insert-{{ id }}-align-none" name="insert-{{ id }}[align]" value="none" checked />
-        <label for="insert-{{ id }}-align-none">{{ 'None'|trans }}</label>
-      </span>
-      <span class="insert-align-controls-left form-type-radio">
-        <input type="radio" id="insert-{{ id }}-align-left" name="insert-{{ id }}[align]" value="left" />
-        <label for="insert-{{ id }}-align-left">{{ 'Left'|trans }}</label>
-      </span>
-      <span class="insert-align-controls-center form-type-radio">
-        <input type="radio" id="insert-{{ id }}-align-center" name="insert-{{ id }}[align]" value="center" />
-        <label for="insert-{{ id }}-align-center">{{ 'Center'|trans }}</label>
-      </span>
-      <span class="insert-align-controls-right form-type-radio">
-        <input type="radio" id="insert-{{ id }}-align-right" name="insert-{{ id }}[align]" value="right" />
-        <label for="insert-{{ id }}-align-right">{{ 'Right'|trans }}</label>
-      </span>
-    </span>
-  </div>
-{% endif %}
-
 {% if insert.styles|length > 1 %}
     <div class="insert-style-select">
       <label for="insert-{{ id }}">{{ 'Style'|trans }}</label>
@@ -51,4 +17,4 @@
   <input type="hidden" class="insert-style" value="{{ insert.default_style }}" />
 {% endif %}
 
-<span class="insert-button-wrapper"><span class="insert-button-overlay"></span><input type="submit" class="button js-form-submit form-submit insert-button" onclick="return false;" value="{{ 'Insert'|trans }}" data-insert-id="{{ insert.id }}" /></span>
+<span class="insert-button-wrapper"><input type="submit" class="button js-form-submit form-submit insert-button" onclick="return false;" value="{{ 'Insert'|trans }}" data-insert-id="{{ insert.id }}" /></span>
\ No newline at end of file
diff --git a/templates/insert-image.html.twig b/templates/insert-image.html.twig
index 3987268..4ca6b55 100644
--- a/templates/insert-image.html.twig
+++ b/templates/insert-image.html.twig
@@ -63,7 +63,7 @@
   }
 %}
 {% if insert_settings.caption %}
-  {% set attach = attach|merge({attributes: {'data-caption': ['title']}}) %}
+  {% set attach = attach|merge({attributes: attach.attributes|merge({'data-caption': ['title']})}) %}
 {% endif %}
 
 <img src="{{ url }}"{{ attributes }} {% if width and height %}width="{{ width }}" height="{{ height }}" {% endif %}{% if classes|length %} class="{{ classes|join(' ') }}"{% endif %} data-insert-type="{{ field_type }}" data-entity-type="{{ entity_type }}" data-entity-uuid="{{ uuid }}" data-insert-attach='{{ attach|json_encode() }}' />
@@ -72,4 +72,4 @@
   </a>
 {% endif %}
 
-{% endapply %}
+{% endapply %}
\ No newline at end of file
diff --git a/tests/src/FunctionalJavaScript/InsertImageCKEditorTest.php b/tests/src/FunctionalJavaScript/InsertImageCKEditorTest.php
index a107c51..ab5a664 100644
--- a/tests/src/FunctionalJavaScript/InsertImageCKEditorTest.php
+++ b/tests/src/FunctionalJavaScript/InsertImageCKEditorTest.php
@@ -71,56 +71,4 @@ class InsertImageCKEditorTest extends InsertImageCKEditorTestBase {
     $this->assertTrue($hasCaption, 'Verified caption being inserted on images already placed: ' . (string) $hasCaption);
   }
 
-  /**
-   *
-   */
-  public function testAlign() {
-    $fieldName = strtolower($this->randomMachineName());
-
-    $this->createImageField($fieldName, ['alt_field' => '0']);
-    $this->updateInsertSettings($fieldName, [
-      'styles' => [
-        'image' => 'image',
-      ],
-      'default' => 'image',
-      'align' => TRUE,
-    ]);
-
-    $images = $this->drupalGetTestFiles('image');
-
-    $this->drupalGet('node/add/article');
-    $page = $this->getSession()->getPage();
-
-    $page->attachFileToField(
-      'files[' . $fieldName . '_0]',
-      \Drupal::service('file_system')->realpath($images[0]->uri)
-    );
-
-    $this->assertSession()->waitForField($fieldName . '[0][fids]');
-
-    $page->findButton('Insert')->click();
-
-    $alignQuery = 'CKEDITOR.instances["edit-body-0-value"].widgets.instances[0].data.align';
-    $align = $this->getSession()->evaluateScript($alignQuery);
-    $this->assertEquals('none', $align, 'Verified initial align attribute: ' . $align);
-
-    $elements = $page->findAll('css', '.insert-align-controls input');
-    end($elements)->click();
-
-    $align = $this->getSession()->evaluateScript($alignQuery);
-    $this->assertEquals('right', $align, 'Verified altering align attribute: ' . $align);
-
-    $page->findField('title[0][value]')->setValue('title');
-    $page->findButton('Save')->click();
-    $this->drupalGet('node/1/edit');
-
-    $align = $this->getSession()->evaluateScript($alignQuery);
-    $this->assertEquals('right', $align, 'Verified align attribute after reloading form of saved node: ' . $align);
-
-    $page->find('css', '.insert-align-controls input')->click();
-
-    $align = $this->getSession()->evaluateScript($alignQuery);
-    $this->assertEquals('none', $align, 'Verified reset align attribute: ' . $align);
-  }
-
-}
+}
\ No newline at end of file
diff --git a/tests/src/FunctionalJavaScript/InsertImageCKEditorTestBase.php b/tests/src/FunctionalJavaScript/InsertImageCKEditorTestBase.php
index 4d4e05e..78c08c0 100644
--- a/tests/src/FunctionalJavaScript/InsertImageCKEditorTestBase.php
+++ b/tests/src/FunctionalJavaScript/InsertImageCKEditorTestBase.php
@@ -29,9 +29,6 @@ abstract class InsertImageCKEditorTestBase extends InsertImageTestBase {
       'name' => 'Full HTML',
       'weight' => 0,
       'filters' => [
-        'filter_align' => [
-          'status' => 1,
-        ],
         'filter_caption' => [
           'status' => 1,
         ],
@@ -48,4 +45,4 @@ abstract class InsertImageCKEditorTestBase extends InsertImageTestBase {
     $editor->save();
   }
 
-}
+}
\ No newline at end of file
diff --git a/tests/src/FunctionalJavaScript/InsertImageTest.php b/tests/src/FunctionalJavaScript/InsertImageTest.php
index ab2df93..677ec8c 100644
--- a/tests/src/FunctionalJavaScript/InsertImageTest.php
+++ b/tests/src/FunctionalJavaScript/InsertImageTest.php
@@ -635,53 +635,4 @@ class InsertImageTest extends InsertImageTestBase {
     $this->assertEquals(1, preg_match('!data-caption="some title"[^>]*>$!', $body->getValue()), 'Verified caption being inserted on images already placed: ' . $body->getValue());
   }
 
-  /**
-   *
-   */
-  public function testAlign() {
-    $fieldName = strtolower($this->randomMachineName());
-
-    $this->createImageField($fieldName, ['alt_field' => '0']);
-    $this->updateInsertSettings($fieldName, [
-      'styles' => [
-        'image' => 'image',
-      ],
-      'default' => 'image',
-      'align' => TRUE,
-    ]);
-
-    $images = $this->drupalGetTestFiles('image');
-
-    $this->drupalGet('node/add/article');
-    $page = $this->getSession()->getPage();
-
-    $page->attachFileToField(
-      'files[' . $fieldName . '_0]',
-      \Drupal::service('file_system')->realpath($images[0]->uri)
-    );
-
-    $this->assertSession()->waitForField($fieldName . '[0][fids]');
-
-    $body = $page->findField('body[0][value]');
-
-    $page->findButton('Insert')->click();
-
-    $this->assertEquals(1, preg_match('!data-align="none"[^>]*>$!', $body->getValue()), 'Verified initial align attribute: ' . $body->getValue());
-
-    $elements = $page->findAll('css', '.insert-align-controls input');
-    end($elements)->click();
-
-    $this->assertEquals(1, preg_match('!data-align="right"[^>]*>$!', $body->getValue()), 'Verified altering align attribute: ' . $body->getValue());
-
-    $page->findField('title[0][value]')->setValue('title');
-    $page->findButton('Save')->click();
-    $this->drupalGet('node/1/edit');
-
-    $this->assertEquals('right', $page->find('css', '.insert-align-controls input:checked')->getValue(), 'Verified align attribute after reloading form: ' . $body->getValue());
-
-    $page->find('css', '.insert-align-controls input')->click();
-
-    $this->assertEquals(1, preg_match('!data-align="none"[^>]*>$!', $body->getValue()), 'Verified reset align attribute: ' . $body->getValue());
-  }
-
-}
+}
\ No newline at end of file
-- 
GitLab