Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Document lists] Make sure that lists isn’t split when making certain changes inside it #11198

Closed
niegowski opened this issue Feb 1, 2022 · 14 comments
Assignees
Labels
package:engine squad:core Issue to be handled by the Core team. type:improvement This issue reports a possible enhancement of an existing feature.

Comments

@niegowski
Copy link
Contributor

niegowski commented Feb 1, 2022

Scenarios (some examples, there's definitely more of those):

  • Widget type around, insertParagraph, insertTable, pasting images into empty list items, etc
  • Inline image ↔ block image, moving list attributes to e.g. the paragraph is created.
  • A scenario in handling Backspace:
* li bar
* li [block image]
* li foo

+ backspace

expected:

* li bar
* li []
* li foo

actual:

* li bar
p []
* li foo

Idea: Make paragraph inherit attributes allowed by the schema of the block/element that was removed.

  • This could be implemented directly in deleteContent().
  • And apply only in cases where we create some new element (e.g. merging one block into the preceding one doesn’t count).
  • Do we need a flag? Maybe it should always be true?
  • But then, what about conflicting attributes (allowed on many elements)?
    • GHS: htmlAttributes, and specific ones: htmlImgAttributes
  • Introduce copyOnReplace in schema.setAttributeProperties()
    • And mark list* attributes
@niegowski niegowski added type:improvement This issue reports a possible enhancement of an existing feature. package:engine squad:core Issue to be handled by the Core team. labels Feb 1, 2022
@Reinmar Reinmar changed the title Make sure that lists isn’t split when making certain changes inside it [Document lists] Make sure that lists isn’t split when making certain changes inside it Feb 2, 2022
@CatStrategist
Copy link
Contributor

CatStrategist commented Feb 17, 2022

Case 1 - inserting new elements

Inserting new elements splits a list into two because inserted elements don't have list attributes.
New elements are inserted through insertContent() function which doesn't know to add list attributes or copy them from the element where the selection is.

insertContent() checks if content to insert is allowed in a current selection's parent. Objects can't be inserted to paragraphs so selection is created after or before paragraph and a new element is inserted there. insertContent() is designed to remove empty paragraph after.

If selection is in list item with content it does not delete paragraph and only inserts new content.

Case 2 - inline image ↔ block image

Image plugin has a functionality to preserve attributes after type change, but inline image is inside a paragraph in a list item, so required attributes are on a paragraph and inline image can't pass them to block image.

When swapping block image to inline image it does not work because attributes are placed on inlineImage element instead of on a paragraph.

In both cases insertContent() is executed so solution may be the same as for case 1.

Case 3 - delete content

@Reinmar
Copy link
Member

Reinmar commented Feb 17, 2022

Meeting notes:

  • The changes may not only affect deleteContent(), but also insertContent().
  • The copyOnReplace may not be a valid name, but we can start with it. Let's check it later once we understand the cases that it needs to handle.
  • @CatStrategist, you can start from building a list of scenarios to cover so we can prioritize them.

@Reinmar Reinmar added the status:in-progress Set automatically when an issue lands in the "In progress" column. We are working on it. label Feb 21, 2022
@CatStrategist
Copy link
Contributor

CatStrategist commented Feb 22, 2022

I reviewed features from https://ckeditor.com/docs/ckeditor5/latest/features/index.html and list is breaking in following cases (still might not be all):

  • Inserting table
  • Inserting media
  • Inserting image
  • Inserting HTML block
  • Inserting paragraphs by widget type around
  • Pasting multiple paragraphs
  • Pasting from Office
  • Page break? Probably correct behaviour
  • Inserting horizontal line
  • Changing between inline and block image
  • Deleting content that is replaced by paragraph (on delete)

Handling is needed in the deleteContent() and the insertContent() . Also need to review what happens when pasting text to a document list.

@CatStrategist
Copy link
Contributor

Most cases are solved with copying list attributes of parent list item to newly inserted block element in insertContent(). Altough it solved most cases it also introduced a question - should we allow rewriting list attributes for all elements? For example page break might not be desirable in a list. It's not really an issue and doesn't break anything. Users can easly go around it - a list just won't automatically break.

Also rewriting attributes changed behaviour of pasting multiple paragraphs. Now instead of completely breaking a list, it adds the first pasted paragraph as one list item and the rest of paragraphs as second list item. It needs further investigation. 🕵️
obraz

@CatStrategist
Copy link
Contributor

Another issue: Inserting page break and horizontal line create a new paragraph after - throwing user out of a list.

@CatStrategist
Copy link
Contributor

Problem with insertContent()

Selectable that we receive in insertContent() might be already moved by getOptimalInsertionRange(). Selection ends up outside element that we want to insert to, losing context and we don't know source element that we should copy attributes from.

@CatStrategist
Copy link
Contributor

Issues and cases that needs to be handled when we want to copy attributes using insertContent()

Case 1 - inserting widgets that use getOptimalInsertionRange()

This function sometimes calculates a range to insert to outside user's current selection. In this case we don't have access to element that we want to copy attributes from.

My current solution is to access an element to copy attributes from by accessing DocumentSelection in `insertContent(). This is the easieast one but after consulting it with @niegowski it may not be ideal and could introduce potential issues with collaboration.

If we won't proceed with the above, current alternative solutions are:
a) change getOptimalInsertionRange() to return Selection with attributes to copy instead of position/range.
This solution would introduce a braking change and would force usage of getOptimalInsertionRange() when inserting content.
b) calculate attributes to copy to outside insertContent() and pass them as parameter.
It would require every integrated feature to take care of retrieving attributes to copy.

Case 2 - widget typearound - no selection

When inserting paragraphs with widget typearound selection could be anywhere. We need to pass context from widget typearound which knows about an element that has attributes to copy.

Case 3 - copying attributes from parent element

Current example: when we change image block to inline image, we need to know to apply attributes to a paragraph surrounding inline image instead of applying them to the inline image.

@CatStrategist
Copy link
Contributor

After further discusions we have some more options:

  • Pass element to get attributes from to insertContent()
  • Pass attributes to apply to insertContent()
  • Use similar behaviour to retaining attributes from PastePlainText plugin

We need to decide what insertContent() should be responsible for.

@niegowski
Copy link
Contributor Author

niegowski commented Mar 8, 2022

Usages of model.insertContent()

Inserting a widget (findOptimalInsertionRange() used only for block widgets)

  • HorizontalLineCommand - DocumentSelection and optionally empty paragraph after it
  • PageBreakCommand - DocumentSelection and optionally empty paragraph after it
  • HtmlEmbedCommand - DocumentSelection (and select the widget)
    • Note: we don't use fOIR() yet here but we should
  • InsertTableCommand - findOptimalInsertionRange() from DocumentSelection
  • MediaEmbedCommand - findOptimalInsertionRange() from DocumentSelection
  • ImageUtils.insertImage() - selectable or fallbacks to findOptimalInsertionRange() from DocumentSelection
    • Inserting inline image sometimes auto-paragraphs that image. How will it know from where to copy block attributes?

Widget type around

  • InsertParagraphCommand - a position from options (created from a selected element and fake caret position)
  • WidgetTypeAround - redirects insertContent to a selection created from a selected element and a fake caret position

Clipboard

  • ClipboardPipeline - DocumentSelection

* This is probably out of scope.

Inline non-widget

  • ShiftEnterCommand - selection.focus or selection.getFirstRange().end (if collapsed)

* This is probably out of scope.

Text

  • InputCommand - range from options or DocumentSelection
  • LinkCommand - DocumentSelection.getFirstPosition()
  • MentionCommand - range from options or first selection range + ' ' after it
  • ReplaceCommand - range from marker
  • TextTransformation - range computed from match

* This is probably out of scope.

Idea

Maybe we should introduce a widget helper that would replace usage of findOptimalInsertionRange() + model.insertContent() (it would still use those internally) but would provide a single consistent API for widget inserting.

  1. Would find the best insertion range (by using findOptimalInsertionRange() internally)
  2. Would know what was the original target of the insertion and would be able to gather attributes that should be applied to the inserted content
  3. Could optionally add an empty paragraph after the inserted object (in case there is no place to put selection)
  4. Would call the model.insertContent() to insert the object with updated attributes that should persist and the optional empty paragraph.
  5. Could optionally set the document selection after inserting an object (on it, in an empty paragraph, etc).

@Reinmar
Copy link
Member

Reinmar commented Mar 9, 2022

model.insertObject()?

Maybe, in the future, we could have more of these specialized methods: insertText(), insertBlock()? We'd need to be cautious to only cover cases that are distinctive in some way (e.g. what's the difference between insertContent() and insertBlock()?). But in this case (object vs content) the difference is quite visible.

@Reinmar
Copy link
Member

Reinmar commented Mar 9, 2022

The below will resolve all the issues that we have in the "Inserting a widget (findOptimalInsertionRange() used only for block widgets)" section.

Very rough idea how it could work:

model.insertObject( object, selectable, offset, {
    setSelection: 'on|in|after',
    findOptimalPosition: true,

    // Maybe:
    doNotInheritBlockAttributes: true
} );

// Simplified implementation of `insertObject()`:
function insertObject( object, selectable, offset, options ) {
    let targetPosition = selectableOrDocumentSelection();

    // Store attributes before we optimize the position.
    const attributesToCopy = findAttributesToCopy( targetPosition );

    if ( options.findOptimalPosition ) {
        targetPosition = findOptimalInsertionPosition( targetPosition );
    }

    model.deleteContent();

    if ( objectIsBlock ) {
        writer.setAttributes( attributesToCopy, object );
        thingToInsert = object;
    } else if ( cannotInsertObjectHere ) {
        block = autoParagraph( object )
        writer.setAttributes( attributesToCopy, block );
        thingToInsert = block
    } else {
        thingToInsert = block
    }

    model.insertContent( thingToInsert, targetPosition );

    // Finally, set selection based on options.setSelection.
}

@Reinmar
Copy link
Member

Reinmar commented Mar 9, 2022

As for the issues related to widget type around, the solution can be hardcoded by us in this feature. This is a very specific feature and most likely no one else does something similar.

@Reinmar
Copy link
Member

Reinmar commented Mar 9, 2022

    // Store attributes before we optimize the position.
    const attributesToCopy = findAttributesToCopy( targetPosition );

The remaining question is based on what we can figure out:

  • where to take the attributes from?
    • the selection may be collapsed and non collapsed, inside a block, in a text, on an object in a block, between blocks, etc.
    • Ideas:
      • starting point:
        • startgetSelectedElement() || selection.anchor.start
        • search up the tree, get all attributes with the right props
      • maybe getSelectedBlocks()
        • question: does it work for a selected block image?
  • where to set them?

oleq added a commit that referenced this issue Apr 1, 2022
…split

Feature (engine): Added a new `insertObject()` method to the `Model` for inserting elements defined as objects by schema into a model (see #11198).

Feature (engine): Added a new `setAllowedAttributes()` method to the `Schema` that validates attributes if they are allowed on given element before setting them (see #11198).

Feature (engine): Added a new `getAttributesWithProperty()` method to the `Schema` that retrieves attributes from a node which have given property (see #11198).

Other (media-embed): Added an optional `findOptimalPosition` parameter to `insertMedia()` function that allows for inserting `media` element without breaking content (see #11198).

Feature (paragraph): Added an optional `options.attributes` parameter to `InsertParagraph` command that allows setting attributes on created paragraph (see #11198).

Internal (list): Document list items should not get split by inserting block objects (widgets). Closes #11198.

Internal (engine): The `findOptimalPosition()` helper is now available in the ckeditor5-engine package for internal use (see #11198).

Internal (table, page-break, horizontal-line, media-embed, html-embed, image): `table`, `pageBreak`, `horizontalLine`, `media`, `imageBlock`,`imageInline` elements are now inserted with `insertObject()` function instead of `insertContent()` (see #11198).
@oleq
Copy link
Member

oleq commented Apr 1, 2022

Closed in #11425.

@oleq oleq closed this as completed Apr 1, 2022
@CKEditorBot CKEditorBot removed the status:in-progress Set automatically when an issue lands in the "In progress" column. We are working on it. label Apr 1, 2022
@CKEditorBot CKEditorBot added this to the upcoming milestone Apr 1, 2022
oleq added a commit that referenced this issue Apr 4, 2022
Feature (list): Introducing the document (advanced) list feature (multiple blocks per list item). Closes #2973. Closes #10812.

Feature (list): Introducing the document (advanced) list properties feature (list styles, start index, reversed list). Closes #11065.

Feature (html-support): Adds support for document list in the `GeneralHtmlSupport` feature. Closes #11454. Closes #11359. Closes #11358.

Feature (engine): Added a new `Model#insertObject()` method for inserting elements defined as objects by model schema (see #11198).

Feature (engine): Introduced inheritable `$container`, `$blockObject`, and `$inlineObject` element types in the model `Schema` (see #11197).

Feature (engine): Introduced `TabObserver` that allows listening to pressing down the `Tab` key in a specified context.

Feature (paragraph): Added an optional `options.attributes` parameter to the `InsertParagraph` command that allows setting attributes on a created paragraph (see #11198).

Feature (core): `MultiCommand` now allows setting a priority (order) of registered sub commands. Closes #11083.

Feature (engine): Added a new `Schema#getAttributesWithProperty()` method that retrieves attributes from a node which have given property (see #11198).

Feature (engine): Added a new `Schema#setAllowedAttributes()` method that validates whether attributes are allowed on a given element before setting them (see #11198).

Other (engine): The `Differ` change entries for `insert` and `remove` types are extended with a map of attributes that were set while inserting an element or that belonged to an element that got removed.

Other (engine): The `DowncastHelpers` are passing an additional parameter to the creator functions (the `data` that provides more context to the element creator callback).

Other (engine): The `isAllowedInsideAttributeElement` option was removed, from now on `AttributeElements` are allowed to wrap any view element.

Other (engine): The `ConversionApi` provided by the `UpcastDispatcher` was extended by an additional `keepEmptyElement()` method that marks an element that was created during splitting a model element that should not get removed on conversion even if it is empty. 

Other (html-support): Updated default schema definitions for various elements taking advantage of the `$container`, `$blockObject`, and `$inlineObject` elements in model schema (see #11197).

Other (table, code-block, list): The handling of `Tab` and `Shift+Tab` keystrokes switched to the `'tab'` view document event and now respects the event context.

Other (media-embed): Added an optional `findOptimalPosition` parameter to the `insertMedia()` helper that allows for inserting `media` model element without breaking the content (see #11198).

Fix (link): The link decorators should be converted on block images only once (should not wrap block image with an additional link).

Internal (engine): Added option for the `DomConverter` to transparently render only the content of the element in the data pipeline.

Internal (engine): Added option for the `DomConverter` to transparently render only the content of the element in the data pipeline.

Internal (engine): The `findOptimalPosition()` helper is now available in the ckeditor5-engine package for internal use (see #11198).

Internal (list): `DocumentListEditing` should extend `$container` (for `blockQuote`, etc.), `$block` (for `paragraph`, `heading2`, etc.), and `$blockObject` (for `table`, `horizontalLine`, etc.) with its attributes. Closes #11197.

Internal (list): `indentList` and `outdentList` commands are now registered with priority in 'Indent' `MultiCommand` , `Tab` and `Tab+Shift` listeners now executes in `li` context in order to not interfere with other plugins' listeners . Closes #11072.

Internal (list): Adds `DocumentListStartCommand` and `DocumentListReversedCommand`. Closes #11166.

Internal (list): Adds `DocumentListStyleCommand`. See #11166.

Internal (list): Adds post-fixer that makes sure that all items in a single list have the same start, reversed, and style properties. Closes #11167.

Internal (list): Deleting a widget which is a document list item should be possible. Closes #11346.

Internal (list): Document list items should not get split by inserting block objects (widgets). Closes #11198.

Internal (list): Implemented backspace and delete handling in and around document lists. Closes #10878.

Internal (list): Implemented handling of `Tab` and `Tab+Shift` keys in document lists. Closes #10880.

Internal (list): Implemented the `DocumentListMergeCommand`. Closes #10977.

Internal (list): Improved enter handling in document lists by allowing to split the list item when the collapsed selection is anchored in the first (but not only) empty block of a list item (see #10879, #10976).

Internal (list): Integrated the enter feature with document lists. Closes #10879. Closes #10976.

Internal (list): Reset document list properties after indent. Closes #11357.

Internal (table, page-break, horizontal-line, media-embed, html-embed, image): `table`, `pageBreak`, `horizontalLine`, `media`, `imageBlock`,`imageInline` elements are now inserted with `insertObject()` function instead of `insertContent()` (see #11198).

Internal (list): Added `DocumentListCommand` and `DocumentListIndentCommand`. Closes #10974. Closes #10975.

MINOR BREAKING CHANGE (html-support): The `$htmlSection`, `$htmlObjectBlock`, and `$htmlObjectInline` element types are no longer available for custom elements registered via `registerBlockElement()` to inherit from. Please use `$container`, `$blockObject`, and `$inlineObject` instead (see #11197).

MINOR BREAKING CHANGE (engine): The `isAllowedInsideAttributeElement` option was removed so `AttributeElements` can wrap any view element (according to positions). Make sure that you are not wrapping any `ContainerElement` by an accident by not checking the target in the converter. Those would previously get wrapped by an `AttributeElement` that immediately would be removed by the `ContainerElement` within it so there would not be any visual effect.

MINOR BREAKING CHANGE (engine): The handling of `Tab` and `Shift+Tab` keystrokes switched to the `'tab'` view document event across the project. If your integration uses `KeystrokeHandler` for `Tab` key handling, we recommend you migrate to the `'tab'` event to avoid unpredicted errors.

MINOR BREAKING CHANGE (engine): If your integration uses `Model#insertContent()` and `findOptimalInsertionRange()` to insert widgets into the content, we recommend you migrate your code to `Model#insertObject()` for best results. This is particularly relevant for compatibility with the document (advanced) lists feature (see #11198).
@CKEditorBot CKEditorBot modified the milestones: upcoming, iteration 52 Apr 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
package:engine squad:core Issue to be handled by the Core team. type:improvement This issue reports a possible enhancement of an existing feature.
Projects
None yet
Development

No branches or pull requests

5 participants