markupeditor 0.9.40 → 0.9.41

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, pos) => {
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, view) => {
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) => {
@@ -37145,6 +37296,7 @@ var focusAfterLoad = true;
37145
37296
  var selectImage = false;
37146
37297
  var insertLink = false;
37147
37298
  var insertImage = false;
37299
+ var insertTable = false;
37148
37300
  var highlightCode = true;
37149
37301
  var markdownShorthand = true;
37150
37302
  var behaviorConfig = {
@@ -37152,6 +37304,7 @@ var behaviorConfig = {
37152
37304
  selectImage: selectImage,
37153
37305
  insertLink: insertLink,
37154
37306
  insertImage: insertImage,
37307
+ insertTable: insertTable,
37155
37308
  highlightCode: highlightCode,
37156
37309
  markdownShorthand: markdownShorthand
37157
37310
  };
@@ -37192,6 +37345,7 @@ var behaviorConfig = {
37192
37345
  * "selectImage": false, // Whether to show a "Select..." button in the Insert Image dialog
37193
37346
  * "insertLink": false, // Whether to defer to the MarkupDelegate rather than use the default LinkDialog
37194
37347
  * "insertImage": false, // Whether to defer to the MarkupDelagate rather than use the default ImageDialog
37348
+ * "insertTable": false, // Whether to defer to the MarkupDelegate rather than use the default Table menu
37195
37349
  * "highlightCode": true, // Whether to highlight code blocks and support language identificaton in UI
37196
37350
  * "markdownShorthand": true // Whether typing markdown shorthand converts to formatting: block-level
37197
37351
  * // (`> `, list markers, ``` ``` ```, `#`) and inline (**, *, _, `, ~~)
@@ -40339,6 +40493,19 @@ function hRuleItem(config) {
40339
40493
  function tableMenuItems(config) {
40340
40494
  let icons = config.toolbar.icons;
40341
40495
  let help = config.toolbar.help;
40496
+ // If `behavior.insertTable` is true, the Table button just invokes the delegate's
40497
+ // `markupInsertTable` method instead of the dropdown below -- same reasoning as
40498
+ // LinkItem/ImageItem. Unlike Link/Image, the delegate decides what to show (an
40499
+ // insert-size picker or a table-editing menu) based on whether the selection is
40500
+ // already in a table, so a single enabled state covers both: "can insert a table
40501
+ // here" or "already in a table" (editing an existing table is always available).
40502
+ if (config.behavior.insertTable && config.delegate?.markupInsertTable) {
40503
+ return cmdItem(config.delegate.markupInsertTable, {
40504
+ enable: (state) => insertTableCommand(1, 1)(state) || isTableSelected(state),
40505
+ title: help.table + keyString('table', config.keymap),
40506
+ icon: icons.table
40507
+ })
40508
+ }
40342
40509
  let items = [];
40343
40510
  let { tableHeader, tableBorder } = config.toolbar.menus;
40344
40511
  items.push(new TableCreateSubmenu({title: 'Insert table', label: 'Insert'}));
@@ -40846,7 +41013,9 @@ function buildKeymap(config, schema) {
40846
41013
  // Insert
40847
41014
  bind(keymap.link, new LinkItem(config).command);
40848
41015
  bind(keymap.image, new ImageItem(config).command);
40849
- bind(keymap.table, new TableInsertItem().command); // TODO: Doesn't work properly
41016
+ // Defers to the delegate when `behavior.insertTable` is set, same as Link/Image;
41017
+ // otherwise inserts a default-sized table, since there's no dialog to fall back to.
41018
+ bind(keymap.table, (config.behavior.insertTable && config.delegate?.markupInsertTable) ? config.delegate.markupInsertTable : insertTableCommand(2, 2));
40850
41019
  // Styling
40851
41020
  bind(keymap.p, setStyleCommand('P'));
40852
41021
  bind(keymap.h1, setStyleCommand('H1'));
@@ -42777,6 +42946,7 @@ const MU = {
42777
42946
  borderTable,
42778
42947
  callbackInsertImage,
42779
42948
  callbackInsertLink,
42949
+ callbackInsertTable,
42780
42950
  callbackSelectImage,
42781
42951
  cancelSearch,
42782
42952
  canUndo,
@@ -42806,8 +42976,9 @@ const MU = {
42806
42976
  insertImage: insertImage$1,
42807
42977
  insertInternalLink,
42808
42978
  insertLink: insertLink$1,
42809
- insertTable,
42979
+ insertTable: insertTable$1,
42810
42980
  insertHRule,
42981
+ justifyColumn,
42811
42982
  loadUserFiles,
42812
42983
  modifyImage,
42813
42984
  openImageDialog,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markupeditor",
3
- "version": "0.9.40",
3
+ "version": "0.9.41",
4
4
  "description": "A web component and API for WYSIWYG HTML editing.",
5
5
  "keywords": [
6
6
  "wysiwyg",