Skip to content

Commit

Permalink
WIP simple table support
Browse files Browse the repository at this point in the history
  • Loading branch information
carlobeltrame committed Mar 26, 2024
1 parent f4941a0 commit 3986d64
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 16 deletions.
4 changes: 3 additions & 1 deletion api/config/packages/exercise_html_purifier.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ exercise_html_purifier:
# to know how to whitelist elements

# # whitelist attributes by tag
# attributes: []
attributes:
td:
colwidth: Length

# # whitelist elements by name
# elements: []
Expand Down
53 changes: 53 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
"@tiptap/extension-paragraph": "2.2.4",
"@tiptap/extension-placeholder": "2.2.4",
"@tiptap/extension-strike": "2.2.4",
"@tiptap/extension-table": "^2.2.4",
"@tiptap/extension-table-cell": "^2.2.4",
"@tiptap/extension-table-header": "^2.2.4",
"@tiptap/extension-table-row": "^2.2.4",
"@tiptap/extension-text": "2.2.4",
"@tiptap/extension-underline": "2.2.4",
"@tiptap/pm": "2.2.4",
Expand Down
133 changes: 123 additions & 10 deletions frontend/src/components/form/tiptap/TiptapEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,61 @@
@click="editor.chain().focus().sinkListItem('listItem').run()"
/>
</template>

<v-divider vertical class="mx-1" />

<TiptapToolbarButton
icon="mdi-table-large-plus"
:disabled="editor.can().addColumnBefore()"
@click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()"

Check failure on line 73 in frontend/src/components/form/tiptap/TiptapEditor.vue

View workflow job for this annotation

GitHub Actions / Lint: Frontend (ESLint)

