Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrap quill patched with Safari polyfill, add value property #14

Merged
merged 11 commits into from
Oct 8, 2018
2 changes: 1 addition & 1 deletion demo/rich-text-editor-basic-demos.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<h3>Rich Text Editor</h3>
<vaadin-demo-snippet id="rich-text-editor-basic-demos-sample-example">
<template preserve-content>
<vaadin-rich-text-editor>Foobar</vaadin-rich-text-editor>
<vaadin-rich-text-editor></vaadin-rich-text-editor>
</template>
</vaadin-demo-snippet>

Expand Down
119 changes: 116 additions & 3 deletions src/vaadin-rich-text-editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,76 @@
<link rel="import" href="../../vaadin-element-mixin/vaadin-element-mixin.html">
<link rel="import" href="../../vaadin-license-checker/vaadin-license-checker.html">

<link rel="import" href="../vendor/quill.html">

<dom-module id="vaadin-rich-text-editor">
<template>
<style>
<style include="quill-snow-styles">
:host {
display: inline-block;
display: block;
}

:host([hidden]) {
display: none !important;
}

[part="editor"] {
min-height: 200px;
}
</style>
<slot></slot>

<!-- Create toolbar container -->
<div part="toolbar">

<!-- Bold -->
<button class="ql-bold" part="btn-bold"></button>

<!-- Italic -->
<button class="ql-italic" part="btn-italic"></button>

<!-- Underline -->
<button class="ql-underline" part="btn-underline"></button>

<!-- Strike -->
<button class="ql-strike" part="btn-strike"></button>

<!-- Header buttons -->
<button type="button" class="ql-header" value="1" part="btn-h1"></button>
<button type="button" class="ql-header" value="2" part="btn-h2"></button>

<!-- Subscript and superscript -->
<button class="ql-script" value="sub" part="btn-sub"></button>
<button class="ql-script" value="super" part="btn-super"></button>

<!-- List buttons -->
<button type="button" class="ql-list" value="bullet" part="btn-bullet-list"></button>
<button type="button" class="ql-list" value="ordered" part="btn-ordered-list"></button>

<!-- Align buttons -->
<button type="button" class="ql-align" value="" part="btn-align-left"></button>
<button type="button" class="ql-align" value="center" part="btn-align-center"></button>
<button type="button" class="ql-align" value="right" part="btn-align-right"></button>
<button type="button" class="ql-align" value="justify" part="btn-align-justify"></button>

<!-- Blockquote -->
<button type="button" class="ql-blockquote" part="btn-blockquote"></button>

<!-- Code block -->
<button type="button" class="ql-code-block" part="btn-code-block"></button>

<!-- Clean -->
<button type="button" class="ql-clean" part="btn-clean"></button>
</div>

<div part="editor"></div>

</template>

<script>
(function() {

const Quill = window.Quill;

/**
* `<vaadin-rich-text-editor>` is a Web Component for rich text editing.
*
Expand All @@ -48,8 +101,68 @@

static get properties() {
return {
/**
* Value is a list of the operations which describe change to the document.
* Each of those operations describe the change at the current index.
* They can be an `insert`, `delete` or `retain`. The format is as follows:
*
* ```js
* [
* { insert: 'Hello World' },
* { insert: '!', attributes: { bold: true }}
* ]
* ```
*
* See also https://github.com/quilljs/delta for detailed documentation.
*/
value: {
type: Array,
observer: '_valueChanged',
notify: true
}
};
}

ready() {
super.ready();

const editor = this.shadowRoot.querySelector('[part="editor"]');
const toolbar = this.shadowRoot.querySelector('[part="toolbar"]');

this._editor = new Quill(editor, {
modules: {
toolbar: toolbar
},
placeholder: 'Type something...',
theme: 'snow'
});

this._editor.on('text-change', (delta, oldDelta, source) => {
this.__userInput = source === 'user';
this.value = this._editor.getContents().ops;
this.__userInput = false;
});
}

_clear() {
this._editor.deleteText(0, this._editor.getLength(), 'silent');
}

_valueChanged(value) {
// prevent editor contents from being reset by user input
if (this.__userInput) {
return;
}
if (value == null || value.length === 0) {
this._clear();
} else if (!Array.isArray(value)) {
console.error('Incorrect value type: array expected, got:', value);
return;
}
const delta = new Quill.imports.delta(value);
// suppress text-change event to prevent infinite loop
this._editor.setContents(delta, 'silent');
}
}

customElements.define(RichTextEditorElement.is, RichTextEditorElement);
Expand Down
169 changes: 169 additions & 0 deletions test/basic.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<!doctype html>

<head>
<meta charset="UTF-8">
<title>vaadin-rich-text-editor tests</title>
<script src="../../web-component-tester/browser.js"></script>
<script src="../../webcomponentsjs/webcomponents-lite.js"></script>
<link rel="import" href="../../test-fixture/test-fixture.html">
<link rel="import" href="../vaadin-rich-text-editor.html">
</head>

<body>
<test-fixture id="default">
<template>
<vaadin-rich-text-editor></vaadin-rich-text-editor>
</template>
</test-fixture>

