Skip to content

Instantly share code, notes, and snippets.

@Daenero
Created November 13, 2020 14:29
Show Gist options
  • Save Daenero/3442213dc5093dc10f30711edb529729 to your computer and use it in GitHub Desktop.
Save Daenero/3442213dc5093dc10f30711edb529729 to your computer and use it in GitHub Desktop.
Fix for Quill.js issue with list indent (use of css class instead of pure HTML)
// https://github.com/quilljs/quill/issues/979
interface NestedElement {
content: string;
indent: number;
classes: string;
}
export function quillDecodeIndent(text: string) {
if (!text || text.length === 0) {
return text;
}
const tempEl = window.document.createElement('div');
tempEl.setAttribute('style', 'display: none;');
tempEl.innerHTML = text;
['ul', 'ol'].forEach((type) => {
// Grab each list, and work on it in turn
Array.from(tempEl.querySelectorAll(type)).forEach((outerListEl) => {
const listChildren = Array.from(outerListEl.children).filter((el) => el.tagName === 'LI');
let lastLiLevel = 0;
const parentElementsStack: Element[] = [];
const root = document.createElement(type);
parentElementsStack.push(root);
listChildren.forEach((e, i) => {
const currentLiLevel = getQuillListLevel(e);
e.className = e.className.replace(getIndentClass(currentLiLevel), '');
const difference = currentLiLevel - lastLiLevel;
lastLiLevel = currentLiLevel;
if (difference > 0) {
let currentDiff = difference;
while (currentDiff > 0) {
let lastLiInCurrentLevel = seekLastElement(parentElementsStack).lastElementChild;
if (!lastLiInCurrentLevel) {
lastLiInCurrentLevel = document.createElement('li');
encode_addChildToCurrentParent(parentElementsStack, lastLiInCurrentLevel);
}
const newList = document.createElement(type);
lastLiInCurrentLevel.appendChild(newList);
parentElementsStack.push(newList);
currentDiff--;
}
}
if (difference < 0) {
let currentDiff = difference;
while (currentDiff < 0) {
parentElementsStack.pop();
currentDiff++;
}
}
encode_addChildToCurrentParent(parentElementsStack, e);
});
outerListEl.innerHTML = root.innerHTML;
});
});
const newContent = tempEl.innerHTML;
tempEl.remove();
return newContent;
}
export function quillEncodeIndent(text: string) {
if (!text || text.length === 0) {
return text;
}
const tempEl = window.document.createElement('div');
tempEl.setAttribute('style', 'display: none;');
tempEl.innerHTML = text;
['ul', 'ol'].forEach((type) => {
Array.from(tempEl.querySelectorAll(type)).forEach((outerListEl) => {
const listResult = Array.from(outerListEl.children)
.filter(e => e.tagName === 'LI')
.map(e => encode_UnwindElement(type.toUpperCase(), e, 0))
.reduce((prev, c) => [...prev, ...c], []) // flatten list
.map(e => encode_GetLi(e))
.reduce((prev, c) => `${prev}${c}`, ''); // merge to one string
outerListEl.innerHTML = listResult;
});
});
const newContent = tempEl.innerHTML;
tempEl.remove();
return newContent;
}
function encode_UnwindElement(listType: string, li: Element, level: number): NestedElement[] {
const childElements = Array.from(li.children)
.filter(innerElement => innerElement.tagName === listType)
.map(innerList =>
Array.from(li.removeChild(innerList).children)
.map(nestedListElement => encode_UnwindElement(listType, innerList.removeChild(nestedListElement), level + 1))
.reduce((prev, c) => [...prev, ...c], []))
.reduce((prev, c) => [...prev, ...c], []);
const current: NestedElement = {
classes: li.className,
content: li.innerHTML,
indent: level
};
return [current, ...childElements];
}
function encode_GetLi(e: NestedElement) {
let cl = '';
if (e.indent > 0) {
cl += `${getIndentClass(e.indent)}`;
}
if (e.classes.length > 0) {
cl += ` ${e.classes}`;
}
return `<li${cl.length > 0 ? ` class="${cl}"` : ''}>${e.content}</li>`;
}
function seekLastElement(list: Element[]): Element {
return list[list.length - 1];
}
function encode_addChildToCurrentParent(parentStack: Element[], child: Element): void {
const currentParent = seekLastElement(parentStack);
currentParent.appendChild(child);
}
function getQuillListLevel(el: Element) {
const className = el.className || '0';
return +className.replace(/[^\d]/g, '');
}
function getIndentClass(level: number) { return `ql-indent-${level}`; }
@vdechef
Copy link

vdechef commented May 10, 2021

Thanks for this code, it was a lifesavier :)

I fixed a small bug when encoding some HTML like below:

<ol>
    <li>
        <ol>
            <li>text indented twice, without content in parent</li>
        </ol>
    </li>
</ol>

