Skip to content

Instantly share code, notes, and snippets.

@WinstonFassett
Created May 1, 2017 18:43
Show Gist options
  • Save WinstonFassett/9752d3abaf0257b8a561a326b705223a to your computer and use it in GitHub Desktop.
Save WinstonFassett/9752d3abaf0257b8a561a326b705223a to your computer and use it in GitHub Desktop.
JS Bin // source http://jsbin.com/defixum
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<link href="http://cdn.quilljs.com/1.2.4/quill.snow.css" rel="stylesheet">
<link href="http://cdn.quilljs.com/1.2.4/quill.bubble.css" rel="stylesheet">
<style id="jsbin-css">
.ql-editor table {
width: 100%;
border-collapse: collapse;
}
.ql-editor table td {
border: 1px solid black;
padding: 5px;
height: 25px;
}
button.ql-table::after { content: "TABLE"; }
.ql-picker.ql-table .ql-picker-label::before { content: "TABLE"; }
button.ql-contain::after { content: "WRAP"; }
button.ql-table-insert-rows::after { content: "ROWS+"; }
button.ql-table-insert-columns::after { content: "COLS+"; }
.ql-table,
.ql-contain {
width: auto !important;
margin-right: -15px;
}
.ql-picker.ql-table {
margin-right: -15px;
font-size: 11px;
font-weight: normal;
}
.ql-picker.ql-table svg {
display: none;
}
.ql-picker.ql-table .ql-picker-label {
padding: 0px 3px;
}
.ql-picker.ql-table .ql-picker-options {
width: 190px;
}
.ql-picker.ql-table .ql-picker-item {
display: block;
float: left;
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
padding: 0px;
margin: 1px;
}
.ql-picker.ql-table .ql-picker-item {
background: lightgrey;
}
.ql-picker-item:nth-child(5):before {
clear: both;
display: block;
content: "";
width: 100%;
}
.ql-picker-item[data-value=newtable_1_1]:before { content: "1x1"; }
.ql-picker-item[data-value=newtable_1_2]:before { content: "1x2"; }
.ql-picker-item[data-value=newtable_1_3]:before { content: "1x3"; }
.ql-picker-item[data-value=newtable_1_4]:before { content: "1x4"; }
.ql-picker-item[data-value=newtable_1_5]:before { content: "1x5"; }
.ql-picker-item[data-value=newtable_2_1]:before { content: "2x1"; }
.ql-picker-item[data-value=newtable_2_2]:before { content: "2x2"; }
.ql-picker-item[data-value=newtable_2_3]:before { content: "2x3"; }
.ql-picker-item[data-value=newtable_2_4]:before { content: "2x4"; }
.ql-picker-item[data-value=newtable_2_5]:before { content: "2x5"; }
.ql-picker-item[data-value=newtable_3_1]:before { content: "3x1"; }
.ql-picker-item[data-value=newtable_3_2]:before { content: "3x2"; }
.ql-picker-item[data-value=newtable_3_3]:before { content: "3x3"; }
.ql-picker-item[data-value=newtable_3_4]:before { content: "3x4"; }
.ql-picker-item[data-value=newtable_3_5]:before { content: "3x5"; }
.ql-picker-item[data-value=newtable_4_1]:before { content: "4x1"; }
.ql-picker-item[data-value=newtable_4_2]:before { content: "4x2"; }
.ql-picker-item[data-value=newtable_4_3]:before { content: "4x3"; }
.ql-picker-item[data-value=newtable_4_4]:before { content: "4x4"; }
.ql-picker-item[data-value=newtable_4_5]:before { content: "4x5"; }
.ql-picker-item[data-value=newtable_5_1]:before { content: "5x1"; }
.ql-picker-item[data-value=newtable_5_2]:before { content: "5x2"; }
.ql-picker-item[data-value=newtable_5_3]:before { content: "5x3"; }
.ql-picker-item[data-value=newtable_5_4]:before { content: "5x4"; }
.ql-picker-item[data-value=newtable_5_5]:before { content: "5x5"; }
.ql-picker-item[data-value=newtable_6_1]:before { content: "6x1"; }
.ql-picker-item[data-value=newtable_6_2]:before { content: "6x2"; }
.ql-picker-item[data-value=newtable_6_3]:before { content: "6x3"; }
.ql-picker-item[data-value=newtable_6_4]:before { content: "6x4"; }
.ql-picker-item[data-value=newtable_6_5]:before { content: "6x5"; }
.ql-picker-item[data-value=newtable_7_1]:before { content: "7x1"; }
.ql-picker-item[data-value=newtable_7_2]:before { content: "7x2"; }
.ql-picker-item[data-value=newtable_7_3]:before { content: "7x3"; }
.ql-picker-item[data-value=newtable_7_4]:before { content: "7x4"; }
.ql-picker-item[data-value=newtable_7_5]:before { content: "7x5"; }
.ql-picker-item[data-value=newtable_8_1]:before { content: "8x1"; }
.ql-picker-item[data-value=newtable_8_2]:before { content: "8x2"; }
.ql-picker-item[data-value=newtable_8_3]:before { content: "8x3"; }
.ql-picker-item[data-value=newtable_8_4]:before { content: "8x4"; }
.ql-picker-item[data-value=newtable_8_5]:before { content: "8x5"; }
.ql-picker-item[data-value=newtable_9_1]:before { content: "9x1"; }
.ql-picker-item[data-value=newtable_9_2]:before { content: "9x2"; }
.ql-picker-item[data-value=newtable_9_3]:before { content: "9x3"; }
.ql-picker-item[data-value=newtable_9_4]:before { content: "9x4"; }
.ql-picker-item[data-value=newtable_9_5]:before { content: "9x5"; }
.ql-picker-item[data-value=newtable_10_1]:before { content: "10x1"; }
.ql-picker-item[data-value=newtable_10_2]:before { content: "10x2"; }
.ql-picker-item[data-value=newtable_10_3]:before { content: "10x3"; }
.ql-picker-item[data-value=newtable_10_4]:before { content: "10x4"; }
.ql-picker-item[data-value=newtable_10_5]:before { content: "10x5"; }
.tdbr, .trbr {
display: none
}
</style>
</head>
<body>
<h1>Quill Table Breaks</h1>
<div id="editor-container">
</div>
Output:
<textarea id='output_delta' style='width:100%; height:200px; padding: 3px;'></textarea>
Orig Delta:
<textarea id='orig_delta' style='width:100%; height:200px; padding: 3px;'></textarea>
Current HTML:
<textarea id='output_html' style='width:100%; height:200px; padding: 3px;'></textarea>
HTML:
<div id="view_html">
</div>
<script src="http://cdn.quilljs.com/1.2.4/quill.js"></script>
<script id="jsbin-javascript">
// quill-table-breaks.js
let Container = Quill.import('blots/container');
let Scroll = Quill.import('blots/scroll');
let Inline = Quill.import('blots/inline');
let Block = Quill.import('blots/block');
let Delta = Quill.import('delta');
let Parchment = Quill.import('parchment');
let BlockEmbed = Quill.import('blots/block/embed');
let TextBlot = Quill.import('blots/text');
class ContainBlot extends Container {
static create(value) {
let tagName = 'contain';
let node = super.create(tagName);
return node;
}
insertBefore(blot, ref) {
if (blot.statics.blotName == this.statics.blotName) {
console.log('############################ Not sure this is clean:')
console.log(blot)
console.log(blot.children.head)
super.insertBefore(blot.children.head, ref);
} else {
super.insertBefore(blot, ref);
}
}
static formats(domNode) {
return domNode.tagName;
}
formats() {
// We don't inherit from FormatBlot
return { [this.statics.blotName]: this.statics.formats(this.domNode) }
}
replace(target) {
if (target.statics.blotName !== this.statics.blotName) {
let item = Parchment.create(this.statics.defaultChild);
target.moveChildren(item);
this.appendChild(item);
}
if (target.parent == null) return;
super.replace(target)
}
}
ContainBlot.blotName = 'contain';
ContainBlot.tagName = 'contain';
ContainBlot.scope = Parchment.Scope.BLOCK_BLOT;
ContainBlot.defaultChild = 'block';
ContainBlot.allowedChildren = [Block, BlockEmbed, Container];
Quill.register(ContainBlot);
class TableRow extends Container {
static create(value) {
let tagName = 'tr';
let node = super.create(tagName);
return node;
}
optimize() {
super.optimize();
var parent = this.parent
if (parent != null && parent.statics.blotName != 'table') {
this.processTable()
}
}
processTable () {
var currentBlot = this
var rows = []
while (currentBlot) {
if (! (currentBlot instanceof TableRow)) {
break
}
rows.push(currentBlot)
currentBlot = currentBlot.next
}
let mark = Parchment.create('block');
this.parent.insertBefore(mark, this.next);
let table = Parchment.create('table');
rows.forEach(function (row) {
table.appendChild(row)
})
table.replace(mark)
}
}
TableRow.blotName = 'tr';
TableRow.tagName = 'tr';
TableRow.scope = Parchment.Scope.BLOCK_BLOT;
TableRow.defaultChild = 'td';
Quill.register(TableRow);
class Table extends Container {
optimize() {
super.optimize();
let next = this.next;
if (next != null && next.prev === this &&
next.statics.blotName === this.statics.blotName &&
next.domNode.tagName === this.domNode.tagName
) {
next.moveChildren(this);
next.remove();
}
}
}
Table.blotName = 'table';
Table.tagName = 'table';
Table.scope = Parchment.Scope.BLOCK_BLOT;
Table.defaultChild = 'tr';
Table.allowedChildren = [TableRow];
Quill.register(Table);
//
//
// CONTAINER TD
//
class TableCell extends ContainBlot {
format() {
return 'td'
}
optimize() {
super.optimize();
let parent = this.parent;
if (parent != null && parent.statics.blotName != 'tr') {
this.processTR()
}
// merge same TD id
let next = this.next;
if (next != null && next.prev === this &&
next.statics.blotName === this.statics.blotName &&
next.domNode.tagName === this.domNode.tagName
) {
next.moveChildren(this);
next.remove();
}
}
processTR () {
// find next row break
var currentBlot = this
var rowItems = [this]
while (currentBlot) {
if (currentBlot.statics.tagName !== 'TD') {
break
}
rowItems.push(currentBlot)
if (currentBlot instanceof RowBreak) {
break
}
currentBlot = currentBlot.next
}
// create row, add row items as TDs
var prevItem
var cellItems = []
var cells = []
rowItems.forEach(function (rowItem) {
cellItems.push(rowItem)
if (rowItem instanceof TableCell) {
prevItem = rowItem
} else if (rowItem instanceof CellBreak) {
cells.push(cellItems)
cellItems = []
}
})
if (cellItems.length > 0) {
cells.push(cellItems)
}
let mark = Parchment.create('block');
this.parent.insertBefore(mark, this.next);
// create row
var row = Parchment.create('tr')
cells.forEach(function (cell) {
// add row elements
cell.forEach(function (cellItem) {
row.appendChild(cellItem)
})
})
row.replace(mark)
}
}
TableCell.blotName = 'td';
TableCell.tagName = 'td';
TableCell.scope = Parchment.Scope.BLOCK_BLOT;
TableCell.defaultChild = 'block';
TableCell.allowedChildren = [Block, BlockEmbed, Container];
Quill.register(TableCell);
Container.order = [
'list', 'contain', // Must be lower
'td', 'tr', 'table' // Must be higher
];
class RowBreak extends BlockEmbed {
formats() {
return { trbr: true }
}
}
RowBreak.blotName = 'trbr'
RowBreak.tagName = 'td'
RowBreak.className = 'trbr'
Quill.register(RowBreak);
class CellBreak extends BlockEmbed {
formats() {
return { tdbr: true }
}
}
CellBreak.blotName = 'tdbr'
CellBreak.tagName = 'td'
CellBreak.className = 'tdbr'
Quill.register(CellBreak);
// END quill-table-breaks.js
// Render UI
var Keyboard = Quill.import('modules/keyboard')
// set up toolbar options
let maxRows = 10;
let maxCols = 5;
let tableOptions = [];
for (let r = 1; r <= maxRows; r++) {
for (let c = 1; c <= maxCols; c++) {
tableOptions.push('newtable_' + r + '_' + c);
}
}
Quill.debug('debug');
var quill = new Quill('#editor-container', {
modules: {
toolbar: {
container: [
[{ 'table': tableOptions }], // new table (cursor needs to be out of table)
['table-insert-rows'], // cursor needs to be in the table
['table-insert-columns'], // cursor needs to be in the table
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ 'header': 1 }, { 'header': 2 }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'script': 'sub'}, { 'script': 'super' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
[{ 'direction': 'rtl' }],
[{ 'size': ['small', false, 'large', 'huge'] }],
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
[{ 'color': [] }, { 'background': [] }],
[{ 'align': [] }],
['link', 'image', 'code-block'],
['clean']
],
handlers: {
table: function (value) {
if(value && value.includes('newtable_')) {
let sizes = value.split('_');
let rows = Number.parseInt(sizes[1])
let columns = Number.parseInt(sizes[2])
let table = Parchment.create('table');
const range = this.quill.getSelection()
if (!range) return
const newLineIndex = getClosestNewLineIndex(this.quill.getContents(), range.index + range.length)
let changeDelta = new Delta().retain(newLineIndex)
changeDelta = changeDelta.insert('\n')
for (let i = 0; i < rows; i++) {
for (let j = 0; j < columns; j++) {
changeDelta = changeDelta.insert('\n', {
td: true
})
if (j < columns - 1) {
changeDelta = changeDelta.insert({ tdbr: true })
}
}
changeDelta = changeDelta.insert({ trbr: true })
}
this.quill.updateContents(changeDelta, Quill.sources.USER)
this.quill.setSelection(newLineIndex + 1)
} else {
// TODO
}
},
'table-insert-rows': function() {
let td = find_td('td')
if(td) {
let col_count = 0
td.parent.children.forEach(function (it) {
if (it instanceof TableCell) {
col_count++
}
})
let table = td.parent.parent;
let new_row = td.parent.clone()
for (var i = col_count - 1; i >= 0; i--) {
let td = Parchment.create('td');
new_row.appendChild(td);
new_row.appendChild(Parchment.create('tdbr'))
};
new_row.appendChild(Parchment.create('trbr'))
table.appendChild(new_row);
}
},
'table-insert-columns': function() {
let td = find_td('td')
if(td) {
let table = td.parent.parent;
td.parent.parent.children.forEach(function(tr) {
let td = Parchment.create('td');
tr.appendChild(td);
tr.appendChild(Parchment.create('tdbr'))
});
}
}
}
},
clipboard: {
matchers: [
['TD, TH', function (node, delta) {
delta.insert("\n", { td: true })
delta.insert({ tdbr: true })
return delta
}],
['TR', function (node, delta) {
delta.insert({ trbr: true })
return delta
}],
]
},
keyboard: {
bindings: {
'backspaceTable': {
key: 8,
format: ['td'],
// offset: 0,
handler: function handleTableBackspace (range, context) {
var formats = quill.getFormat(range.index-1, 1)
if (formats.tdbr || formats.trbr) {
// prevent deletion of table break
return false
}
return true
}
}
}
},
},
placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble'
});
// global for console debugging
QuillInstance = quill
quill.on('text-change', function(delta, source) {
document.getElementById("output_delta").value=JSON.stringify(quill.editor.getDelta(), null, 2)
document.getElementById("output_html").value=quill.root.innerHTML;
document.getElementById("view_html").innerHTML=quill.root.innerHTML;
})
// use sample delta
var delta = getSampleDelta()
document.getElementById("orig_delta").value=JSON.stringify(delta, null, 2)
quill.setContents(delta);
function getClosestNewLineIndex (contents, index) {
return index + contents.map((op) => {
return typeof op.insert === 'string' ? op.insert : ' '
}).join('')
.slice(index)
.indexOf('\n')
}
function find_td(what) {
let leaf = quill.getLeaf(quill.getSelection()['index']);
let blot = leaf[0];
for(;blot!=null && blot.statics.blotName!=what;) {
blot=blot.parent;
}
return blot; // return TD or NULL
}
function getSampleDelta () {
return {
"ops": [
{
"insert": "Test Tables"
},
{
"insert": "\n",
"attributes": {
"header": 1
}
},
{
"insert": "Empty 3x3 table from toolbar"
},
{
"insert": "\n",
"attributes": {
"header": 2
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\nPopulated 3x3 table (from toolbar)"
},
{
"insert": "\n",
"attributes": {
"header": 2
}
},
{
"insert": "Col 1"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Col 2"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Col 3"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "a"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": "b"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": "c"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "123"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "456"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "d"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "4"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "7"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\nPasted Table"
},
{
"insert": "\n",
"attributes": {
"header": 2
}
},
{
"insert": "Company"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Contact"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Country"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Alfreds Futterkiste"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Maria Anders"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Germany"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Centro comercial Moctezuma"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Francisco Chang"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Mexico"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Ernst Handel"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Roland Mendel"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Austria"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Island Trading"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Helen Bennett"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "UK"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Laughing Bacchus Winecellars"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Yoshi Tannamuri"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Canada"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Magazzini Alimentari Riuniti"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Giovanni Rovelli"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Italy"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\n"
}
]
}
}
</script>
<script id="jsbin-source-html" type="text/html"><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<link href="//cdn.quilljs.com/1.2.4/quill.snow.css" rel="stylesheet">
<link href="//cdn.quilljs.com/1.2.4/quill.bubble.css" rel="stylesheet">
</head>
<body>
<h1>Quill Table Breaks</h1>
<div id="editor-container">
</div>
Output:
<textarea id='output_delta' style='width:100%; height:200px; padding: 3px;'></textarea>
Orig Delta:
<textarea id='orig_delta' style='width:100%; height:200px; padding: 3px;'></textarea>
Current HTML:
<textarea id='output_html' style='width:100%; height:200px; padding: 3px;'></textarea>
HTML:
<div id="view_html">
</div>
<script src="//cdn.quilljs.com/1.2.4/quill.js"><\/script>
</body>
</html></script>
<script id="jsbin-source-css" type="text/css">
.ql-editor table {
width: 100%;
border-collapse: collapse;
}
.ql-editor table td {
border: 1px solid black;
padding: 5px;
height: 25px;
}
button.ql-table::after { content: "TABLE"; }
.ql-picker.ql-table .ql-picker-label::before { content: "TABLE"; }
button.ql-contain::after { content: "WRAP"; }
button.ql-table-insert-rows::after { content: "ROWS+"; }
button.ql-table-insert-columns::after { content: "COLS+"; }
.ql-table,
.ql-contain {
width: auto !important;
margin-right: -15px;
}
.ql-picker.ql-table {
margin-right: -15px;
font-size: 11px;
font-weight: normal;
}
.ql-picker.ql-table svg {
display: none;
}
.ql-picker.ql-table .ql-picker-label {
padding: 0px 3px;
}
.ql-picker.ql-table .ql-picker-options {
width: 190px;
}
.ql-picker.ql-table .ql-picker-item {
display: block;
float: left;
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
padding: 0px;
margin: 1px;
}
.ql-picker.ql-table .ql-picker-item {
background: lightgrey;
}
.ql-picker-item:nth-child(5):before {
clear: both;
display: block;
content: "";
width: 100%;
}
.ql-picker-item[data-value=newtable_1_1]:before { content: "1x1"; }
.ql-picker-item[data-value=newtable_1_2]:before { content: "1x2"; }
.ql-picker-item[data-value=newtable_1_3]:before { content: "1x3"; }
.ql-picker-item[data-value=newtable_1_4]:before { content: "1x4"; }
.ql-picker-item[data-value=newtable_1_5]:before { content: "1x5"; }
.ql-picker-item[data-value=newtable_2_1]:before { content: "2x1"; }
.ql-picker-item[data-value=newtable_2_2]:before { content: "2x2"; }
.ql-picker-item[data-value=newtable_2_3]:before { content: "2x3"; }
.ql-picker-item[data-value=newtable_2_4]:before { content: "2x4"; }
.ql-picker-item[data-value=newtable_2_5]:before { content: "2x5"; }
.ql-picker-item[data-value=newtable_3_1]:before { content: "3x1"; }
.ql-picker-item[data-value=newtable_3_2]:before { content: "3x2"; }
.ql-picker-item[data-value=newtable_3_3]:before { content: "3x3"; }
.ql-picker-item[data-value=newtable_3_4]:before { content: "3x4"; }
.ql-picker-item[data-value=newtable_3_5]:before { content: "3x5"; }
.ql-picker-item[data-value=newtable_4_1]:before { content: "4x1"; }
.ql-picker-item[data-value=newtable_4_2]:before { content: "4x2"; }
.ql-picker-item[data-value=newtable_4_3]:before { content: "4x3"; }
.ql-picker-item[data-value=newtable_4_4]:before { content: "4x4"; }
.ql-picker-item[data-value=newtable_4_5]:before { content: "4x5"; }
.ql-picker-item[data-value=newtable_5_1]:before { content: "5x1"; }
.ql-picker-item[data-value=newtable_5_2]:before { content: "5x2"; }
.ql-picker-item[data-value=newtable_5_3]:before { content: "5x3"; }
.ql-picker-item[data-value=newtable_5_4]:before { content: "5x4"; }
.ql-picker-item[data-value=newtable_5_5]:before { content: "5x5"; }
.ql-picker-item[data-value=newtable_6_1]:before { content: "6x1"; }
.ql-picker-item[data-value=newtable_6_2]:before { content: "6x2"; }
.ql-picker-item[data-value=newtable_6_3]:before { content: "6x3"; }
.ql-picker-item[data-value=newtable_6_4]:before { content: "6x4"; }
.ql-picker-item[data-value=newtable_6_5]:before { content: "6x5"; }
.ql-picker-item[data-value=newtable_7_1]:before { content: "7x1"; }
.ql-picker-item[data-value=newtable_7_2]:before { content: "7x2"; }
.ql-picker-item[data-value=newtable_7_3]:before { content: "7x3"; }
.ql-picker-item[data-value=newtable_7_4]:before { content: "7x4"; }
.ql-picker-item[data-value=newtable_7_5]:before { content: "7x5"; }
.ql-picker-item[data-value=newtable_8_1]:before { content: "8x1"; }
.ql-picker-item[data-value=newtable_8_2]:before { content: "8x2"; }
.ql-picker-item[data-value=newtable_8_3]:before { content: "8x3"; }
.ql-picker-item[data-value=newtable_8_4]:before { content: "8x4"; }
.ql-picker-item[data-value=newtable_8_5]:before { content: "8x5"; }
.ql-picker-item[data-value=newtable_9_1]:before { content: "9x1"; }
.ql-picker-item[data-value=newtable_9_2]:before { content: "9x2"; }
.ql-picker-item[data-value=newtable_9_3]:before { content: "9x3"; }
.ql-picker-item[data-value=newtable_9_4]:before { content: "9x4"; }
.ql-picker-item[data-value=newtable_9_5]:before { content: "9x5"; }
.ql-picker-item[data-value=newtable_10_1]:before { content: "10x1"; }
.ql-picker-item[data-value=newtable_10_2]:before { content: "10x2"; }
.ql-picker-item[data-value=newtable_10_3]:before { content: "10x3"; }
.ql-picker-item[data-value=newtable_10_4]:before { content: "10x4"; }
.ql-picker-item[data-value=newtable_10_5]:before { content: "10x5"; }
.tdbr, .trbr {
display: none
}</script>
<script id="jsbin-source-javascript" type="text/javascript">// quill-table-breaks.js
let Container = Quill.import('blots/container');
let Scroll = Quill.import('blots/scroll');
let Inline = Quill.import('blots/inline');
let Block = Quill.import('blots/block');
let Delta = Quill.import('delta');
let Parchment = Quill.import('parchment');
let BlockEmbed = Quill.import('blots/block/embed');
let TextBlot = Quill.import('blots/text');
class ContainBlot extends Container {
static create(value) {
let tagName = 'contain';
let node = super.create(tagName);
return node;
}
insertBefore(blot, ref) {
if (blot.statics.blotName == this.statics.blotName) {
console.log('############################ Not sure this is clean:')
console.log(blot)
console.log(blot.children.head)
super.insertBefore(blot.children.head, ref);
} else {
super.insertBefore(blot, ref);
}
}
static formats(domNode) {
return domNode.tagName;
}
formats() {
// We don't inherit from FormatBlot
return { [this.statics.blotName]: this.statics.formats(this.domNode) }
}
replace(target) {
if (target.statics.blotName !== this.statics.blotName) {
let item = Parchment.create(this.statics.defaultChild);
target.moveChildren(item);
this.appendChild(item);
}
if (target.parent == null) return;
super.replace(target)
}
}
ContainBlot.blotName = 'contain';
ContainBlot.tagName = 'contain';
ContainBlot.scope = Parchment.Scope.BLOCK_BLOT;
ContainBlot.defaultChild = 'block';
ContainBlot.allowedChildren = [Block, BlockEmbed, Container];
Quill.register(ContainBlot);
class TableRow extends Container {
static create(value) {
let tagName = 'tr';
let node = super.create(tagName);
return node;
}
optimize() {
super.optimize();
var parent = this.parent
if (parent != null && parent.statics.blotName != 'table') {
this.processTable()
}
}
processTable () {
var currentBlot = this
var rows = []
while (currentBlot) {
if (! (currentBlot instanceof TableRow)) {
break
}
rows.push(currentBlot)
currentBlot = currentBlot.next
}
let mark = Parchment.create('block');
this.parent.insertBefore(mark, this.next);
let table = Parchment.create('table');
rows.forEach(function (row) {
table.appendChild(row)
})
table.replace(mark)
}
}
TableRow.blotName = 'tr';
TableRow.tagName = 'tr';
TableRow.scope = Parchment.Scope.BLOCK_BLOT;
TableRow.defaultChild = 'td';
Quill.register(TableRow);
class Table extends Container {
optimize() {
super.optimize();
let next = this.next;
if (next != null && next.prev === this &&
next.statics.blotName === this.statics.blotName &&
next.domNode.tagName === this.domNode.tagName
) {
next.moveChildren(this);
next.remove();
}
}
}
Table.blotName = 'table';
Table.tagName = 'table';
Table.scope = Parchment.Scope.BLOCK_BLOT;
Table.defaultChild = 'tr';
Table.allowedChildren = [TableRow];
Quill.register(Table);
//
//
// CONTAINER TD
//
class TableCell extends ContainBlot {
format() {
return 'td'
}
optimize() {
super.optimize();
let parent = this.parent;
if (parent != null && parent.statics.blotName != 'tr') {
this.processTR()
}
// merge same TD id
let next = this.next;
if (next != null && next.prev === this &&
next.statics.blotName === this.statics.blotName &&
next.domNode.tagName === this.domNode.tagName
) {
next.moveChildren(this);
next.remove();
}
}
processTR () {
// find next row break
var currentBlot = this
var rowItems = [this]
while (currentBlot) {
if (currentBlot.statics.tagName !== 'TD') {
break
}
rowItems.push(currentBlot)
if (currentBlot instanceof RowBreak) {
break
}
currentBlot = currentBlot.next
}
// create row, add row items as TDs
var prevItem
var cellItems = []
var cells = []
rowItems.forEach(function (rowItem) {
cellItems.push(rowItem)
if (rowItem instanceof TableCell) {
prevItem = rowItem
} else if (rowItem instanceof CellBreak) {
cells.push(cellItems)
cellItems = []
}
})
if (cellItems.length > 0) {
cells.push(cellItems)
}
let mark = Parchment.create('block');
this.parent.insertBefore(mark, this.next);
// create row
var row = Parchment.create('tr')
cells.forEach(function (cell) {
// add row elements
cell.forEach(function (cellItem) {
row.appendChild(cellItem)
})
})
row.replace(mark)
}
}
TableCell.blotName = 'td';
TableCell.tagName = 'td';
TableCell.scope = Parchment.Scope.BLOCK_BLOT;
TableCell.defaultChild = 'block';
TableCell.allowedChildren = [Block, BlockEmbed, Container];
Quill.register(TableCell);
Container.order = [
'list', 'contain', // Must be lower
'td', 'tr', 'table' // Must be higher
];
class RowBreak extends BlockEmbed {
formats() {
return { trbr: true }
}
}
RowBreak.blotName = 'trbr'
RowBreak.tagName = 'td'
RowBreak.className = 'trbr'
Quill.register(RowBreak);
class CellBreak extends BlockEmbed {
formats() {
return { tdbr: true }
}
}
CellBreak.blotName = 'tdbr'
CellBreak.tagName = 'td'
CellBreak.className = 'tdbr'
Quill.register(CellBreak);
// END quill-table-breaks.js
// Render UI
var Keyboard = Quill.import('modules/keyboard')
// set up toolbar options
let maxRows = 10;
let maxCols = 5;
let tableOptions = [];
for (let r = 1; r <= maxRows; r++) {
for (let c = 1; c <= maxCols; c++) {
tableOptions.push('newtable_' + r + '_' + c);
}
}
Quill.debug('debug');
var quill = new Quill('#editor-container', {
modules: {
toolbar: {
container: [
[{ 'table': tableOptions }], // new table (cursor needs to be out of table)
['table-insert-rows'], // cursor needs to be in the table
['table-insert-columns'], // cursor needs to be in the table
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ 'header': 1 }, { 'header': 2 }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'script': 'sub'}, { 'script': 'super' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
[{ 'direction': 'rtl' }],
[{ 'size': ['small', false, 'large', 'huge'] }],
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
[{ 'color': [] }, { 'background': [] }],
[{ 'align': [] }],
['link', 'image', 'code-block'],
['clean']
],
handlers: {
table: function (value) {
if(value && value.includes('newtable_')) {
let sizes = value.split('_');
let rows = Number.parseInt(sizes[1])
let columns = Number.parseInt(sizes[2])
let table = Parchment.create('table');
const range = this.quill.getSelection()
if (!range) return
const newLineIndex = getClosestNewLineIndex(this.quill.getContents(), range.index + range.length)
let changeDelta = new Delta().retain(newLineIndex)
changeDelta = changeDelta.insert('\n')
for (let i = 0; i < rows; i++) {
for (let j = 0; j < columns; j++) {
changeDelta = changeDelta.insert('\n', {
td: true
})
if (j < columns - 1) {
changeDelta = changeDelta.insert({ tdbr: true })
}
}
changeDelta = changeDelta.insert({ trbr: true })
}
this.quill.updateContents(changeDelta, Quill.sources.USER)
this.quill.setSelection(newLineIndex + 1)
} else {
// TODO
}
},
'table-insert-rows': function() {
let td = find_td('td')
if(td) {
let col_count = 0
td.parent.children.forEach(function (it) {
if (it instanceof TableCell) {
col_count++
}
})
let table = td.parent.parent;
let new_row = td.parent.clone()
for (var i = col_count - 1; i >= 0; i--) {
let td = Parchment.create('td');
new_row.appendChild(td);
new_row.appendChild(Parchment.create('tdbr'))
};
new_row.appendChild(Parchment.create('trbr'))
table.appendChild(new_row);
}
},
'table-insert-columns': function() {
let td = find_td('td')
if(td) {
let table = td.parent.parent;
td.parent.parent.children.forEach(function(tr) {
let td = Parchment.create('td');
tr.appendChild(td);
tr.appendChild(Parchment.create('tdbr'))
});
}
}
}
},
clipboard: {
matchers: [
['TD, TH', function (node, delta) {
delta.insert("\n", { td: true })
delta.insert({ tdbr: true })
return delta
}],
['TR', function (node, delta) {
delta.insert({ trbr: true })
return delta
}],
]
},
keyboard: {
bindings: {
'backspaceTable': {
key: 8,
format: ['td'],
// offset: 0,
handler: function handleTableBackspace (range, context) {
var formats = quill.getFormat(range.index-1, 1)
if (formats.tdbr || formats.trbr) {
// prevent deletion of table break
return false
}
return true
}
}
}
},
},
placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble'
});
// global for console debugging
QuillInstance = quill
quill.on('text-change', function(delta, source) {
document.getElementById("output_delta").value=JSON.stringify(quill.editor.getDelta(), null, 2)
document.getElementById("output_html").value=quill.root.innerHTML;
document.getElementById("view_html").innerHTML=quill.root.innerHTML;
})
// use sample delta
var delta = getSampleDelta()
document.getElementById("orig_delta").value=JSON.stringify(delta, null, 2)
quill.setContents(delta);
function getClosestNewLineIndex (contents, index) {
return index + contents.map((op) => {
return typeof op.insert === 'string' ? op.insert : ' '
}).join('')
.slice(index)
.indexOf('\n')
}
function find_td(what) {
let leaf = quill.getLeaf(quill.getSelection()['index']);
let blot = leaf[0];
for(;blot!=null && blot.statics.blotName!=what;) {
blot=blot.parent;
}
return blot; // return TD or NULL
}
function getSampleDelta () {
return {
"ops": [
{
"insert": "Test Tables"
},
{
"insert": "\n",
"attributes": {
"header": 1
}
},
{
"insert": "Empty 3x3 table from toolbar"
},
{
"insert": "\n",
"attributes": {
"header": 2
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\nPopulated 3x3 table (from toolbar)"
},
{
"insert": "\n",
"attributes": {
"header": 2
}
},
{
"insert": "Col 1"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Col 2"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Col 3"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "a"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": "b"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": "c"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "123"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "456"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "d"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "4"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "7"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\nPasted Table"
},
{
"insert": "\n",
"attributes": {
"header": 2
}
},
{
"insert": "Company"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Contact"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Country"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Alfreds Futterkiste"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Maria Anders"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Germany"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Centro comercial Moctezuma"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Francisco Chang"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Mexico"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Ernst Handel"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Roland Mendel"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Austria"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Island Trading"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Helen Bennett"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "UK"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Laughing Bacchus Winecellars"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Yoshi Tannamuri"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Canada"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Magazzini Alimentari Riuniti"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Giovanni Rovelli"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Italy"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\n"
}
]
}
}
</script></body>
</html>
.ql-editor table {
width: 100%;
border-collapse: collapse;
}
.ql-editor table td {
border: 1px solid black;
padding: 5px;
height: 25px;
}
button.ql-table::after { content: "TABLE"; }
.ql-picker.ql-table .ql-picker-label::before { content: "TABLE"; }
button.ql-contain::after { content: "WRAP"; }
button.ql-table-insert-rows::after { content: "ROWS+"; }
button.ql-table-insert-columns::after { content: "COLS+"; }
.ql-table,
.ql-contain {
width: auto !important;
margin-right: -15px;
}
.ql-picker.ql-table {
margin-right: -15px;
font-size: 11px;
font-weight: normal;
}
.ql-picker.ql-table svg {
display: none;
}
.ql-picker.ql-table .ql-picker-label {
padding: 0px 3px;
}
.ql-picker.ql-table .ql-picker-options {
width: 190px;
}
.ql-picker.ql-table .ql-picker-item {
display: block;
float: left;
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
padding: 0px;
margin: 1px;
}
.ql-picker.ql-table .ql-picker-item {
background: lightgrey;
}
.ql-picker-item:nth-child(5):before {
clear: both;
display: block;
content: "";
width: 100%;
}
.ql-picker-item[data-value=newtable_1_1]:before { content: "1x1"; }
.ql-picker-item[data-value=newtable_1_2]:before { content: "1x2"; }
.ql-picker-item[data-value=newtable_1_3]:before { content: "1x3"; }
.ql-picker-item[data-value=newtable_1_4]:before { content: "1x4"; }
.ql-picker-item[data-value=newtable_1_5]:before { content: "1x5"; }
.ql-picker-item[data-value=newtable_2_1]:before { content: "2x1"; }
.ql-picker-item[data-value=newtable_2_2]:before { content: "2x2"; }
.ql-picker-item[data-value=newtable_2_3]:before { content: "2x3"; }
.ql-picker-item[data-value=newtable_2_4]:before { content: "2x4"; }
.ql-picker-item[data-value=newtable_2_5]:before { content: "2x5"; }
.ql-picker-item[data-value=newtable_3_1]:before { content: "3x1"; }
.ql-picker-item[data-value=newtable_3_2]:before { content: "3x2"; }
.ql-picker-item[data-value=newtable_3_3]:before { content: "3x3"; }
.ql-picker-item[data-value=newtable_3_4]:before { content: "3x4"; }
.ql-picker-item[data-value=newtable_3_5]:before { content: "3x5"; }
.ql-picker-item[data-value=newtable_4_1]:before { content: "4x1"; }
.ql-picker-item[data-value=newtable_4_2]:before { content: "4x2"; }
.ql-picker-item[data-value=newtable_4_3]:before { content: "4x3"; }
.ql-picker-item[data-value=newtable_4_4]:before { content: "4x4"; }
.ql-picker-item[data-value=newtable_4_5]:before { content: "4x5"; }
.ql-picker-item[data-value=newtable_5_1]:before { content: "5x1"; }
.ql-picker-item[data-value=newtable_5_2]:before { content: "5x2"; }
.ql-picker-item[data-value=newtable_5_3]:before { content: "5x3"; }
.ql-picker-item[data-value=newtable_5_4]:before { content: "5x4"; }
.ql-picker-item[data-value=newtable_5_5]:before { content: "5x5"; }
.ql-picker-item[data-value=newtable_6_1]:before { content: "6x1"; }
.ql-picker-item[data-value=newtable_6_2]:before { content: "6x2"; }
.ql-picker-item[data-value=newtable_6_3]:before { content: "6x3"; }
.ql-picker-item[data-value=newtable_6_4]:before { content: "6x4"; }
.ql-picker-item[data-value=newtable_6_5]:before { content: "6x5"; }
.ql-picker-item[data-value=newtable_7_1]:before { content: "7x1"; }
.ql-picker-item[data-value=newtable_7_2]:before { content: "7x2"; }
.ql-picker-item[data-value=newtable_7_3]:before { content: "7x3"; }
.ql-picker-item[data-value=newtable_7_4]:before { content: "7x4"; }
.ql-picker-item[data-value=newtable_7_5]:before { content: "7x5"; }
.ql-picker-item[data-value=newtable_8_1]:before { content: "8x1"; }
.ql-picker-item[data-value=newtable_8_2]:before { content: "8x2"; }
.ql-picker-item[data-value=newtable_8_3]:before { content: "8x3"; }
.ql-picker-item[data-value=newtable_8_4]:before { content: "8x4"; }
.ql-picker-item[data-value=newtable_8_5]:before { content: "8x5"; }
.ql-picker-item[data-value=newtable_9_1]:before { content: "9x1"; }
.ql-picker-item[data-value=newtable_9_2]:before { content: "9x2"; }
.ql-picker-item[data-value=newtable_9_3]:before { content: "9x3"; }
.ql-picker-item[data-value=newtable_9_4]:before { content: "9x4"; }
.ql-picker-item[data-value=newtable_9_5]:before { content: "9x5"; }
.ql-picker-item[data-value=newtable_10_1]:before { content: "10x1"; }
.ql-picker-item[data-value=newtable_10_2]:before { content: "10x2"; }
.ql-picker-item[data-value=newtable_10_3]:before { content: "10x3"; }
.ql-picker-item[data-value=newtable_10_4]:before { content: "10x4"; }
.ql-picker-item[data-value=newtable_10_5]:before { content: "10x5"; }
.tdbr, .trbr {
display: none
}
// quill-table-breaks.js
let Container = Quill.import('blots/container');
let Scroll = Quill.import('blots/scroll');
let Inline = Quill.import('blots/inline');
let Block = Quill.import('blots/block');
let Delta = Quill.import('delta');
let Parchment = Quill.import('parchment');
let BlockEmbed = Quill.import('blots/block/embed');
let TextBlot = Quill.import('blots/text');
class ContainBlot extends Container {
static create(value) {
let tagName = 'contain';
let node = super.create(tagName);
return node;
}
insertBefore(blot, ref) {
if (blot.statics.blotName == this.statics.blotName) {
console.log('############################ Not sure this is clean:')
console.log(blot)
console.log(blot.children.head)
super.insertBefore(blot.children.head, ref);
} else {
super.insertBefore(blot, ref);
}
}
static formats(domNode) {
return domNode.tagName;
}
formats() {
// We don't inherit from FormatBlot
return { [this.statics.blotName]: this.statics.formats(this.domNode) }
}
replace(target) {
if (target.statics.blotName !== this.statics.blotName) {
let item = Parchment.create(this.statics.defaultChild);
target.moveChildren(item);
this.appendChild(item);
}
if (target.parent == null) return;
super.replace(target)
}
}
ContainBlot.blotName = 'contain';
ContainBlot.tagName = 'contain';
ContainBlot.scope = Parchment.Scope.BLOCK_BLOT;
ContainBlot.defaultChild = 'block';
ContainBlot.allowedChildren = [Block, BlockEmbed, Container];
Quill.register(ContainBlot);
class TableRow extends Container {
static create(value) {
let tagName = 'tr';
let node = super.create(tagName);
return node;
}
optimize() {
super.optimize();
var parent = this.parent
if (parent != null && parent.statics.blotName != 'table') {
this.processTable()
}
}
processTable () {
var currentBlot = this
var rows = []
while (currentBlot) {
if (! (currentBlot instanceof TableRow)) {
break
}
rows.push(currentBlot)
currentBlot = currentBlot.next
}
let mark = Parchment.create('block');
this.parent.insertBefore(mark, this.next);
let table = Parchment.create('table');
rows.forEach(function (row) {
table.appendChild(row)
})
table.replace(mark)
}
}
TableRow.blotName = 'tr';
TableRow.tagName = 'tr';
TableRow.scope = Parchment.Scope.BLOCK_BLOT;
TableRow.defaultChild = 'td';
Quill.register(TableRow);
class Table extends Container {
optimize() {
super.optimize();
let next = this.next;
if (next != null && next.prev === this &&
next.statics.blotName === this.statics.blotName &&
next.domNode.tagName === this.domNode.tagName
) {
next.moveChildren(this);
next.remove();
}
}
}
Table.blotName = 'table';
Table.tagName = 'table';
Table.scope = Parchment.Scope.BLOCK_BLOT;
Table.defaultChild = 'tr';
Table.allowedChildren = [TableRow];
Quill.register(Table);
//
//
// CONTAINER TD
//
class TableCell extends ContainBlot {
format() {
return 'td'
}
optimize() {
super.optimize();
let parent = this.parent;
if (parent != null && parent.statics.blotName != 'tr') {
this.processTR()
}
// merge same TD id
let next = this.next;
if (next != null && next.prev === this &&
next.statics.blotName === this.statics.blotName &&
next.domNode.tagName === this.domNode.tagName
) {
next.moveChildren(this);
next.remove();
}
}
processTR () {
// find next row break
var currentBlot = this
var rowItems = [this]
while (currentBlot) {
if (currentBlot.statics.tagName !== 'TD') {
break
}
rowItems.push(currentBlot)
if (currentBlot instanceof RowBreak) {
break
}
currentBlot = currentBlot.next
}
// create row, add row items as TDs
var prevItem
var cellItems = []
var cells = []
rowItems.forEach(function (rowItem) {
cellItems.push(rowItem)
if (rowItem instanceof TableCell) {
prevItem = rowItem
} else if (rowItem instanceof CellBreak) {
cells.push(cellItems)
cellItems = []
}
})
if (cellItems.length > 0) {
cells.push(cellItems)
}
let mark = Parchment.create('block');
this.parent.insertBefore(mark, this.next);
// create row
var row = Parchment.create('tr')
cells.forEach(function (cell) {
// add row elements
cell.forEach(function (cellItem) {
row.appendChild(cellItem)
})
})
row.replace(mark)
}
}
TableCell.blotName = 'td';
TableCell.tagName = 'td';
TableCell.scope = Parchment.Scope.BLOCK_BLOT;
TableCell.defaultChild = 'block';
TableCell.allowedChildren = [Block, BlockEmbed, Container];
Quill.register(TableCell);
Container.order = [
'list', 'contain', // Must be lower
'td', 'tr', 'table' // Must be higher
];
class RowBreak extends BlockEmbed {
formats() {
return { trbr: true }
}
}
RowBreak.blotName = 'trbr'
RowBreak.tagName = 'td'
RowBreak.className = 'trbr'
Quill.register(RowBreak);
class CellBreak extends BlockEmbed {
formats() {
return { tdbr: true }
}
}
CellBreak.blotName = 'tdbr'
CellBreak.tagName = 'td'
CellBreak.className = 'tdbr'
Quill.register(CellBreak);
// END quill-table-breaks.js
// Render UI
var Keyboard = Quill.import('modules/keyboard')
// set up toolbar options
let maxRows = 10;
let maxCols = 5;
let tableOptions = [];
for (let r = 1; r <= maxRows; r++) {
for (let c = 1; c <= maxCols; c++) {
tableOptions.push('newtable_' + r + '_' + c);
}
}
Quill.debug('debug');
var quill = new Quill('#editor-container', {
modules: {
toolbar: {
container: [
[{ 'table': tableOptions }], // new table (cursor needs to be out of table)
['table-insert-rows'], // cursor needs to be in the table
['table-insert-columns'], // cursor needs to be in the table
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ 'header': 1 }, { 'header': 2 }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'script': 'sub'}, { 'script': 'super' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
[{ 'direction': 'rtl' }],
[{ 'size': ['small', false, 'large', 'huge'] }],
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
[{ 'color': [] }, { 'background': [] }],
[{ 'align': [] }],
['link', 'image', 'code-block'],
['clean']
],
handlers: {
table: function (value) {
if(value && value.includes('newtable_')) {
let sizes = value.split('_');
let rows = Number.parseInt(sizes[1])
let columns = Number.parseInt(sizes[2])
let table = Parchment.create('table');
const range = this.quill.getSelection()
if (!range) return
const newLineIndex = getClosestNewLineIndex(this.quill.getContents(), range.index + range.length)
let changeDelta = new Delta().retain(newLineIndex)
changeDelta = changeDelta.insert('\n')
for (let i = 0; i < rows; i++) {
for (let j = 0; j < columns; j++) {
changeDelta = changeDelta.insert('\n', {
td: true
})
if (j < columns - 1) {
changeDelta = changeDelta.insert({ tdbr: true })
}
}
changeDelta = changeDelta.insert({ trbr: true })
}
this.quill.updateContents(changeDelta, Quill.sources.USER)
this.quill.setSelection(newLineIndex + 1)
} else {
// TODO
}
},
'table-insert-rows': function() {
let td = find_td('td')
if(td) {
let col_count = 0
td.parent.children.forEach(function (it) {
if (it instanceof TableCell) {
col_count++
}
})
let table = td.parent.parent;
let new_row = td.parent.clone()
for (var i = col_count - 1; i >= 0; i--) {
let td = Parchment.create('td');
new_row.appendChild(td);
new_row.appendChild(Parchment.create('tdbr'))
};
new_row.appendChild(Parchment.create('trbr'))
table.appendChild(new_row);
}
},
'table-insert-columns': function() {
let td = find_td('td')
if(td) {
let table = td.parent.parent;
td.parent.parent.children.forEach(function(tr) {
let td = Parchment.create('td');
tr.appendChild(td);
tr.appendChild(Parchment.create('tdbr'))
});
}
}
}
},
clipboard: {
matchers: [
['TD, TH', function (node, delta) {
delta.insert("\n", { td: true })
delta.insert({ tdbr: true })
return delta
}],
['TR', function (node, delta) {
delta.insert({ trbr: true })
return delta
}],
]
},
keyboard: {
bindings: {
'backspaceTable': {
key: 8,
format: ['td'],
// offset: 0,
handler: function handleTableBackspace (range, context) {
var formats = quill.getFormat(range.index-1, 1)
if (formats.tdbr || formats.trbr) {
// prevent deletion of table break
return false
}
return true
}
}
}
},
},
placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble'
});
// global for console debugging
QuillInstance = quill
quill.on('text-change', function(delta, source) {
document.getElementById("output_delta").value=JSON.stringify(quill.editor.getDelta(), null, 2)
document.getElementById("output_html").value=quill.root.innerHTML;
document.getElementById("view_html").innerHTML=quill.root.innerHTML;
})
// use sample delta
var delta = getSampleDelta()
document.getElementById("orig_delta").value=JSON.stringify(delta, null, 2)
quill.setContents(delta);
function getClosestNewLineIndex (contents, index) {
return index + contents.map((op) => {
return typeof op.insert === 'string' ? op.insert : ' '
}).join('')
.slice(index)
.indexOf('\n')
}
function find_td(what) {
let leaf = quill.getLeaf(quill.getSelection()['index']);
let blot = leaf[0];
for(;blot!=null && blot.statics.blotName!=what;) {
blot=blot.parent;
}
return blot; // return TD or NULL
}
function getSampleDelta () {
return {
"ops": [
{
"insert": "Test Tables"
},
{
"insert": "\n",
"attributes": {
"header": 1
}
},
{
"insert": "Empty 3x3 table from toolbar"
},
{
"insert": "\n",
"attributes": {
"header": 2
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\nPopulated 3x3 table (from toolbar)"
},
{
"insert": "\n",
"attributes": {
"header": 2
}
},
{
"insert": "Col 1"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Col 2"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Col 3"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "a"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": "b"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": "c"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "123"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "456"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "d"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "4"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "7"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\nPasted Table"
},
{
"insert": "\n",
"attributes": {
"header": 2
}
},
{
"insert": "Company"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Contact"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Country"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Alfreds Futterkiste"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Maria Anders"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Germany"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Centro comercial Moctezuma"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Francisco Chang"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Mexico"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Ernst Handel"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Roland Mendel"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Austria"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Island Trading"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Helen Bennett"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "UK"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Laughing Bacchus Winecellars"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Yoshi Tannamuri"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Canada"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "Magazzini Alimentari Riuniti"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Giovanni Rovelli"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": "Italy"
},
{
"insert": "\n",
"attributes": {
"td": "TD"
}
},
{
"insert": {
"tdbr": true
},
"attributes": {
"tdbr": true
}
},
{
"insert": {
"trbr": true
},
"attributes": {
"trbr": true
}
},
{
"insert": "\n"
}
]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment