@h-k-dev/angular-email-editor 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,8 @@ import { NodeSelection, TextSelection, Plugin, PluginKey, EditorState, Selection
3
3
  import { keymap } from 'prosemirror-keymap';
4
4
  import { splitBlockAs, chainCommands, newlineInCode, createParagraphNear, liftEmptyBlock, setBlockType, exitCode, lift, wrapIn, toggleMark as toggleMark$1, baseKeymap } from 'prosemirror-commands';
5
5
  import { tableEditing, fixTables, splitCell, mergeCells, deleteTable, deleteColumn, deleteRow, addRowAfter, addRowBefore, setCellAttr, removeColumn as removeColumn$1, removeRow, addColumn as addColumn$1, addRow, addColumnAfter, addColumnBefore, TableMap, isInTable, selectedRect, CellSelection, nextCell, goToNextCell } from 'prosemirror-tables';
6
+ import { allowedFontSizes, isEmailFontStack, matchEmailFont } from '@h-k-dev/angular-email-editor/fonts';
7
+ export * from '@h-k-dev/angular-email-editor/fonts';
6
8
  import { AddMarkStep, RemoveMarkStep, Transform } from 'prosemirror-transform';
7
9
  import { closeHistory, redo, undo, history } from 'prosemirror-history';
8
10
  import { InputRule, inputRules, wrappingInputRule, textblockTypeInputRule } from 'prosemirror-inputrules';
@@ -1686,9 +1688,8 @@ function isUppercase(state, schema) {
1686
1688
  });
1687
1689
  return found;
1688
1690
  }
1689
- const ALLOWED_SIZES = [10, 12, 14, 16, 18, 24, 32];
1690
1691
  function isSafeFontSize(value) {
1691
- return ALLOWED_SIZES.includes(value);
1692
+ return allowedFontSizes.includes(value);
1692
1693
  }
