Skip to content

Instantly share code, notes, and snippets.

@ccashwell
Last active November 2, 2017 22:35
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ccashwell/e2ceac25d7195e4715a93587da89d747 to your computer and use it in GitHub Desktop.
Save ccashwell/e2ceac25d7195e4715a93587da89d747 to your computer and use it in GitHub Desktop.
Building Proof-of-Concept Table Support for Quill.js using React Components
import Quill from 'quill'
import TableComponent from 'TableComponent'
import ReactDOM from 'react-dom'
const QuillEmbed = Quill.import('blots/block/embed');
class QuillTable extends QuillEmbed {
static create(values) {
let node = super.create(values);
if (!Array.isArray(values)) {
try {
values = JSON.parse(values);
} catch (e) {
alert('Error: Unable to interpret supplied content');
return node;
}
}
ReactDOM.render(<TableComponent data={values} />, node);
return node;
}
static value(node) {
let output = [];
let tableNode = node.firstChild.firstChild;
if(tableNode.tagName === 'TABLE') {
let rows = tableNode.rows;
for (let i = 0; i < rows.length; i++) {
output[i] = [];
for (let j = 0; j < rows[i].cells.length; j++) {
let cell = rows[i].cells[j];
let cellAttributes = Object.assign({}, TableComponent.cellOptions);
cellAttributes.colSpan = cell.colSpan;
output[i].push({
options: cellAttributes,
value: cell.innerHTML
});
}
}
}
return output;
}
}
QuillTable.blotName = 'table';
QuillTable.tagName = 'div';
export { QuillTable as default };
import React from 'react'
import { randomHex } from 'lib/util' // this just returns a random 6-char hex string
export default class TableComponent extends React.Component {
constructor(props) {
super(props);
let { columnHeaders, rows } = this.preprocess(props.data);
this.state = { columnHeaders, rows }
}
static cellOptions = {
colSpan: 1,
style: { border: '1px dotted red', padding: '10px' }
}
render() {
let output = (
<div className='financial-table'>
{ this.buildTable() }
<button onClick={this.onAddRow.bind(this)}>(+) Add Row</button>
<button onClick={this.onAddColumn.bind(this)}>(+) Add Column</button>
</div>
)
return(output)
}
buildTable() {
return(
<table contentEditable={false} style={{ border: '1px solid #ccc', padding: '10px', width: '100%' }}>
{ this.buildTableHeader(this.state.columnHeaders) }
{ this.buildTableBody(this.state.rows) }
</table>
)
}
buildTableHeader(headerColumns) {
return <thead>{this.buildTableRow(headerColumns, 'th')}</thead>;
}
buildTableRow(rowData, CellType = 'td') {
let columns = [];
for (let cell of rowData) {
let cellOptions = Object.assign({}, TableComponent.cellOptions, cell.options);
columns.push(
<CellType
key={randomHex()}
dangerouslySetInnerHTML={{__html: window.unescape(cell.value)}}
onClick={this.onEditCell.bind(this)} {...cellOptions} />
)
}
return <tr key={randomHex()}>{columns}</tr>;
}
buildTableBody(tableRows) {
return <tbody>{tableRows.map((row) => this.buildTableRow(row))}</tbody>
}
onAddRow() {
let value = prompt('What default value should it have?', 'Default Value');
this.setState({
rows: this.state.rows.concat([
new Array(this.state.columnHeaders.length).fill({ value })
])
})
}
onAddColumn() {
let column = prompt('What is the name of the new column?', 'New Column');
let value = prompt('What default value should it have?', 'Default Value');
this.setState({
columnHeaders: this.state.columnHeaders.concat([{ value: column }]),
rows: this.state.rows.map((row) => row.concat([{ value }]))
})
}
onEditCell(e) {
if (e.target) e.target.innerHTML = prompt('Enter new value', e.target.innerHTML);
}
onUpdate() {
if (typeof this.props.onUpdate == 'function') this.props.onUpdate(this);
}
preprocess(data = []) {
let columnHeaders = data[0] || [];
let rows = data.slice(-(data.length - 1)) || [];
let applyProperties = (value) => {
let options = TableComponent.cellOptions;
let cell = { options: {} };
if (typeof value === 'object' && value.hasOwnProperty('value')) {
Object.assign(cell.options, TableComponent.cellOptions, value.options);
cell.value = value.value;
} else {
cell.value = value;
}
return cell;
}
return {
columnHeaders: columnHeaders.map(applyProperties),
rows: rows.map((row) => row.map(applyProperties))
}
}
}
@ccashwell
Copy link
Author

This is just a quick example of how to implement custom Embed elements using React components that'll work more or less as expected in Quill.js. In case it's unclear how the table structure is generated from JSON, here's some sample input:

[
  ["Column A", "Column B", "Column C"],
  ["value 1", { "options": { "style": "color: red" }, "value": "value 2" }, "value 3"],
  ["value 4", { "options": { "colSpan": 2, "style": "font-weight: bold" }, "value": "value 5" }]
]

@LeeWilson
Copy link

Hey,

I was trying to test your proof of concept, how would you actually implement this?

Cheers,

Lee

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment