Update CKEditor 5 to 39.0.0

Created on 27 July 2023, over 1 year ago
Updated 11 August 2023, over 1 year ago

Problem/Motivation



https://github.com/ckeditor/ckeditor5/releases/tag/v39.0.0

This release adds support for empty inline elements, which is a data loss critical and one of the last few remaining top priorities in 🌱 [meta] [upstream] Prioritized CKEditor 5 upstream blockers Active . The lack of this support is a blocker for many downstream projects. See #18 + #19 + #21 for details.

⚠️ This is a major version release, but the only BC break is in CKBox, which no one in the Drupal world uses. So, it’s not a breaking change for the Drupal ecosystem.

(A separate 38.2.0 release for the CKEditor 5 users not using CKBox would add so much unnecessary workload to the team that they could not justify this, see #10 by @Reinmar from the CKEditor 5 team.)

Proposed resolution

  1. Update core/package.json
  2. cd core
  3. yarn install
  4. yarn build
  5. yarn build:ckeditor5-types

Remaining tasks

None.

User interface changes

None.

API changes

None.

Data model changes

None.

Release notes snippet

CKEditor 5 has been updated to 39.0.0. This fixes a long-standing data loss critical bug: until now, CKEditor 5 stripped all inline empty elements, which are typically used for inline icons using markup such as <i class="icon icon-druplicon"></i> or <span class="icon icon-druplicon"></span>. Going forward, this is supported, but requires the Source Editing plugin to be configured to allow <i class> or <span class>, respectively. This must be configured manually, which is better than in CKEditor 4, where it required custom code.

📌 Task
Status

Fixed

Version

10.1

Component
CKEditor 5 

Last updated 2 days ago

Created by

Live updates comments and jobs are added and updated live.
  • JavaScript

    Affects the content, performance, or handling of Javascript.

Sign in to follow issues

Comments & Activities

  • Issue created by @dk-massive
  • Status changed to Postponed over 1 year ago
  • As of this writing 38.2.0 is not at a stable release.

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    There's no 38.2.0 stable release yet, only an alpha: https://github.com/ckeditor/ckeditor5/releases/tag/v38.2.0-alpha.1 😅

    But +1 to everything you said 😊

    Note that this will never be committed to 10.0.x, only to 10.1.x.

  • Assigned to wim leers
  • Status changed to Needs work over 1 year ago
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    All changes must first land in 11.x.

  • 🇬🇧United Kingdom catch

    If this really doesn't break any contrib integrations, I think we should backport it to 10.1.x, but let's get it into 11.x first.

  • last update over 1 year ago
    29,918 pass, 3 fail
  • @wim-leers opened merge request.
  • Issue was unassigned.
  • Status changed to Needs review over 1 year ago
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺
  • last update over 1 year ago
    29,922 pass, 2 fail
  • last update over 1 year ago
    29,922 pass, 2 fail
  • Status changed to Needs work over 1 year ago
  • 🇺🇸United States smustgrave

    Reran tests twice but they consistently fail.

  • 🇵🇱Poland Reinmar Warsaw, Poland

    A bit more context on the versioning from our side:

    CKE5 ecosystem is around ~100 pkgs now. In the past, we versioned them independently, just like semver dictates. Some were in e.g. 5.0.0, other, that changed slower were 1.1.10 let's say. All good.

    But, that created an awful DX – you could never know which packages are actually meant to work together. It'd be fine if you were using the very latest versions of all of them, but sometimes you can't. So, when trying to hardlock to some older CKE5 version, you'd need to somehow figure out versions of all CKE5 packages that were shipped at the same time.

    Theoretically, you may ask, why is it necessary to get those shipped at the same time if you use semver and track all BCs? Couldn't I get some of the latest, but keep some of the older ones if... well, if what?

    Let's welcome npm on stage. And transient dependencies.

    Let's say you use two CKE5 features – FeatA and FeatB. cke5-featA@1.0.0 requires cke5-engine in version ^1.0.0, cke5-featB@1.0.1 relies on a newer version of the engine (^2.0.0) as it was released a year later. As a developer integrating CKE5 you'll only see the versions of featA and featB pkgs. So you think all's good.

    What will npm do if you try to install this setup?

    It will install the cke5-engine package twice to satisfy both feature packages needs. That makes some sense in Node.js, for which npm was built, although, causes issues there too from time to time. But it makes zero sense for the web, where you don't want and very often cannot include one library twice in different versions. Hence our "ckeditor-duplicated-modules" check.

    This isn't a problem isolated to CKEditor 5 itself. We've seen people running into similar issues with other projects too. And we researched how did some of them resolved this madness.

    The most sane resolution was to ditch semver. This kinda works (but again, not often) for libraries, but makes no sense for ecosystems of packages. I don't remember anymore what projects we followed (I think babel and Angular) but we realised that by having all our pkgs in the same version we're resolving many integrators issues.

    But then, the side effect is that a braking change in one package needs to somehow be reflected on the version of all packages. Something that happened in this case. A very narrowly used CKBox package had to have a BC due to a new major version of CKBox itself that had to rebuild its configuration. This isn't a widely used package yet so investing our limited capacity into building compat layer and/or shipping this without a BC on CKE5 side could not be justified. It's literally cheaper to go on a call with everyone using this package and resolving this for them :D

    So, my recommendation is to analyze the changelog of CKE5 itself where we're trying (not always successfully, but we're hopefully improving on this) to list all BCs (that we divide into two categories based on their impact) and figure out whether this is a major or minor release for your use case. The version number of CKEditor 5 is a secondary thing and I started thinking we should use only the X of X.Y.Z :D

  • Assigned to spokje
  • 🇳🇱Netherlands spokje

    Looking into test-failures.

  • 🇳🇱Netherlands spokje

    Looks like CKEditor replaced the inner workings of <figcaption>-element in 39.0.0.

    Where it was a data-placeholder-attribute with the text in it's value, now the text lives inside the <figcaption>-element.

    So at the very least the now failing \Drupal\Tests\ckeditor5\FunctionalJavascript\MediaTest::testEditableCaption needs refactoring to adapt to those changes.

    Unsure if this is an intentional CKEditor5 behaviour change or not, so let's await a verdict on that before actually changing it.

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    The failure:

    There was 1 error:
    
    1) Drupal\Tests\ckeditor5\FunctionalJavascript\MediaTest::testEditableCaption
    Behat\Mink\Exception\ElementHtmlException: The attribute "data-placeholder" was not found in the element matching css "figcaption".
    
    /Users/wim.leers/core/vendor/behat/mink/src/WebAssert.php:829
    /Users/wim.leers/core/vendor/behat/mink/src/WebAssert.php:563
    /Users/wim.leers/core/vendor/behat/mink/src/WebAssert.php:606
    /Users/wim.leers/core/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php:531
    /Users/wim.leers/core/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
    

    Debugged test with:

    diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php
    index 4c03af0ea2..d62272728a 100644
    --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php
    +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php
    @@ -526,6 +526,8 @@ public function testEditableCaption() {
         $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-widget.drupal-media img'));
         $assert_session->elementExists('css', '[data-drupal-media-preview][aria-label="Screaming hairy armadillo"]');
         $assert_session->elementContains('css', 'figcaption', '');
    +    var_dump($assert_session->elementExists('css', 'figcaption')->getOuterHtml());
    +    $this->assertSession()->waitForElement('css', 'body.foo', 900*1000);
         $assert_session->elementAttributeContains('css', 'figcaption', 'data-placeholder', 'Enter media caption');
     
         // Test if you leave the caption blank, but change another attribute,
    

    Unfortunately there is a legitimate regression here:

    i.e. what I see is worse than what @Spokje describes: the placeholder text is just gone for me? 😅

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    Looks like this is likely a consequence of this minor breaking change in the changelog:

    engine: The enablePlaceholder() helper now uses a placeholder property of the passed element. It no longer takes the placeholder text as a text argument.
    
  • last update over 1 year ago
    556 pass, 2 fail
  • last update over 1 year ago
    580 pass
  • Assigned to wim leers
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    👍 Just pushed a fix per #14.

    👍 Fortunately, not a single Drupal contrib module has JS that calls enablePlaceholder(), so it should still be safe to update to CKEditor 5 39 despite this BC break. The fact that it solves 🐛 [upstream] [GHS] CKEditor 5 removes empty inline elements Fixed alone has far more positive Drupal ecosystem impact than the potential negative.

    😰 But … in a basic manual test, I see that there's a hard crash when adding the

    </code> button to the <q>Basic HTML</q> text editor, that causes a crash even <em>before</em> the editor boots (meaning: it never even shows up):
    <code>
    ckeditor5.js?ryt4dm:472 TypeError: Cannot read properties of null (reading 'parent')
        at s (drupalMedia.js?ryt4dm:1:1553)
        at Object.getRelatedElement (drupalMedia.js?ryt4dm:1:9664)
        at q._updateToolbarsVisibility (ckeditor5-dll.js?v=39.0.0:5:620814)
        at n.<anonymous> (ckeditor5-dll.js?v=39.0.0:5:619839)
        at n.fire (ckeditor5-dll.js?v=39.0.0:5:569001)
        at n.update (ckeditor5-dll.js?v=39.0.0:5:534585)
        at Object.fire (ckeditor5-dll.js?v=39.0.0:5:569001)
        at We (ckeditor5-dll.js?v=39.0.0:5:160498)
        at He.fire (ckeditor5-dll.js?v=39.0.0:5:159889)
        at Mo.<anonymous> (ckeditor5-dll.js?v=39.0.0:5:217112)
    

    Debugging that now.

  • 🇳🇱Netherlands spokje

    i.e. what I see is worse than what @Spokje describes: the placeholder text is just gone for me? 😅

    That's because @Spokje described it rather poorly, luckily you found it yourself. 😅

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    getClosestSelectedDrupalMediaWidget() was last refined ~1.5 year ago in #3264775: [drupalMedia] Toolbar should be visible when element inside is focused (back when we were stabilizing CKEditor 5!) and looks like this:

    export function getClosestSelectedDrupalMediaWidget(selection) {
      const viewElement = selection.getSelectedElement();
      if (viewElement && isDrupalMediaWidget(viewElement)) {
        return viewElement;
      }
    
      let parent = selection.getFirstPosition().parent;
    
      while (parent) {
        if (parent.is('element') && isDrupalMediaWidget(parent)) {
          return parent;
        }
    
        parent = parent.parent;
      }
    
      return null;
    }
    

    What's happening is that selection.getFirstPosition() returns null and hence the .parent expression causes a fatal error.

    What's missing is:

      // Perhaps nothing is selected.
      if (selection.getFirstPosition() === null) {
        return null;
      };
    

    Curiously:

    1. this seems to have been a bug all this time? :O
    2. but it worked fine for the past several years
    3. no mention of getFirstPosition() behaving differently in the CKE5 39 release notes, although it has many mentions of Selection-related changes
    4. In 38.1.0, Selection.getFirstPosition() returns the first element in the document, even when there is NO SELECTION 😳 — and that is what 39.0.0 changes and arguably fixes

    So we seem to have ourselves a https://en.wikipedia.org/wiki/Heisenbug, although it's one with a trivial and safe fix… 🙃

    (All using Chrome 115.)

  • last update over 1 year ago
    580 pass
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    htmlSupport.allowEmpty is a new configuration option that will allow us to solve 🐛 [upstream] [GHS] CKEditor 5 removes empty inline elements Fixed . In manual testing I can already confirm it works 👍

    What's missing: test coverage. Tackling that first over at #3337298-22: [upstream] [GHS] CKEditor 5 removes empty inline elements .

  • last update over 1 year ago
    575 pass, 1 fail
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    Since markup like <i class="fab fa-drupal"></i> or <span class="icon my-icon"></span> can only be created through the SourceEditing CKEditor 5 plugin, I added the necessary test coverage there: #3337298-23: [upstream] [GHS] CKEditor 5 removes empty inline elements .

    Copying the test from that issue into this MR, because it's necessary to justify this update: this either fixes a critical data loss bug or it doesn't.

    First, let's verify that the test fails even on version 39 of CKEditor 5, because the test alone of course does not specify the htmlSupport.allowEmpty config.

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    I discussed #19 in detail with @Reinmar and @witeksocha.

    Conclusion:

    1. For unrestricted text formats, #3337298-23: [upstream] [GHS] CKEditor 5 removes empty inline elements MUST NOT assume that all tags can have empty inline elements.
    2. Instead, even for unrestricted text formats, which elements ought to support empty elements should be explicitly configured.
    3. This matches the behavior in CKEditor 4, where it could not even be configured through the UI, it required custom code — see #3337298-7: [upstream] [GHS] CKEditor 5 removes empty inline elements .

    So, adjusting the test coverage slightly for that: even for unrestricted text formats, we should expect

    <p>Before <i class="fab fa-drupal"></i>
    

    to be transformed to

    <p>Before and after.</p>
    

    Cherry-picked #3337298-25: [upstream] [GHS] CKEditor 5 removes empty inline elements here 👍

  • last update over 1 year ago
    575 pass, 1 fail
  • last update over 1 year ago
    583 pass, 1 fail
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    And now the actual fix for 🐛 [upstream] [GHS] CKEditor 5 removes empty inline elements Fixed , which is possible only here, thanks to CKEditor 5 39 containing the upstream fix that we must correctly configure 🤓

    The fix is very simple: configure htmlSupport.allowEmpty, with the list of HTML tags that is configured in the Source Editing view. That means this is must be manually configured. There is no automatic upgrade path. Again, #3337298-7: [upstream] [GHS] CKEditor 5 removes empty inline elements is crucial here: in CKEditor 4 it didn't require manual configuration, but custom code!

    Added release notes for that reason — for both the next minor as well as the next patch release.

  • Issue was unassigned.
  • Status changed to Needs review over 1 year ago
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    Following up on the Heisenbug I discovered in #17:

    1. Kuba Niegowski from the CKEditor 5 team confirmed that this same bug was solved ~1 year ago in the ckeditor5-image package, where the drupalMedia plugin had copied this pattern from. See https://github.com/ckeditor/ckeditor5/issues/11972.
    2. Previously, the isFocused state was waiting until a 50 ms timeout had passed or the selection was changed, that was changed in https://github.com/ckeditor/ckeditor5/pull/14648. Thanks again to Kuba for pinpointing this! 👏

      So there indeed was a slight behavior change, but even then, the bugfix here (https://git.drupalcode.org/project/drupal/-/merge_requests/4530/diffs?co...) is still necessary — we basically got lucky that we didn't trigger this bug until now 😅

    Also: @Spokje said this in Slack:

    FWIW: I couldn't reproduce neither in latest Chrome nor FF whilst being on commit 571b949481

    Which again just confirms that it's very likely a browser execution timing thing which we should not be relying on 👍

    Issue summary updated, ready for final review now!

  • Status changed to Needs work over 1 year ago
  • 🇳🇱Netherlands spokje

    You might want to fix the remaining test failure first? 😈😇

  • last update over 1 year ago
    586 pass
  • Status changed to Needs review over 1 year ago
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    I forgot to adjust the expectations for dynamic plugin configuration test coverage in SourceEditingPluginTest, fixed 👍

    EDIT: cross-posted with @Spokje in #24 😅

  • last update over 1 year ago
    29,952 pass
  • Status changed to RTBC over 1 year ago
  • 🇺🇸United States smustgrave

    Test updates look good. I took a look at 🐛 [upstream] [GHS] CKEditor 5 removes empty inline elements Fixed but should that be postponed first until this lands.

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    #26: 🐛 [upstream] [GHS] CKEditor 5 removes empty inline elements Fixed is adding only test coverage — and I've included that test coverage in this MR. If this MR gets merged, then we can mark that other issue as too 😊

    • longwave committed f5de3b7a on 11.x
      Issue #3377562 by Wim Leers, Spokje, Reinmar, witeksocha: Update...
  • Status changed to Downport over 1 year ago
  • 🇬🇧United Kingdom longwave UK

    Committed f5de3b7 and pushed to 11.x. Thanks!

    Patch does not apply cleanly to 10.1.x, leaving open for backport as previously discussed.

  • 🇬🇧United Kingdom longwave UK
  • Assigned to wim leers
  • Status changed to Needs work over 1 year ago
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    On it.

  • last update over 1 year ago
    29,439 pass, 2 fail
  • @wim-leers opened merge request.
  • Issue was unassigned.
  • Status changed to Needs review over 1 year ago
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺
  • Status changed to Needs work over 1 year ago
  • 🇺🇸United States smustgrave

    Failure seems legit to the issue. FYI marking https://www.drupal.org/project/drupal/issues/3337298 🐛 [upstream] [GHS] CKEditor 5 removes empty inline elements Fixed as fixed too since it got merged 11.x

  • Status changed to Needs review over 1 year ago
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    I failed to cherry-pick it correctly 🙈

    Passes tests now 👍

  • last update over 1 year ago
    29,463 pass
  • Status changed to RTBC over 1 year ago
  • 🇺🇸United States smustgrave

    All green!

  • 🇬🇧United Kingdom catch

    I think this could use a change record, we could probably move the details about the empty inline elements to the CR then too.

    IMO ckeditor is applying semver overly strictly and should give themselves a bit more leeway with @internal to reduce their major release frequency. Given that and that the only change required here fixed a hidden testing bug, I think we should get this into the next patch release and unblock ckeditor4 migrations a bit more.

  • last update over 1 year ago
    29,464 pass
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    Done: https://www.drupal.org/node/3379650 .

    RE "more @internal" — yes, I've had that exact conversation with @Reinmar 1–2 years ago. The struggle they have is the same Drupal had ~5 years ago and to some extent still has today: not explicitly marking what the actual public API is means that everything is an API 🙈 Their move to TypeScript recently should help with that, but in vanilla JavaScript there is no such thing as private/protected which makes this even harder for them!

  • last update over 1 year ago
    29,465 pass
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    https://github.com/ckeditor/ckeditor5/releases/tag/v39.0.1 has been released, with the sole BC break that affected Drupal fixed: the enablePlaceholder() thing mentioned in #14 and #15.

    I propose to first merge this MR as-is, and to open a new issue for 39.0.1? That will reduce risk further for the Drupal ecosystem.

    However, if core committers prefer that, I can instead update this 10.1.x MR to update to 39.0.1 immediately, but then I'll need a new MR for this issue against 11.x, since the already merged one only updated us to 39.0.0.

    • catch committed 3cc755c1 on 10.1.x
      Issue #3377562 by Wim Leers, Spokje, longwave, witeksocha, Reinmar:...
  • Status changed to Fixed over 1 year ago
  • 🇬🇧United Kingdom catch

    Given it's fixing a bc break we already changed code for, and we're not going to change the code back, let's do a new issue. We've still got 2-3 weeks before the next 10.1 patch release so time to monitor a bit. CR looks good.

    Committed 3cc755c and pushed to 10.1.x. Thanks!

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    Done: 📌 Update CKEditor 5 to 39.0.1 Fixed .

    CR published (checked with @catch first).

  • Automatically closed - issue fixed for 2 weeks with no activity.

Production build 0.71.5 2024