1693
1694
  /**
1694
1695
  * Parse a font-size value from the DOM into one of our allowed sizes.
@@ -1705,8 +1706,8 @@ function parseFontSize(raw) {
1705
1706
  // A size off the ladder — a builder's 11px, 13px, 15px — snaps to the
1706
1707
  // nearest rung, upward on a tie: read as the size it is nearest to, not
1707
1708
  // dropped to the default. (The schema still emits only its own rungs.)
1708
- let nearest = ALLOWED_SIZES[0];
1709
- for (const size of ALLOWED_SIZES) {
1709
+ let nearest = allowedFontSizes[0];
1710
+ for (const size of allowedFontSizes) {
1710
1711
  const better = Math.abs(size - n) < Math.abs(nearest - n) ||
1711
1712
  (Math.abs(size - n) === Math.abs(nearest - n) && size > nearest);
1712
1713
  if (better)
@@ -1714,79 +1715,19 @@ function parseFontSize(raw) {
1714
1715
  }
1715
1716
  return nearest;
1716
1717
  }
1717
- /**
1718
- * The sizes the toolbar picker offers: the phone-safe subset (≥14px). Below
1719
- * ~13px iOS auto-inflates text and reflows the layout (responsiveness ledger,
1720
- * principle 8), so we don't *offer* those — but {@link parseFontSize} still
1721
- * accepts the full {@link ALLOWED_SIZES} range, because a hand-typed size in
1722
- * the HTML source pane is the author's own responsibility, exactly like a
1723
- * hand-typed hex colour.
1724
- */
1725
- const emailFontSizes = [14, 16, 18, 24, 32];
1726
- /**
1727
- * The curated font stacks — the picker offers only these, no free-form fonts
1728
- * (principle 7: if a mainstream client can't render it, we don't emit it).
1729
- *
1730
- * Deliberately built from *single-word* family identifiers plus a generic
1731
- * fallback: a stack like `Courier New` would round-trip through the CSSOM
1732
- * (serialization builds real elements and re-reads them) and come back quoted
1733
- * as `"Courier New"` in Chrome but unquoted in jsdom — a byte-instability that
1734
- * breaks canonical determinism and makes tests disagree with the runtime (the
1735
- * same trap the longhand/`rgb()` rule guards against). Bare identifiers and
1736
- * generic keywords serialize identically everywhere.
1737
- */
1738
- const emailFontFamilies = [
1739
- { name: 'Sans-serif', stack: 'Arial, Helvetica, sans-serif' },
1740
- { name: 'Serif', stack: 'Georgia, Times, serif' },
1741
- { name: 'Monospace', stack: 'Courier, monospace' },
1742
- { name: 'System', stack: 'system-ui, sans-serif' },
1743
- ];
1744
- /** Normalise a `font-family` value to a comparison key: lower-cased, quotes
1745
- dropped, whitespace around commas collapsed. Lets a hand-typed or CSSOM-
1746
- reserialized stack match a curated one regardless of cosmetic differences. */
1747
- function fontFamilyKey(raw) {
1748
- return raw
1749
- .toLowerCase()
1750
- .replace(/["']/g, '')
1751
- .replace(/\s*,\s*/g, ',')
1752
- .trim();
1753
- }
1754
- const FONT_STACK_BY_KEY = new Map(emailFontFamilies.map((f) => [fontFamilyKey(f.stack), f.stack]));
1755
- /** Each family a curated stack names — a face or its generic — to the first
1756
- stack naming it: `helvetica` and `sans-serif` to Sans-serif, `times` and
1757
- `serif` to Serif. */
1758
- const FONT_STACK_BY_FAMILY = new Map();
1759
- for (const { stack } of emailFontFamilies) {
1760
- for (const family of fontFamilyKey(stack).split(',')) {
1761
- if (!FONT_STACK_BY_FAMILY.has(family))
1762
- FONT_STACK_BY_FAMILY.set(family, stack);
1763
- }
1764
- }
1718
+ /** Whether `value` is one of the curated stacks (`@h-k-dev/angular-email-editor/fonts`). */
1765
1719
  function isSafeFontFamily(value) {
1766
- return typeof value === 'string' && FONT_STACK_BY_KEY.has(fontFamilyKey(value));
1720
+ return isEmailFontStack(value);
1767
1721
  }
1768
1722
  /**
1769
- * Parse a font-family value from the DOM into one of our curated stacks,
1723
+ * Parse a font-family value from the DOM into one of the curated stacks,
1770
1724
  * returning the *canonical* stack string (so a cosmetically different but
1771
1725
  * equivalent input normalises to the exact bytes we emit). A stack of
1772
- * someone else's reads as the curated one it falls back to — the first of
1773
- * its families a curated stack names, its generic last: a builder's web
1774
- * font (`Ubuntu, Helvetica, Arial, sans-serif`) is Sans-serif, the face a
1775
- * client without that font draws anyway. Nothing to fall back to, no font.
1726
+ * someone else's reads as the curated one it falls back to
1727
+ * ({@link matchEmailFont}). Nothing to fall back to, no font.
1776
1728
  */
1777
1729
  function parseFontFamily(raw) {
1778
- if (!raw)
1779
- return null;
1780
- const key = fontFamilyKey(raw);
1781
- const exact = FONT_STACK_BY_KEY.get(key);
1782
- if (exact)
1783
- return exact;
1784
- for (const family of key.split(',')) {
1785
- const stack = FONT_STACK_BY_FAMILY.get(family.trim());
1786
- if (stack)
1787
- return stack;
1788
- }
1789
- return null;
1730
+ return matchEmailFont(raw)?.stack ?? null;
1790
1731
  }
1791
1732
  /**
1792
1733
  * TipTap-style `textStyle` mark: a `<span style="…">` container holding inline
@@ -2290,11 +2231,12 @@ function tokens(value) {
2290
2231
  * engine handles both — so merged cells are a real feature here, and imported
2291
2232
  * mail keeps its shape instead of being flattened.
2292
2233
  */
2293
- // The serialized table is intentionally borderless — grid lines are an
2294
- // editor-only editing aid (see the `.ProseMirror table` rules in the app's
2295
- // global styles), not part of the email a recipient receives. Padding is a
2296
- // fixed, responsive value (horizontal padding eats a phone's width, so it
2297
- // stays modest).
2234
+ // The table element itself carries no border or padding — those are per cell
2235
+ // (`border` and `padding` attrs, which is what tells the three `TableKind`s
2236
+ // apart). The faint grid shown while editing is an editor-only aid (the
2237
+ // layout-guide rules in the app's global styles), not part of the email.
2238
+ // Preset padding is a fixed, modest value (horizontal padding eats a phone's
2239
+ // width).
2298
2240
  // `table-layout: fixed` is the difference between a table you can type in and
2299
2241
  // one that jumps: without it, column widths are computed from *content*, so
2300
2242
  // every keystroke re-lays out the whole grid and the column you are typing in
@@ -2312,19 +2254,23 @@ function tokens(value) {
2312
2254
  40% of the container, in every client and in the editor alike). The
2313
2255
  offset serializes as `margin-left` — inline margins on tables are what
2314
2256
  Outlook's own Word composer emits, so its engine reads them, and a client
2315
- that strips them degrades the table gracefully to left-aligned. */
2316
- const tableStyle = (width, offset) => `width: ${formatPct(width)};` +
2257
+ that strips them degrades the table gracefully to left-aligned.
2258
+ `spacing` (px, or null for none) is the room above and below the table —
2259
+ `margin-top`/`margin-bottom`, block only, never the sides: real space
2260
+ between the table and the lines of text around it, which a client that
2261
+ strips it degrades to a table flush against its neighbours. */
2262
+ const tableStyle = (width, offset, spacing = null) => `width: ${formatPct(width)};` +
2317
2263
  (offset > 0 ? ` margin-left: ${formatPct(offset)};` : '') +
2264
+ (spacing ? ` margin-top: ${spacing}px; margin-bottom: ${spacing}px;` : '') +
2318
2265
  ' table-layout: fixed; border-collapse: collapse;';
2319
2266
  // Fixed layout means a long unbroken word can no longer widen its column, so it
2320
2267
  // would spill out of the cell instead. The editor never showed that (the
2321
2268
  // editable root sets `word-wrap` for its own reasons, and it inherits), which
2322
2269
  // is exactly why it has to be said out loud here — otherwise the overflow shows
2323
2270
  // up only in the recipient's client.
2324
- // No padding here: the email carries only *authored* padding (the `padding`
2325
- // attr, parsed off user markup). The comfortable default spacing seen while
2326
- // composing is editorial — an `.aee-editor` CSS rule the serializer never
2327
- // sees — so it can also reserve the room the block affordances live in.
2271
+ // No padding here: the email carries only the cell's own `padding` attr
2272
+ // (authored, or a bordered/borderless preset's), and the editor draws exactly
2273
+ // that — it adds no cell spacing of its own.
2328
2274
  const cellStyle = (valign) => `vertical-align: ${valign}; overflow-wrap: break-word;`;
2329
2275
  /** A cell's horizontal alignment off parsed markup: the inline style first,
2330
2276
  then the legacy `align` attribute — the pair real mail is written with,
@@ -2369,13 +2315,15 @@ function parsePadding(dom) {
2369
2315
  `<td>` is the bulletproof border in email — Outlook's Word engine renders
2370
2316
  it), with `border-collapse` on the table keeping the grid single-lined. */
2371
2317
  const TABLE_BORDER_COLOR = 'rgb(208, 215, 229)';
2372
- /** The bordered table's *real* cell padding — Excel-style breathing room, so
2373
- the rendered email is readable inside its grid. Deliberately the same
2374
- value as the editor's editorial default spacing (the app's `.aee-editor
2375
- table td` rule): a bordered and a plain table feel identical while
2376
- composing; only what the recipient gets differs — the preset ships its
2377
- padding, the plain table ships none unless authored. */
2318
+ /** The *real* cell padding a bordered or borderless table ships —
2319
+ Excel-style breathing room, so the rendered email is readable. A layout
2320
+ table ships none unless authored, and is drawn flush in the editor too. */
2378
2321
  const TABLE_CELL_PADDING = '8px 12px';
2322
+ /** The room a bordered or borderless table keeps above and below itself, in
2323
+ px — real (`margin-top`/`margin-bottom` on the table, see `tableStyle`),
2324
+ so the email sets the table off from the text around it exactly as the
2325
+ editor does. Block only: the sides stay on the text column. */
2326
+ const TABLE_BLOCK_SPACING = 16;
2379
2327
  /** Canonical percentage: one decimal at most, no trailing zero (25%, 33.3%). */
2380
2328
  const formatPct = (n) => `${Math.round(n * 10) / 10}%`;
2381
2329
  /** No column below 10%: at 320px that is ~32px — the width where a column
@@ -2400,7 +2348,18 @@ function pctOf(raw) {
2400
2348
  function parseTableBox(dom) {
2401
2349
  const width = Math.min(Math.max(pctOf(dom.style?.width || dom.getAttribute('width') || '') ?? 100, MIN_TABLE_PCT), 100);
2402
2350
  const offset = Math.min(Math.max(pctOf(dom.style?.marginLeft || '') ?? 0, 0), 100 - width);
2403
- return { width, offset };
2351
+ return { width, offset, spacing: parseBlockSpacing(dom) };
2352
+ }
2353
+ /** The room above and below a table off parsed markup: px, the same above as
2354
+ below — the one shape `tableStyle` writes. Anything else (one side only,
2355
+ em, %, `auto`) is not a look this schema sells, and repairs to none. */
2356
+ function parseBlockSpacing(dom) {
2357
+ const top = (dom.style?.marginTop || '').trim();
2358
+ if (top !== (dom.style?.marginBottom || '').trim())
2359
+ return null;
2360
+ const match = /^(\d+(?:\.\d+)?)px$/.exec(top);
2361
+ const spacing = match ? Number.parseFloat(match[1]) : 0;
2362
+ return spacing > 0 ? spacing : null;
2404
2363
  }
2405
2364
  /** A `colspan`/`rowspan` off parsed markup: 1 when absent, malformed, zero or
2406
2365
  negative — real mail carries all of those, and the grid must stay sane. */
@@ -2513,12 +2472,14 @@ const TableCell = defineNode({
2513
2472
  // Same attr name the library's commands expect, different unit on
2514
2473
  // purpose; pixel widths never parse in (see the node docs).
2515
2474
  colwidth: { default: null },
2516
- // Authored padding only (px, via `parsePadding`); `null` — the default —
2517
- // serializes none, and the editor's comfortable spacing is CSS-only.
2475
+ // Real padding (px, via `parsePadding`): authored, or the bordered and
2476
+ // borderless presets' `TABLE_CELL_PADDING`. `null` — the default, and a
2477
+ // layout table's — serializes none, and the editor draws none. Cells added to a uniformly padded table inherit
2478
+ // it (see `unifyCellPresets`).
2518
2479
  padding: { default: null },
2519
2480
  // The cell's grid-line colour (`border: 1px solid <color>`), or null
2520
- // for the borderless default. Set for every cell of a `/bordered-table`
2521
- // insert; kept uniform by the repair plugin (see `unifyCellBorders`).
2481
+ // for no line. Set for every cell of a bordered insert (`/table`); kept
2482
+ // uniform by the repair plugin (see `unifyCellPresets`).
2522
2483
  border: { default: null },
2523
2484
  // A fill colour on the cell (the most bulletproof background in email —
2524
2485
  // `background-color` on `<td>` renders even in Outlook). From the curated
@@ -2569,7 +2530,9 @@ const Table = defineNode({
2569
2530
  allowGapCursor: false,
2570
2531
  // Width and left offset in percent of the container; the defaults
2571
2532
  // (100, 0) serialize identically to the pre-attr canonical form.
2572
- attrs: { width: { default: 100 }, offset: { default: 0 } },
2533
+ // `spacing`: px of room above and below (`TABLE_BLOCK_SPACING` for the
2534
+ // bordered and borderless kinds), null — the default — for none.
2535
+ attrs: { width: { default: 100 }, offset: { default: 0 }, spacing: { default: null } },
2573
2536
  parseDOM: [{ tag: 'table', getAttrs: (dom) => parseTableBox(dom) }],
2574
2537
  // <tbody> wrapper matches what mail clients expect and what the HTML
2575
2538
  // parser re-injects, so serialize → parse → serialize is a fixpoint.
@@ -2580,14 +2543,15 @@ const Table = defineNode({
2580
2543
  'table',
2581
2544
  {
2582
2545
  role: 'presentation',
2583
- style: tableStyle(node.attrs['width'], node.attrs['offset']),
2546
+ style: tableStyle(node.attrs['width'], node.attrs['offset'], node.attrs['spacing']),
2584
2547
  },
2585
2548
  ['tbody', 0],
2586
2549
  ],
2587
2550
  },
2588
2551
  commands: ({ schema }) => ({
2589
- /** The default shape is the one /table inserts — three rows by two. */
2590
- insertTable: (rows = 3, cols = 2) => insertTableFocused(schema, rows, cols),
2552
+ /** The default shape and kind are the ones /table inserts — three rows
2553
+ by two, bordered (see `TableKind`). */
2554
+ insertTable: (rows = 3, cols = 2, kind = 'bordered') => insertTableFocused(schema, rows, cols, kind),
2591
2555
  // Selection-relative structure edits, straight from the library: each one
2592
2556
  // understands merged cells and multi-cell selections for free. The
2593
2557
  // add-column pair is wrapped: on a table with declared widths, the library
@@ -2727,15 +2691,15 @@ const Table = defineNode({
2727
2691
  // a rectangle. Covers everything that reaches the editor as a transaction
2728
2692
  // — paste, drops, `setContent`, an import landing on the html signal —
2729
2693
  // while `repairTables` covers the pure parse path (see `html.ts`).
2730
- // `unifyCellBorders` rides the same pass: cells that structural edits add
2731
- // arrive with the default null border and inherit a uniform table's.
2694
+ // `unifyCellPresets` rides the same pass: cells that structural edits add
2695
+ // arrive with a null border and padding and inherit a uniform table's.
2732
2696
  new Plugin({
2733
2697
  key: new PluginKey('tableRepair'),
2734
2698
  appendTransaction: (transactions, oldState, newState) => {
2735
2699
  if (!transactions.some((tr) => tr.docChanged))
2736
2700
  return undefined;
2737
2701
  const tr = fixTables(newState, oldState) ?? newState.tr;
2738
- unifyCellBorders(tr);
2702
+ unifyCellPresets(tr, insertedRanges([...transactions, tr]));
2739
2703
  return tr.docChanged ? tr : undefined;
2740
2704
  },
2741
2705
  }),
@@ -2746,23 +2710,32 @@ const Table = defineNode({
2746
2710
  // Three rows by two: a table asked for by name starts with somewhere to
2747
2711
  // write a heading line and two entries under it, which is what an email
2748
2712
  // table almost always turns out to be. A picker (the app's grid) says its
2749
- // own size; this is only what `/table` means on its own.
2713
+ // own size; this is only what `/table` means on its own. One item per
2714
+ // `TableKind`, the bordered default first.
2750
2715
  actions: ({ schema }) => [
2751
2716
  {
2752
2717
  id: 'table',
2753
2718
  section: 'layout',
2754
2719
  title: 'Table',
2755
- keywords: ['table', 'grid', 'rows', 'columns'],
2756
- icon: 'table_chart',
2757
- command: insertTableFocused(schema, 3, 2),
2720
+ keywords: ['table', 'bordered', 'grid', 'rows', 'columns', 'excel', 'lines'],
2721
+ icon: 'grid_on',
2722
+ command: insertTableFocused(schema, 3, 2, 'bordered'),
2758
2723
  },
2759
2724
  {
2760
- id: 'bordered-table',
2725
+ id: 'borderless-table',
2761
2726
  section: 'layout',
2762
- title: 'Bordered table',
2763
- keywords: ['bordered-table', 'table', 'borders', 'grid', 'excel', 'lines'],
2764
- icon: 'grid_on',
2765
- command: insertTableFocused(schema, 3, 2, TABLE_BORDER_COLOR),
2727
+ title: 'Borderless table',
2728
+ keywords: ['borderless-table', 'table', 'borderless', 'no lines', 'padding'],
2729
+ icon: 'border_clear',
2730
+ command: insertTableFocused(schema, 3, 2, 'borderless'),
2731
+ },
2732
+ {
2733
+ id: 'layout-table',
2734
+ section: 'layout',
2735
+ title: 'Layout table',
2736
+ keywords: ['layout-table', 'table', 'layout', 'invisible', 'side by side'],
2737
+ icon: 'view_quilt',
2738
+ command: insertTableFocused(schema, 3, 2, 'layout'),
2766
2739
  },
2767
2740
  ],
2768
2741
  });
@@ -2788,13 +2761,13 @@ function setCellBackground(color) {
2788
2761
  /** Inserts a table and drops the cursor into its first cell. The table is
2789
2762
  located after insertion (rather than by fragile nodeSize math) and
2790
2763
  `cellStart` resolves the exact text position inside cell (0, 0). */
2791
- function insertTableFocused(schema, rows, cols, border = null) {
2764
+ function insertTableFocused(schema, rows, cols, kind) {
2792
2765
  return (state, dispatch) => {
2793
2766
  // Asked, not told: answer before building anything — a toolbar asks on
2794
2767
  // every transaction.
2795
2768
  if (!dispatch)
2796
2769
  return true;
2797
- const table = buildTable(schema, rows, cols, border);
2770
+ const table = buildTable(schema, rows, cols, kind);
2798
2771
  const from = state.selection.from;
2799
2772
  const tr = state.tr.replaceSelectionWith(table);
2800
2773
  let tablePos = -1;
@@ -3130,49 +3103,90 @@ function isColumnBoundaryClean(map, index) {
3130
3103
  }
3131
3104
  return true;
3132
3105
  }
3133
- function buildTable(schema, rows, cols, border) {
3106
+ function buildTable(schema, rows, cols, kind) {
3134
3107
  const cellType = schema.nodes['tableCell'];
3135
3108
  const rowType = schema.nodes['tableRow'];
3136
3109
  const tableType = schema.nodes['table'];
3137
- // The bordered preset ships real padding with its grid (see
3138
- // TABLE_CELL_PADDING); a plain table ships none unless authored.
3139
- const attrs = { border, padding: border ? TABLE_CELL_PADDING : null };
3110
+ // Bordered and borderless ship real padding (TABLE_CELL_PADDING); a layout
3111
+ // table ships none — its spacing is the editor's alone.
3112
+ const attrs = {
3113
+ border: kind === 'bordered' ? TABLE_BORDER_COLOR : null,
3114
+ padding: kind === 'layout' ? null : TABLE_CELL_PADDING,
3115
+ };
3140
3116
  const makeRow = () => rowType.create(null, Array.from({ length: cols }, () => cellType.createAndFill(attrs)));
3141
- return tableType.create(null, Array.from({ length: rows }, makeRow));
3117
+ return tableType.create({ spacing: kind === 'layout' ? null : TABLE_BLOCK_SPACING }, Array.from({ length: rows }, makeRow));
3118
+ }
3119
+ /** The ranges of the final document that the steps of `trs` (applied one
3120
+ after another) inserted — what is *new* in it. Earlier ranges are mapped
3121
+ through every later step, so all of them speak the last doc's positions. */
3122
+ function insertedRanges(trs) {
3123
+ let ranges = [];
3124
+ for (const tr of trs) {
3125
+ for (const map of tr.mapping.maps) {
3126
+ ranges = ranges.map(([from, to]) => [map.map(from, -1), map.map(to, 1)]);
3127
+ map.forEach((_oldStart, _oldEnd, newStart, newEnd) => {
3128
+ if (newEnd > newStart)
3129
+ ranges.push([newStart, newEnd]);
3130
+ });
3131
+ }
3132
+ }
3133
+ return ranges;
3142
3134
  }
3143
3135
  /**
3144
- * Keeps a *uniformly* bordered table uniform: when every bordered cell wears
3145
- * the same colour and some cells have none — which is what any structural
3146
- * edit produces, since `addRow`/`addColumn`/the pills create cells with the
3147
- * default null border — the bare cells inherit it. A table with *mixed*
3148
- * border colours (authored, imported) is left exactly as written: repair,
3149
- * not opinion. `setNodeMarkup` never changes node sizes, so positions
3150
- * collected up front stay valid as the jobs apply.
3136
+ * Keeps a preset table uniform: `addRow`/`addColumn`/the pills/Tab past the
3137
+ * end all create cells with a null `border` and `padding`, which would leave
3138
+ * a bordered or borderless table with cells the email draws differently from
3139
+ * their neighbours.
3140
+ *
3141
+ * - **Border**: when every bordered cell wears the same colour, the bare
3142
+ * cells inherit it. Mixed colours (authored, imported) stay as written.
3143
+ * - **Padding**: stricter — only cells *this transaction added* to a table
3144
+ * that was *already there* inherit a uniform padding. Real mail pads a
3145
+ * spacer cell here and there, and an import must not spread that over
3146
+ * every cell: repair, not opinion.
3147
+ *
3148
+ * `setNodeMarkup` never changes node sizes, so positions collected up front
3149
+ * stay valid as the jobs apply.
3151
3150
  */
3152
- function unifyCellBorders(tr) {
3151
+ function unifyCellPresets(tr, inserted) {
3152
+ const isNew = (from, to) => inserted.some(([f, t]) => f <= from && to <= t);
3153
3153
  const jobs = [];
3154
3154
  tr.doc.descendants((node, pos) => {
3155
3155
  if (node.type.name !== 'table')
3156
3156
  return true;
3157
- const colors = new Set();
3158
- let bare = 0;
3157
+ const borders = new Set();
3158
+ const paddings = new Set();
3159
+ let unbordered = 0;
3159
3160
  node.descendants((cell) => {
3160
3161
  if (cell.type.name !== 'tableCell')
3161
3162
  return true;
3162
3163
  const border = cell.attrs['border'];
3164
+ const padding = cell.attrs['padding'];
3163
3165
  if (border)
3164
- colors.add(border);
3166
+ borders.add(border);
3165
3167
  else
3166
- bare++;
3167
- return true;
3168
+ unbordered++;
3169
+ if (padding)
3170
+ paddings.add(padding);
3171
+ return false;
3168
3172
  });
3169
- if (colors.size === 1 && bare > 0) {
3170
- const border = [...colors][0];
3173
+ const border = borders.size === 1 && unbordered > 0 ? [...borders][0] : null;
3174
+ const padding = paddings.size === 1 && !isNew(pos, pos + node.nodeSize) ? [...paddings][0] : null;
3175
+ if (border || padding) {
3171
3176
  node.descendants((cell, rel) => {
3172
- if (cell.type.name === 'tableCell' && !cell.attrs['border']) {
3173
- jobs.push({ pos: pos + 1 + rel, attrs: { ...cell.attrs, border } });
3177
+ if (cell.type.name !== 'tableCell')
3178
+ return true;
3179
+ const at = pos + 1 + rel;
3180
+ const attrs = { ...cell.attrs };
3181
+ if (border && !attrs['border'])
3182
+ attrs['border'] = border;
3183
+ if (padding && !attrs['padding'] && isNew(at, at + cell.nodeSize)) {
3184
+ attrs['padding'] = padding;
3174
3185
  }
3175
- return true;
3186
+ if (attrs['border'] !== cell.attrs['border'] || attrs['padding'] !== cell.attrs['padding']) {
3187
+ jobs.push({ pos: at, attrs });
3188
+ }
3189
+ return false;
3176
3190
  });
3177
3191
  }
3178
3192
  return false; // cells handled; nothing tabular nests deeper
@@ -5607,7 +5621,9 @@ const Columns = defineNode({
5607
5621
  insertColumns: (count = 2) => insertColumns(schema, count),
5608
5622
  /** Fill the column the cursor is in (or clear it with `null`). */
5609
5623
  setColumnBackground: (color) => setColumnBackground(color),
5610
- // Structural edits for the block menu. Named apart from the table's
5624
+ // Caret-relative structural edits, for a host's own controls (the
5625
+ // block's pills use the position-addressed `appendColumn` /
5626
+ // `removeLastColumn`). Named apart from the table's
5611
5627
  // `addColumnAfter`/`deleteColumn` — commands merge into one flat record.
5612
5628
  addColumn: () => addColumn,
5613
5629
  removeColumn: () => removeColumn,
@@ -5627,12 +5643,17 @@ const Columns = defineNode({
5627
5643
  // column's edge hops straight into the neighbouring column — the layout
5628
5644
  // twin of the table's cell-to-cell arrows. Mod-A is the table's scoped
5629
5645
  // select-all applied here: column content first, block from an empty
5630
- // column, the stock whole-document one from the block selection on.
5646
+ // column, the stock whole-document one from the block selection on. And
5647
+ // Delete over whole columns removes them — the block's only delete.
5631
5648
  plugins: () => [
5632
5649
  keymap({
5633
5650
  ArrowLeft: columnArrow(-1),
5634
5651
  ArrowRight: columnArrow(1),
5635
5652
  'Mod-a': selectColumnContent,
5653
+ Backspace: deleteFullySelectedColumns,
5654
+ 'Mod-Backspace': deleteFullySelectedColumns,
5655
+ Delete: deleteFullySelectedColumns,
5656
+ 'Mod-Delete': deleteFullySelectedColumns,
5636
5657
  }),
5637
5658
  ],
5638
5659
  actions: ({ schema }) => [
@@ -5663,6 +5684,16 @@ function hasColumnChildren(dom) {
5663
5684
  }
5664
5685
  return false;
5665
5686
  }
5687
+ /** How many empty lines a fresh column starts with. Two, not one: the block
5688
+ has no padding of the editor's own, so a one-line column is a sliver the
5689
+ eye reads as a stray rule rather than a place to write — two lines give
5690
+ it the shape of a column, the way /table starts with rows to fill. */
5691
+ const COLUMN_STARTING_LINES = 2;
5692
+ /** An empty column, {@link COLUMN_STARTING_LINES} lines tall. */
5693
+ function freshColumn(schema, attrs = null) {
5694
+ const lines = Array.from({ length: COLUMN_STARTING_LINES }, () => schema.nodes['paragraph'].createAndFill());
5695
+ return schema.nodes['column'].create(attrs, lines);
5696
+ }
5666
5697
  /** Inserts an n-column block and drops the cursor into the first column. */
5667
5698
  function insertColumns(schema, count) {
5668
5699
  return (state, dispatch) => {
@@ -5670,10 +5701,9 @@ function insertColumns(schema, count) {
5670
5701
  // every transaction.
5671
5702
  if (!dispatch)
5672
5703
  return true;
5673
- const colType = schema.nodes['column'];
5674
5704
  const columnsType = schema.nodes['columns'];
5675
5705
  const maxWidth = columnMaxWidth(count);
5676
- const columns = Array.from({ length: count }, () => colType.createAndFill({ maxWidth }));
5706
+ const columns = Array.from({ length: count }, () => freshColumn(schema, { maxWidth }));
5677
5707
  const node = columnsType.create(null, columns);
5678
5708
  const from = state.selection.from;
5679
5709
  const tr = state.tr.replaceSelectionWith(node);
@@ -5780,9 +5810,7 @@ const addColumn = (state, dispatch) => {
5780
5810
  const existing = [];
5781
5811
  ctx.node.forEach((col) => existing.push(col));
5782
5812
  const columns = resplitColumns(existing, count);
5783
- const fresh = state.schema.nodes['column'].createAndFill({
5784
- maxWidth: columnMaxWidth(count),
5785
- });
5813
+ const fresh = freshColumn(state.schema, { maxWidth: columnMaxWidth(count) });
5786
5814
  columns.splice(ctx.index + 1, 0, fresh);
5787
5815
  const tr = state.tr.replaceWith(ctx.pos, ctx.pos + ctx.node.nodeSize, ctx.node.type.create(ctx.node.attrs, columns));
5788
5816
  // The fresh column starts after the block's opening token (+1) and every
@@ -5823,6 +5851,107 @@ const deleteColumns = (state, dispatch) => {
5823
5851
  dispatch?.(state.tr.delete(ctx.pos, ctx.pos + ctx.node.nodeSize).scrollIntoView());
5824
5852
  return true;
5825
5853
  };
5854
+ /** Restructures the columns block at `pos` in place: drops the columns
5855
+ `keep` refuses, optionally appends an empty one, and re-splits every cap
5856
+ evenly. Step by step — never a whole-block replace — so the selection
5857
+ maps through untouched: a caret in a kept column stays exactly where it
5858
+ was, one in a removed column lands at the nearest valid spot. The pills'
5859
+ and the select-to-delete gesture's shared tail. */
5860
+ function restructureColumns(state, pos, block, keep, append = false) {
5861
+ const tr = state.tr;
5862
+ const starts = [];
5863
+ let at = pos + 1;
5864
+ block.forEach((col) => {
5865
+ starts.push(at);
5866
+ at += col.nodeSize;
5867
+ });
5868
+ // Back to front, so the earlier starts stay valid.
5869
+ for (let index = block.childCount - 1; index >= 0; index--) {
5870
+ if (!keep(index))
5871
+ tr.delete(starts[index], starts[index] + block.child(index).nodeSize);
5872
+ }
5873
+ if (append) {
5874
+ const end = tr.mapping.map(pos + block.nodeSize - 1, -1);
5875
+ tr.insert(end, freshColumn(state.schema));
5876
+ }
5877
+ const rebuilt = tr.doc.nodeAt(pos);
5878
+ const maxWidth = columnMaxWidth(rebuilt.childCount);
5879
+ rebuilt.forEach((col, offset) => {
5880
+ tr.setNodeMarkup(pos + 1 + offset, undefined, { ...col.attrs, maxWidth });
5881
+ });
5882
+ return tr;
5883
+ }
5884
+ /** The `+` pill on the block's right flank: an empty column after the last,
5885
+ up to {@link MAX_COLUMNS}; every cap re-splits evenly. Addressed by the
5886
+ block's position, not the caret — the pill acts on the block it is on. */
5887
+ function appendColumn(pos) {
5888
+ return (state, dispatch) => {
5889
+ const block = state.doc.nodeAt(pos);
5890
+ if (block?.type.name !== 'columns' || block.childCount >= MAX_COLUMNS)
5891
+ return false;
5892
+ dispatch?.(restructureColumns(state, pos, block, () => true, true));
5893
+ return true;
5894
+ };
5895
+ }
5896
+ /** The `−` pill on the block's left flank: the last column goes — the `+`'s
5897
+ exact inverse — and the rest re-split evenly. Refuses on the last one
5898
+ left: the block itself goes by selecting it whole and pressing Delete. */
5899
+ function removeLastColumn(pos) {
5900
+ return (state, dispatch) => {
5901
+ const block = state.doc.nodeAt(pos);
5902
+ if (block?.type.name !== 'columns' || block.childCount <= 1)
5903
+ return false;
5904
+ dispatch?.(restructureColumns(state, pos, block, (index) => index < block.childCount - 1));
5905
+ return true;
5906
+ };
5907
+ }
5908
+ /**
5909
+ * Backspace/Delete with whole columns selected — the table's gesture on the
5910
+ * layout block: select a column's content end to end (Mod-A in it, or a
5911
+ * drag) and the column goes; select several whole columns and they go;
5912
+ * select every column and the block goes. Anything less falls through to
5913
+ * the ordinary delete, which clears the selected text.
5914
+ */
5915
+ const deleteFullySelectedColumns = (state, dispatch) => {
5916
+ const selection = state.selection;
5917
+ if (!(selection instanceof TextSelection) || selection.empty)
5918
+ return false;
5919
+ const { $from, $to } = selection;
5920
+ let depth = -1;
5921
+ for (let d = $from.depth; d > 0; d--) {
5922
+ if ($from.node(d).type.name === 'columns') {
5923
+ depth = d;
5924
+ break;
5925
+ }
5926
+ }
5927
+ if (depth < 0 || $to.depth < depth || $to.before(depth) !== $from.before(depth))
5928
+ return false;
5929
+ const block = $from.node(depth);
5930
+ const pos = $from.before(depth);
5931
+ const first = $from.index(depth);
5932
+ const last = $to.index(depth);
5933
+ // Whole columns: the selection starts where the first column's text starts
5934
+ // and ends where the last column's text ends.
5935
+ const start = (index) => {
5936
+ let at = pos + 1;
5937
+ for (let i = 0; i < index; i++)
5938
+ at += block.child(i).nodeSize;
5939
+ return at;
5940
+ };
5941
+ const textStart = Selection.findFrom(state.doc.resolve(start(first) + 1), 1, true);
5942
+ const textEnd = Selection.findFrom(state.doc.resolve(start(last) + block.child(last).nodeSize - 1), -1, true);
5943
+ if (!textStart || !textEnd || $from.pos > textStart.from || $to.pos < textEnd.to)
5944
+ return false;
5945
+ if (!dispatch)
5946
+ return true;
5947
+ if (first === 0 && last === block.childCount - 1) {
5948
+ dispatch(state.tr.delete(pos, pos + block.nodeSize).scrollIntoView());
5949
+ return true;
5950
+ }
5951
+ const keep = (index) => index < first || index > last;
5952
+ dispatch(restructureColumns(state, pos, block, keep).scrollIntoView());
5953
+ return true;
5954
+ };
5826
5955
  /**
5827
5956
  * Tab / Shift-Tab inside a columns block: move to the next / previous column,
5828
5957
  * and past the block's ends hand off to the surrounding blocks. Always claims
@@ -5921,7 +6050,12 @@ const selectColumnContent = (state, dispatch) => {
5921
6050
  if (depth < 0)
5922
6051
  return false;
5923
6052
  const column = $from.node(depth);
5924
- const empty = column.childCount === 1 && column.firstChild.childCount === 0;
6053
+ // Empty: nothing but blank lines (a fresh column has two).
6054
+ let empty = true;
6055
+ column.forEach((block) => {
6056
+ if (!block.isTextblock || block.childCount > 0)
6057
+ empty = false;
6058
+ });
5925
6059
  if (dispatch) {
5926
6060
  if (empty) {
5927
6061
  dispatch(state.tr.setSelection(NodeSelection.create(state.doc, $from.before(depth - 1))));
@@ -13681,7 +13815,7 @@ class TableView {
13681
13815
  this.#box = this.dom.appendChild(document.createElement('div'));
13682
13816
  this.#box.className = 'aee-table-box';
13683
13817
  this.#table = this.#box.appendChild(document.createElement('table'));
13684
- this.#table.setAttribute('style', tableStyle(node.attrs['width'], node.attrs['offset']));
13818
+ this.#table.setAttribute('style', tableStyle(node.attrs['width'], node.attrs['offset'], node.attrs['spacing']));
13685
13819
  this.#table.setAttribute('role', 'presentation');
13686
13820
  this.#colgroup = this.#table.appendChild(document.createElement('colgroup'));
13687
13821
  this.contentDOM = this.#table.appendChild(document.createElement('tbody'));
@@ -13811,7 +13945,7 @@ class TableView {
13811
13945
  // is CSS-inset to the wrapper's content box (inside the editor-only
13812
13946
  // gutter), so a child's `left` percentage is a share of the table's own
13813
13947
  // area — table-relative shares are converted once, here.
13814
- this.#table.setAttribute('style', tableStyle(tableWidth, offset));
13948
+ this.#table.setAttribute('style', tableStyle(tableWidth, offset, node.attrs['spacing']));
13815
13949
  // An authored table (a template's, parsed in the `email` mode) shows its
13816
13950
  // own attributes, as the email does — see authored.ts.
13817
13951
  applyAuthoredAttributes(this.#table, node, 0);
@@ -13981,7 +14115,9 @@ class TableView {
13981
14115
  rows are the same object (ProseMirror's persistent tree), so a keystroke
13982
14116
  is one new cell and a handful of identity compares. */
13983
14117
  function sameTableChrome(a, b) {
13984
- if (a.attrs['width'] !== b.attrs['width'] || a.attrs['offset'] !== b.attrs['offset']) {
14118
+ if (a.attrs['width'] !== b.attrs['width'] ||
14119
+ a.attrs['offset'] !== b.attrs['offset'] ||
14120
+ a.attrs['spacing'] !== b.attrs['spacing']) {
13985
14121
  return false;
13986
14122
  }
13987
14123
  if (a.childCount !== b.childCount)
@@ -14113,6 +14249,8 @@ class ColumnsView {
14113
14249
  contentDOM;
14114
14250
  #box;
14115
14251
  #lines;
14252
+ #append;
14253
+ #remove;
14116
14254
  #node;
14117
14255
  #view;
14118
14256
  #getPos;
@@ -14135,6 +14273,14 @@ class ColumnsView {
14135
14273
  this.#lines.className = 'aee-col-lines';
14136
14274
  this.#lines.contentEditable = 'false';
14137
14275
  this.#lines.setAttribute('aria-hidden', 'true');
14276
+ // The block's two pills, the table's `+` turned into a stepper: `+` on
14277
+ // the right flank appends a column, `−` on the left takes the last one
14278
+ // away. Each sits in a sensor zone over the gutter beside the box — only
14279
+ // non-editable ground, so the zone can stay hit-testable and the pointer
14280
+ // never crosses dead space on its way over. No delete button: the block
14281
+ // goes the table's way, selected whole and Delete.
14282
+ this.#append = this.#pill('end', '+', 'Add column', appendColumn);
14283
+ this.#remove = this.#pill('start', '−', 'Remove last column', removeLastColumn);
14138
14284
  this.#render(node);
14139
14285
  }
14140
14286
  update(node) {
@@ -14150,11 +14296,34 @@ class ColumnsView {
14150
14296
  target === this.#box ||
14151
14297
  target === this.#lines ||
14152
14298
  this.#lines.contains(target) ||
14299
+ // The pills and their zones are ours too.
14300
+ (target instanceof Element && !!target.closest('.aee-add-zone')) ||
14153
14301
  // The contentDOM's own attributes are ours; its children are ProseMirror's.
14154
14302
  (target === this.contentDOM && record.type === 'attributes'));
14155
14303
  }
14304
+ #pill(side, glyph, label, command) {
14305
+ const zone = this.#box.appendChild(document.createElement('div'));
14306
+ zone.className = `aee-add-zone aee-add-zone--${side}`;
14307
+ zone.contentEditable = 'false';
14308
+ zone.setAttribute('aria-hidden', 'true');
14309
+ const pill = zone.appendChild(document.createElement('div'));
14310
+ pill.className = `aee-add-pill aee-add-pill--${side}`;
14311
+ pill.textContent = glyph;
14312
+ pill.setAttribute('role', 'button');
14313
+ pill.setAttribute('title', label);
14314
+ // preventDefault keeps the caret where it is; the command runs on click.
14315
+ pill.addEventListener('mousedown', (event) => event.preventDefault());
14316
+ pill.addEventListener('click', () => {
14317
+ command(this.#getPos())(this.#view.state, this.#view.dispatch);
14318
+ });
14319
+ return pill;
14320
+ }
14156
14321
  #render(node) {
14157
14322
  this.#box.setAttribute('style', containerStyle(node.attrs['align']));
14323
+ // A pill with nothing to do says so: no `+` past the most columns, no
14324
+ // `−` on the last one.
14325
+ this.#append.toggleAttribute('data-disabled', node.childCount >= MAX_COLUMNS);
14326
+ this.#remove.toggleAttribute('data-disabled', node.childCount <= 1);
14158
14327
  // An authored columns block shows its own attributes (authored.ts).
14159
14328
  applyAuthoredAttributes(this.#box, node, 0);
14160
14329
  const caps = columnCaps(node);
@@ -17359,29 +17528,43 @@ function discardTransaction(view) {
17359
17528
  /**
17360
17529
  * Where a floating menu goes for an anchor line: under it, clamped to its
17361
17530
  * scroll container, flipped above when below would overflow the visible
17362
- * area. Read-only — it measures and returns, and the caller writes `left` /
17363
- * `top` in its own write phase, so measuring never follows a write in the
17364
- * same task. The element must be `position: absolute` inside a
17531
+ * area. When it fits on neither side it goes to the side with more room and
17532
+ * says how tall it may be there (`maxHeight`) — flip, then size, as Floating
17533
+ * UI does — so the container never clips it.
17534
+ *
17535
+ * Read-only — it measures and returns, and the caller writes `left` / `top`
17536
+ * (and the height cap) in its own write phase, so measuring never follows a
17537
+ * write in the same task. The element must be `position: absolute` inside a
17365
17538
  * `position: relative` scroll container; its size is read as rendered, so
17366
- * call it after its contents are in.
17539
+ * call it after its contents are in. A caller that caps the box itself
17540
+ * passes `height`, the box's height without that cap — a box measured at the
17541
+ * last cap would choose its side by the old room, not its own size.
17367
17542
  */
17368
- function measureMenuPlacement(anchor, element, offset) {
17543
+ function measureMenuPlacement(anchor, element, offset, height = element.offsetHeight) {
17369
17544
  const container = element.offsetParent ?? element.parentElement;
17370
17545
  if (!container)
17371
17546
  return null;
17372
17547
  const containerRect = container.getBoundingClientRect();
17373
- const { offsetWidth: width, offsetHeight: height } = element;
17548
+ const width = element.offsetWidth;
17374
17549
  const { scrollLeft, scrollTop, scrollWidth, clientHeight } = container;
17375
17550
  const left = Math.min(Math.max(anchor.left - containerRect.left + scrollLeft, 0), Math.max(scrollWidth - width, 0));
17376
- // Below the line; flip above when it would overflow the visible part of
17377
- // the scroll container.
17378
- let top = anchor.bottom - containerRect.top + scrollTop + offset;
17379
- if (top + height > scrollTop + clientHeight) {
17380
- const above = anchor.top - containerRect.top + scrollTop;
17381
- if (above - height - offset >= scrollTop)
17382
- top = above - height - offset;
17383
- }
17384
- return { left, top };
17551
+ // The box's top edge below the line, its bottom edge above it, and the
17552
+ // visible part of the scroll container each leaves.
17553
+ const below = anchor.bottom - containerRect.top + scrollTop + offset;
17554
+ const above = anchor.top - containerRect.top + scrollTop - offset;
17555
+ const roomBelow = scrollTop + clientHeight - below;
17556
+ const roomAbove = above - scrollTop;
17557
+ if (height <= roomBelow)
17558
+ return { left, top: below, maxHeight: null };
17559
+ if (height <= roomAbove)
17560
+ return { left, top: above - height, maxHeight: null };
17561
+ // Fits neither: the side with more room, shrunk to it. A line out of
17562
+ // sight on both sides leaves no room to shrink to — below, as it is.
17563
+ if (roomAbove > roomBelow && roomAbove > 0)
17564
+ return { left, top: scrollTop, maxHeight: roomAbove };
17565
+ if (roomBelow > 0)
17566
+ return { left, top: below, maxHeight: roomBelow };
17567
+ return { left, top: below, maxHeight: null };
17385
17568
  }
17386
17569
 
17387
17570
  /** The dialect installed on an editor, as plugin state, so the send intent
@@ -18840,5 +19023,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
18840
19023
  * Generated bundle index. Do not edit.
18841
19024
  */
18842
19025
 
18843
- export { AUTHORED_ATTR, AUTHORED_SPEC_KEY, AUTHORED_STYLE_DEFAULTS_KEY, AuthoredEnter, BLOCKQUOTE_STYLE, BLOCK_MENU_FOCUS_KEY, BUTTON_FILL, BUTTON_STYLE, BaseKeymap, Blockquote, Bold, BulletList, Button, CARET_ROOM, CLIENT_LABELS, COMMENT_TAG, CONTENT_ATTRS, CSS_SUPPORT, ClearFormatting, CodeLine, CodeLineIndent, Column, ColumnResize, Columns, ColumnsResize, DOCUMENT_CLASS, DOCUMENT_TAGS, DUAL_BACKGROUND_MIN_RATIO, DUAL_CONTRAST_DARK, DUAL_CONTRAST_LIGHT, DUAL_CONTRAST_MIN_RATIO, Divider, Document$1 as Document, DocumentStyles, EMAIL_ATTRIBUTES, EMAIL_CSS_PROPERTIES, EMAIL_SAFE_STYLE_PROPERTIES, EMAIL_SAFE_TAGS, EMAIL_TAGS, EMAIL_TAG_ATTRIBUTES, EmailParagraph, FILE_CHOOSER_POINTER_WAIT, FILL_TEXT_COLOR, FILL_TEXT_COLOR_LIGHT, FILL_TEXT_COLOR_RGB, FOLD_WIDTH, FORMAT_WIDTH, GMAIL_CLIP_BYTES, GUIDES_ACTIVE_CLASS, GUIDES_PEEK_CLASS, GUIDES_PEEK_DELAY_MS, Gapcursor, HEADING_SIZES, HardBreak, Heading, History, HtmlComment, HtmlCommentInline, HtmlElement, HtmlInline, HtmlLanguage, HtmlRaw, HtmlTextElement, HtmlVoid, HtmlVoidBlock, IMPORT_VIEWPORT_WIDTH, INDENT_MAX, INDENT_STEP, INLINE_TAGS, INLINE_VOID_TAGS, IN_SELECTION_CLASS, Image, InlineAtomSelection, InlineImageStore, InlineImages, Italic, LIST_STYLE, LayoutGuides, Link, ListItem, MAX_COLUMNS, MAX_IMAGE_WIDTH, MAX_MERGE_TAG_LENGTH, MAX_UNBROKEN_RUN, MIN_COLUMN_CAP, MIN_COLUMN_PCT, MIN_FONT_SIZE, MIN_IMAGE_WIDTH, MIN_TABLE_PCT, MJML_WORLDLY_HTML, MergeTag, NoTextDrag, OrderedList, PARSE_SCOPE_KEY, PLACEHOLDER_WIDTH, Paragraph, PasteHygiene, QuotedHistory, REFUSED_TAGS, RENDERING_CLIENTS, SECTION_CONTENT_MAX, SECTION_INSET, SECTION_PADDING, STACK_CLASS, STACK_RULE, Section, SplitKeepingMarks, TABLE_BORDER_COLOR, TABLE_CELL_PADDING, TABLE_SECTION_TAGS, TYPING_REST, Table, TableCell, TableRow, Text, TextStyle, UNSET_BUTTON_HREF, Underline, VOID_TAGS, acceptProposal, addColumn, addColumnAtEnd, addRowAtEnd, analyzeAngularExpression, angularDialect, angularRequiredFields, applyAuthoredAttributes, applyEmailVocabulary, attributeString, authoredAttribute, authoredMarkupOf, authoredViewAttributes, bareButtons, buttonStyle, canonicalHead, canonicalMergeTagInner, canonicalRender, caretCellPos, caretInsideMergeTag, claimedImageFiles, clearCells, clientList, columnCaps, columnWidths, completionContextAt, containerStyle, contrastRatio, correctStoredMistakes, createAngularExpressions, createBlockMenu, createBubbleMenu, createButtonEdit, createContentProposal, createContentStream, createDOMParser, createEditor, createHtmlAutocomplete, createHtmlLanguage, createImageDrag, createInlineImages, createMergeTagResolution, createOffsetMapper, createQuotedHistory, createSchema, createSendIntent, createSourceMarks, createSuggestionMenu, createTableHandles, declarationProperty, declaredBackground, decodeDataUrl, defaultBubbleShouldShow, defineExtension, defineMark, defineNode, deleteColumns, deleteFullySelected, discardProposal, distributePadding, docText, dropDeadOfficeClasses, dropHidden, dropMediaQueries, dropZeroSizedWhitespace, duplicateColumn, duplicateRow, editQuotedHistory, effectiveWidths, emailBackgroundPalette, emailDocument, emailExtensions, emailFontFamilies, emailFontSizes, emailPlainText, emailSizeBudget, emailTextPalette, embedsAsLinks, entitySpans, escapeAttribute$2 as escapeAttribute, escapeText$1 as escapeText, expressionDialect, expressionDialectKey, extensionActions, extensionSuggestions, fillTextColor, filledPlaceholderAttrs, filterEmailStyle, findColumnContext, findColumnsContext, findCssIssues, findListDepth, findSectionContext, findTableContext, fitImageWidth, foldHTML, formatHTML, formatPct, forwardDocument, forwardQuote, hasBlockChildren$1 as hasBlockChildren, hasDocumentEnvelope, headingStyle, holdsBlock, holdsColumn, hrefProblem, htmlSourceExtensions, imageDropTarget, importLoss, importedDocument, inheritTextStyles, inheritedMarkup, inlineBootstrapTableBorders, inlineImageCids, inlineImageRegistry, inlineImagesKey, inlineStyles, insertHTML, insertMergeTag, isActionEnabled, isBlock$1 as isBlock, isBlockLevel, isBlockish, isBooleanAttribute, isEmailAttribute, isEmailCssProperty, isEmailTag, isFillTextColor, isInlineAtom, isMarkActive, isMergeTagExpression, isMergeTagPath, isNodeActive, isPadded, isProposing, isQuotedHistoryFolded, isSafeColor, isSafeFontFamily, isSafeFontSize, isSafeUrl$1 as isSafeUrl, isSectionImageUrl, isStreaming, italicStarInputRegex, italicStarPasteRegex, italicUnderscoreInputRegex, italicUnderscorePasteRegex, keepGmailRules, layoutBlockAt, linkClickPlugin, linkPastePlugin, linkRangeAt, lintHTML, lowerInlinesAroundBlocks, markAttrsOf, markStackingColumns, marksAcrossBreak, materializeComments, measureMenuPlacement, mediaMatches, mergeTagAt, mergeTagExpressions, mergeTagFields, mergeTagRanges, mergeTagResolutionPending, moveColumn, moveRow, normalizeCommentText, normalizeCss, normalizeHref, normalizeMergeTagText, noteOwnWidths, openFileChooser, openTag$1 as openTag, openTags, outlookColumns, paddingOf, parseAngularExpression, parseDocument, parseFontFamily, parseFontSize, parseHTML, parsePadding, parseRules, parseTextTransform, passesDualBackground, passesDualContrast, preserveExtensions, preservedAttributes, promoteInlineImages, promoteMergeTags, proposalRange, proposeContent, quotedHistory, readEnvelope, readImageFile, realizeComments, relativeLuminance, removeColumn, removeQuotedHistory, renderForClient, renderedPath, repairLinkSchemes, repairTables, replaceHTML, replyDocument, replyEnvelope, replyQuote, reviseProposal, rewriteInlineImageSources, richTextExtensions, ruleForScope, ruleScope, sanitizePastedHTML, scanHTML, scanMergeTags, scopeOf$1 as scopeOf, scopedDocumentCss, selectColumn, selectRow, selectedButton, selectedImage, selectedImageAlt, selectedMarkAtom, selectionInsideMergeTag, serializeDocument, serializeToHTML, setCellBackground, setColumnBackground, setColumnBoundary, setColumnsBoundary, setQuotedHistory, setSectionBackground, setSectionImage, setTableBox, setTableWidth, soleInlineAtom, splitDeclarations$2 as splitDeclarations, splitListItemKeepMarks, starInputRegex, starPasteRegex, stopProposal, streamContent, streamedRange, streamingRange, suggestionSectionTitles, tableStyle, textOffsetAt, textRegions, toEmailSafeColor, toInboundMessage, toggleList, trimInlineBoxes, underscoreInputRegex, underscorePasteRegex, unwrapLayoutTables, unwrapOfficeParagraphMarks, verbatimElement, videoPage, watchMenuScroll, withAttribute, withAuthoredMarkup, withQuoted, withQuotedHistory, withStyleDeclaration, withoutAttribute, withoutStyleDeclaration };
19026
+ export { AUTHORED_ATTR, AUTHORED_SPEC_KEY, AUTHORED_STYLE_DEFAULTS_KEY, AuthoredEnter, BLOCKQUOTE_STYLE, BLOCK_MENU_FOCUS_KEY, BUTTON_FILL, BUTTON_STYLE, BaseKeymap, Blockquote, Bold, BulletList, Button, CARET_ROOM, CLIENT_LABELS, COLUMN_STARTING_LINES, COMMENT_TAG, CONTENT_ATTRS, CSS_SUPPORT, ClearFormatting, CodeLine, CodeLineIndent, Column, ColumnResize, Columns, ColumnsResize, DOCUMENT_CLASS, DOCUMENT_TAGS, DUAL_BACKGROUND_MIN_RATIO, DUAL_CONTRAST_DARK, DUAL_CONTRAST_LIGHT, DUAL_CONTRAST_MIN_RATIO, Divider, Document$1 as Document, DocumentStyles, EMAIL_ATTRIBUTES, EMAIL_CSS_PROPERTIES, EMAIL_SAFE_STYLE_PROPERTIES, EMAIL_SAFE_TAGS, EMAIL_TAGS, EMAIL_TAG_ATTRIBUTES, EmailParagraph, FILE_CHOOSER_POINTER_WAIT, FILL_TEXT_COLOR, FILL_TEXT_COLOR_LIGHT, FILL_TEXT_COLOR_RGB, FOLD_WIDTH, FORMAT_WIDTH, GMAIL_CLIP_BYTES, GUIDES_ACTIVE_CLASS, GUIDES_PEEK_CLASS, GUIDES_PEEK_DELAY_MS, Gapcursor, HEADING_SIZES, HardBreak, Heading, History, HtmlComment, HtmlCommentInline, HtmlElement, HtmlInline, HtmlLanguage, HtmlRaw, HtmlTextElement, HtmlVoid, HtmlVoidBlock, IMPORT_VIEWPORT_WIDTH, INDENT_MAX, INDENT_STEP, INLINE_TAGS, INLINE_VOID_TAGS, IN_SELECTION_CLASS, Image, InlineAtomSelection, InlineImageStore, InlineImages, Italic, LIST_STYLE, LayoutGuides, Link, ListItem, MAX_COLUMNS, MAX_IMAGE_WIDTH, MAX_MERGE_TAG_LENGTH, MAX_UNBROKEN_RUN, MIN_COLUMN_CAP, MIN_COLUMN_PCT, MIN_FONT_SIZE, MIN_IMAGE_WIDTH, MIN_TABLE_PCT, MJML_WORLDLY_HTML, MergeTag, NoTextDrag, OrderedList, PARSE_SCOPE_KEY, PLACEHOLDER_WIDTH, Paragraph, PasteHygiene, QuotedHistory, REFUSED_TAGS, RENDERING_CLIENTS, SECTION_CONTENT_MAX, SECTION_INSET, SECTION_PADDING, STACK_CLASS, STACK_RULE, Section, SplitKeepingMarks, TABLE_BLOCK_SPACING, TABLE_BORDER_COLOR, TABLE_CELL_PADDING, TABLE_SECTION_TAGS, TYPING_REST, Table, TableCell, TableRow, Text, TextStyle, UNSET_BUTTON_HREF, Underline, VOID_TAGS, acceptProposal, addColumn, addColumnAtEnd, addRowAtEnd, analyzeAngularExpression, angularDialect, angularRequiredFields, appendColumn, applyAuthoredAttributes, applyEmailVocabulary, attributeString, authoredAttribute, authoredMarkupOf, authoredViewAttributes, bareButtons, buttonStyle, canonicalHead, canonicalMergeTagInner, canonicalRender, caretCellPos, caretInsideMergeTag, claimedImageFiles, clearCells, clientList, columnCaps, columnWidths, completionContextAt, containerStyle, contrastRatio, correctStoredMistakes, createAngularExpressions, createBlockMenu, createBubbleMenu, createButtonEdit, createContentProposal, createContentStream, createDOMParser, createEditor, createHtmlAutocomplete, createHtmlLanguage, createImageDrag, createInlineImages, createMergeTagResolution, createOffsetMapper, createQuotedHistory, createSchema, createSendIntent, createSourceMarks, createSuggestionMenu, createTableHandles, declarationProperty, declaredBackground, decodeDataUrl, defaultBubbleShouldShow, defineExtension, defineMark, defineNode, deleteColumns, deleteFullySelected, discardProposal, distributePadding, docText, dropDeadOfficeClasses, dropHidden, dropMediaQueries, dropZeroSizedWhitespace, duplicateColumn, duplicateRow, editQuotedHistory, effectiveWidths, emailBackgroundPalette, emailDocument, emailExtensions, emailPlainText, emailSizeBudget, emailTextPalette, embedsAsLinks, entitySpans, escapeAttribute$2 as escapeAttribute, escapeText$1 as escapeText, expressionDialect, expressionDialectKey, extensionActions, extensionSuggestions, fillTextColor, filledPlaceholderAttrs, filterEmailStyle, findColumnContext, findColumnsContext, findCssIssues, findListDepth, findSectionContext, findTableContext, fitImageWidth, foldHTML, formatHTML, formatPct, forwardDocument, forwardQuote, hasBlockChildren$1 as hasBlockChildren, hasDocumentEnvelope, headingStyle, holdsBlock, holdsColumn, hrefProblem, htmlSourceExtensions, imageDropTarget, importLoss, importedDocument, inheritTextStyles, inheritedMarkup, inlineBootstrapTableBorders, inlineImageCids, inlineImageRegistry, inlineImagesKey, inlineStyles, insertHTML, insertMergeTag, isActionEnabled, isBlock$1 as isBlock, isBlockLevel, isBlockish, isBooleanAttribute, isEmailAttribute, isEmailCssProperty, isEmailTag, isFillTextColor, isInlineAtom, isMarkActive, isMergeTagExpression, isMergeTagPath, isNodeActive, isPadded, isProposing, isQuotedHistoryFolded, isSafeColor, isSafeFontFamily, isSafeFontSize, isSafeUrl$1 as isSafeUrl, isSectionImageUrl, isStreaming, italicStarInputRegex, italicStarPasteRegex, italicUnderscoreInputRegex, italicUnderscorePasteRegex, keepGmailRules, layoutBlockAt, linkClickPlugin, linkPastePlugin, linkRangeAt, lintHTML, lowerInlinesAroundBlocks, markAttrsOf, markStackingColumns, marksAcrossBreak, materializeComments, measureMenuPlacement, mediaMatches, mergeTagAt, mergeTagExpressions, mergeTagFields, mergeTagRanges, mergeTagResolutionPending, moveColumn, moveRow, normalizeCommentText, normalizeCss, normalizeHref, normalizeMergeTagText, noteOwnWidths, openFileChooser, openTag$1 as openTag, openTags, outlookColumns, paddingOf, parseAngularExpression, parseDocument, parseFontFamily, parseFontSize, parseHTML, parsePadding, parseRules, parseTextTransform, passesDualBackground, passesDualContrast, preserveExtensions, preservedAttributes, promoteInlineImages, promoteMergeTags, proposalRange, proposeContent, quotedHistory, readEnvelope, readImageFile, realizeComments, relativeLuminance, removeColumn, removeLastColumn, removeQuotedHistory, renderForClient, renderedPath, repairLinkSchemes, repairTables, replaceHTML, replyDocument, replyEnvelope, replyQuote, reviseProposal, rewriteInlineImageSources, richTextExtensions, ruleForScope, ruleScope, sanitizePastedHTML, scanHTML, scanMergeTags, scopeOf$1 as scopeOf, scopedDocumentCss, selectColumn, selectRow, selectedButton, selectedImage, selectedImageAlt, selectedMarkAtom, selectionInsideMergeTag, serializeDocument, serializeToHTML, setCellBackground, setColumnBackground, setColumnBoundary, setColumnsBoundary, setQuotedHistory, setSectionBackground, setSectionImage, setTableBox, setTableWidth, soleInlineAtom, splitDeclarations$2 as splitDeclarations, splitListItemKeepMarks, starInputRegex, starPasteRegex, stopProposal, streamContent, streamedRange, streamingRange, suggestionSectionTitles, tableStyle, textOffsetAt, textRegions, toEmailSafeColor, toInboundMessage, toggleList, trimInlineBoxes, underscoreInputRegex, underscorePasteRegex, unwrapLayoutTables, unwrapOfficeParagraphMarks, verbatimElement, videoPage, watchMenuScroll, withAttribute, withAuthoredMarkup, withQuoted, withQuotedHistory, withStyleDeclaration, withoutAttribute, withoutStyleDeclaration };
18844
19027
  //# sourceMappingURL=h-k-dev-angular-email-editor.mjs.map