DEV Community

Cover image for Create Custom Block in Quill like Video, Link, Banner.
Deepak Jaiswal
Deepak Jaiswal

Posted on

2

Create Custom Block in Quill like Video, Link, Banner.

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>
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (1)

Collapse
 
deepakjaiswal profile image
Deepak Jaiswal •

I am added block. if anyone need any custom block comment here i will give you best answer from our side. thank you.

Heroku

This site is powered by Heroku

Heroku was created by developers, for developers. Get started today and find out why Heroku has been the platform of choice for brands like DEV for over a decade.

Sign Up

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay