ckeditor / ckeditor5

Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.
https://ckeditor.com/ckeditor-5
Other
9.49k stars 3.7k forks source link

Formatting, for example font color, is lost when all content is removed / Preserve font styles / Change default font #10517

Open Steveiwonder opened 3 years ago

Steveiwonder commented 3 years ago

📝 Provide detailed reproduction steps (if any)

  1. Enter some text into the content area
  2. Apply some formatting, change font size, color etc.
  3. Delete all that content
  4. Type new content

✔️ Expected result

I expect to see the newly typed content with the last selected font styles (similar to word, powerpoint, etc)

❌ Actual result

The font has the editors default styles applied

❓ Possible solution

Don't remove the last span element that is the document editor, as this has the style

If you'd like to see this fixed sooner, add a 👍 reaction to this post.

Reinmar commented 3 years ago

Why do you think it should work this way?

A common use case for removing all content is to start from scratch. Why keeping the formatting then?

Steveiwonder commented 3 years ago

Hey @Reinmar, if you have formatted text in the editor, select all or some text, then start typing, you'd expect styling to be maintained, this is pretty common. Try it out in Word, PowerPoint, TinyMCE, Quill, DevExpress, Summernote, Google Docs and many others.

I have a solution which seems to behave in all of our use cases for anyone that would like to use it.

import Plugin from "@ckeditor/ckeditor5-core/src/plugin";
import MoveOperation from "@ckeditor/ckeditor5-engine/src/model/operation/moveoperation";
import InsertOperation from "@ckeditor/ckeditor5-engine/src/model/operation/insertoperation";
import Text from "@ckeditor/ckeditor5-engine/src/model/text";
export const HIDDEN_CHARACTER = "\u200B";
export default class PreserveFontStyle extends Plugin {
  init() {
    this.editor.model.document.on("change:data", (evt, data) => {
      if (!data.operations) {
        return;
      }

      const insertOperations = data.operations.filter(
        (x) => x instanceof InsertOperation
      );

      const moveOperations = data.operations.filter(
        (x) => x instanceof MoveOperation
      );

      // handle new insert, remove zeroWidthSpace
      if (insertOperations.length > 0) {
        try {
          const insertOperation = insertOperations[insertOperations.length - 1];
          const textNode = insertOperation.position.textNode;
          if (textNode) {
            const data = textNode.data;
            if (data.length > 1 && data.includes(HIDDEN_CHARACTER)) {
              // strip out the hidden character now we don't need it.
              this.editor.model.change((writer) => {
                const newItem = writer.createText(
                  data.replace(HIDDEN_CHARACTER, ""),
                  textNode.getAttributes()
                );
                writer.remove(textNode);
                this.editor.model.insertContent(newItem, textNode.range);
              });
            }
          }
        } catch (e) {
          console.debug(e);
        }
      }
      if (!moveOperations || moveOperations.length <= 0) {
        return;
      }
      try {
        // grab last move operation
        const moveOperation = moveOperations[moveOperations.length - 1];
        try {
          console.log(moveOperation.sourcePosition.nodeAfter.data);
        } catch {}

        // ensure we are moving from "main" to the "graveyard"
        if (
          moveOperation.sourcePosition.root.rootName != "main" ||
          moveOperation.targetPosition.root.rootName != "$graveyard"
        ) {
          return;
        }

        // get the first node and ensure it's a text node
        const deletedTextNode = moveOperation.targetPosition.nodeAfter;
        if (!(deletedTextNode instanceof Text)) {
          return;
        }
        // get all attributes to re-apply
        const attrs = [...deletedTextNode.getAttributes()];
        if (attrs.length == 0) {
          return;
        }
        // now we need to check for the paragraph is empty, if it is, we insert an invisible character
        // inserting this invisible character maintains styles
        if (
          !moveOperation.sourcePosition ||
          !moveOperation.sourcePosition.parent
        ) {
          return;
        }
        const parentParagraph = moveOperation.sourcePosition.parent;
        if (!parentParagraph) {
          return;
        }
        if (
          parentParagraph.name == "paragraph" &&
          parentParagraph.childCount == 0
        ) {
          // if we are not the last paragraph or, the first character within the deleted text nodes data is not the hidden character
          // insert the hidden character to preserve styling.
          if (
            !parentParagraph.previousSibling ||
            (deletedTextNode.data &&
              deletedTextNode.data[0] != HIDDEN_CHARACTER)
          ) {
            this.editor.model.change((writer) => {
              writer.insertText(HIDDEN_CHARACTER, attrs, parentParagraph, 0);
            });
          } else if (parentParagraph.previousSibling) {
            // Remove any empty paragraphs
            this.editor.model.change((writer) => {
              writer.remove(parentParagraph);
            });
          }

          return;
        }

        const insertedTextNode = moveOperation.sourcePosition.nodeAfter;
        if (!insertedTextNode) {
          return;
        }

        // when the parent paragraph has more than one child, do nothing
        // prevents merging
        if (insertedTextNode.parent.childCount != 1) {
          return;
        }
        this.editor.model.change((writer) => {
          writer.setAttributes(attrs, insertedTextNode);
        });
      } catch (e) {
        console.debug(e);
      }
    });
  }
}
Steveiwonder commented 3 years ago

Another issue with the same problem: https://github.com/ckeditor/ckeditor5/issues/9946

dkrahn commented 2 years ago

Hi @Reinmar, I think it could remove all format if you have no text and then remove text again(with backspace, delete...). The main reason I see to keep styles is to be able to, for example, change the text(not format) of the whole paragraph. Today you have to keep a character and start typing and only after you can remove the old character. This may also solve a problem where you change the text alignment the whole text(2 paragraphs) being the last one empty. You will see that the first one aligns correcly but the second one doesn't.

CKEditorBot commented 1 year ago

There has been no activity on this issue for the past year. We've marked it as stale and will close it in 30 days. We understand it may be relevant, so if you're interested in the solution, leave a comment or reaction under this issue.