The resulting HTML was containing an empty <li> tag, resulting in something like this:

    1. text indented twice, without content in parent

Here is the small fix for the encode_GetLi function. Maybe you could update your gist with it, to help others ?

function encode_GetLi(e: NestedElement) {
    if (e.content.length === 0) {
        return ""
    }
    let cl = '';
    if (e.indent > 0) {
        cl += `${getIndentClass(e.indent)}`;
    }
    if (e.classes.length > 0) {
        cl += ` ${e.classes}`;
    }
    return `<li${cl.length > 0 ? ` class="${cl}"` : ''}>${e.content}</li>`;
}

@LauraBrandt
Copy link

I would also like to express my thanks! This was super helpful.

I also made a small change I wanted to mention in case it's useful for someone else.

The ordered lists output I got from quillDecodeIndent were using numeric markers for all levels, so to keep it consistent with Quill, which repeats [numeric, lower alpha, lower roman], I added this line just before the outermost forEach in quillDecodeIndent:

const listTypes = ['1', 'a', 'i'];

and then added the type attribute to the <ol> just after creating newList:

const newList = document.createElement(type);
if (type === 'ol') {
  newList.setAttribute('type', listTypes[currentLiLevel % 3]);
}
lastLiInCurrentLevel.appendChild(newList);

@daviferreira
Copy link

Thank you :)

@ArpitaHunka
Copy link

It will work with normal java script?

@adithyavinjamoori
Copy link

This works fine if I use only UL or OL. There is a major issue if I use UL and OL together.

Have you faced this issue?

@dims337
Copy link

dims337 commented Jul 12, 2022

I would also like to express my thanks! This was super helpful.

I also made a small change I wanted to mention in case it's useful for someone else.

The ordered lists output I got from quillDecodeIndent were using numeric markers for all levels, so to keep it consistent with Quill, which repeats [numeric, lower alpha, lower roman], I added this line just before the outermost forEach in quillDecodeIndent:

const listTypes = ['1', 'a', 'i'];

and then added the type attribute to the <ol> just after creating newList:

const newList = document.createElement(type);
if (type === 'ol') {
  newList.setAttribute('type', listTypes[currentLiLevel % 3]);
}
lastLiInCurrentLevel.appendChild(newList);

fixed the nested indent issue, thanks

@vdechef
Copy link

vdechef commented Nov 16, 2022

I would also like to express my thanks! This was super helpful.

I also made a small change I wanted to mention in case it's useful for someone else.

The ordered lists output I got from quillDecodeIndent were using numeric markers for all levels, so to keep it consistent with Quill, which repeats [numeric, lower alpha, lower roman], I added this line just before the outermost forEach in quillDecodeIndent:

const listTypes = ['1', 'a', 'i'];

and then added the type attribute to the <ol> just after creating newList:

const newList = document.createElement(type);
if (type === 'ol') {
  newList.setAttribute('type', listTypes[currentLiLevel % 3]);
}
lastLiInCurrentLevel.appendChild(newList);

Thanks, it fixed my problem :)

@PremKolar
Copy link

thank you so much! works like a charm.
jah bless 🙏

@Mena489
Copy link

Mena489 commented Apr 22, 2023

How to write this in php please?

@Er1c5G
Copy link

Er1c5G commented Jul 4, 2023

How to using this with react-quill?

@isaac-bowen
Copy link

isaac-bowen commented Oct 23, 2024

This answer worked for me! I ended up adding the following clipboard matcher as well:

// Processes any <li> element inserted into Quill via a delta.
// Ensures "ql-indent-*" classes are correctly converted into a delta
// with the appropriate number of indents for the Quill editor.

const nestedListHandler = (node, delta) => {
  const indentClass = Array.from(node.classList).find(className => className.startsWith("ql-indent-"));

  if (indentClass) {
    const indentLevel = indentClass.replace("ql-indent-", "");
    const indentLevelInt = parseInt(indentLevel) + 1;

    let newOps = [];
    let hasIndented = false;

    delta.ops.forEach(op => {
      newOps.push(op);

      if (!hasIndented) {
        newOps.unshift({
          retain: op.insert.length,
          attributes: {indent: indentLevelInt}
        });

        hasIndented = true;
      }
    });

    delta.ops = newOps;
  }

  return delta;
};

new Quill(domElement, {
    modules: {
      history: {
        delay: 1000,
        maxStack: 10,
        userOnly: false
      },
      toolbar: [
        ["bold", "italic", "underline"],
        [{align: ""}],
        [{"list": "ordered"}, {"list": "bullet"}],
      ],
      clipboard: {
        matchVisual: true,
        matchers: [
          ["li", nestedListHandler], // Handle any li elements being converted to deltas
        ]
      }
    },
    placeholder: placeholder,
    theme: "snow",
    formats: ["bold", "italic", "underline", "list", "align"]
  });

This works in combination with the original answer to further support for nested lists in Quill's deltas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment