Commit 5bf84c66 authored by chris's avatar chris

add basic table support

parent 07b19071
......@@ -2,7 +2,6 @@ import {
BasePackage,
ProseArticle,
Document as SubstanceDocument,
TablePackage,
} from 'substance'
//setup
......@@ -48,11 +47,9 @@ import SpellCheckTogglePackage from './elements/spellcheck_toggle/SpellCheckTogg
import ChangeCasePackage from './elements/change_case/ChangeCasePackage'
import QuoteMarksPackage from './elements/quote_mark/QuoteMarksPackage'
import ListPackage from './elements/list/ListPackage'
import TablePackage from './elements/table/TablePackage'
// import InlineNotePackage from './elements/inline_note/InlineNotePackage'
//TODO Need to recreate them?
// import ChapterNumber from './elements/chapterNumber/ChapterNumberPackage'
const config = {
name: 'simple-editor',
configure: (config, options) => {
......
......@@ -15,3 +15,4 @@
@import './notification/notification';
@import './change_case/changecase';
@import './table/table';
import { uuid, InsertNodeCommand } from 'substance'
class InsertTableCommand extends InsertNodeCommand {
createNodeData(tx) {
// row-1
let a1 = tx.create({
id: uuid('table-cell'),
type: 'table-cell',
content: 'A1',
})
let b1 = tx.create({
id: uuid('table-cell'),
type: 'table-cell',
content: 'B1',
})
// row-2
let a2 = tx.create({
id: uuid('table-cell'),
type: 'table-cell',
content: 'A2',
})
let b2 = tx.create({
id: uuid('table-cell'),
type: 'table-cell',
content: 'B2',
})
return {
id: uuid('table'),
type: 'table',
// null values mark merged cells
cells: [[a1.id, b1.id], [a2.id, b2.id]],
}
}
}
export default InsertTableCommand
import { BlockNode } from 'substance'
export default class Table extends BlockNode {
getRowCount() {
return this.cells.length
}
getColCount() {
if (this.cells.length > 0) {
return this.cells[0].length
} else {
return 0
}
}
getCellAt(row, col) {
let cellId = this.cells[row][col]
if (cellId) {
return this.document.get(cellId)
}
}
}
Table.schema = {
type: 'table',
cells: { type: ['array', 'array', 'id'], default: [], owned: true },
}
import { TextNode } from 'substance'
export default class TableCell extends TextNode {}
TableCell.schema = {
type: 'table-cell',
rowspan: { type: 'number', default: 0 },
colspan: { type: 'number', default: 0 },
}
import { Component, TextPropertyEditor } from 'substance'
export default class TableCellComponent extends Component {
render($$) {
let node = this.props.node
let el = $$('td').addClass('sc-table-cell')
el.append(
$$(TextPropertyEditor, {
path: node.getPath(),
disabled: this.props.disabled,
}).ref('editor'),
)
if (node.rowspan > 0) {
el.attr('rowspan', node.rowspan)
}
if (node.colspan > 0) {
el.attr('colspan', node.colspan)
}
return el
}
grabFocus() {
let node = this.props.node
this.context.editorSession.setSelection({
type: 'property',
path: node.getPath(),
startOffset: node.getLength(),
surfaceId: this.refs.editor.id,
})
}
}
TableCellComponent.prototype._isTableCellComponent = true
export default {
type: 'table-cell',
tagName: 'td',
import: function(el, node, converter) {
node.content = converter.annotatedText(el, [node.id, 'content'])
let colspan = el.attr('colspan')
let rowspan = el.attr('rowspan')
if (colspan) {
node.colspan = Number(colspan)
}
if (rowspan) {
node.rowspan = Number(rowspan)
}
},
export: function(node, el, converter) {
el.append(converter.annotatedText([node.id, 'content']))
if (node.rowspan > 0) {
el.attr('rowspan', node.rowspan)
}
if (node.colspan > 0) {
el.attr('colspan', node.colspan)
}
return el
},
}
import { Component } from 'substance'
import TableCellComponent from './TableCellComponent'
class TableComponent extends Component {
render($$) {
let el = $$('table').addClass('sc-table')
let node = this.props.node
let doc = this.props.node.getDocument()
let cells = this.props.node.cells
let rowCount = node.getRowCount()
let colCount = node.getColCount()
for (let i = 0; i < rowCount; i++) {
let rowEl = $$('tr')
for (let j = 0; j < colCount; j++) {
let cellId = cells[i][j]
// Merged cells (cellId is null) are skipped
if (cellId) {
let cellNode = doc.get(cellId)
let cellEl = $$(TableCellComponent, {
node: cellNode,
disabled: this.props.disabled,
}).ref(cellNode.id)
rowEl.append(cellEl)
}
}
el.append(rowEl)
}
el.on('click', this.onClick)
el.on('dblclick', this.onDblClick)
return el
}
onClick(event) {
event.stopPropagation()
// console.log('Clicked on Table', this.props.node.id, event.target)
}
// TODO: this should only be used for the initial table state
onDblClick(event) {
event.stopPropagation()
// console.log('DblClicked on Table', this.props.node.id, event.target)
// HACK: assuming that if the event.target has a surface
// it is a TextPropertyEditor of a cell
let comp = Component.unwrap(event.target)
if (comp) {
let cellComp
if (comp._isTableCellComponent) {
cellComp = comp
} else if (comp._isTextPropertyEditor) {
cellComp = comp.getParent()
} else if (comp._isTextPropertyComponent) {
cellComp = comp.getParent().getParent()
} else {
console.warn('TODO: find the right cell')
}
if (cellComp) {
cellComp.grabFocus()
}
}
}
grabFocus() {
let cellId = this.props.node.cells[0][0]
if (cellId) {
let comp = this.refs[cellId]
comp.grabFocus()
}
}
}
TableComponent.hasDropzones = true
export default TableComponent
import { times } from 'lodash'
export default {
type: 'table',
tagName: 'table',
/*
WARNING: This includes a pretty naive implementation for considering
rowspans and colspans.
TODO: Create test suite for this converter
*/
import: function(el, node, converter) {
let trs = el.find('tbody').getChildren()
let colCount = 0
let cells = []
let rowspans = [] // we remember active rowspans here
for (let i = 0; i < trs.length; i++) {
let tds = trs[i].getChildren()
let row = []
colCount = Math.max(tds.length, colCount)
for (let j = 0; j < tds.length; j++) {
let td = tds[j]
// if there is an open rowspan
if (rowspans[j] > 1) {
row.push(null)
rowspans[j] -= 1 // count down until exhausted
}
let tableCell = converter.convertElement(td)
row.push(tableCell.id)
if (tableCell.rowspan > 1) {
rowspans[j] = tableCell.rowspan
}
if (tableCell.colspan > 1) {
// Add null values for colspans
times(tableCell.colspan - 1, () => {
row.push(null)
})
}
}
cells.push(row)
}
node.cells = cells
},
export: function(node, el, converter) {
let $$ = converter.$$
let rowCount = node.getRowCount()
let colCount = node.getColCount()
for (let i = 0; i < rowCount; i++) {
let rowEl = $$('tr')
for (let j = 0; j < colCount; j++) {
let cellId = node.cells[i][j]
// Merged cells (cellId is null) are skipped
if (cellId) {
let cellEl = converter.convertNode(cellId)
rowEl.append(cellEl)
}
}
el.append(rowEl)
}
return el
},
}
import Table from './Table'
import TableCell from './TableCell'
import TableComponent from './TableComponent'
import TableHTMLConverter from './TableHTMLConverter'
import TableCellHTMLConverter from './TableCellHTMLConverter'
import InsertTableCommand from './InsertTableCommand'
export default {
name: 'table',
configure: function(config) {
config.addNode(Table)
config.addNode(TableCell)
config.addComponent('table', TableComponent)
config.addConverter('html', TableHTMLConverter)
config.addConverter('html', TableCellHTMLConverter)
config.addConverter('xml', TableHTMLConverter)
config.addConverter('xml', TableCellHTMLConverter)
config.addCommand('insert-table', InsertTableCommand, {
nodeType: 'table',
commandGroup: 'insert',
})
config.addIcon('insert-table', { fontawesome: 'fa-table' })
config.addLabel('insert-table', {
en: 'Table',
})
config.addLabel('table', {
en: 'Table',
})
config.addLabel('table-cell.content', {
en: 'Cell',
})
},
Table,
TableCell,
}
.sc-table {
display: table;
width: 100%;
}
.sc-table .sc-table-cell {
border: 1px solid #ddd;
padding: 5px;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment