Quill: TypeScript classes not compatible with quill dist

Created on 4 Jan 2017  路  16Comments  路  Source: quilljs/quill

I'm having issues extending Parchment blots (specifically Inline, and Block) using TypeScript's class definition transpilation to ES5.

When TypeScript is configured to output ES6 code (ie, does not transpile class definitions) the following code works as expected (should be called when formatting is logged to the console).

But when TypeScript is configured to output ES5 code (and does transpile class definitions), the formats method in SampleBlot is not called.

Steps for Reproduction

// editor.ts (TypeScript file)
import * as Quill from 'quill';

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

class SampleBlot extends Inline {
  static formats(node) {
    console.log('should be called when formatting');
    return 'H1'; // just returning an h1 tag for demonstration purposes
  }
}
HeaderBlot.blotName = 'sample';
HeaderBlot.tagName = 'H1';

setTimeout(() => {
  quill.format('sample', 1);
}, 1000); // arbitrarily update format after 1 second (for demonstration)

Quill.register('formats/sample', SampleBlot); // have also tried Quill.register(SampleBlot);
const quill = new Quill(document.querySelector('#editor');

Expected behavior:
formats method is called and should be called when formatting is logged to the console.

Actual behavior:
formats method is not called and the console is empty.

Platforms:
Quill 1.1.5
TypeScript 2.1.4
Chrome 55

Most helpful comment

Hi we today got it. Let me show you:

const ReactQuill = require('react-quill');
const Quill = ReactQuill.Quill;
import * as Parchment from "parchment";

const QuillBlockInline = Quill.import('blots/inline');

class BlockInline extends Parchment.default.Inline {}
BlockInline.prototype = QuillBlockInline.prototype;

...

class Link extends BlockInline {

  static blotName = 'link';
  static tagName = 'A';
  static SANITIZED_URL = 'about:blank';

  static create(value: any) {
    let node = (super.create(value) as any);
    let href: string = this.sanitize(value);
    node.setAttribute('href', href);

...

    return node;
  }

  static formats(domNode: any) {
    return domNode.getAttribute('href');
  }

  static sanitize(url: string) {
    return sanitize(url, ['http', 'https', 'mailto', 'ftp', 'tel']) ? url : this.SANITIZED_URL;
  }

  static value(node: any) {
    return node.getAttribute('href');
  }

  format(name: string, value: string) {
    if (name !== this.statics.blotName || !value) return super.format(name, value);
    value = Link.sanitize(value);
    this.domNode.setAttribute('href', value);
  }
}

function sanitize(url: string, protocols: string[]) {
  let anchor = document.createElement('a');
  anchor.href = url;
  let protocol = anchor.href.slice(0, anchor.href.indexOf(':'));
  return protocols.indexOf(protocol) > -1;
}

export default Link;

All 16 comments

One workaround may be to forego using class definitions for my extension and use ES5 the whole way through. However, I'm struggling to find documentation on how this might be accomplished.

I'm also seeing this issue:

import * as Quill from 'quill';

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

class TestBlot extends Block {
  public static blotName = 'test';
  public static tagName = 'blockquote';
}

console.log(Block.scope) // => 10
console.log(TestBlot.scope) // => undefined

When I try this same code with babel, it works:

import Quill from 'quill';

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

class TestBlot extends Block {
  public static blotName = 'test';
  public static tagName = 'blockquote';
}

console.log(Block.scope) // => 10
console.log(TestBlot.scope) // => 10
// in parchment package
class BlockBlot extends FormatBlot { ... }

// in quill package
class Block extends Parchment.Block { ... }

Consuming the dist file with typescript is broken but with babel it is fine? I wonder if it's because the parchment package is transpiled by typescript but quill package is transpiled by babel?

I was able to get TypeScript compilation to work correctly with the following example:

const Block: IBlock = Quill.import('blots/block');
const Parchment: IParchment = Quill.import('parchment');

class LineHeightBlot extends Block {
    static blotName = 'lineHeight';
    static tagName = 'SPAN';
    static scope = Parchment.Scope.BLOCK;

    static create(value) {
        const node = super.create();
        node.style['line-height'] = value;
        return node;
    }

    static formats(node) {
        return $(node).css('line-height');
    }
}

What's strange to me is that I had to include the scope as well as the create method.

@arahansen I am facing the same issue. Where did you get reference to IBlock and IParchment. Can you show your complete code please.

Those interfaces are stubbed out as

interface IBlock extends ObjectConstructor {
    create(...any): HTMLElement;
}

@arahansen This fixed my issue.

@rajivchugh @arahansen when using TypeScript are you referencing type-definitions for Parchment as well?
I maintain the quill type def on DefinitelyTyped and I have not included the Parchment definitions because Parchment is written in TS and now exposes its own definitions.

I am going to give your code a spin and see if I can help with the Quill Definitions in any way. They need an upgrade anyways.

Thank you for your work on Quill + DefinitelyTyped @sumitkm by the way!

Thanks @jhchen, happy to make a tiny contrib to the awesome "Quill" :-)

@arahansen The workaround does not work when I try to extend Container for example (actually when I try to use this or super on the not static methods).
@jhchen Any plans to rewrite quill to TS? :)

@arahansen Any idea how to write an specific LinkBlot in typescript.
I am trying for hours without success...

BTW, using quill with typescript is really hard!!!

Here is an example what I tried:

import * as Quill from 'quill';
const Link = Quill.import('formats/link');
const Parchment: any = Quill.import('parchment');


class LinkBlot extends Link {
  static blotName = 'link';
  static tagName = 'A';
  static scope = Parchment.Scope.INLINE;

  constructor() {
    super();
    console.log('LinkBlot created', this);
  }

  static create(value) {
    console.log('create', value);
    const node = super.create(value);
    return node;
  }

  static formats(domNode) {
    return domNode.getAttribute('href');
  }

  static sanitize(url) {
    return super.sanitize(url);
  }

  format(name, value) {
    super.format(name, value);
  }
}

LinkBlot.blotName = 'link';
LinkBlot.tagName = 'A';

Parchment.register(LinkBlot);

The result when I insert a link is the following error:

TypeError: Cannot set property '__blot' of undefined
    at LinkBlot.ShadowBlot.attach (quill.js:5029)
    at LinkBlot.ContainerBlot.attach (quill.js:3851)
    at LinkBlot.FormatBlot.attach (quill.js:4102)
    at LinkBlot.ShadowBlot (quill.js:4988)
    at LinkBlot.ContainerBlot [as constructor] (quill.js:3844)
    at LinkBlot.FormatBlot [as constructor] (quill.js:4090)
    at LinkBlot.InlineBlot [as constructor] (quill.js:8683)
    at LinkBlot.Inline (quill.js:1817)
    at LinkBlot.Link (quill.js:4527)
    at new LinkBlot (quill-link-adapter.ts:24)
    at Object.create (quill.js:186)
    at TextBlot.ShadowBlot.wrap (quill.js:5119)
    at TextBlot.ShadowBlot.formatAt (quill.js:5047)
    at quill.js:3912

I have the sample problem when trying to customize the Link Blot using TypeScript. It is simply a nightmare. Will try to continue tomorrow but did not yet find solution.

@rh78 I ended up with something like this. I think it is suboptimal but it works:

import * as Quill from 'quill';
const Link = Quill.import('formats/link');

export function registerCustomLinkBehaviour(): void {

  Link.sanitize = function (url): string {
    ....

    return url;
 };

  Link.create = function (value): HTMLAnchorElement {
    let link: HTMLAnchorElement = document.createElement('a');
    ....
    return link;
  };

}

Hi we today got it. Let me show you:

const ReactQuill = require('react-quill');
const Quill = ReactQuill.Quill;
import * as Parchment from "parchment";

const QuillBlockInline = Quill.import('blots/inline');

class BlockInline extends Parchment.default.Inline {}
BlockInline.prototype = QuillBlockInline.prototype;

...

class Link extends BlockInline {

  static blotName = 'link';
  static tagName = 'A';
  static SANITIZED_URL = 'about:blank';

  static create(value: any) {
    let node = (super.create(value) as any);
    let href: string = this.sanitize(value);
    node.setAttribute('href', href);

...

    return node;
  }

  static formats(domNode: any) {
    return domNode.getAttribute('href');
  }

  static sanitize(url: string) {
    return sanitize(url, ['http', 'https', 'mailto', 'ftp', 'tel']) ? url : this.SANITIZED_URL;
  }

  static value(node: any) {
    return node.getAttribute('href');
  }

  format(name: string, value: string) {
    if (name !== this.statics.blotName || !value) return super.format(name, value);
    value = Link.sanitize(value);
    this.domNode.setAttribute('href', value);
  }
}

function sanitize(url: string, protocols: string[]) {
  let anchor = document.createElement('a');
  anchor.href = url;
  let protocol = anchor.href.slice(0, anchor.href.indexOf(':'));
  return protocols.indexOf(protocol) > -1;
}

export default Link;

So this is almost 2 years later but I came on this due to similar issues.
@maku you're receiving the TypeError: Cannot set property '__blot' of undefined message is because of the constructor missing arguments. When quill calls new on your blot it passes 2 arguments, and you, in turn, need to pass them to your super call.

Like this

`

export class ImageShortcodeBlot extends BlockShortcodeBlot {
  static blotName: string = 'imageShortcode';

  static tagName: string = 'DIV';

  static create(domNode: HTMLElement) {
    if (this.isImageShortcode(domNode)) {
      const data = domNode.dataset;
      const img = document.createElement('IMG') as HTMLImageElement;
      img.src = `some url`;
      img.setAttribute('data-shortcode', 'image');
      return img;
    }

    return super.create(domNode);
  }

  static isImageShortcode(domNode: HTMLElement): boolean {
    return domNode.dataset && domNode.dataset.shortcode === 'image';
  }

  actionSheet: ImageShortcodeActionSheet;

  constructor(public domNode: HTMLElement, value?: HTMLElement) {
    super(domNode, value);
    if (ImageShortcodeBlot.isImageShortcode(domNode)) {
      this.actionSheet = AppInjector.get(ImageShortcodeActionSheet);
      this.data = value.dataset;
      this.modelTag = value;
      domNode.addEventListener('click', () => {
        this.actionSheet.present({});
        this.actionSheet.onRemove.subscribe(() => {
          this.hRemove();
        });
      });
    }
  }
}

`

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lastmjs picture lastmjs  路  3Comments

visore picture visore  路  3Comments

kheraud picture kheraud  路  3Comments

rsdrsd picture rsdrsd  路  3Comments

DaniilVeriga picture DaniilVeriga  路  3Comments