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?

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