@h-k-dev/angular-email-editor 0.0.4 → 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.
@@ -2231,11 +2231,12 @@ function tokens(value) {
2231
2231
  * engine handles both — so merged cells are a real feature here, and imported
2232
2232
  * mail keeps its shape instead of being flattened.
2233
2233
  */
2234
- // The serialized table is intentionally borderless — grid lines are an
2235
- // editor-only editing aid (see the `.ProseMirror table` rules in the app's
2236
- // global styles), not part of the email a recipient receives. Padding is a
2237
- // fixed, responsive value (horizontal padding eats a phone's width, so it
2238
- // 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).
2239
2240
  // `table-layout: fixed` is the difference between a table you can type in and
2240
2241
  // one that jumps: without it, column widths are computed from *content*, so
2241
2242
  // every keystroke re-lays out the whole grid and the column you are typing in
@@ -2253,19 +2254,23 @@ function tokens(value) {
2253
2254
  40% of the container, in every client and in the editor alike). The
2254
2255
  offset serializes as `margin-left` — inline margins on tables are what
2255
2256
  Outlook's own Word composer emits, so its engine reads them, and a client
2256
- that strips them degrades the table gracefully to left-aligned. */
2257
- 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)};` +
2258
2263
  (offset > 0 ? ` margin-left: ${formatPct(offset)};` : '') +
2264
+ (spacing ? ` margin-top: ${spacing}px; margin-bottom: ${spacing}px;` : '') +
2259
2265
  ' table-layout: fixed; border-collapse: collapse;';
2260
2266
  // Fixed layout means a long unbroken word can no longer widen its column, so it
2261
2267
  // would spill out of the cell instead. The editor never showed that (the
2262
2268
  // editable root sets `word-wrap` for its own reasons, and it inherits), which
2263
2269
  // is exactly why it has to be said out loud here — otherwise the overflow shows
2264
2270
  // up only in the recipient's client.
2265
- // No padding here: the email carries only *authored* padding (the `padding`
2266
- // attr, parsed off user markup). The comfortable default spacing seen while
2267
- // composing is editorial — an `.aee-editor` CSS rule the serializer never
2268
- // 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.
2269
2274
  const cellStyle = (valign) => `vertical-align: ${valign}; overflow-wrap: break-word;`;
2270
2275
  /** A cell's horizontal alignment off parsed markup: the inline style first,
2271
2276
  then the legacy `align` attribute — the pair real mail is written with,
@@ -2310,13 +2315,15 @@ function parsePadding(dom) {
2310
2315
  `<td>` is the bulletproof border in email — Outlook's Word engine renders
2311
2316
  it), with `border-collapse` on the table keeping the grid single-lined. */
2312
2317
  const TABLE_BORDER_COLOR = 'rgb(208, 215, 229)';
2313
- /** The bordered table's *real* cell padding — Excel-style breathing room, so
2314
- the rendered email is readable inside its grid. Deliberately the same
2315
- value as the editor's editorial default spacing (the app's `.aee-editor
2316
- table td` rule): a bordered and a plain table feel identical while
2317
- composing; only what the recipient gets differs — the preset ships its
2318
- 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. */
2319
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;
2320
2327
  /** Canonical percentage: one decimal at most, no trailing zero (25%, 33.3%). */
2321
2328
  const formatPct = (n) => `${Math.round(n * 10) / 10}%`;
2322
2329
  /** No column below 10%: at 320px that is ~32px — the width where a column
@@ -2341,7 +2348,18 @@ function pctOf(raw) {
2341
2348
  function parseTableBox(dom) {
2342
2349
  const width = Math.min(Math.max(pctOf(dom.style?.width || dom.getAttribute('width') || '') ?? 100, MIN_TABLE_PCT), 100);
2343
2350
  const offset = Math.min(Math.max(pctOf(dom.style?.marginLeft || '') ?? 0, 0), 100 - width);
2344
- 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;
2345
2363
  }
2346
2364
  /** A `colspan`/`rowspan` off parsed markup: 1 when absent, malformed, zero or
2347
2365
  negative — real mail carries all of those, and the grid must stay sane. */
@@ -2454,12 +2472,14 @@ const TableCell = defineNode({
2454
2472
  // Same attr name the library's commands expect, different unit on
2455
2473
  // purpose; pixel widths never parse in (see the node docs).
2456
2474
  colwidth: { default: null },
2457
- // Authored padding only (px, via `parsePadding`); `null` — the default —
2458
- // 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`).
2459
2479
  padding: { default: null },
