Create Custom Block in Quill like Video, Link, Banner.
Tags: react, angular, webdev, html
Published: 2023-08-14 18:09:01

i am creating this post as my experience. sometime we need to create custom block in quill to create block for our need. i create some block.

## index.html

```
<div class="" id="editor-wrapper" (keyup)="keyup($event)" (click)="keyup($event)">

</div>
<div><button (click)="download()">Download</button></div>
<div><button (click)="insert()">Insert Hr</button></div>
<div id="customDropdown" style="display: none;">
  <!-- Add your dropdown options here -->
  <button>Option 1</button>
  <button>Option 2</button>
  <button>Option 3</button>
</div>
```

## editor.ts

```
declare const Quill: any;
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
declare const quillBetterTable: any;
declare const htmlToPdfmake: any;
declare const pdfMake: any;
import jsPDF from 'jspdf';

Quill.register({
  'modules/better-table': quillBetterTable
}, true);
const Link = Quill.import("formats/link");

const BlockEmbed = Quill.import("blots/block/embed");

class VideoBlot extends BlockEmbed {
  static create(obj:any) {
    console.log(obj)
    let node = super.create(obj?.url?obj?.url:obj);
    let iframe = document.createElement('iframe');
    let con = document.createElement('span');
    con.setAttribute('style', 'display:flex;');
    con.setAttribute('class', 'resize-container');
    con.innerHTML=`
        <button data-size="1" class="re-1">1x</button>
        <button data-size="2" class="re-2">2x</button>
        <button data-size="3" class="re-3">3x</button>
        <button data-size="4" class="re-4">4x</button>
    `
    // con.addEventListener('click',(e:any)=>{
    //   node.setAttribute('data-width', 'embed-responsive-'+e.target.dataset.size);
    //   console.log("first",e.target.dataset.size)
    // })

    node.setAttribute('data-width', obj?.size?obj?.size:'embed-responsive-'+3);
    // Set styles for wrapper
    node.setAttribute('class', 'embed-responsive embed-responsive-16by9');
    node.appendChild(con)
    // Set styles for iframe
    iframe.setAttribute('frameborder', '0');
    iframe.setAttribute('allowfullscreen', 'true');
    iframe.setAttribute('src', obj?.url?obj?.url:obj);
    // Append iframe as child to wrapper
    node.appendChild(iframe);
    return node;
  }

  static value(domNode:any) {
    const url=domNode.getElementsByTagName('iframe')[0].getAttribute('src');
    const size=domNode.getAttribute('data-width');
    return {url,size}
  }

  // format(name:any, value:any) {
  //   // Override the format method to handle your custom attribute
  //   if (name === 'custom-attribute') {
  //     const previousValue = this['domNode'].getAttribute('data-custom-attribute');
  //     if (value) {
  //       this['domNode'].setAttribute('data-custom-attribute', value);
  //     } else {
  //       this['domNode'].removeAttribute('data-custom-attribute');
  //     }
  //     // Use the previousValue as needed
  //   } else {
  //     super.format(name, value);
  //   }
  //   console.log('Previous value:', name,value);
  // }
}
VideoBlot['blotName'] = 'video';
VideoBlot['tagName'] = 'div';

Quill.register(VideoBlot, true);

var CustomLink = Quill.import('formats/link');
CustomLink.sanitize = function(url:any) {
  return url; // Customize the URL sanitization if needed
};

class CustomLinkFormat extends CustomLink {
  static create(value:any) {
    const node = super.create(value.url);
    console.log(value);
    node.setAttribute('data-custom-attribute', value.customAttribute); // Set the custom attribute
    return node;
  }

  static formats(node:any) {
    const format = super.formats(node);
    console.log(node)
    format.customAttribute = node.getAttribute('data-custom-attribute')||'1'; // Get the custom attribute
    return format;
  }
}
Quill.register(CustomLinkFormat, true);

var Inline = Quill.import('blots/inline');

// Define the custom format class
class CustomFormat extends Inline {
  static create(v:any) {
    const node = super.create();
    const d=document.createElement('sup');
    d.classList.add('custom-format');
    d.appendChild(node)
    return d;
  }
}

// Assign a CSS class name to the custom format
CustomFormat['blotName'] = 'highlight';
CustomFormat['tagName'] = 'span';

// Register the custom format with Quill
Quill.register(CustomFormat);


// Extend ListContainer module
// const Block = Quill.import('blots/block');

// class CustomListContainer extends Block {
//   static create(value:any) {
//     const node = super.create(value);
//     node.classList.add('custom-list-container');
//     return node;
//   }
// }

// CustomListContainer['tagName'] = 'div';
// CustomListContainer['allowedChildren'] = [Block, CustomListContainer];
// CustomListContainer['scope'] = Block.scope;
// CustomListContainer['defaultChild'] = 'block';


// // Override the default ListItem module to use the custom list container
// const ListItem = Quill.import('formats/list');

// class CustomListItem extends ListItem {   
//   format(name:any, value:any) {
//     if (name === 'list' && value) {
//       const isOrdered = value === 'ordered';
//       const CustomContainer:any = isOrdered ? 'OL' : 'UL';
//       const container:any = this['parent'];
//       if (!(container instanceof CustomContainer)) {
//         const newContainer = this['scroll'].create(CustomContainer);
//         container.replaceWith(newContainer);
//         newContainer.appendChild(this['domNode']);
//       }
//     }
//     super.format(name, value);
//   }
// }


import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-rich-editor',
  templateUrl: './rich-editor.component.html',
  styleUrls: ['./rich-editor.component.css']
})
export class RichEditorComponent implements OnInit{
  data:any=`<p><br></p>`
  quill:any;
  constructor(public sanitizer: DomSanitizer){

  }
  ngOnInit(): void {
    setTimeout(()=>{
      this.initEditor()
    },1000)
    // document.querySelector('.re-1')?.addEventListener('click',()=>{
    //   console.log('sadasa')
    // })

  }

  initEditor(){
    this.quill = new Quill('#editor-wrapper', {
      theme: 'snow',
      modules: {
        toolbar:{
          container:[
          'video',
          'image',
          'link',
          'align',
          'customLink',
          { 'script': 'sub'}, { 'script': 'super' },
          {'list':'ordered'},{'list':'bullet'}
        ],
        handlers:{
          'customLink':(v:any)=>{
            console.log(v)
            this.quill.format('link', {
              url: 'https://example.com',
              customAttribute: 'custom value'
            });
          },
          },

      },
        table: false,  // disable table module
        'better-table': {
          operationMenu: {
            items: {
              unmergeCells: {
                text: 'Another unmerge cells name'
              }
            }
          }
        },
        keyboard: {
          bindings: quillBetterTable.keyboardBindings
        },
      },
    })

    var customDropdown:any = document.getElementById('customDropdown');
    var dropdownOpen = false;

    this.quill.on('text-change', (delta:any, oldDelta:any, source:any)=> {
      console.log(delta,oldDelta,source)
      var range = this.quill.getSelection();
      if (range && range.length === 0) {
        var cursorPosition = range.index;
        var lineText = this.quill.getText(0, cursorPosition);

        // Define the trigger text or condition to open the dropdown
        var triggerText = '/'; // Example: Open the dropdown when the user types '@dropdown'

        if (lineText.endsWith(triggerText)) {
          if (!dropdownOpen) {
            // Get the bounds of the current cursor position
            var bounds = this.quill.getBounds(range.index);

            // Get the offset position of the Quill editor
            var editorBounds:any = document.getElementById('editor-wrapper')?.getBoundingClientRect();
            var editorOffsetTop = editorBounds.top + window.pageYOffset;
            var editorOffsetLeft = editorBounds.left + window.pageXOffset;

            // Position the dropdown below the cursor, considering the editor offset
            customDropdown.style.left = (bounds.left - editorOffsetLeft) + 'px';
            customDropdown.style.top = (bounds.top - editorOffsetTop + bounds.height) + 'px';
            customDropdown.style.display = 'block';
            dropdownOpen = true;
          }
        } else {
          if (dropdownOpen) {
            // Close the dropdown
            customDropdown.style.display = 'none';
            dropdownOpen = false;
          }
        }
      }
    });

    document.addEventListener('click', function(event) {
      // Close the dropdown if a click event occurs outside the dropdown
      if (!customDropdown.contains(event.target)) {
        customDropdown.style.display = 'none';
        dropdownOpen = false;
      }
    });

  }
  click(x:any){
    console.log(x)
  }

  download(){
   let x=this.quill.root.innerHTML;
  const htmlString = '<ol><li class="child">Item 1</li><li>Item 2</li></ol><ol><li class="chi">Item 1</li><li>Item 2</li></ol>';

// Replace <ol> tags with <ul> tags for child elements with the class "child"
x= x.replaceAll(/<ol\b([^>]*)>(.*?<li\s+data-list="bullet">.*?<\/li>.*?)<\/ol>/gi, '<ul $1>$2</ul>')



    var html = htmlToPdfmake(x);
  // console.log(html)
  var docDefinition = {
    content: [
      html
    ],
    styles:{

    }
  };

  var pdfDocGenerator = pdfMake.createPdf(docDefinition);
  pdfDocGenerator.download()

  // var doc:any = new jsPDF();
  // doc.html(this.quill.root.innerHTML, {
  //   callback: function (docs:any) {
  //     docs.save('quill_content.pdf');
  //   }
  // });

  }

  insert(){
    const range=this.quill.getSelection();
    const is=this.quill.getFormat(range.index,range.length)
    console.log(is)
    if(is.highlight)
    this.quill.format('highlight', false);
    else this.quill.format('highlight', true);
  }

  keyup(event:any){
    console.log(event)
  }
}
```


## style.css

```
.dropdown {
  position: relative;
  display: inline-block;
}

.dropdown-toggle {
  padding: 10px 15px;
  background-color: #f5f5f5;
  border: 1px solid #ccc;
  border-radius: 4px;
  cursor: pointer;
}

.dropdown-menu {
  position: absolute;
  top: 100%;
  left: 0;
  display: none;
  min-width: 160px;
  padding: 5px 0;
  background-color: #fff;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
  z-index: 1;
}

.dropdown-menu.show {
  display: block;
}

.dropdown-item {
  display: block;
  padding: 5px 10px;
  color: #333;
  text-decoration: none;
  transition: background-color 0.3s;
}

.dropdown-item:hover {
  background-color: #f5f5f5;
}
```