Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3986d64

Browse files
committedMar 26, 2024·
WIP simple table support
1 parent f4941a0 commit 3986d64

File tree

5 files changed

+264
-16
lines changed

5 files changed

+264
-16
lines changed
 

‎api/config/packages/exercise_html_purifier.yaml

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ exercise_html_purifier:
1212
# to know how to whitelist elements
1313

1414
# # whitelist attributes by tag
15-
# attributes: []
15+
attributes:
16+
td:
17+
colwidth: Length
1618

1719
# # whitelist elements by name
1820
# elements: []

‎frontend/package-lock.json

+53
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎frontend/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
"@tiptap/extension-paragraph": "2.2.4",
4242
"@tiptap/extension-placeholder": "2.2.4",
4343
"@tiptap/extension-strike": "2.2.4",
44+
"@tiptap/extension-table": "^2.2.4",
45+
"@tiptap/extension-table-cell": "^2.2.4",
46+
"@tiptap/extension-table-header": "^2.2.4",
47+
"@tiptap/extension-table-row": "^2.2.4",
4448
"@tiptap/extension-text": "2.2.4",
4549
"@tiptap/extension-underline": "2.2.4",
4650
"@tiptap/pm": "2.2.4",

‎frontend/src/components/form/tiptap/TiptapEditor.vue

