markupeditor 0.9.40 → 0.9.42

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,7 @@
3
3
  "selectImage": false,
4
4
  "insertLink": false,
5
5
  "insertImage": false,
6
+ "insertTable": false,
6
7
  "highlightCode": true,
7
8
  "markdownShorthand": true
8
9
  }
@@ -15149,6 +15149,16 @@ const tNodes = tableNodes({
15149
15149
  attrs.style = (attrs.style || '') + `background-color: ${value};`;
15150
15150
  },
15151
15151
  },
15152
+ align: {
15153
+ default: null,
15154
+ getFromDOM(dom) {
15155
+ return dom.style.textAlign || null;
15156
+ },
15157
+ setDOMAttr(value, attrs) {
15158
+ if (value)
15159
+ attrs.style = (attrs.style || '') + `text-align: ${value};`;
15160
+ },
15161
+ },
15152
15162
  }
15153
15163
  });
15154
15164
  tNodes.table.attrs = {class: {default: null}};
@@ -34840,7 +34850,7 @@ function _getSelectionState() {
34840
34850
  state['thead'] = tableAttributes.thead;
34841
34851
  state['tbody'] = tableAttributes.tbody;
34842
34852
  state['header'] = tableAttributes.header;
34843
- state['colspan'] = tableAttributes.colspan;
34853
+ state['colspan'] = (tableAttributes.colspan ?? 0) > 1; // A real boolean; Swift's SelectionState.colspan is typed Bool and cannot decode a raw JS number
34844
34854
  state['rows'] = tableAttributes.rows;
34845
34855
  state['cols'] = tableAttributes.cols;
34846
34856
  state['row'] = tableAttributes.row;
@@ -35041,8 +35051,8 @@ function _getTableAttributes(state) {
35041
35051
  case nodeTypes.table_header:
35042
35052
  attributes.thead = true; // We selected the header
35043
35053
  attributes.tbody = false;
35044
- attributes.row = $pos.index() + 1; // The row will be 1 by definition
35045
- attributes.col = 1; // Headers are always colspanned, so col=1
35054
+ attributes.row = 0; // Header cells are row 0 by definition, not a body row index
35055
+ attributes.col = $pos.index() + 1; // A header cell's own column position, whether colspanned or per-column
35046
35056
  return true;
35047
35057
  case nodeTypes.table_row:
35048
35058
  attributes.row = $pos.index() + 1; // We are in some row, but could be the header row
@@ -35165,8 +35175,8 @@ function callbackInsertLink() {
35165
35175
 
35166
35176
  /**
35167
35177
  * Callback to signal that the user wants to insert an image.
35168
- *
35169
- * The messageHandler will need to bring up a dialog to identify
35178
+ *
35179
+ * The messageHandler will need to bring up a dialog to identify
35170
35180
  * the image and then execute insertImage.
35171
35181
  */
35172
35182
  function callbackInsertImage() {
@@ -35174,6 +35184,17 @@ function callbackInsertImage() {
35174
35184
  _callback(JSON.stringify(messageDict));
35175
35185
  }
35176
35186
 
35187
+ /**
35188
+ * Callback to signal that the user wants to insert or edit a table.
35189
+ *
35190
+ * The messageHandler will need to bring up a dialog to size a new table
35191
+ * (and then execute insertTable) or edit the table the selection is in.
35192
+ */
35193
+ function callbackInsertTable() {
35194
+ let messageDict = { 'messageType' : 'insertTable' };
35195
+ _callback(JSON.stringify(messageDict));
35196
+ }
35197
+
35177
35198
  /**
35178
35199
  * Callback to signal that user-provided CSS and/or script files have
35179
35200
  * been loaded.
@@ -36011,7 +36032,7 @@ function _copyImage(node) {
36011
36032
  * @param {number} rows The number of rows in the table to be created.
36012
36033
  * @param {number} cols The number of columns in the table to be created.
36013
36034
  */
36014
- function insertTable(rows, cols) {
36035
+ function insertTable$1(rows, cols) {
36015
36036
  const view = activeView();
36016
36037
  if ((rows < 1) || (cols < 1)) return;
36017
36038
  let command = insertTableCommand(rows, cols);
@@ -36021,6 +36042,7 @@ function insertTable(rows, cols) {
36021
36042
  function insertTableCommand(rows, cols) {
36022
36043
  const commandAdapter = (viewState, dispatch, view) => {
36023
36044
  let state = view?.state ?? viewState;
36045
+ if (isTableSelected(state)) return false; // Tables cannot be nested
36024
36046
  const nodeTypes = state.schema.nodes;
36025
36047
  const table_rows = [];
36026
36048
  for (let j = 0; j < rows; j++) {
@@ -36060,15 +36082,8 @@ function insertTableCommand(rows, cols) {
36060
36082
  } return (pPos == undefined); // Keep going if pPos hasn't been defined
36061
36083
  });
36062
36084
  }
36063
- // Set the selection in the first cell, apply it to the state and the view.
36064
- // We have to special-case for empty documents to get selection in the 1st cell.
36065
- let empty = (view.state.doc.textContent.length == 0);
36066
- let textSelection;
36067
- if (empty) {
36068
- textSelection = TextSelection.near(transaction.doc.resolve(pPos), -1);
36069
- } else {
36070
- textSelection = TextSelection.near(transaction.doc.resolve(pPos));
36071
- }
36085
+ // Set the selection in the first cell, applied to the state and the view.
36086
+ let textSelection = TextSelection.near(transaction.doc.resolve(pPos));
36072
36087
  transaction = transaction.setSelection(textSelection);
36073
36088
  state = state.apply(transaction);
36074
36089
  view.updateState(state);
@@ -36098,12 +36113,20 @@ function addRow(direction) {
36098
36113
  return result;
36099
36114
  }
36100
36115
  function addRowCommand(direction) {
36101
- const commandAdapter = (state, dispatch, view) => {
36116
+ const commandAdapter = (viewState, dispatch, view) => {
36117
+ let state = view?.state ?? viewState;
36118
+ let result;
36102
36119
  if (direction === 'BEFORE') {
36103
- return addRowBefore(state, dispatch);
36120
+ result = addRowBefore(state, (tr) => { state = state.apply(tr); });
36104
36121
  } else {
36105
- return addRowAfter(state, dispatch);
36106
- } };
36122
+ result = addRowAfter(state, (tr) => { state = state.apply(tr); });
36123
+ } if (!result) return false;
36124
+ _propagateColumnAlign(state, (tr) => { state = state.apply(tr); });
36125
+ if (dispatch) {
36126
+ view.updateState(state);
36127
+ }
36128
+ return true;
36129
+ };
36107
36130
 
36108
36131
  return commandAdapter;
36109
36132
  }
@@ -36130,14 +36153,18 @@ function addColCommand(direction) {
36130
36153
  let state = view?.state ?? viewState;
36131
36154
  if (!isTableSelected(state)) return false;
36132
36155
  const startSelection = new TextSelection(state.selection.$anchor, state.selection.$head);
36156
+ const wasColspannedHeader = _hasColspannedHeader(state);
36133
36157
  let offset = 0;
36134
36158
  if (direction === 'BEFORE') {
36135
36159
  addColumnBefore(state, (tr) => { state = state.apply(tr); });
36136
36160
  offset = 4; // An empty cell
36137
36161
  } else {
36138
36162
  addColumnAfter(state, (tr) => { state = state.apply(tr); });
36139
- } _mergeHeaders(state, (tr) => { state = state.apply(tr); });
36140
-
36163
+ } // Re-merge only a colspanned caption header that this add just split into two cells.
36164
+ // A genuine per-column header must not be merged -- it should just gain one more cell.
36165
+ if (wasColspannedHeader) {
36166
+ _mergeHeaders(state, (tr) => { state = state.apply(tr); });
36167
+ }
36141
36168
  if (dispatch) {
36142
36169
  const $anchor = state.tr.doc.resolve(startSelection.from + offset);
36143
36170
  const $head = state.tr.doc.resolve(startSelection.to + offset);
@@ -36180,7 +36207,8 @@ function addHeaderCommand(colspan = true) {
36180
36207
  toggleHeaderRow(state, (tr) => { state = state.apply(tr); });
36181
36208
  if (colspan) {
36182
36209
  _mergeHeaders(state, (tr) => { state = state.apply(tr); });
36183
- }
36210
+ } _propagateColumnAlign(state, (tr) => { state = state.apply(tr); });
36211
+
36184
36212
  if (dispatch) {
36185
36213
  // At this point, the state.selection is in the new header row we just added. By definition,
36186
36214
  // the header is placed before the original selection, so we can add its size to the
@@ -36316,6 +36344,129 @@ function _mergeHeaders(state, dispatch) {
36316
36344
  const newState = state.apply(transaction);
36317
36345
  mergeCells(newState, dispatch);
36318
36346
  }}
36347
+ /**
36348
+ * Return whether the table's header row is currently a single colspanned caption cell
36349
+ * (colspan > 1), as opposed to a genuine per-column header (multiple table_header cells,
36350
+ * each colspan 1) or no header at all. Used by addColCommand to decide whether adding a
36351
+ * column should re-merge the header row (maintaining a caption) or leave it alone (a real
36352
+ * per-column header simply gains one more per-column cell).
36353
+ *
36354
+ * @ignore
36355
+ */
36356
+ function _hasColspannedHeader(state) {
36357
+ const nodeTypes = state.schema.nodes;
36358
+ const headers = [];
36359
+ let tableAttributes = _getTableAttributes(state);
36360
+ if (!tableAttributes.table) return false;
36361
+ state.tr.doc.nodesBetween(tableAttributes.from, tableAttributes.to, (node) => {
36362
+ if (node.type == nodeTypes.table_header) {
36363
+ headers.push(node);
36364
+ return false;
36365
+ }
36366
+ return true;
36367
+ });
36368
+ return (headers.length === 1) && ((headers[0].attrs.colspan ?? 1) > 1);
36369
+ }
36370
+ /**
36371
+ * Propagate each column's existing alignment onto any cell in that column that doesn't have
36372
+ * one yet -- e.g., a cell in a row addRowCommand or addHeaderCommand just inserted, which
36373
+ * otherwise defaults to align: null and would silently drop the column's alignment on export.
36374
+ *
36375
+ * Columns are identified by a cell's index within its row, which assumes every row has the
36376
+ * same number of cells -- true for any table this editor can produce or import, since this
36377
+ * schema has no colspanned/rowspanned body cells (only a header row can be colspanned, and a
36378
+ * colspanned header cell is skipped here since it doesn't belong to a single column).
36379
+ *
36380
+ * @ignore
36381
+ */
36382
+ function _propagateColumnAlign(state, dispatch) {
36383
+ const nodeTypes = state.schema.nodes;
36384
+ let tableAttributes = _getTableAttributes(state);
36385
+ if (!tableAttributes.table) return;
36386
+ const tr = state.tr;
36387
+ const columnAligns = {};
36388
+ tr.doc.nodesBetween(tableAttributes.from, tableAttributes.to, (node, pos) => {
36389
+ if ((node.type == nodeTypes.table_cell) || (node.type == nodeTypes.table_header)) {
36390
+ if ((node.attrs.colspan ?? 1) > 1) return false; // A colspanned caption cell doesn't belong to a single column
36391
+ const col = tr.doc.resolve(pos).index();
36392
+ if ((columnAligns[col] === undefined) && node.attrs.align) {
36393
+ columnAligns[col] = node.attrs.align;
36394
+ }
36395
+ return false;
36396
+ }
36397
+ return true;
36398
+ });
36399
+ if (Object.keys(columnAligns).length === 0) return;
36400
+ let changed = false;
36401
+ tr.doc.nodesBetween(tableAttributes.from, tableAttributes.to, (node, pos) => {
36402
+ if ((node.type == nodeTypes.table_cell) || (node.type == nodeTypes.table_header)) {
36403
+ if ((node.attrs.colspan ?? 1) > 1) return false;
36404
+ const col = tr.doc.resolve(pos).index();
36405
+ const align = columnAligns[col];
36406
+ if (align && !node.attrs.align) {
36407
+ tr.setNodeMarkup(pos, null, {...node.attrs, align});
36408
+ changed = true;
36409
+ }
36410
+ return false;
36411
+ }
36412
+ return true;
36413
+ });
36414
+ if (changed && dispatch) {
36415
+ dispatch(tr);
36416
+ }
36417
+ }
36418
+ /**
36419
+ * Justify (set text alignment on) the column containing the selection. Applies to every cell
36420
+ * in that column -- header and body alike -- not just the selected cell, since GFM's Markdown
36421
+ * alignment is a per-column, not per-cell, concept. A colspanned (single-cell caption) header
36422
+ * has no single column to target and is left untouched.
36423
+ *
36424
+ * @param {'left' | 'center' | 'right' | null} align The alignment to set, or null/falsy to clear it.
36425
+ */
36426
+ function justifyColumn(align) {
36427
+ if (_tableSelected()) {
36428
+ const view = activeView();
36429
+ let command = justifyColumnCommand(align);
36430
+ let result = command(view.state, view.dispatch, view);
36431
+ stateChanged(view);
36432
+ view.focus();
36433
+ return result;
36434
+ }
36435
+ }
36436
+ function justifyColumnCommand(align) {
36437
+ // 'left' means "clear", matching an unaligned GFM column having no delimiter-row marker
36438
+ // at all -- an alias here rather than relying on every caller to pass null/undefined.
36439
+ const resolvedAlign = (align && align !== 'left') ? align : null;
36440
+ const commandAdapter = (state, dispatch) => {
36441
+ if (!isTableSelected(state)) return false;
36442
+ const nodeTypes = state.schema.nodes;
36443
+ const tableAttributes = _getTableAttributes(state);
36444
+ if (!tableAttributes.table) return false;
36445
+ if (tableAttributes.thead && (tableAttributes.colspan > 1)) return false; // Colspanned caption header selected; no single column to target
36446
+ const col = tableAttributes.col;
36447
+ if (!col) return false;
36448
+ const tr = state.tr;
36449
+ let changed = false;
36450
+ tr.doc.nodesBetween(tableAttributes.from, tableAttributes.to, (node, pos) => {
36451
+ if ((node.type == nodeTypes.table_cell) || (node.type == nodeTypes.table_header)) {
36452
+ if ((node.attrs.colspan ?? 1) > 1) return false; // Skip a colspanned caption cell; it doesn't belong to a single column
36453
+ if ((tr.doc.resolve(pos).index() + 1) === col) {
36454
+ tr.setNodeMarkup(pos, null, {...node.attrs, align: resolvedAlign});
36455
+ changed = true;
36456
+ }
36457
+ return false;
36458
+ }
36459
+ return true;
36460
+ });
36461
+ if (changed && dispatch) {
36462
+ dispatch(tr);
36463
+ }
36464
+ return changed;
36465
+ };
36466
+
36467
+ return commandAdapter;
36468
+ }
36469
+
36319
36470
  function isHRuleSelected(state) {
36320
36471
  let hRuleSelected = false;
36321
36472
  state.doc.nodesBetween(state.selection.from, state.selection.to, (node) => {
@@ -36343,6 +36494,37 @@ function tableHasHeader(state) {
36343
36494
  return _getTableAttributes(state).header === true
36344
36495
  }
36345
36496
 
36497
+ /**
36498
+ * Handle Mod-Enter when the selection is inside a table: insert a paragraph
36499
+ * immediately after the table and move the selection into it.
36500
+ *
36501
+ * Mirrors prosemirror-commands' `exitCode`, which does the same for code_block
36502
+ * via its `code` node spec. Bound alongside (not instead of) `exitCode` in
36503
+ * keymap.js; returning false when the selection isn't in a table lets that
36504
+ * binding fall through to `exitCode` as usual.
36505
+ *
36506
+ * @ignore
36507
+ * @returns {boolean} True if a paragraph was inserted after the table; else false.
36508
+ */
36509
+ function exitTableCommand(state, dispatch) {
36510
+ const { $head, $anchor } = state.selection;
36511
+ if (!$head.sameParent($anchor)) return false
36512
+ const tableAttributes = _getTableAttributes(state);
36513
+ if (!tableAttributes.table) return false
36514
+ const paragraphType = state.schema.nodes.paragraph;
36515
+ const pos = tableAttributes.to;
36516
+ const $after = state.doc.resolve(pos);
36517
+ const after = $after.index();
36518
+ if (!$after.parent.canReplaceWith(after, after, paragraphType)) return false
36519
+ if (dispatch) {
36520
+ const tr = state.tr.replaceWith(pos, pos, paragraphType.createAndFill());
36521
+ tr.setSelection(TextSelection.near(tr.doc.resolve(pos)));
36522
+ dispatch(tr.scrollIntoView());
36523
+ stateChanged(activeView());
36524
+ }
36525
+ return true
36526
+ }
36527
+
36346
36528
  function setBorderCommand(border) {
36347
36529
  const commandAdapter = (viewState, dispatch, view) => {
36348
36530
  let state = view?.state ?? viewState;
@@ -37145,6 +37327,7 @@ var focusAfterLoad = true;
37145
37327
  var selectImage = false;
37146
37328
  var insertLink = false;
37147
37329
  var insertImage = false;
37330
+ var insertTable = false;
37148
37331
  var highlightCode = true;
37149
37332
  var markdownShorthand = true;
37150
37333
  var behaviorConfig = {
@@ -37152,6 +37335,7 @@ var behaviorConfig = {
37152
37335
  selectImage: selectImage,
37153
37336
  insertLink: insertLink,
37154
37337
  insertImage: insertImage,
37338
+ insertTable: insertTable,
37155
37339
  highlightCode: highlightCode,
37156
37340
  markdownShorthand: markdownShorthand
37157
37341
  };
@@ -37192,6 +37376,7 @@ var behaviorConfig = {
37192
37376
  * "selectImage": false, // Whether to show a "Select..." button in the Insert Image dialog
37193
37377
  * "insertLink": false, // Whether to defer to the MarkupDelegate rather than use the default LinkDialog
37194
37378
  * "insertImage": false, // Whether to defer to the MarkupDelagate rather than use the default ImageDialog
37379
+ * "insertTable": false, // Whether to defer to the MarkupDelegate rather than use the default Table menu
37195
37380
  * "highlightCode": true, // Whether to highlight code blocks and support language identificaton in UI
37196
37381
  * "markdownShorthand": true // Whether typing markdown shorthand converts to formatting: block-level
37197
37382
  * // (`> `, list markers, ``` ``` ```, `#`) and inline (**, *, _, `, ~~)
@@ -40339,6 +40524,19 @@ function hRuleItem(config) {
40339
40524
  function tableMenuItems(config) {
40340
40525
  let icons = config.toolbar.icons;
40341
40526
  let help = config.toolbar.help;
40527
+ // If `behavior.insertTable` is true, the Table button just invokes the delegate's
40528
+ // `markupInsertTable` method instead of the dropdown below -- same reasoning as
40529
+ // LinkItem/ImageItem. Unlike Link/Image, the delegate decides what to show (an
40530
+ // insert-size picker or a table-editing menu) based on whether the selection is
40531
+ // already in a table, so a single enabled state covers both: "can insert a table
40532
+ // here" or "already in a table" (editing an existing table is always available).
40533
+ if (config.behavior.insertTable && config.delegate?.markupInsertTable) {
40534
+ return cmdItem(config.delegate.markupInsertTable, {
40535
+ enable: (state) => insertTableCommand(1, 1)(state) || isTableSelected(state),
40536
+ title: help.table + keyString('table', config.keymap),
40537
+ icon: icons.table
40538
+ })
40539
+ }
40342
40540
  let items = [];
40343
40541
  let { tableHeader, tableBorder } = config.toolbar.menus;
40344
40542
  items.push(new TableCreateSubmenu({title: 'Insert table', label: 'Insert'}));
@@ -40821,6 +41019,10 @@ function buildKeymap(config, schema) {
40821
41019
  bind("Shift-Enter", handleShiftEnter);
40822
41020
  // The MarkupEditor needs to be notified of state changes on Delete, like Backspace
40823
41021
  bind("Delete", chainCommands(handleDelete, deleteNewlineForward));
41022
+ // Escape a table the same way Mod-Enter already escapes a code_block (via
41023
+ // prosemirror-commands' exitCode, bound in baseKeymap): if the selection isn't
41024
+ // in a table, exitTableCommand returns false and this falls through to exitCode.
41025
+ bind("Mod-Enter", exitTableCommand);
40824
41026
  // Table navigation by Tab/Shift-Tab
40825
41027
  bind('Tab', goToNextCell(1));
40826
41028
  bind('Shift-Tab', goToNextCell(-1));
@@ -40846,7 +41048,9 @@ function buildKeymap(config, schema) {
40846
41048
  // Insert
40847
41049
  bind(keymap.link, new LinkItem(config).command);
40848
41050
  bind(keymap.image, new ImageItem(config).command);
40849
- bind(keymap.table, new TableInsertItem().command); // TODO: Doesn't work properly
41051
+ // Defers to the delegate when `behavior.insertTable` is set, same as Link/Image;
41052
+ // otherwise inserts a default-sized table, since there's no dialog to fall back to.
41053
+ bind(keymap.table, (config.behavior.insertTable && config.delegate?.markupInsertTable) ? config.delegate.markupInsertTable : insertTableCommand(2, 2));
40850
41054
  // Styling
40851
41055
  bind(keymap.p, setStyleCommand('P'));
40852
41056
  bind(keymap.h1, setStyleCommand('H1'));
@@ -42777,6 +42981,7 @@ const MU = {
42777
42981
  borderTable,
42778
42982
  callbackInsertImage,
42779
42983
  callbackInsertLink,
42984
+ callbackInsertTable,
42780
42985
  callbackSelectImage,
42781
42986
  cancelSearch,
42782
42987
  canUndo,
@@ -42806,8 +43011,9 @@ const MU = {
42806
43011
  insertImage: insertImage$1,
42807
43012
  insertInternalLink,
42808
43013
  insertLink: insertLink$1,
42809
- insertTable,
43014
+ insertTable: insertTable$1,
42810
43015
  insertHRule,
43016
+ justifyColumn,
42811
43017
  loadUserFiles,
42812
43018
  modifyImage,
42813
43019
  openImageDialog,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markupeditor",
3
- "version": "0.9.40",
3
+ "version": "0.9.42",
4
4
  "description": "A web component and API for WYSIWYG HTML editing.",
5
5
  "keywords": [
6
6
  "wysiwyg",