diff --git a/css/ImageHandler.css b/css/ImageHandler.css
index f0dff7610b6c8abe4c07497a82f16f46db91171f..fd50d15578f1aae94956a4f11b04dc1d66ae5ff4 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 bfdddc92571af01590868ff1ef0eff1cbc1a657b..6607502fcb6a6593ae86df0096a5d4aef1c279b3 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 3925d4b3af512ce9d6a3caf981777e70eba817da..2561b40dac03f110f3b36193009b16877fa95e1b 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 ba20221fb348782cb5608983cf546a3d22b4f1a3..0000000000000000000000000000000000000000
--- 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 e48b9cfa7c4a0d11d7974fa35bce720a4137ce99..0000000000000000000000000000000000000000
--- 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 75c818a5d03c6cef1ea19936ad120a3f69f217f2..1bdb3b5a6df4d1a0405d9dfff4e611fa1fef695c 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 c62709f552f37203ee55a0c11bbaa145162317d8..081810b3a2f894c5734f733e01041df38a890e6f 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 681908a6d4212cc90674e10d1347231fde5ccd9f..c9d9354e3cd136574556a645952393d1b8bafdaf 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 ea569ffcd832dbf422cdc578af2b0301443cb567..b597743c4f4b6707a86c262af136f097d3543c11 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 1cb26c8135872bbec6f6fb32aebbcbd4121afe19..eee03f427f19838706db5cae3e0d6c69eb9282c9 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 dc4ab01dce32ecdfc46169065085f12bafb896b2..0000000000000000000000000000000000000000
--- 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 066ed4bde714506397062e69fc4bae5bf3deea97..0000000000000000000000000000000000000000
--- 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 013d55f09059ddfbc7505323535d01f9c8f87a4c..0000000000000000000000000000000000000000
--- 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 9f14afdc0cca4aa6e58f9747af028d27ec0fb3d9..0000000000000000000000000000000000000000
--- 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 2cf96039d91216762b40fccd4b498bdce0bd85c1..9d286e87f63f553fa8c21894f38fc99056d06eb9 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 21c65bb02770381f156d0f07546f2fc6e97c53a3..a7357a94c24e97c06d58b2cfb632aa8eb91d3619 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 69d3f2c3e6f13d60b8c5f3a1e34877cfc0f24235..6c61d8075202351b89d264f53d97c818e2cb64b1 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 39872686f7fc335db5f7dddb5038751468858518..4ca6b5503f2fd5572215c4d5a66ff62d8c3e66e0 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 a107c51b5b8e3d76cff8c608419d871a5eea4dfa..ab5a664a23824c2af0df38000510b4ea541f18a5 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 4d4e05e4c7ec73fd52f8b160dd3b9fe3dad87b47..78c08c0179cd44f2b0108d26dcee9e5a74a0d56e 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 ab2df93d326876ca89f6eeb05350a0b92eb825d7..677ec8cab8bf58e9224dbaf858d5f0a92eb9f2e6 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