-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
Fix paste to empty node losing structure of first block #4489
Fix paste to empty node losing structure of first block #4489
Conversation
🦋 Changeset detectedLatest commit: 86620e0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
I'm not sure special casing this to empty blocks is a good idea and probably leads to some of the problems you've found. Instead, it should be based on what is being inserted. My recommendation is:
This, in my experience, matches user expectation quite well. |
I've found problems either way. In the example below on production, pasting two paragraphs breaks the example list by introducing an incorrect structure (ul > blockquote). This PR does not address this, but the example demonstrates that you probably can't fix all issues in
This PR is trying to address a scenario where copying a heading (one top level block in the fragment) was getting inserted as plain text because it is the first node in the fragment. The only way I've found to copy a heading block is by copying something above it as well. The suggestion doesn't change that - we would still need to copy two things in order to paste the heading. I guess this is a preference so it's difficult to argue, but it's awkward. A reasonable alternative, possibly the most robust solution, would be to express intent to copy a block or just the innards of a block when copying (notion does this i believe). If we were to go down that route, this PR would be rendered obsolete but there would be no change in behavior for empty blocks as the fragment would be pasted as-is, therefore this could be treated as an interim solution. IOW the PR assumes pasting into an empty node means "paste the fragment as-is" (plugins not taken into account, they would need overrides either way probably).
Is it possible to copy a fragment with mixed blocks and inlines? I'm unable to make this happen. Also, the docs state: "But elements cannot contain some children that are blocks and some that are inlined.".
Didn't understand this. Thanks for the review! |
Ah I didn't realise you were trying to achieve that. I don't think copying a word out of a heading and pasting it into a paragraph (even an empty one) should insert a heading. Copying a heading and some text below it, though, that's clearly trying to insert structure.
That might be ok for copying within Slate, but not pasting external content.
It isn't possible, which is the point I was trying to leverage in my suggestion to use fragment details. If there is more than one node in the fragment and the first is a block node they must all be block nodes. That's how I recommend detecting whether the user intended to paste as blocks or just block contents.
I meant if you implement my suggestion, keep this code, which deletes the block if it was empty and nothing was merged into it (although thinking about it now |
As a user, I want to copy a heading and paste it in this scenario, not the heading and some text above it, or any text at all. Without the copy handler change, how do i tell slate this? With the current implementation, i can't manually create a fragment that would insert the heading as a heading without hacks as far as i can tell, i need to instead override
External content would default to some behavior and maybe have some undesired effects depending on how it's structured, but presumably C/P from slate to slate should be a first-class citizen as we are in control of the selection at that point as opposed to an external source, IOW we can make a decision based on intent, rather than content. That said, if i wanted to insert a fragment consisting of a single block from an external source to the end of a document (in this case an empty node), why would slate assume i wanted to insert the first node as text?
But whenever i copy from slate, the fragment is always the tree from root to selection (block > inline). Can you show me an example of copying where the fragment consist of inlines? |
Sometimes yes, sometimes no. I can understand that you as a user would like to do this as the default. I'm trying to say that in my experience most users don't.
If we're talking manual override, use
Then
Inserting into an empty node is a reasonable case to override the merge. I haven't been clear about that here, I think I only mentioned it on a related Slack thread talking about #4486. My apologies. Your approach isn't totally wrong, but I don't believe it solves the general case issue described in #4486.
From Slate? No. But external content, or manually crafted API calls, could easily do it. |
The current behavior is the source of many bug reports and complaints from end users. I found at least 5 issues that were related (losing the first block of what gets pasted, getting converted into text instead).
Just because it lives in the text transform doesn't mean
By this argument a lot of the current In terms of the proposed change, to me it feels like the smallest, least impactful change that is consistent with the current codebase. It's a long standing issue that's frustrating to a lot of users. If we want to refactor and change the way this works to be purer, that's fine, but I wouldn't block the PR for that given the current state of things.
The relevant comment from slack for posterity was:
Possibly, though I would argue that it's significantly better as a default than what we're currently shipping. :)
Since we have no control at the point of copy of external content, the only point at which we can handle it is paste. When external content is pasted it won't be slate ast, but either html, markdown, or plain text presumably. This change doesn't remove the need for a paste deserializer. Or am I missing something? In terms of manually crafted API calls, wouldn't it be better to suggest that such API calls should provide a valid slate tree or are there use cases I've not considered? |
Solving the problem of losing the first block in a multi-block paste is exactly the fix I'm suggesting. This PR doesn't fix that except in one specific 'paste into empty block' case.
Insert Fragment is useful for far more than just pasting. I can appreciate that I offered
It's useful for the intended purpose, which is partly why in TinyMCE we wrote our own implementation that mostly follows what I've suggested (I suspect we rewrote it instead of PR it because I wasn't a contributor at the time).
I'm not blocking the PR. It's approved and I haven't requested changes. But it doesn't completely fix the issue it claims to, as it only applies when pasting into empty blocks. I offered a suggestion that is probably only a small tweak and would cover the general case.
That's fine, but it's marked as "will close 4486" :)
I'm not sure how that's relevant to
I'm sure the code assumes the inserted fragment provides a valid slate tree. But pre-normalising it might only cause more issues. |
I think part of the current behavior is that if you paste into a non-empty block, the intent actually is that you want to merge the content of the first pasted block into the block. If you didn't want to merge it in, you'd paint into an empty block. At least that's my understanding/expectation and it seems to be how many other editors (e.g. Google Docs) behaves. The current intent that feels wrong is pasting into an empty block which is what was addressed.
ok.
ok... after exploring this for a few days, I no longer believe that any small tweak for this block of code is a small undertaking. 😂
ok |
I have asked how i can achieve copy/paste of a heading as a heading, where i copy just the heading. Multi-block specialization does not fix this, so what do you suggest given the following constraints:
Slate, as far as i can tell cannot do this without overriding insertFragment, or can it? |
Google docs isn't a great example, because it's not a HTML editor. But that's getting a bit off topic. I think if multiple blocks are pasted, regardless of where they are pasted, the expectation is to not merge the first block. There are certainly examples of both merge and not merge across the editor market.
So merge this and we'll make a new ticket if you think 4486 is fixed. I might even see if my team would be interested in a PR to integrate our custom
It's generated a lot of discussion, because I bring 20 years of experience in creating rich text editors 🤭 but it's not a lot of code. I'm just doing a bad job of explaining what I think we need and why.
Your PR does this as long as you abide by the restriction of pasting into an empty block. I know I started this mess by suggesting it not do that, but we've gone around in circles and I don't want to drag the discussion on any further. |
Ha, fair enough.
I think that was my original question. It feels like slate pretty deliberately made the choice to merge the first block (otherwise you could paste into an empty block below it). There's logic that seems to try to handle this. Originally I was thinking that maybe you only merge if the block is of the same type you're merging into (e.g. merge a list item into a list item). My assumption is it was implemented as a merge with intent, but the insert into an empty block scenario wasn't considered given the lack of tests for it compared to the other scenarios.
cool.
It's funny, I worked on RTE in a browser from 2000-2003, felt like it was an exercise in futility and hoped to avoid it forever (other than some minimal work I did along the way on the Dojo editor). But here I am, 20 years later working on it again. :)
Fair enough. I think we'll land it later today and see what else can be done to improve things, and go from there. Thanks for making the time. |
Description
When pasting into an empty block, insert the entire fragment as-is, thereby preserving the structure of the first block.
Issue
Fixes: #4486
Example
Right - current, left - PR.
Caveat
Since the first node is not handled in a special way anymore, there will be a change to how complex structures like lists behave (GIF below). I feel that slate cannot handle this well on it's own without knowing the semantics of those elements (is it a list, is it a table, how does one paste a table into a table etc), and it should be up to the "plugin" that defines what a list is to define that behavior. If this is not acceptable, i propose we allow the clients to choose between two insert strategies, as we really need the PR behavior to work and list behavior for us is solved by plate.
Note however that slate handles pasting to a list (a flat list too) nicely, whereas pasting outside of a list is not good, whereas the PR is the opposite - pasting outside the list looks good, whereas pasting into a list introduces a sublist.
Right - current, left - PR.
Checks
yarn test
.yarn lint
. (Fix errors withyarn fix
.)yarn start
.)yarn changeset add
.)