{
+ const html = buildTableHtml({ rows: 1, cols: 1, header: false, border: false, align: "center" })
+ expect(html).toBe('
')
+})
+
test("buildTableHtml clamps cols to 20", () => {
const html = buildTableHtml({ rows: 1, cols: 99, header: false })
expect(html.match(//gu) ?? []).toHaveLength(20)
diff --git a/VueApp/src/components/__tests__/rich-text-editor.test.ts b/VueApp/src/components/__tests__/rich-text-editor.test.ts
index 5fa076fce..d4441e342 100644
--- a/VueApp/src/components/__tests__/rich-text-editor.test.ts
+++ b/VueApp/src/components/__tests__/rich-text-editor.test.ts
@@ -5,13 +5,15 @@ import RichTextEditor from "@/components/RichTextEditor.vue"
import EditorImageDialog from "@/components/editor/EditorImageDialog.vue"
import EditorLinkDialog from "@/components/editor/EditorLinkDialog.vue"
import EditorTableDialog from "@/components/editor/EditorTableDialog.vue"
+import RecordFormDialog from "@/components/RecordFormDialog.vue"
import { MAX_TABLE_COLS } from "@/components/editor/editor-html"
+import type { TableAlign } from "@/components/editor/editor-html"
//
diff --git a/VueApp/src/components/editor/editor-html.ts b/VueApp/src/components/editor/editor-html.ts
index 6284ce65f..ea1750d5e 100644
--- a/VueApp/src/components/editor/editor-html.ts
+++ b/VueApp/src/components/editor/editor-html.ts
@@ -120,22 +120,34 @@ function tbody(rowCount: number, bodyRow: string): string {
const MAX_TABLE_ROWS = 50
const MAX_TABLE_COLS = 20
+/** Table alignment, written as the presentational align attribute; "" leaves the attribute off. */
+type TableAlign = "" | "left" | "center" | "right"
+
+interface TableOptions {
+ rows: number
+ cols: number
+ header: boolean
+ /** Off writes border="0", CKEditor's (VIPER 1) marker for a layout table, which base.css leaves unstyled. */
+ border?: boolean
+ align?: TableAlign
+}
+
/**
* Build a skeleton. `rows` is the total row count including the header row when `header`
* is true. Every cell holds so the caret can enter it in contenteditable. A trailing
* `
` is appended so the user can type below the table.
*/
-function buildTableHtml(opts: { rows: number; cols: number; header: boolean }): string {
+function buildTableHtml(opts: TableOptions): string {
const rows = clamp(opts.rows, 1, MAX_TABLE_ROWS)
const cols = clamp(opts.cols, 1, MAX_TABLE_COLS)
const headerRow = `${"| | ".repeat(cols)} `
const bodyRow = `${"| | ".repeat(cols)} `
- const table = opts.header
- ? `${headerRow}${tbody(rows - 1, bodyRow)} `
- : ``
- return `${table}
`
+ const border = (opts.border ?? true) ? 1 : 0
+ const align = opts.align ? ` align="${opts.align}"` : ""
+ const inner = opts.header ? `${headerRow}${tbody(rows - 1, bodyRow)}` : tbody(rows, bodyRow)
+ return `
`
}
export {
@@ -150,4 +162,4 @@ export {
MAX_TABLE_ROWS,
MAX_TABLE_COLS,
}
-export type { LinkKind }
+export type { LinkKind, TableAlign, TableOptions }
diff --git a/VueApp/src/styles/base.css b/VueApp/src/styles/base.css
index 5dd213e58..de42ed570 100644
--- a/VueApp/src/styles/base.css
+++ b/VueApp/src/styles/base.css
@@ -238,7 +238,7 @@ div.breadcrumbs {
.q-table__bottom,
.q-table__middle table thead tr:first-child th {
/* bg color is important for th; just specify one */
- background-color: #eee;
+ background-color: var(--table-header);
white-space: nowrap;
}
@@ -621,33 +621,41 @@ header .q-avatar {
line-height: 1.4;
}
-/* Tables and images inside sanitized CMS content, in the live block and the editor alike. Tables
- carry no default borders, so a table inserted from the editor would be invisible without these.
- A table too wide for the block scrolls inside it (overflow-x on the two containers) instead of
- widening the page. */
+/* Tables and images inside sanitized CMS content, in the live block and the editor alike. A table
+ too wide for the block scrolls inside it (overflow-x on the two containers) instead of widening
+ the page. */
.content-block,
.content-block-editor .q-editor__content {
overflow-x: auto;
}
-.content-block table,
-.content-block-editor .q-editor__content table {
+:is(.content-block, .content-block-editor .q-editor__content) table {
max-width: 100%;
+}
+
+/* Only tables that opt in with a border attribute get borders, cell padding and a header fill. The
+ editor's table dialog emits border="1"; legacy VIPER 1 content used the same attribute, with
+ border="0" meaning "no borders". Tables without it (mostly layout tables in migrated content)
+ keep the browser defaults they rendered with in VIPER 1. */
+:is(.content-block, .content-block-editor .q-editor__content) table[border]:not([border="0"]) {
border-collapse: collapse;
}
-.content-block th,
-.content-block td,
-.content-block-editor .q-editor__content th,
-.content-block-editor .q-editor__content td {
+:is(.content-block, .content-block-editor .q-editor__content) table[border]:not([border="0"]) :is(th, td) {
padding: 0.25rem 0.5rem;
border: 1px solid var(--ucdavis-black-20);
}
-.content-block th,
-.content-block-editor .q-editor__content th {
- /* The table-header token, matching the q-table header fill above */
- background-color: #eee;
+:is(.content-block, .content-block-editor .q-editor__content) table[border]:not([border="0"]) th {
+ background-color: var(--table-header);
+}
+
+/* A table without visible borders has nothing marking its cells in the editor's formatted view, so
+ the editor alone draws a dotted guide, as VIPER 1's CKEditor did with its showborders plugin. The
+ live block has no such rule, so the page still renders the table borderless. */
+.content-block-editor .q-editor__content table:not([border]) :is(th, td),
+.content-block-editor .q-editor__content table[border="0"] :is(th, td) {
+ border: 1px dotted var(--ucdavis-black-20);
}
.content-block img,
diff --git a/VueApp/src/styles/colors.css b/VueApp/src/styles/colors.css
index 644bce9d1..4fd6c6532 100644
--- a/VueApp/src/styles/colors.css
+++ b/VueApp/src/styles/colors.css
@@ -65,6 +65,10 @@
ring in base.css / site.css so components can reference a token instead
of repeating the hex. */
--focus-ring-color: #258cfb;
+
+ /* Table header fill, shared by q-table headers and bordered CMS content
+ tables so the two treatments cannot drift apart. */
+ --table-header: #eee;
}
/* Background utility classes — UC Davis palette */
diff --git a/test/Services/HtmlSanitizerServiceTests.cs b/test/Services/HtmlSanitizerServiceTests.cs
index aa94f8293..73ddfa216 100644
--- a/test/Services/HtmlSanitizerServiceTests.cs
+++ b/test/Services/HtmlSanitizerServiceTests.cs
@@ -140,6 +140,10 @@ public void Strips_legacy_custom_tags(string input)
[InlineData("x", "target=\"_blank\"", "rel=\"noopener\"")]
[InlineData("x", "download=\"file.pdf\"", "href=\"https://example.com/file.pdf\"")]
[InlineData("", "", "scope=\"col\"")]
+ [InlineData("", "border=\"1\"", "")]
+ [InlineData("", "align=\"center\"", "align=\"right\"")]
+ [InlineData("", "cellpadding=\"4\"", "cellspacing=\"0\"")]
+ [InlineData("", "valign=\"top\"", " | ")]
public void Preserves_allowed_constructs(string input, string mustContain1, string mustContain2)
{
var output = _sanitizer.Sanitize(input);
diff --git a/web/Services/HtmlSanitizerService.cs b/web/Services/HtmlSanitizerService.cs
index 0001f47ec..366e9ead0 100644
--- a/web/Services/HtmlSanitizerService.cs
+++ b/web/Services/HtmlSanitizerService.cs
@@ -51,6 +51,9 @@ private static HtmlSanitizer BuildSanitizer(bool allowDiffMarkers)
{
"href", "src", "alt", "title", "class", "id", "name",
"width", "height", "colspan", "rowspan", "scope",
+ // Presentational table attributes migrated VIPER 1 content carries (the set legacy
+ // antisamy-cms.xml allowed); border additionally selects bordered styling in base.css.
+ "border", "align", "valign", "cellpadding", "cellspacing",
"target", "rel", "download",
"style"
})
| |