Replace `editor.chain().focus().insertTable({·rows:·3,·cols:·3,·withHeaderRow:·true·}).run()` with `⏎················editor⏎··················.chain()⏎··················.focus()⏎··················.insertTable({·rows:·3,·cols:·3,·withHeaderRow:·true·})⏎··················.run()⏎··············`
/>
</div>
</v-toolbar>
<v-divider v-if="$vuetify.breakpoint.smAndUp" />
<v-toolbar
v-if="shouldShowTableOptions && $vuetify.breakpoint.smAndUp"
class="elevation-0"
dense
color="transparent"
>
<TiptapToolbarButton
icon="mdi-table-column-plus-before"
:disabled="!editor.can().addColumnBefore()"
@click="editor.chain().focus().addColumnBefore().run()"
/>
<TiptapToolbarButton
icon="mdi-table-column-plus-after"
:disabled="!editor.can().addColumnAfter()"
@click="editor.chain().focus().addColumnAfter().run()"
/>
<TiptapToolbarButton
icon="mdi-table-column-remove"
:disabled="!editor.can().deleteColumn()"
@click="editor.chain().focus().deleteColumn().run()"
/>
<v-divider vertical class="mx-1" />
<TiptapToolbarButton
icon="mdi-table-row-plus-before"
:disabled="!editor.can().addRowBefore()"
@click="editor.chain().focus().addRowBefore().run()"
/>
<TiptapToolbarButton
icon="mdi-table-row-plus-after"
:disabled="!editor.can().addRowAfter()"
@click="editor.chain().focus().addRowAfter().run()"
/>
<TiptapToolbarButton
icon="mdi-table-row-remove"
:disabled="!editor.can().deleteRow()"
@click="editor.chain().focus().deleteRow().run()"
/>
<v-divider vertical class="mx-1" />
<TiptapToolbarButton
icon="mdi-table-border"
:disabled="!editor.can().toggleHeaderCell()"
@click="editor.chain().focus().toggleHeaderCell().run()"
/>
</v-toolbar>
<v-divider class="ec-tiptap-toolbar__mobile-divider" />
<v-toolbar
class="elevation-0 ec-tiptap-toolbar--second"
Expand Down Expand Up @@ -120,6 +173,10 @@ import Bold from '@tiptap/extension-bold'
import Italic from '@tiptap/extension-italic'
import Strike from '@tiptap/extension-strike'
import Underline from '@tiptap/extension-underline'
import Table from '@tiptap/extension-table'
import TableCell from '@tiptap/extension-table-cell'
import TableHeader from '@tiptap/extension-table-header'
import TableRow from '@tiptap/extension-table-row'
import History from '@tiptap/extension-history'
import Placeholder from '@tiptap/extension-placeholder'
import TiptapToolbarButton from '@/components/form/tiptap/TiptapToolbarButton.vue'
Expand Down Expand Up @@ -180,6 +237,13 @@ export default {
AutoLinkDecoration,
// headings currently disabled (see issue #2657)
HardBreak,
Table.configure({
resizable: true,
allowTableNodeSelection: true,
}),
TableRow,
TableHeader,
TableCell,
]
)
}
Expand All @@ -199,15 +263,6 @@ export default {
},
// copied from @tiptap/extension-bubble-menu
shouldShow: ({ view, state, from, to }) => {
const { doc, selection } = state
const { empty } = selection
// Sometime check for `empty` is not enough.
// Doubleclick an empty paragraph returns a node size of 2.
// So we check also for an empty text size.
const isEmptyTextBlock =
!doc.textBetween(from, to).length && isTextSelection(state.selection)
// Don't show if selection is within of an autolink
if (this.withExtensions) {
const links = AutoLinkKey.getState(state).find(
Expand All @@ -227,12 +282,19 @@ export default {
const hasEditorFocus = view.hasFocus() || isChildOfMenu
if (!hasEditorFocus || empty || isEmptyTextBlock || !this.editor.isEditable) {
if (!hasEditorFocus || !this.editor.isEditable) {
return false
}
return true
},
shouldShowTableOptions: ({ from, to }) => {

Check failure on line 291 in frontend/src/components/form/tiptap/TiptapEditor.vue

View workflow job for this annotation

GitHub Actions / Lint: Frontend (ESLint)

'from' is defined but never used. Allowed unused args must match /^_$/u

Check failure on line 291 in frontend/src/components/form/tiptap/TiptapEditor.vue

View workflow job for this annotation

GitHub Actions / Lint: Frontend (ESLint)

'to' is defined but never used. Allowed unused args must match /^_$/u
if (this.withExtensions && this.from > -1 && this.to > -1) {
return this.editor.can().addColumnBefore()
}
return false
},
}
},
computed: {
Expand Down Expand Up @@ -328,6 +390,57 @@ div.editor:deep(.editor__content .ProseMirror) {
line-height: 1.5;
}
div.editor:deep(.editor__content) .resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}
div.editor:deep(.editor__content table) {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
margin: 0;
overflow: hidden;
td, th {

Check failure on line 405 in frontend/src/components/form/tiptap/TiptapEditor.vue

View workflow job for this annotation

GitHub Actions / Lint: Frontend (ESLint)

Insert `⏎·`
padding: 0.2rem 0.5rem 0;
min-width: 1em;
border: 1px solid rgba(0, 0, 0, 0.38);
vertical-align: top;
text-align: left;
box-sizing: border-box;
position: relative;
> * {
margin-bottom: 0;
}
}
th {
font-weight: bold;
background-color: #f1f3f5;
}
.selectedCell:after {
z-index: 2;
position: absolute;
content: "";

Check failure on line 427 in frontend/src/components/form/tiptap/TiptapEditor.vue

View workflow job for this annotation

GitHub Actions / Lint: Frontend (ESLint)

Replace `""` with `''`
left: 0; right: 0; top: 0; bottom: 0;

Check failure on line 428 in frontend/src/components/form/tiptap/TiptapEditor.vue

View workflow job for this annotation

GitHub Actions / Lint: Frontend (ESLint)

Replace `·right:·0;·top:·0;` with `⏎····right:·0;⏎····top:·0;⏎···`
background: rgba(200, 200, 255, 0.4);
pointer-events: none;
}
.column-resize-handle {
position: absolute;
right: -2px;
top: 0;
bottom: -2px;
width: 4px;
background-color: #adf;
pointer-events: none;
}
}
.theme--light.v-input--is-disabled div.editor:deep(.editor__content .ProseMirror) {
color: rgba(0, 0, 0, 0.38);
}
Expand Down
86 changes: 81 additions & 5 deletions pdf/src/campPrint/RichText.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,24 @@ import { decode } from 'html-entities'
// eslint-disable-next-line vue/prefer-import-from-vue
import { h } from '@vue/runtime-core'
function visit(node, parent = null) {
function visit(node, parent = null, index = 0) {
const rule = rules.find((rule) => rule.shouldProcessNode(node, parent))
if (!rule) {
console.log('unknown HTML node type', node)
return null
}
return rule.processNode(node, parent)
return rule.processNode(node, parent, index)
}
function visitChildren(children, parent) {
return children.length
? children.map((child) => visit(child, parent))
: [visit({ type: 'text', content: '&nbsp;' }, parent)]
? children.map((child, idx) => visit(child, parent, idx))
: [visit({ type: 'text', content: '&nbsp;' }, parent, 0)]
}
const tableContextStack = []
const rules = [
{
shouldProcessNode: (node) => node.type === 'text',
Expand Down Expand Up @@ -95,6 +97,54 @@ const rules = [
)
},
},
{
shouldProcessNode: (node) => node.type === 'tag' && node.name === 'table',
processNode: (node) => {
tableContextStack.push([])
const result = h('View', { class: 'table' }, visitChildren(node.children, node))
tableContextStack.pop()
return result
},
},
{
shouldProcessNode: (node) => node.type === 'tag' && node.name === 'colgroup',
processNode: (node) => {
visitChildren(node.children, node)
return null
},
},
{
shouldProcessNode: (node) => node.type === 'tag' && node.name === 'col',
processNode: (node) => {
const width = Math.floor(
parseInt(node.attrs.style?.match(/width:\s*(\d+)px;/)[1]) / 1.33
)
const tableContext = tableContextStack.pop()
tableContext.push(width)
tableContextStack.push(tableContext)
return null
},
},
{
shouldProcessNode: (node) => node.type === 'tag' && node.name === 'tbody',
processNode: (node) =>
h('View', { class: 'tbody' }, visitChildren(node.children, node)),
},
{
shouldProcessNode: (node) => node.type === 'tag' && node.name === 'tr',
processNode: (node) => h('View', { class: 'tr' }, visitChildren(node.children, node)),
},
{
shouldProcessNode: (node) =>
node.type === 'tag' && (node.name === 'td' || node.name === 'th'),
processNode: (node, _, index) => {
const width = tableContextStack[tableContextStack.length - 1][index]
const style = width
? { flexBasis: width, flexGrow: 0, flexShrink: 0 }
: { flexBasis: 1.33, flexGrow: 1 }
return h('View', { class: node.name, style }, visitChildren(node.children, node))
},
},
]
function calculateListNumber(node, parent) {
Expand All @@ -120,7 +170,7 @@ export default {
},
},
render() {
return [this.parsed].flat().map((node) => visit(node))
return [this.parsed].flat().map((node, idx) => visit(node, null, idx))
},
}
</script>
Expand All @@ -140,4 +190,30 @@ export default {
.strikethrough {
text-decoration: line-through;
}
.table {
borderLeft: 1pt solid black;
borderTop: 1pt solid black;
width: 100%;
}
.tr {
flex-direction: row;
align-items: stretch;
width: 100%;
}
.th {
font-weight: bold;
background-color: #f1f3f5;
border-right: 1pt solid black;
border-bottom: 1pt solid black;
padding: 2pt 4pt 0;
flex-grow: 1;
flex-basis: 1;
}
.td {
border-right: 1pt solid black;
border-bottom: 1pt solid black;
padding: 2pt 4pt 0;
flex-grow: 1;
flex-basis: 1;
}
</pdf-style>

0 comments on commit 3986d64

Please sign in to comment.