+123-10
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,61 @@
6464
@click="editor.chain().focus().sinkListItem('listItem').run()"
6565
/>
6666
</template>
67+
68+
<v-divider vertical class="mx-1" />
69+
70+
<TiptapToolbarButton
71+
icon="mdi-table-large-plus"
72+
:disabled="editor.can().addColumnBefore()"
73+
@click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()"
74+
/>
6775
</div>
6876
</v-toolbar>
77+
<v-divider v-if="$vuetify.breakpoint.smAndUp" />
78+
<v-toolbar
79+
v-if="shouldShowTableOptions && $vuetify.breakpoint.smAndUp"
80+
class="elevation-0"
81+
dense
82+
color="transparent"
83+
>
84+
<TiptapToolbarButton
85+
icon="mdi-table-column-plus-before"
86+
:disabled="!editor.can().addColumnBefore()"
87+
@click="editor.chain().focus().addColumnBefore().run()"
88+
/>
89+
<TiptapToolbarButton
90+
icon="mdi-table-column-plus-after"
91+
:disabled="!editor.can().addColumnAfter()"
92+
@click="editor.chain().focus().addColumnAfter().run()"
93+
/>
94+
<TiptapToolbarButton
95+
icon="mdi-table-column-remove"
96+
:disabled="!editor.can().deleteColumn()"
97+
@click="editor.chain().focus().deleteColumn().run()"
98+
/>
99+
<v-divider vertical class="mx-1" />
100+
<TiptapToolbarButton
101+
icon="mdi-table-row-plus-before"
102+
:disabled="!editor.can().addRowBefore()"
103+
@click="editor.chain().focus().addRowBefore().run()"
104+
/>
105+
<TiptapToolbarButton
106+
icon="mdi-table-row-plus-after"
107+
:disabled="!editor.can().addRowAfter()"
108+
@click="editor.chain().focus().addRowAfter().run()"
109+
/>
110+
<TiptapToolbarButton
111+
icon="mdi-table-row-remove"
112+
:disabled="!editor.can().deleteRow()"
113+
@click="editor.chain().focus().deleteRow().run()"
114+
/>
115+
<v-divider vertical class="mx-1" />
116+
<TiptapToolbarButton
117+
icon="mdi-table-border"
118+
:disabled="!editor.can().toggleHeaderCell()"
119+
@click="editor.chain().focus().toggleHeaderCell().run()"
120+
/>
121+
</v-toolbar>
69122
<v-divider class="ec-tiptap-toolbar__mobile-divider" />
70123
<v-toolbar
71124
class="elevation-0 ec-tiptap-toolbar--second"
@@ -120,6 +173,10 @@ import Bold from '@tiptap/extension-bold'
120173
import Italic from '@tiptap/extension-italic'
121174
import Strike from '@tiptap/extension-strike'
122175
import Underline from '@tiptap/extension-underline'
176+
import Table from '@tiptap/extension-table'
177+
import TableCell from '@tiptap/extension-table-cell'
178+
import TableHeader from '@tiptap/extension-table-header'
179+
import TableRow from '@tiptap/extension-table-row'
123180
import History from '@tiptap/extension-history'
124181
import Placeholder from '@tiptap/extension-placeholder'
125182
import TiptapToolbarButton from '@/components/form/tiptap/TiptapToolbarButton.vue'
@@ -180,6 +237,13 @@ export default {
180237
AutoLinkDecoration,
181238
// headings currently disabled (see issue #2657)
182239
HardBreak,
240+
Table.configure({
241+
resizable: true,
242+
allowTableNodeSelection: true,
243+
}),
244+
TableRow,
245+
TableHeader,
246+
TableCell,
183247
]
184248
)
185249
}
@@ -199,15 +263,6 @@ export default {
199263
},
200264
// copied from @tiptap/extension-bubble-menu
201265
shouldShow: ({ view, state, from, to }) => {
202-
const { doc, selection } = state
203-
const { empty } = selection
204-
205-
// Sometime check for `empty` is not enough.
206-
// Doubleclick an empty paragraph returns a node size of 2.
207-
// So we check also for an empty text size.
208-
const isEmptyTextBlock =
209-
!doc.textBetween(from, to).length && isTextSelection(state.selection)
210-
211266
// Don't show if selection is within of an autolink
212267
if (this.withExtensions) {
213268
const links = AutoLinkKey.getState(state).find(
@@ -227,12 +282,19 @@ export default {
227282
228283
const hasEditorFocus = view.hasFocus() || isChildOfMenu
229284
230-
if (!hasEditorFocus || empty || isEmptyTextBlock || !this.editor.isEditable) {
285+
if (!hasEditorFocus || !this.editor.isEditable) {
231286
return false
232287
}
233288
234289
return true
235290
},
291+
shouldShowTableOptions: ({ from, to }) => {
292+
if (this.withExtensions && this.from > -1 && this.to > -1) {
293+
return this.editor.can().addColumnBefore()
294+
}
295+
296+
return false
297+
},
236298
}
237299
},
238300
computed: {
@@ -328,6 +390,57 @@ div.editor:deep(.editor__content .ProseMirror) {
328390
line-height: 1.5;
329391
}
330392
393+
div.editor:deep(.editor__content) .resize-cursor {
394+
cursor: ew-resize;
395+
cursor: col-resize;
396+
}
397+
398+
div.editor:deep(.editor__content table) {
399+
border-collapse: collapse;
400+
table-layout: fixed;
401+
width: 100%;
402+
margin: 0;
403+
overflow: hidden;
404+
405+
td, th {
406+
padding: 0.2rem 0.5rem 0;
407+
min-width: 1em;
408+
border: 1px solid rgba(0, 0, 0, 0.38);
409+
vertical-align: top;
410+
text-align: left;
411+
box-sizing: border-box;
412+
position: relative;
413+
414+
> * {
415+
margin-bottom: 0;
416+
}
417+
}
418+
419+
th {
420+
font-weight: bold;
421+
background-color: #f1f3f5;
422+
}
423+
424+
.selectedCell:after {
425+
z-index: 2;
426+
position: absolute;
427+
content: "";
428+
left: 0; right: 0; top: 0; bottom: 0;
429+
background: rgba(200, 200, 255, 0.4);
430+
pointer-events: none;
431+
}
432+
433+
.column-resize-handle {
434+
position: absolute;
435+
right: -2px;
436+
top: 0;
437+
bottom: -2px;
438+
width: 4px;
439+
background-color: #adf;
440+
pointer-events: none;
441+
}
442+
}
443+
331444
.theme--light.v-input--is-disabled div.editor:deep(.editor__content .ProseMirror) {
332445
color: rgba(0, 0, 0, 0.38);
333446
}

‎pdf/src/campPrint/RichText.vue

+81-5
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,24 @@ import { decode } from 'html-entities'
55
// eslint-disable-next-line vue/prefer-import-from-vue
66
import { h } from '@vue/runtime-core'
77
8-
function visit(node, parent = null) {
8+
function visit(node, parent = null, index = 0) {
99
const rule = rules.find((rule) => rule.shouldProcessNode(node, parent))
1010
if (!rule) {
1111
console.log('unknown HTML node type', node)
1212
return null
1313
}
1414
15-
return rule.processNode(node, parent)
15+
return rule.processNode(node, parent, index)
1616
}
1717
1818
function visitChildren(children, parent) {
1919
return children.length
20-
? children.map((child) => visit(child, parent))
21-
: [visit({ type: 'text', content: '&nbsp;' }, parent)]
20+
? children.map((child, idx) => visit(child, parent, idx))
21+
: [visit({ type: 'text', content: '&nbsp;' }, parent, 0)]
2222
}
2323
24+
const tableContextStack = []
25+
2426
const rules = [
2527
{
2628
shouldProcessNode: (node) => node.type === 'text',
@@ -95,6 +97,54 @@ const rules = [
9597
)
9698
},
9799
},
100+
{
101+
shouldProcessNode: (node) => node.type === 'tag' && node.name === 'table',
102+
processNode: (node) => {
103+
tableContextStack.push([])
104+
const result = h('View', { class: 'table' }, visitChildren(node.children, node))
105+
tableContextStack.pop()
106+
return result
107+
},
108+
},
109+
{
110+
shouldProcessNode: (node) => node.type === 'tag' && node.name === 'colgroup',
111+
processNode: (node) => {
112+
visitChildren(node.children, node)
113+
return null
114+
},
115+
},
116+
{
117+
shouldProcessNode: (node) => node.type === 'tag' && node.name === 'col',
118+
processNode: (node) => {
119+
const width = Math.floor(
120+
parseInt(node.attrs.style?.match(/width:\s*(\d+)px;/)[1]) / 1.33
121+
)
122+
const tableContext = tableContextStack.pop()
123+
tableContext.push(width)
124+
tableContextStack.push(tableContext)
125+
return null
126+
},
127+
},
128+
{
129+
shouldProcessNode: (node) => node.type === 'tag' && node.name === 'tbody',
130+
processNode: (node) =>
131+
h('View', { class: 'tbody' }, visitChildren(node.children, node)),
132+
},
133+
{
134+
shouldProcessNode: (node) => node.type === 'tag' && node.name === 'tr',
135+
processNode: (node) => h('View', { class: 'tr' }, visitChildren(node.children, node)),
136+
},
137+
{
138+
shouldProcessNode: (node) =>
139+
node.type === 'tag' && (node.name === 'td' || node.name === 'th'),
140+
processNode: (node, _, index) => {
141+
const width = tableContextStack[tableContextStack.length - 1][index]
142+
const style = width
143+
? { flexBasis: width, flexGrow: 0, flexShrink: 0 }
144+
: { flexBasis: 1.33, flexGrow: 1 }
145+
return h('View', { class: node.name, style }, visitChildren(node.children, node))
146+
},
147+
},
98148
]
99149
100150
function calculateListNumber(node, parent) {
@@ -120,7 +170,7 @@ export default {
120170
},
121171
},
122172
render() {
123-
return [this.parsed].flat().map((node) => visit(node))
173+
return [this.parsed].flat().map((node, idx) => visit(node, null, idx))
124174
},
125175
}
126176
</script>
@@ -140,4 +190,30 @@ export default {
140190
.strikethrough {
141191
text-decoration: line-through;
142192
}
193+
.table {
194+
borderLeft: 1pt solid black;
195+
borderTop: 1pt solid black;
196+
width: 100%;
197+
}
198+
.tr {
199+
flex-direction: row;
200+
align-items: stretch;
201+
width: 100%;
202+
}
203+
.th {
204+
font-weight: bold;
205+
background-color: #f1f3f5;
206+
border-right: 1pt solid black;
207+
border-bottom: 1pt solid black;
208+
padding: 2pt 4pt 0;
209+
flex-grow: 1;
210+
flex-basis: 1;
211+
}
212+
.td {
213+
border-right: 1pt solid black;
214+
border-bottom: 1pt solid black;
215+
padding: 2pt 4pt 0;
216+
flex-grow: 1;
217+
flex-basis: 1;
218+
}
143219
</pdf-style>

0 commit comments

Comments
 (0)
Please sign in to comment.