@ckeditor/ckeditor5-heading 48.1.1 → 48.2.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { Command, Plugin } from '@ckeditor/ckeditor5-core/dist/index.js';
6
6
  import { Paragraph } from '@ckeditor/ckeditor5-paragraph/dist/index.js';
7
- import { first, priorities, Collection } from '@ckeditor/ckeditor5-utils/dist/index.js';
7
+ import { first, priorities, Collection, logWarning } from '@ckeditor/ckeditor5-utils/dist/index.js';
8
8
  import { UIModel, createDropdown, addListToDropdown, MenuBarMenuView, MenuBarMenuListView, MenuBarMenuListItemView, MenuBarMenuListItemButtonView, ButtonView } from '@ckeditor/ckeditor5-ui/dist/index.js';
9
9
  import { IconHeading6, IconHeading5, IconHeading4, IconHeading3, IconHeading2, IconHeading1 } from '@ckeditor/ckeditor5-icons/dist/index.js';
10
10
  import { ViewDowncastWriter, enableViewPlaceholder, hideViewPlaceholder, needsViewPlaceholder, showViewPlaceholder } from '@ckeditor/ckeditor5-engine/dist/index.js';
@@ -523,6 +523,14 @@ const titleLikeElements = new Set([
523
523
  // </title>
524
524
  //
525
525
  // See: https://github.com/ckeditor/ckeditor5/issues/2005.
526
+ //
527
+ // Title is scoped to roots whose `modelElement` is the generic `$root`. Custom root
528
+ // `modelElement` names (including `$inlineRoot`) are intentionally not supported:
529
+ // the title structure (`title` + `title-content` + paragraph body placeholder) relies on
530
+ // the root accepting `$block` content, which is not guaranteed for custom or inline roots.
531
+ // Runtime codepaths below additionally guard on `schema.checkChild( root, 'title' )` so
532
+ // the plugin gracefully no-ops on roots where the schema does not allow the title element.
533
+ // eslint-disable-next-line ckeditor5-rules/no-literal-dollar-root -- registered only on the default `$root` by design
526
534
  model.schema.register('title', {
527
535
  isBlock: true,
528
536
  allowIn: '$root'
@@ -578,6 +586,31 @@ const titleLikeElements = new Set([
578
586
  this._attachPlaceholders();
579
587
  // Attach Tab handling.
580
588
  this._attachTabPressHandling();
589
+ this._warnIfNoSupportedRoot();
590
+ }
591
+ /**
592
+ * Logs a single warning when none of the editor's roots can host the title structure. The Title feature
593
+ * only operates on roots whose `modelElement` is the default `$root`; roots configured with a custom
594
+ * `modelElement` are silently skipped at runtime. If no root supports the structure, the plugin is
595
+ * effectively a no-op and the integrator likely wants to know.
596
+ */ _warnIfNoSupportedRoot() {
597
+ const model = this.editor.model;
598
+ for (const root of model.document.getRoots()){
599
+ if (model.schema.checkChild(root, 'title')) {
600
+ return;
601
+ }
602
+ }
603
+ /**
604
+ * The Title feature was loaded, but none of the editor's roots supports the `title` element. The feature
605
+ * only operates on roots whose `modelElement` is the default `$root`; roots configured with a custom
606
+ * `modelElement` (including `$inlineRoot`) are silently skipped, so `getTitle()` / `getBody()` fall back
607
+ * to the regular data getter and no title structure is ever inserted.
608
+ *
609
+ * To use the Title feature, ensure at least one root uses the default `$root` model element. Otherwise,
610
+ * remove the Title plugin from this editor's plugin list.
611
+ *
612
+ * @error title-no-supported-root
613
+ */ logWarning('title-no-supported-root');
581
614
  }
582
615
  /**
583
616
  * Returns the title of the document. Note that because this plugin does not allow any formatting inside
@@ -593,6 +626,10 @@ const titleLikeElements = new Set([
593
626
  */ getTitle(options = {}) {
594
627
  const rootName = options.rootName ? options.rootName : undefined;
595
628
  const titleElement = this._getTitleElement(rootName);
629
+ // Root does not support the title structure (custom/inline root) — nothing to stringify.
630
+ if (!titleElement) {
631
+ return '';
632
+ }
596
633
  const titleContentElement = titleElement.getChild(0);
597
634
  return this.editor.data.stringify(titleContentElement, options);
598
635
  }
@@ -612,12 +649,25 @@ const titleLikeElements = new Set([
612
649
  const model = editor.model;
613
650
  const rootName = options.rootName ? options.rootName : undefined;
614
651
  const root = editor.model.document.getRoot(rootName);
652
+ // Root does not support the title structure (custom/inline root) — the whole root is the body.
653
+ // Delegate to the regular data getter so mixed-root callers receive useful content.
654
+ if (!model.schema.checkChild(root, 'title')) {
655
+ return data.get({
656
+ ...options,
657
+ rootName: root.rootName
658
+ });
659
+ }
660
+ // Root is empty / missing the expected title element (e.g. detached root or transient state) — no body to stringify.
661
+ const firstChild = root.getChild(0);
662
+ if (!firstChild || !firstChild.is('element', 'title')) {
663
+ return '';
664
+ }
615
665
  const view = editor.editing.view;
616
666
  const viewWriter = new ViewDowncastWriter(view.document);
617
667
  const rootRange = model.createRangeIn(root);
618
668
  const viewDocumentFragment = viewWriter.createDocumentFragment();
619
669
  // Find all markers that intersects with body.
620
- const bodyStartPosition = model.createPositionAfter(root.getChild(0));
670
+ const bodyStartPosition = model.createPositionAfter(firstChild);
621
671
  const bodyRange = model.createRange(bodyStartPosition, model.createPositionAt(root, 'end'));
622
672
  const markers = new Map();
623
673
  for (const marker of model.markers){
@@ -638,7 +688,12 @@ const titleLikeElements = new Set([
638
688
  /**
639
689
  * Returns the `title` element when it is in the document. Returns `undefined` otherwise.
640
690
  */ _getTitleElement(rootName) {
641
- const root = this.editor.model.document.getRoot(rootName);
691
+ const model = this.editor.model;
692
+ const root = model.document.getRoot(rootName);
693
+ // Root does not support the title structure (custom/inline root).
694
+ if (!model.schema.checkChild(root, 'title')) {
695
+ return;
696
+ }
642
697
  for (const child of root.getChildren()){
643
698
  if (isTitle(child)) {
644
699
  return child;
@@ -675,6 +730,10 @@ const titleLikeElements = new Set([
675
730
  let changed = false;
676
731
  const model = this.editor.model;
677
732
  for (const modelRoot of this.editor.model.document.getRoots()){
733
+ // Skip roots that do not support the title structure (custom/inline root).
734
+ if (!model.schema.checkChild(modelRoot, 'title')) {
735
+ continue;
736
+ }
678
737
  const titleElements = Array.from(modelRoot.getChildren()).filter(isTitle);
679
738
  const firstTitleElement = titleElements[0];
680
739
  const firstRootChild = modelRoot.getChild(0);
@@ -711,10 +770,13 @@ const titleLikeElements = new Set([
711
770
  * Model post-fixer callback that adds an empty paragraph at the end of the document
712
771
  * when it is needed for the placeholder purposes.
713
772
  */ _fixBodyElement(writer) {
773
+ const schema = this.editor.model.schema;
714
774
  let changed = false;
715
775
  for (const rootName of this.editor.model.document.getRootNames()){
716
776
  const modelRoot = this.editor.model.document.getRoot(rootName);
717
- if (modelRoot.childCount < 2) {
777
+ // Only insert the paragraph body placeholder when the root supports the title structure.
778
+ // Custom/inline roots that do not accept `title` are intentionally skipped, matching `_fixTitleElement`.
779
+ if (modelRoot.childCount < 2 && schema.checkChild(modelRoot, 'title')) {
718
780
  const placeholder = writer.createElement('paragraph');
719
781
  writer.insert(placeholder, modelRoot, 1);
720
782
  this._bodyPlaceholder.set(rootName, placeholder);
@@ -731,6 +793,10 @@ const titleLikeElements = new Set([
731
793
  for (const rootName of this.editor.model.document.getRootNames()){
732
794
  const root = this.editor.model.document.getRoot(rootName);
733
795
  const placeholder = this._bodyPlaceholder.get(rootName);
796
+ // Roots that do not support the title structure never had a body placeholder created.
797
+ if (!placeholder) {
798
+ continue;
799
+ }
734
800
  if (shouldRemoveLastParagraph(placeholder, root)) {
735
801
  this._bodyPlaceholder.delete(rootName);
736
802
  writer.remove(placeholder);
@@ -768,7 +834,14 @@ const titleLikeElements = new Set([
768
834
  if (viewRoot.isEmpty) {
769
835
  continue;
770
836
  }
771
- // If `viewRoot` is not empty, then we can expect at least two elements in it.
837
+ // Skip roots whose schema does not support the title structure (custom/inline root).
838
+ // Their view root won't have the expected title+body layout.
839
+ // A title-allowed root always has a paragraph body placeholder created by `_fixBodyElement`,
840
+ // so the second view child is guaranteed to exist once this guard passes.
841
+ const modelRoot = editor.editing.mapper.toModelElement(viewRoot);
842
+ if (!editor.model.schema.checkChild(modelRoot, 'title')) {
843
+ continue;
844
+ }
772
845
  const body = viewRoot.getChild(1);
773
846
  const oldBody = bodyViewElements.get(viewRoot.rootName);
774
847
  // If body element has changed we need to disable placeholder on the previous element and enable on the new one.
@@ -822,6 +895,10 @@ const titleLikeElements = new Set([
822
895
  const selectedElement = first(selection.getSelectedBlocks());
823
896
  const selectionPosition = selection.getFirstPosition();
824
897
  const root = editor.model.document.getRoot(selectionPosition.root.rootName);
898
+ // Root does not support the title structure (custom/inline root) — no title to jump to.
899
+ if (!model.schema.checkChild(root, 'title')) {
900
+ return;
901
+ }
825
902
  const title = root.getChild(0);
826
903
  const body = root.getChild(1);
827
904
  if (selectedElement === body && selectionPosition.isAtStart) {
@@ -835,6 +912,9 @@ const titleLikeElements = new Set([
835
912
  /**
836
913
  * A view-to-model converter for the h1 that appears at the beginning of the document (a title element).
837
914
  *
915
+ * Matches only the synthetic upcast parent named `$root` (the default generic root element). Title is not supported
916
+ * for roots whose `modelElement` is customized, so this converter intentionally does not fire on them.
917
+ *
838
918
  * @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
839
919
  * @param evt An object containing information about the fired event.
840
920
  * @param data An object containing conversion input, a placeholder for conversion output and possibly other values.
@@ -842,6 +922,9 @@ const titleLikeElements = new Set([
842
922
  */ function dataViewModelH1Insertion(evt, data, conversionApi) {
843
923
  const modelCursor = data.modelCursor;
844
924
  const viewItem = data.viewItem;
925
+ // Testing against a literal `$root` is intentional: this converter must not fire on roots whose `modelElement`
926
+ // is customized, because the Title feature only registers its schema against `$root`.
927
+ // eslint-disable-next-line ckeditor5-rules/no-literal-dollar-root -- name-agnostic check would fire for unsupported custom roots
845
928
  if (!modelCursor.isAtStart || !modelCursor.parent.is('element', '$root')) {
846
929
  return;
847
930
  }
@@ -927,7 +1010,7 @@ const titleLikeElements = new Set([
927
1010
  * Returns true when the last paragraph in the document was created only for the placeholder
928
1011
  * purpose and it's not needed anymore. Returns false otherwise.
929
1012
  */ function shouldRemoveLastParagraph(placeholder, root) {
930
- if (!placeholder || !placeholder.is('element', 'paragraph') || placeholder.childCount) {
1013
+ if (!placeholder.is('element', 'paragraph') || placeholder.childCount) {
931
1014
  return false;
932
1015
  }
933
1016
  if (root.childCount <= 2 || root.getChild(root.childCount - 1) !== placeholder) {