<script>
describe('rich text editor', () => {
let rte, editor;

beforeEach(() => {
rte = fixture('default');
editor = rte._editor;
});

describe('custom element definition', () => {
it('should have proper tag name', () => {
expect(rte.localName).to.be.equal('vaadin-rich-text-editor');
});

it('should not expose class name globally', () => {
expect(window.RichTextEditorElement).not.to.be.ok;
});

it('should have a valid version number', () => {
expect(customElements.get('vaadin-rich-text-editor').version).to.match(/^(\d+\.)?(\d+\.)?(\*|\d+)$/);
});
});

describe('toolbar controls', () => {
let btn;

['bold', 'italic', 'underline', 'strike', 'blockquote', 'code-block'].forEach(fmt => {
it(`should apply ${fmt} formatting when clicking the "btn-${fmt}" button`, () => {
btn = rte.shadowRoot.querySelector(`[part="btn-${fmt}"]`);
btn.click();
editor.insertText(0, 'Foo', 'user');
expect(editor.getFormat(0, 2)[fmt]).to.be.true;
});

it(`should undo ${fmt} formatting when clicking the "btn-${fmt}" button`, () => {
btn = rte.shadowRoot.querySelector(`[part="btn-${fmt}"]`);
editor.format(fmt, true);
btn.click();
expect(editor.getFormat(0)).to.be.empty.object;
});
});

['sub', 'super'].forEach(scr => {
it(`should apply ${scr} script when clicking the "btn-${scr}" button`, () => {
btn = rte.shadowRoot.querySelector(`[part="btn-${scr}"]`);
btn.click();
editor.insertText(0, 'Foo', 'user');
expect(editor.getFormat(0, 2).script).to.be.equal(scr);
});

it(`should undo ${scr} script when clicking the "btn-${scr}" button`, () => {
btn = rte.shadowRoot.querySelector(`[part="btn-${scr}"]`);
editor.format('script', scr);
btn.click();
expect(editor.getFormat(0)).to.be.empty.object;
});
});

['ordered', 'bullet'].forEach(type => {
it(`should create ${type} list when clicking the "btn-${type}-list" button`, () => {
btn = rte.shadowRoot.querySelector(`[part="btn-${type}-list"]`);

btn.click();
expect(editor.getFormat(0).list).to.be.equal(type);

btn.click();
expect(editor.getFormat(0).list).to.be.empty;
});
});

[1, 2].forEach(level => {
it(`should create <h${level}> header when clicking the "btn-h${level}" button`, () => {
btn = rte.shadowRoot.querySelector(`[part="btn-h${level}"]`);

btn.click();
expect(editor.getFormat(0).header).to.be.equal(level);

btn.click();
expect(editor.getFormat(0).header).to.be.empty;
});
});

['center', 'right', 'justify'].forEach(align => {
it(`should apply ${align} alignment when clicking the toolbar button`, () => {
btn = rte.shadowRoot.querySelector(`[part="btn-align-${align}"]`);

btn.click();
expect(editor.getFormat(0).align).to.be.equal(align);

btn = rte.shadowRoot.querySelector('[part="btn-align-left"]');
btn.click();
expect(editor.getFormat(0).align).to.be.empty;
});
});

it('should clear formatting when clicking the "btn-clean" button', () => {
editor.format('bold', true);
editor.format('underline', true);

btn = rte.shadowRoot.querySelector('[part="btn-clean"]');
btn.click();
expect(editor.getFormat(0)).to.be.empty.object;
});
});

describe('value', () => {
it('should represent the "delta" of the editor', done => {
rte.addEventListener('value-changed', e => {
expect(e.detail.value).to.be.array;
expect(e.detail.value).to.have.length(1);
expect(e.detail.value[0]).to.deep.equal({insert: 'Foo\n'});
done();
});
editor.insertText(0, 'Foo', 'user');
});

it('should update the "delta" when set from outside', () => {
rte.value = [{insert: 'Foo'}];
expect(editor.getText()).to.equal('Foo\n');
});

it('should error when setting value of the incorrect type', () => {
const origError = console.error;
const spy = console.error = sinon.spy();
rte.value = 'Foo';
console.error = origError;
expect(editor.getText()).to.not.include('Foo');
expect(spy.called).to.be.true;
});

it('should clear the editor contents when value is set to null', () => {
editor.insertText(0, 'Foo', 'user');
rte.value = null;
expect(editor.getText()).to.equal('\n');
});

it('should clear the editor contents when value is set to undefined', () => {
editor.insertText(0, 'Foo', 'user');
rte.value = undefined;
expect(editor.getText()).to.equal('\n');
});

it('should clear the editor contents when value is set to empty array', () => {
editor.insertText(0, 'Foo', 'user');
rte.value = [];
expect(editor.getText()).to.equal('\n');
});
});
});
</script>
</body>
40 changes: 0 additions & 40 deletions test/sample-test.html

This file was deleted.

2 changes: 1 addition & 1 deletion test/test-suites.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
window.RichTextEditorSuites = [
'sample-test.html'
'basic.html'
];
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading