slab / quill

Quill is a modern WYSIWYG editor built for compatibility and extensibility
https://quilljs.com
BSD 3-Clause "New" or "Revised" License
43.22k stars 3.36k forks source link

List nesting/indent HTML is not semantic #979

Open JoshuaDoshua opened 8 years ago

JoshuaDoshua commented 8 years ago

Indent behavior could be improved.

Indenting a list item should wrap the current list item in a new <ul><li> or <ol><li> and nest it in the closest <li> tag (or do nothing if you are at the highest level or there is only one item in the list)

The current indent button simply adds a class to the <li>, which looks great but outputs bad HTML. The generated content would not be exportable to other platforms.

v1.0.3

This seems a bit more complicated than a simple fix. If I have time I'll try to get a PR in, but wanted to get the topic open for discussion

Aidenbuis commented 2 years ago

I implemented the fix by @Daenero but I experience the problem of users double indenting list items, which didn't get decoded right. To prevent the double tabbing I limited the indenting of list items with a custom handler for the Tab event in the Quill options. Might be handy for someone else :)

keyboard: {
    bindings: {
        indent: {
            key: 9,
            format: ['blockquote', 'indent', 'list'],
            handler: function (this: any, range: any) {
                // We want to disable the indentation if:
                // - (1) The current line is the first line and the indent level is 0 (not indented)
                // - (2) The current line is a list and the previous line is not a list
                // - (3) The current line is a list and the previous line too, but the previous lines indentation level is already one level lower

                const currentLineFormats = this.quill.getFormat(
                    range.index
                )
                const previousLineFormats =
                    this.quill.getFormat(range.index - 1)
                const currentLineIsTheFirstLine =
                    range.index === 0
                const currentLineIsAList =
                    currentLineFormats.list !== undefined
                const previousLineIsAList =
                    previousLineFormats.list !== undefined
                const currentLineIndent =
                    currentLineFormats.indent || 0
                const previousLineIndent =
                    previousLineFormats.indent || 0

                if (
                    (currentLineIsTheFirstLine &&
                        currentLineIndent === 0) ||
                    (currentLineIsAList &&
                        !previousLineIsAList) ||
                    (currentLineIsAList &&
                        previousLineIsAList &&
                        previousLineIndent ===
                            currentLineIndent - 1)
                ) {
                    return
                }

                this.quill.format(
                    'indent',
                    '+1',
                    Quill.sources.USER
                )
            },
        },
    },
},
adithyavinjamoori commented 2 years ago

I got a few problems with code updated by @rlansky / @maggask. Nested lists should be placed inside previous "li" element, otherwise we can get empty list item.

Actual:

<li>1</li>
<li>2</li>
<li>
    <ul>
        <li>2.1</li>
    </ul>
</li>

Expected:

<li>1</li>
<li>2
    <ul>
        <li>2.1</li>
    </ul>
</li>

I have fixed this behavior in my Gist here - https://gist.github.com/Daenero/3442213dc5093dc10f30711edb529729.

And also, as in example from @Vovan-VE, you can also encode HTML nested list to Quill format.

I guess it should help.

you saved a lot of debuggin time. Thanks @Daenero

nawlbergs commented 11 months ago

3 more year and we're at a decade. What is the suggested workaround? use the Daenero code?

paleo commented 9 months ago

Damn, Quill's inability to handle bulleted lists properly is a major flaw.

paleo commented 9 months ago

Will Quill 2 solve the problem of nested lists?

https://github.com/quilljs/quill/releases/tag/v2.0.0-beta.0

milelo commented 8 months ago

I've just replaced my google editor with Quill. It was looking good however I've just come across this issue in testing, unfortunately not being able to have true nested lists is a deal breaker for me. Its back to google or find a better alternative. Please fix this in release 2.0.

paleo commented 7 months ago

Here is a good workaround: https://github.com/nozer/quill-delta-to-html

timotheedorand commented 6 months ago

This custom clipboard matchers fixed the issue on my side. Thanks to Subtletree on this issue: https://github.com/quilljs/quill/issues/1225

const Delta = Quill.import('delta');