2460
2480
  // The cell's grid-line colour (`border: 1px solid <color>`), or null
2461
- // for the borderless default. Set for every cell of a `/bordered-table`
2462
- // 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`).
2463
2483
  border: { default: null },
2464
2484
  // A fill colour on the cell (the most bulletproof background in email —
2465
2485
  // `background-color` on `<td>` renders even in Outlook). From the curated
@@ -2510,7 +2530,9 @@ const Table = defineNode({
2510
2530
  allowGapCursor: false,
2511
2531
  // Width and left offset in percent of the container; the defaults
2512
2532
  // (100, 0) serialize identically to the pre-attr canonical form.
2513
- 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 } },
2514
2536
  parseDOM: [{ tag: 'table', getAttrs: (dom) => parseTableBox(dom) }],
2515
2537
  // <tbody> wrapper matches what mail clients expect and what the HTML
2516
2538
  // parser re-injects, so serialize → parse → serialize is a fixpoint.
@@ -2521,14 +2543,15 @@ const Table = defineNode({
2521
2543
  'table',
2522
2544
  {
2523
2545
  role: 'presentation',
2524
- style: tableStyle(node.attrs['width'], node.attrs['offset']),
2546
+ style: tableStyle(node.attrs['width'], node.attrs['offset'], node.attrs['spacing']),
2525
2547
  },
2526
2548
  ['tbody', 0],
2527
2549
  ],
2528
2550
  },
2529
2551
  commands: ({ schema }) => ({
2530
- /** The default shape is the one /table inserts — three rows by two. */
2531
- 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),
2532
2555
  // Selection-relative structure edits, straight from the library: each one
2533
2556
  // understands merged cells and multi-cell selections for free. The
2534
2557
  // add-column pair is wrapped: on a table with declared widths, the library
@@ -2668,15 +2691,15 @@ const Table = defineNode({
2668
2691
  // a rectangle. Covers everything that reaches the editor as a transaction
2669
2692
  // — paste, drops, `setContent`, an import landing on the html signal —
2670
2693
  // while `repairTables` covers the pure parse path (see `html.ts`).
2671
- // `unifyCellBorders` rides the same pass: cells that structural edits add
2672
- // 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.
2673
2696
  new Plugin({
2674
2697
  key: new PluginKey('tableRepair'),
2675
2698
  appendTransaction: (transactions, oldState, newState) => {
2676
2699
  if (!transactions.some((tr) => tr.docChanged))
2677
2700
  return undefined;
2678
2701
  const tr = fixTables(newState, oldState) ?? newState.tr;
2679
- unifyCellBorders(tr);
2702
+ unifyCellPresets(tr, insertedRanges([...transactions, tr]));
2680
2703
  return tr.docChanged ? tr : undefined;
2681
2704
  },
2682
2705
  }),
@@ -2687,23 +2710,32 @@ const Table = defineNode({
2687
2710
  // Three rows by two: a table asked for by name starts with somewhere to
2688
2711
  // write a heading line and two entries under it, which is what an email
2689
2712
  // table almost always turns out to be. A picker (the app's grid) says its
2690
- // 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.
2691
2715
  actions: ({ schema }) => [
2692
2716
  {
2693
2717
  id: 'table',
2694
2718
  section: 'layout',
2695
2719
  title: 'Table',
2696
- keywords: ['table', 'grid', 'rows', 'columns'],
2697
- icon: 'table_chart',
2698
- 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'),
2699
2723
  },
2700
2724
  {
2701
- id: 'bordered-table',
2725
+ id: 'borderless-table',
2702
2726
  section: 'layout',
2703
- title: 'Bordered table',
2704
- keywords: ['bordered-table', 'table', 'borders', 'grid', 'excel', 'lines'],
2705
- icon: 'grid_on',
2706
- 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'),
2707
2739
  },
2708
2740
  ],
2709
2741
  });
@@ -2729,13 +2761,13 @@ function setCellBackground(color) {
2729
2761
  /** Inserts a table and drops the cursor into its first cell. The table is
2730
2762
  located after insertion (rather than by fragile nodeSize math) and
2731
2763
  `cellStart` resolves the exact text position inside cell (0, 0). */
2732
- function insertTableFocused(schema, rows, cols, border = null) {
2764
+ function insertTableFocused(schema, rows, cols, kind) {
2733
2765
  return (state, dispatch) => {
2734
2766
  // Asked, not told: answer before building anything — a toolbar asks on
2735
2767
  // every transaction.
2736
2768
  if (!dispatch)
2737
2769
  return true;
2738
- const table = buildTable(schema, rows, cols, border);
2770
+ const table = buildTable(schema, rows, cols, kind);
2739
2771
  const from = state.selection.from;
2740
2772
  const tr = state.tr.replaceSelectionWith(table);
2741
2773
  let tablePos = -1;
@@ -3071,49 +3103,90 @@ function isColumnBoundaryClean(map, index) {
3071
3103
  }
3072
3104
  return true;
3073
3105
  }
3074
- function buildTable(schema, rows, cols, border) {
3106
+ function buildTable(schema, rows, cols, kind) {
3075
3107
  const cellType = schema.nodes['tableCell'];
3076
3108
  const rowType = schema.nodes['tableRow'];
3077
3109
  const tableType = schema.nodes['table'];
3078
- // The bordered preset ships real padding with its grid (see
3079
- // TABLE_CELL_PADDING); a plain table ships none unless authored.
3080
- 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
+ };
3081
3116
  const makeRow = () => rowType.create(null, Array.from({ length: cols }, () => cellType.createAndFill(attrs)));
3082
- 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;
3083
3134
  }
3084
3135
  /**
3085
- * Keeps a *uniformly* bordered table uniform: when every bordered cell wears
3086
- * the same colour and some cells have none — which is what any structural
3087
- * edit produces, since `addRow`/`addColumn`/the pills create cells with the
3088
- * default null border — the bare cells inherit it. A table with *mixed*
3089
- * border colours (authored, imported) is left exactly as written: repair,
3090
- * not opinion. `setNodeMarkup` never changes node sizes, so positions
3091
- * 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.
3092
3150
  */
3093
- function unifyCellBorders(tr) {
3151
+ function unifyCellPresets(tr, inserted) {
3152
+ const isNew = (from, to) => inserted.some(([f, t]) => f <= from && to <= t);
3094
3153
  const jobs = [];
3095
3154
  tr.doc.descendants((node, pos) => {
3096
3155
  if (node.type.name !== 'table')
3097
3156
  return true;
3098
- const colors = new Set();
3099
- let bare = 0;
3157
+ const borders = new Set();
3158
+ const paddings = new Set();
3159
+ let unbordered = 0;
3100
3160
  node.descendants((cell) => {
3101
3161
  if (cell.type.name !== 'tableCell')
3102
3162
  return true;
3103
3163
  const border = cell.attrs['border'];
3164
+ const padding = cell.attrs['padding'];
3104
3165
  if (border)
3105
- colors.add(border);
3166
+ borders.add(border);
3106
3167
  else
3107
- bare++;
3108
- return true;
3168
+ unbordered++;
3169
+ if (padding)
3170
+ paddings.add(padding);
3171
+ return false;
3109
3172
  });
3110
- if (colors.size === 1 && bare > 0) {
3111
- 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) {
3112
3176
  node.descendants((cell, rel) => {
3113
- if (cell.type.name === 'tableCell' && !cell.attrs['border']) {
3114
- 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;
3115
3185
  }
3116
- return true;
3186
+ if (attrs['border'] !== cell.attrs['border'] || attrs['padding'] !== cell.attrs['padding']) {
3187
+ jobs.push({ pos: at, attrs });
3188
+ }
3189
+ return false;
3117
3190
  });
3118
3191
  }
3119
3192
  return false; // cells handled; nothing tabular nests deeper
@@ -5548,7 +5621,9 @@ const Columns = defineNode({
5548
5621
  insertColumns: (count = 2) => insertColumns(schema, count),
5549
5622
  /** Fill the column the cursor is in (or clear it with `null`). */
5550
5623
  setColumnBackground: (color) => setColumnBackground(color),
5551
- // 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
5552
5627
  // `addColumnAfter`/`deleteColumn` — commands merge into one flat record.
5553
5628
  addColumn: () => addColumn,
5554
5629
  removeColumn: () => removeColumn,
@@ -5568,12 +5643,17 @@ const Columns = defineNode({
5568
5643
  // column's edge hops straight into the neighbouring column — the layout
5569
5644
  // twin of the table's cell-to-cell arrows. Mod-A is the table's scoped
5570
5645
  // select-all applied here: column content first, block from an empty
5571
- // 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.
5572
5648
  plugins: () => [
5573
5649
  keymap({
5574
5650
  ArrowLeft: columnArrow(-1),
5575
5651
  ArrowRight: columnArrow(1),
5576
5652
  'Mod-a': selectColumnContent,
5653
+ Backspace: deleteFullySelectedColumns,
5654
+ 'Mod-Backspace': deleteFullySelectedColumns,
5655
+ Delete: deleteFullySelectedColumns,
5656
+ 'Mod-Delete': deleteFullySelectedColumns,
5577
5657
  }),
5578
5658
  ],
5579
5659
  actions: ({ schema }) => [
@@ -5604,6 +5684,16 @@ function hasColumnChildren(dom) {
5604
5684
  }
5605
5685
  return false;
5606
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
+ }
5607
5697
  /** Inserts an n-column block and drops the cursor into the first column. */
5608
5698
  function insertColumns(schema, count) {
5609
5699
  return (state, dispatch) => {
@@ -5611,10 +5701,9 @@ function insertColumns(schema, count) {
5611
5701
  // every transaction.
5612
5702
  if (!dispatch)
5613
5703
  return true;
5614
- const colType = schema.nodes['column'];
5615
5704
  const columnsType = schema.nodes['columns'];
5616
5705
  const maxWidth = columnMaxWidth(count);
5617
- const columns = Array.from({ length: count }, () => colType.createAndFill({ maxWidth }));
5706
+ const columns = Array.from({ length: count }, () => freshColumn(schema, { maxWidth }));
5618
5707
  const node = columnsType.create(null, columns);
5619
5708
  const from = state.selection.from;
5620
5709
  const tr = state.tr.replaceSelectionWith(node);
@@ -5721,9 +5810,7 @@ const addColumn = (state, dispatch) => {
5721
5810
  const existing = [];
5722
5811
  ctx.node.forEach((col) => existing.push(col));
5723
5812
  const columns = resplitColumns(existing, count);
5724
- const fresh = state.schema.nodes['column'].createAndFill({
5725
- maxWidth: columnMaxWidth(count),
5726
- });
5813
+ const fresh = freshColumn(state.schema, { maxWidth: columnMaxWidth(count) });
5727
5814
  columns.splice(ctx.index + 1, 0, fresh);
5728
5815
  const tr = state.tr.replaceWith(ctx.pos, ctx.pos + ctx.node.nodeSize, ctx.node.type.create(ctx.node.attrs, columns));
5729
5816
  // The fresh column starts after the block's opening token (+1) and every
@@ -5764,6 +5851,107 @@ const deleteColumns = (state, dispatch) => {
5764
5851
  dispatch?.(state.tr.delete(ctx.pos, ctx.pos + ctx.node.nodeSize).scrollIntoView());
5765
5852
  return true;
5766
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
+ };
5767
5955
  /**
5768
5956
  * Tab / Shift-Tab inside a columns block: move to the next / previous column,
5769
5957
  * and past the block's ends hand off to the surrounding blocks. Always claims
@@ -5862,7 +6050,12 @@ const selectColumnContent = (state, dispatch) => {
5862
6050
  if (depth < 0)
5863
6051
  return false;
5864
6052
  const column = $from.node(depth);
5865
- 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
+ });
5866
6059
  if (dispatch) {
5867
6060
  if (empty) {
5868
6061
  dispatch(state.tr.setSelection(NodeSelection.create(state.doc, $from.before(depth - 1))));
@@ -13622,7 +13815,7 @@ class TableView {
13622
13815
  this.#box = this.dom.appendChild(document.createElement('div'));
13623
13816
  this.#box.className = 'aee-table-box';
13624
13817
  this.#table = this.#box.appendChild(document.createElement('table'));
13625
- 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']));
13626
13819
  this.#table.setAttribute('role', 'presentation');
13627
13820
  this.#colgroup = this.#table.appendChild(document.createElement('colgroup'));
13628
13821
  this.contentDOM = this.#table.appendChild(document.createElement('tbody'));
@@ -13752,7 +13945,7 @@ class TableView {
13752
13945
  // is CSS-inset to the wrapper's content box (inside the editor-only
13753
13946
  // gutter), so a child's `left` percentage is a share of the table's own
13754
13947
  // area — table-relative shares are converted once, here.
13755
- this.#table.setAttribute('style', tableStyle(tableWidth, offset));
13948
+ this.#table.setAttribute('style', tableStyle(tableWidth, offset, node.attrs['spacing']));
13756
13949
  // An authored table (a template's, parsed in the `email` mode) shows its
13757
13950
  // own attributes, as the email does — see authored.ts.
13758
13951
  applyAuthoredAttributes(this.#table, node, 0);
@@ -13922,7 +14115,9 @@ class TableView {
13922
14115
  rows are the same object (ProseMirror's persistent tree), so a keystroke
13923
14116
  is one new cell and a handful of identity compares. */
13924
14117
  function sameTableChrome(a, b) {
13925
- 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']) {
13926
14121
  return false;
13927
14122
  }
13928
14123
  if (a.childCount !== b.childCount)
@@ -14054,6 +14249,8 @@ class ColumnsView {
14054
14249
  contentDOM;
14055
14250
  #box;
14056
14251
  #lines;
14252
+ #append;
14253
+ #remove;
14057
14254
  #node;
14058
14255
  #view;
14059
14256
  #getPos;
@@ -14076,6 +14273,14 @@ class ColumnsView {
14076
14273
  this.#lines.className = 'aee-col-lines';
14077
14274
  this.#lines.contentEditable = 'false';
14078
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);
14079
14284
  this.#render(node);
14080
14285
  }
14081
14286
  update(node) {
@@ -14091,11 +14296,34 @@ class ColumnsView {
14091
14296
  target === this.#box ||
14092
14297
  target === this.#lines ||
14093
14298
  this.#lines.contains(target) ||
14299
+ // The pills and their zones are ours too.
14300
+ (target instanceof Element && !!target.closest('.aee-add-zone')) ||
14094
14301
  // The contentDOM's own attributes are ours; its children are ProseMirror's.
14095
14302
  (target === this.contentDOM && record.type === 'attributes'));
14096
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
+ }
14097
14321
  #render(node) {
14098
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);
14099
14327
  // An authored columns block shows its own attributes (authored.ts).
14100
14328
  applyAuthoredAttributes(this.#box, node, 0);
14101
14329
  const caps = columnCaps(node);
@@ -17300,29 +17528,43 @@ function discardTransaction(view) {
17300
17528
  /**
17301
17529
  * Where a floating menu goes for an anchor line: under it, clamped to its
17302
17530
  * scroll container, flipped above when below would overflow the visible
17303
- * area. Read-only — it measures and returns, and the caller writes `left` /
17304
- * `top` in its own write phase, so measuring never follows a write in the
17305
- * 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
17306
17538
  * `position: relative` scroll container; its size is read as rendered, so
17307
- * 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.
17308
17542
  */
17309
- function measureMenuPlacement(anchor, element, offset) {
17543
+ function measureMenuPlacement(anchor, element, offset, height = element.offsetHeight) {
17310
17544
  const container = element.offsetParent ?? element.parentElement;
17311
17545
  if (!container)
17312
17546
  return null;
17313
17547
  const containerRect = container.getBoundingClientRect();
17314
- const { offsetWidth: width, offsetHeight: height } = element;
17548
+ const width = element.offsetWidth;
17315
17549
  const { scrollLeft, scrollTop, scrollWidth, clientHeight } = container;
17316
17550
  const left = Math.min(Math.max(anchor.left - containerRect.left + scrollLeft, 0), Math.max(scrollWidth - width, 0));
17317
- // Below the line; flip above when it would overflow the visible part of
17318
- // the scroll container.
17319
- let top = anchor.bottom - containerRect.top + scrollTop + offset;
17320
- if (top + height > scrollTop + clientHeight) {
17321
- const above = anchor.top - containerRect.top + scrollTop;
17322
- if (above - height - offset >= scrollTop)
17323
- top = above - height - offset;
17324
- }
17325
- 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 };
17326
17568
  }
17327
17569
 
17328
17570
  /** The dialect installed on an editor, as plugin state, so the send intent
@@ -18781,5 +19023,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
18781
19023
  * Generated bundle index. Do not edit.
18782
19024
  */
18783
19025
 
18784
- 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, 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 };
18785
19027
  //# sourceMappingURL=h-k-dev-angular-email-editor.mjs.map