@ckeditor/ckeditor5-heading 48.1.1 → 48.2.0-alpha.1
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 +89 -6
- package/dist/index.js.map +1 -1
- package/dist/title.d.ts +7 -0
- package/package.json +8 -8
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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) {
|