markupeditor 0.9.39 → 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}};
@@ -32817,6 +32827,12 @@ function isRecognizedLanguage(name) {
32817
32827
  * the same language for display/matching purposes, matching isRecognizedLanguage's
32818
32828
  * own case-insensitivity.
32819
32829
  *
32830
+ * Filtered through isRecognizedLanguage, so a genuinely custom/unrecognized
32831
+ * language name typed into one block is no longer suggested for reuse in
32832
+ * another. Trade-off accepted for a positive, Markdown-agnostic rule ("only
32833
+ * present languages this app actually supports") rather than a per-language
32834
+ * exclusion list.
32835
+ *
32820
32836
  * @param {Node} doc A ProseMirror document node.
32821
32837
  * @returns {string[]}
32822
32838
  */
@@ -32827,7 +32843,7 @@ function presentCodeLanguages(doc) {
32827
32843
  languages.add(node.attrs.language.toLowerCase());
32828
32844
  }
32829
32845
  });
32830
- return Array.from(languages).sort()
32846
+ return Array.from(languages).filter(isRecognizedLanguage).sort()
32831
32847
  }
32832
32848
 
32833
32849
  // hljs never has classPrefix reconfigured elsewhere in this codebase, so this
@@ -34834,7 +34850,7 @@ function _getSelectionState() {
34834
34850
  state['thead'] = tableAttributes.thead;
34835
34851
  state['tbody'] = tableAttributes.tbody;
34836
34852
  state['header'] = tableAttributes.header;
34837
- 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
34838
34854
  state['rows'] = tableAttributes.rows;
34839
34855
  state['cols'] = tableAttributes.cols;
34840
34856
  state['row'] = tableAttributes.row;
@@ -35035,8 +35051,8 @@ function _getTableAttributes(state) {
35035
35051
  case nodeTypes.table_header:
35036
35052
  attributes.thead = true; // We selected the header
35037
35053
  attributes.tbody = false;
35038
- attributes.row = $pos.index() + 1; // The row will be 1 by definition
35039
- 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
35040
35056
  return true;
35041
35057
  case nodeTypes.table_row:
35042
35058
  attributes.row = $pos.index() + 1; // We are in some row, but could be the header row
@@ -35159,8 +35175,8 @@ function callbackInsertLink() {
35159
35175
 
35160
35176
  /**
35161
35177
  * Callback to signal that the user wants to insert an image.
35162
- *
35163
- * The messageHandler will need to bring up a dialog to identify
35178
+ *
35179
+ * The messageHandler will need to bring up a dialog to identify
35164
35180
  * the image and then execute insertImage.
35165
35181
  */
35166
35182
  function callbackInsertImage() {
@@ -35168,6 +35184,17 @@ function callbackInsertImage() {
35168
35184
  _callback(JSON.stringify(messageDict));
35169
35185
  }
35170
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
+
35171
35198
  /**
35172
35199
  * Callback to signal that user-provided CSS and/or script files have
35173
35200
  * been loaded.
@@ -36005,7 +36032,7 @@ function _copyImage(node) {
36005
36032
  * @param {number} rows The number of rows in the table to be created.
36006
36033
  * @param {number} cols The number of columns in the table to be created.
36007
36034
  */
36008
- function insertTable(rows, cols) {
36035
+ function insertTable$1(rows, cols) {
36009
36036
  const view = activeView();
36010
36037
  if ((rows < 1) || (cols < 1)) return;
36011
36038
  let command = insertTableCommand(rows, cols);
@@ -36015,6 +36042,7 @@ function insertTable(rows, cols) {
36015
36042
  function insertTableCommand(rows, cols) {
36016
36043
  const commandAdapter = (viewState, dispatch, view) => {
36017
36044
  let state = view?.state ?? viewState;
36045
+ if (isTableSelected(state)) return false; // Tables cannot be nested
36018
36046
  const nodeTypes = state.schema.nodes;
36019
36047
  const table_rows = [];
36020
36048
  for (let j = 0; j < rows; j++) {
@@ -36054,15 +36082,8 @@ function insertTableCommand(rows, cols) {
36054
36082
  } return (pPos == undefined); // Keep going if pPos hasn't been defined
36055
36083
  });
36056
36084
  }
36057
- // Set the selection in the first cell, apply it to the state and the view.
36058
- // We have to special-case for empty documents to get selection in the 1st cell.
36059
- let empty = (view.state.doc.textContent.length == 0);
36060
- let textSelection;
36061
- if (empty) {
36062
- textSelection = TextSelection.near(transaction.doc.resolve(pPos), -1);
36063
- } else {
36064
- textSelection = TextSelection.near(transaction.doc.resolve(pPos));
36065
- }
36085
+ // Set the selection in the first cell, applied to the state and the view.
36086
+ let textSelection = TextSelection.near(transaction.doc.resolve(pPos));
36066
36087
  transaction = transaction.setSelection(textSelection);
36067
36088
  state = state.apply(transaction);
36068
36089
  view.updateState(state);
@@ -36092,12 +36113,20 @@ function addRow(direction) {
36092
36113
  return result;
36093
36114
  }
36094
36115
  function addRowCommand(direction) {
36095
- const commandAdapter = (state, dispatch, view) => {
36116
+ const commandAdapter = (viewState, dispatch, view) => {
36117
+ let state = view?.state ?? viewState;
36118
+ let result;
36096
36119
  if (direction === 'BEFORE') {
36097
- return addRowBefore(state, dispatch);
36120
+ result = addRowBefore(state, (tr) => { state = state.apply(tr); });
36098
36121
  } else {
36099
- return addRowAfter(state, dispatch);
36100
- } };
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
+ };
36101
36130
 
36102
36131
  return commandAdapter;
36103
36132
  }
@@ -36124,14 +36153,18 @@ function addColCommand(direction) {
36124
36153
  let state = view?.state ?? viewState;
36125
36154
  if (!isTableSelected(state)) return false;
36126
36155
  const startSelection = new TextSelection(state.selection.$anchor, state.selection.$head);
36156
+ const wasColspannedHeader = _hasColspannedHeader(state);
36127
36157
  let offset = 0;
36128
36158
  if (direction === 'BEFORE') {
36129
36159
  addColumnBefore(state, (tr) => { state = state.apply(tr); });
36130
36160
  offset = 4; // An empty cell
36131
36161
  } else {
36132
36162
  addColumnAfter(state, (tr) => { state = state.apply(tr); });
36133
- } _mergeHeaders(state, (tr) => { state = state.apply(tr); });
36134
-
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
+ }
36135
36168
  if (dispatch) {
36136
36169
  const $anchor = state.tr.doc.resolve(startSelection.from + offset);
36137
36170
  const $head = state.tr.doc.resolve(startSelection.to + offset);
@@ -36174,7 +36207,8 @@ function addHeaderCommand(colspan = true) {
36174
36207
  toggleHeaderRow(state, (tr) => { state = state.apply(tr); });
36175
36208
  if (colspan) {
36176
36209
  _mergeHeaders(state, (tr) => { state = state.apply(tr); });
36177
- }
36210
+ } _propagateColumnAlign(state, (tr) => { state = state.apply(tr); });
36211
+
36178
36212
  if (dispatch) {
36179
36213
  // At this point, the state.selection is in the new header row we just added. By definition,
36180
36214
  // the header is placed before the original selection, so we can add its size to the
@@ -36310,6 +36344,129 @@ function _mergeHeaders(state, dispatch) {
36310
36344
  const newState = state.apply(transaction);
36311
36345
  mergeCells(newState, dispatch);
36312
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
+
36313
36470
  function isHRuleSelected(state) {
36314
36471
  let hRuleSelected = false;
36315
36472
  state.doc.nodesBetween(state.selection.from, state.selection.to, (node) => {
@@ -37139,6 +37296,7 @@ var focusAfterLoad = true;
37139
37296
  var selectImage = false;
37140
37297
  var insertLink = false;
37141
37298
  var insertImage = false;
37299
+ var insertTable = false;
37142
37300
  var highlightCode = true;
37143
37301
  var markdownShorthand = true;
37144
37302
  var behaviorConfig = {
@@ -37146,6 +37304,7 @@ var behaviorConfig = {
37146
37304
  selectImage: selectImage,
37147
37305
  insertLink: insertLink,
37148
37306
  insertImage: insertImage,
37307
+ insertTable: insertTable,
37149
37308
  highlightCode: highlightCode,
37150
37309
  markdownShorthand: markdownShorthand
37151
37310
  };
@@ -37186,6 +37345,7 @@ var behaviorConfig = {
37186
37345
  * "selectImage": false, // Whether to show a "Select..." button in the Insert Image dialog
37187
37346
  * "insertLink": false, // Whether to defer to the MarkupDelegate rather than use the default LinkDialog
37188
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
37189
37349
  * "highlightCode": true, // Whether to highlight code blocks and support language identificaton in UI
37190
37350
  * "markdownShorthand": true // Whether typing markdown shorthand converts to formatting: block-level
37191
37351
  * // (`> `, list markers, ``` ``` ```, `#`) and inline (**, *, _, `, ~~)
@@ -40333,6 +40493,19 @@ function hRuleItem(config) {
40333
40493
  function tableMenuItems(config) {
40334
40494
  let icons = config.toolbar.icons;
40335
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
+ }
40336
40509
  let items = [];
40337
40510
  let { tableHeader, tableBorder } = config.toolbar.menus;
40338
40511
  items.push(new TableCreateSubmenu({title: 'Insert table', label: 'Insert'}));
@@ -40840,7 +41013,9 @@ function buildKeymap(config, schema) {
40840
41013
  // Insert
40841
41014
  bind(keymap.link, new LinkItem(config).command);
40842
41015
  bind(keymap.image, new ImageItem(config).command);
40843
- 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));
40844
41019
  // Styling
40845
41020
  bind(keymap.p, setStyleCommand('P'));
40846
41021
  bind(keymap.h1, setStyleCommand('H1'));
@@ -42771,6 +42946,7 @@ const MU = {
42771
42946
  borderTable,
42772
42947
  callbackInsertImage,
42773
42948
  callbackInsertLink,
42949
+ callbackInsertTable,
42774
42950
  callbackSelectImage,
42775
42951
  cancelSearch,
42776
42952
  canUndo,
@@ -42800,8 +42976,9 @@ const MU = {
42800
42976
  insertImage: insertImage$1,
42801
42977
  insertInternalLink,
42802
42978
  insertLink: insertLink$1,
42803
- insertTable,
42979
+ insertTable: insertTable$1,
42804
42980
  insertHRule,
42981
+ justifyColumn,
42805
42982
  loadUserFiles,
42806
42983
  modifyImage,
42807
42984
  openImageDialog,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markupeditor",
3
- "version": "0.9.39",
3
+ "version": "0.9.41",
4
4
  "description": "A web component and API for WYSIWYG HTML editing.",
5
5
  "keywords": [
6
6
  "wysiwyg",