Solution
This is what I have, it may not be super perfect but it does what we need to do. I needed to unregister the one Umbraco ships with, which I done in an entrypoint.ts file I had.
manifest.ts
export const manifests: Array<UmbExtensionManifest> = [
{
"name": "[Consilium] Tiptap Wrap Tables",
"alias": "Consilium.Tiptap.WrapTables",
"type": "tiptapExtension",
"meta": {
"group": "Consilium",
"label": "Wrap Tables in HTML",
"description": "Wraps tables in a div with class 'gsc-table__container' to allow for custom styling.",
"icon": "icon-grid-2x2",
},
api: () => import('./wrap-table/wrap-table'),
}
];
wrap-table.ts
import { UmbTiptapExtensionApiBase } from '@umbraco-cms/backoffice/tiptap';
import { WrapTable } from './wrap-table-extension';
import { UmbTableCell, UmbTableHeader, UmbTableRow } from '@umbraco-cms/backoffice/external/tiptap';
import { css } from '@umbraco-cms/backoffice/external/lit';
export default class WrapTableTipTapExtensionApi extends UmbTiptapExtensionApiBase {
// Umbraco's Table Extenion has 'UmbTableHeader, UmbTableRow, UmbTableCell'
// We are simply replacing/extending UmbTable with WrapTable
getTiptapExtensions = () => [WrapTable, UmbTableHeader, UmbTableRow, UmbTableCell];
// I dont like I need to copy/paste from source
// Was hoping I could get it from the UmbTiptapTableExtensionApi
// https://github.com/umbraco/Umbraco-CMS/blob/main/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/table.tiptap-api.ts#L11
override getStyles = () => css`
.tableWrapper {
margin: 1.5rem 0;
table {
border-color: rgba(0, 0, 0, 0.1);
border-radius: 0.25rem;
border-spacing: 0;
box-sizing: border-box;
max-width: 100%;
td,
th {
box-sizing: border-box;
position: relative;
min-width: 50px;
border: 1px solid var(--uui-color-border);
padding: 0.5rem;
text-align: left;
vertical-align: top;
&:first-of-type:not(a),
&:first-of-type:not(a) {
margin-top: 0;
}
p {
margin: 0;
}
p + p {
margin-top: 0.75rem;
}
}
th {
font-weight: bold;
}
.column-resize-handle {
cursor: ew-resize;
cursor: col-resize;
display: flex;
position: absolute;
top: 0;
bottom: -2px;
right: -0.25rem;
width: 0.5rem;
}
.column-resize-handle:before {
margin-left: 0.5rem;
height: 100%;
width: 1px;
}
.column-resize-handle:before {
content: '';
}
.selectedCell {
background-color: color-mix(in srgb, var(--uui-color-surface-emphasis) 50%, transparent);
border-color: var(--uui-color-selected);
}
.grip-column,
.grip-row {
position: absolute;
z-index: 10;
display: flex;
cursor: pointer;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.2);
uui-symbol-more {
visibility: hidden;
}
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
&.selected {
border-color: rgba(0, 0, 0, 0.3);
background-color: rgba(0, 0, 0, 0.3);
box-shadow:
0 0 #0000,
0 0 #0000,
0 0 rgba(0, 0, 0, 0.05);
}
&:hover uui-symbol-more,
&.selected uui-symbol-more {
visibility: visible;
}
}
.grip-column {
border-left-width: 1px;
top: -0.75rem;
left: 0;
height: 0.75rem;
width: calc(100% + 1px);
margin-left: -1px;
}
.grip-row {
border-top-width: 1px;
flex-direction: column;
top: 0;
left: -0.75rem;
height: calc(100% + 1px);
width: 0.75rem;
margin-top: -1px;
uui-symbol-more {
transform: rotate(90deg);
}
}
}
}
`;
}
wrap-table-extension.ts
import { createColGroup } from '@tiptap/extension-table';
import type { DOMOutputSpec } from '@tiptap/pm/model';
import { mergeAttributes, UmbTable } from '@umbraco-cms/backoffice/external/tiptap';
// Extend UmbTable which is extending the Tiptap Table extension
// https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing
// https://github.com/umbraco/Umbraco-CMS/blob/main/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-table.extension.ts#L37
export const WrapTable = UmbTable.extend({
// Uses same code from UmbTable which in turn is inhering from Tiptap's Table extension
// https://github.com/ueberdosis/tiptap/blob/next/packages/extension-table/src/table/table.ts#L258C3-L271C5
renderHTML({ node, HTMLAttributes }) {
const { colgroup, tableWidth, tableMinWidth } = createColGroup(node, this.options.cellMinWidth);
// Filter out the wrapper's class from HTMLAttributes
const filteredHTMLAttributes = { ...HTMLAttributes };
if (filteredHTMLAttributes.class) {
filteredHTMLAttributes.class = filteredHTMLAttributes.class
.split(' ')
.filter((cls: string) => cls !== 'gsc-table__container')
.join(' ');
if (!filteredHTMLAttributes.class.trim()) {
delete filteredHTMLAttributes.class;
}
}
// Updated DOMOutpputSpec to include the 'gsc-table__container' class
const table: DOMOutputSpec = [
'div',
{ class: 'gsc-table__container' },
[
'table',
mergeAttributes(this.options.HTMLAttributes, filteredHTMLAttributes, {
style: tableWidth ? `width: ${tableWidth}` : `min-width: ${tableMinWidth}`,
}),
colgroup,
['tbody', 0],
],
];
return table;
},
// https://github.com/ueberdosis/tiptap/blob/next/packages/extension-table/src/table/table.ts#L254C3-L256C5
parseHTML() {
return [
{
tag: 'div.gsc-table__container',
}
]
}
});
entrypoint.ts
import { UmbEntryPointOnInit, UmbEntryPointOnUnload } from '@umbraco-cms/backoffice/extension-api';
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { client } from './api/client.gen';
// load up the manifests here
export const onInit: UmbEntryPointOnInit = (_host, _extensionRegistry) => {
// Will use only to add in Open API config with generated TS OpenAPI HTTPS Client
// Do the OAuth token handshake stuff
_host.consumeContext(UMB_AUTH_CONTEXT, async (authContext) => {
// Get the token info from Umbraco
const config = authContext?.getOpenApiConfiguration();
client.setConfig({
baseUrl: config?.base,
credentials: config?.credentials,
auth: () => config?.token(), // Dont need to use the interceptor approach anymore
});
});
// Unregister Umbraco TipTap table extension
// As we have our own that it extends theirs to wrap with a div with a class
//_extensionRegistry.unregister('Umb.Tiptap.Table');
_extensionRegistry.exclude('Umb.Tiptap.Table');
};
export const onUnload: UmbEntryPointOnUnload = (_host, _extensionRegistry) => {
// Use to do any cleanup work when the entry point is unloaded
};