function matchMsWordList(node, delta) {
  // Clone the operations
  let ops = delta.ops.map((op) => Object.assign({}, op));

  // Trim the front of the first op to remove the bullet/number
  let bulletOp = ops.find((op) => op.insert && op.insert.trim().length);
  if (!bulletOp) { return delta }

  bulletOp.insert = bulletOp.insert.trimLeft();
  let listPrefix = bulletOp.insert.match(/^.*?(^·|\.)/) || bulletOp.insert[0];
  bulletOp.insert = bulletOp.insert.substring(listPrefix[0].length, bulletOp.insert.length).trimLeft();

  // Trim the newline off the last op
  let last = ops[ops.length-1];
  last.insert = last.insert.substring(0, last.insert.length - 1);

  // Determine the list type
  let listType = listPrefix[0].length === 1 ? 'bullet' : 'ordered';

  // Determine the list indent
  let style = node.getAttribute('style').replace(/\n+/g, '');
  let levelMatch = style.match(/level(\d+)/);
  let indent = levelMatch ? levelMatch[1] - 1 : 0;

  // Add the list attribute
  ops.push({insert: '\n', attributes: {list: listType, indent}})

  return new Delta(ops);
}

function maybeMatchMsWordList(node, delta) {
  if (delta.ops[0].insert.trimLeft()[0] === '·') {
    return matchMsWordList(node, delta);
  }

  return delta;
}

const MSWORD_MATCHERS = [
  ['p.MsoListParagraphCxSpFirst', matchMsWordList],
  ['p.MsoListParagraphCxSpMiddle', matchMsWordList],
  ['p.MsoListParagraphCxSpLast', matchMsWordList],
  ['p.MsoListParagraph', matchMsWordList],
  ['p.msolistparagraph', matchMsWordList],
  ['p.MsoNormal', maybeMatchMsWordList]
];

// When instantiating a quill editor
let quill = new Quill('#editor', {
  modules: {
    clipboard: { matchers: MSWORD_MATCHERS }
  },
  placeholder: 'Compose an epic...',
  theme: 'snow'
});

https://github.com/quilljs/quill/issues/1225#issuecomment-992267444

Flexo013 commented 6 months ago

@luin Do you know whether there is any progress being made on this issue? It the second highest upvoted issue on this repo after all.

luin commented 6 months ago

It's worth noting that you can use quill.getSemanticHTML() to get an expected HTML output: Playground. This function can be particularly useful for exporting Quill content to external platforms.

timotheedorand commented 6 months ago

Upgrading to Quill v2.0.0-rc.2 fixed the issue on my side. https://github.com/quilljs/quill/releases/tag/v2.0.0-rc.2

ivanShagarov commented 6 months ago

Upgrading to Quill v2.0.0-rc.2 fixed the issue on my side. https://github.com/quilljs/quill/releases/tag/v2.0.0-rc.2

@timotheedorand Did you add some config to get the proper lists in this version? I've just upgraded to version Quill v2.0.0-rc.3, but no changes, still getting flat lists.

timotheedorand commented 6 months ago

@timotheedorand Did you add some config to get the proper lists in this version? I've just upgraded to version Quill v2.0.0-rc.3, but no changes, still getting flat lists.

Nothing specific, I've took the React Code they provide on the playground: https://quilljs.com/playground/react In fact, if you copy paste an indented list from Word there, it works great.

Screenshot 2024-03-21 at 13 48 06
ivanShagarov commented 6 months ago

Nothing specific, I've took the React Code they provide on the playground: https://quilljs.com/playground/react In fact, if you copy paste an indented list from Word there, it works great.

Yes, if you paste from Word it works well, but it works bad when pasting to word, or trying to use Quill's html in other pages/emails.

timotheedorand commented 6 months ago

@ivanShagarov indeed, from Quill to Word doesn't work natively. I fixed this by using getSemanticHTML()

// Editor.js
        quill.on(Quill.events.TEXT_CHANGE, () => {
            onTextChangeRef.current?.(quill.getSemanticHTML());
        });
luin commented 6 months ago

Yes, if you paste from Word it works well, but it works bad when pasting to word

When you copy content from Quill, semantic HTML is copied instead of just the innerHTML of the editor. Actually Quill internally calls the quill.getSemanticHTML() function. That means the expectation is that copying from Quill and pasting into external sites/apps should work (list indentations should be kept). If that's not the case, that would be a bug in quill.getSemanticHTML() and please feel free to raise a GitHub issue for it and I'll take a look.

ivanShagarov commented 6 months ago

@timotheedorand @luin Thank you guys! quill.getSemanticHTML() did the job :)