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}`; }
@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