@jackuait/blok 0.7.0 → 0.7.2

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.
Files changed (29) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-Ufr5cPq-.mjs → blok-3RuPZd3G.mjs} +1207 -1174
  3. package/dist/chunks/{constants-DT17zmu_.mjs → constants-BkelccB1.mjs} +185 -164
  4. package/dist/chunks/{tools-CJIETS-H.mjs → tools-rsbC2UUN.mjs} +31 -12
  5. package/dist/full.mjs +3 -3
  6. package/dist/react.mjs +2 -2
  7. package/dist/tools.mjs +2 -2
  8. package/package.json +1 -1
  9. package/src/components/inline-tools/inline-tool-marker.ts +11 -0
  10. package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +1 -9
  11. package/src/components/modules/caret.ts +13 -1
  12. package/src/components/modules/drag/utils/drag.constants.ts +1 -1
  13. package/src/components/modules/paste/google-docs-preprocessor.ts +96 -38
  14. package/src/components/modules/toolbar/blockSettings.ts +1 -1
  15. package/src/components/modules/toolbar/index.ts +10 -1
  16. package/src/components/modules/toolbar/inline/index.ts +24 -2
  17. package/src/components/modules/toolbar/styles.ts +1 -1
  18. package/src/components/selection/cursor.ts +7 -0
  19. package/src/components/ui/toolbox.ts +14 -0
  20. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +1 -1
  21. package/src/components/utils/popover/components/search-input/search-input.const.ts +1 -0
  22. package/src/components/utils/popover/components/search-input/search-input.ts +32 -1
  23. package/src/components/utils/popover/popover-desktop.ts +9 -1
  24. package/src/components/utils/popover/popover-inline.ts +8 -0
  25. package/src/styles/main.css +39 -11
  26. package/src/tools/paragraph/index.ts +3 -5
  27. package/src/tools/table/index.ts +70 -0
  28. package/src/tools/table/table-cell-blocks.ts +15 -3
  29. package/src/tools/table/table-cell-clipboard.ts +32 -5
@@ -37,9 +37,11 @@
37
37
  `all: initial !important` — main editor wrapper (outermost boundary).
38
38
  Resets every property to its CSS initial value with !important priority,
39
39
  blocking even `!important` host-page styles from cascading in.
40
- Blok's own Tailwind utility classes (applied to the same elements or
41
- descendants) override this because they have equal or higher specificity
42
- and appear later in the cascade.
40
+ However, `!important` declarations always beat normal declarations
41
+ regardless of specificity or source order, so Tailwind utility classes
42
+ applied to the wrapper element itself (e.g. `relative`, `z-1`) are
43
+ overridden. Properties that the wrapper needs must be explicitly
44
+ re-applied with `!important` below (same pattern as font-family/color).
43
45
 
44
46
  [data-blok-popover]:not([data-blok-popover-inline])
45
47
  Inherited-properties-only reset — toolbox/settings popovers, appended
@@ -72,6 +74,27 @@
72
74
  */
73
75
  [data-blok-interface=blok] {
74
76
  all: initial !important;
77
+
78
+ /*
79
+ Re-apply layout properties that `all: initial` resets.
80
+ The wrapper must be a positioned containing block so that
81
+ absolutely-positioned children (inline toolbar, main toolbar)
82
+ resolve against it rather than the document root. Without this,
83
+ the inline toolbar renders off-screen when the page is scrolled.
84
+ */
85
+ position: relative !important;
86
+ box-sizing: border-box !important;
87
+ display: block !important;
88
+ z-index: 1 !important;
89
+ }
90
+
91
+ /*
92
+ RTL direction is set conditionally via JS (ui.ts) using the
93
+ data-blok-rtl attribute. `all: initial` resets direction to `ltr`,
94
+ so we must re-apply it with !important when the attribute is present.
95
+ */
96
+ [data-blok-interface=blok][data-blok-rtl=true] {
97
+ direction: rtl !important;
75
98
  }
76
99
 
77
100
  /* Font family — user-configurable via config.style.fontFamily */
@@ -327,8 +350,9 @@
327
350
  resize: vertical;
328
351
  }
329
352
 
330
- /* Remove inner padding in Chrome and Safari on macOS. */
331
- :where([data-blok-interface], [data-blok-popover]) ::-webkit-search-decoration {
353
+ /* Remove inner padding and native cancel button in Chrome and Safari on macOS. */
354
+ :where([data-blok-interface], [data-blok-popover]) ::-webkit-search-decoration,
355
+ :where([data-blok-interface], [data-blok-popover]) ::-webkit-search-cancel-button {
332
356
  -webkit-appearance: none;
333
357
  }
334
358
 
@@ -394,10 +418,13 @@
394
418
  outline: none;
395
419
  }
396
420
 
397
- /* Never show a focus outline on contenteditable blocks — the text cursor
398
- is sufficient affordance regardless of how focus was triggered. */
399
- [data-blok-interface] [contenteditable]:focus-visible,
400
- [data-blok-popover] [contenteditable]:focus-visible {
421
+ /* Never show a focus outline on text-entry elements — the text cursor
422
+ is sufficient affordance regardless of how focus was triggered.
423
+ Browsers always match :focus-visible on these elements even on mouse
424
+ click (per CSS Selectors L4), so the :focus-visible restore rule below
425
+ would otherwise override element-level outline-hidden utilities. */
426
+ [data-blok-interface] :is([contenteditable], input, textarea):focus-visible,
427
+ [data-blok-popover] :is([contenteditable], input, textarea):focus-visible {
401
428
  outline: none;
402
429
  }
403
430
 
@@ -952,8 +979,9 @@
952
979
  * When the user types "/" to open the toolbox, the contenteditable
953
980
  * transforms to look like a search input with a placeholder.
954
981
  */
955
- [data-blok-slash-search] {
956
- @apply bg-search-input-bg rounded-lg transition-colors duration-150 max-w-[240px];
982
+ [data-blok-slash-search],
983
+ [data-blok-slash-search]:focus-visible {
984
+ @apply bg-search-input-bg rounded-[4px] transition-colors duration-150 max-w-[240px];
957
985
  }
958
986
 
959
987
  [data-blok-slash-search]::after {
@@ -346,11 +346,9 @@ export class Paragraph implements BlockTool {
346
346
 
347
347
  this._data = data;
348
348
 
349
- queueMicrotask(() => {
350
- if (this._element) {
351
- this._element.innerHTML = this._data.text || '';
352
- }
353
- });
349
+ if (this._element) {
350
+ this._element.innerHTML = this._data.text || '';
351
+ }
354
352
  }
355
353
 
356
354
  /**
@@ -1395,6 +1395,20 @@ export class Table implements BlockTool {
1395
1395
  return;
1396
1396
  }
1397
1397
 
1398
+ /**
1399
+ * Single-cell (1×1) payloads should insert content inline at the caret
1400
+ * position rather than replacing the entire target cell. This matches user
1401
+ * expectations: copying one cell and pasting into another cell (or the same
1402
+ * cell) appends/inserts the text instead of overwriting.
1403
+ */
1404
+ if (payload.rows === 1 && payload.cols === 1) {
1405
+ e.preventDefault();
1406
+ e.stopPropagation();
1407
+ this.insertSingleCellPayloadInline(payload.cells[0][0]);
1408
+
1409
+ return;
1410
+ }
1411
+
1398
1412
  e.preventDefault();
1399
1413
  e.stopPropagation();
1400
1414
 
@@ -1406,6 +1420,62 @@ export class Table implements BlockTool {
1406
1420
  this.pastePayloadIntoCells(gridEl, payload, targetRowIndex, targetColIndex);
1407
1421
  }
1408
1422
 
1423
+ /**
1424
+ * Insert the content of a single clipboard cell at the current caret position.
1425
+ * Extracts text from each block and joins with line breaks.
1426
+ */
1427
+ private insertSingleCellPayloadInline(cell: { blocks: ClipboardBlockData[] }): void {
1428
+ const html = cell.blocks
1429
+ .map((block) => {
1430
+ if (typeof block.data.text === 'string') {
1431
+ return block.data.text;
1432
+ }
1433
+
1434
+ return '';
1435
+ })
1436
+ .filter(Boolean)
1437
+ .join('<br>');
1438
+
1439
+ if (!html) {
1440
+ return;
1441
+ }
1442
+
1443
+ const selection = window.getSelection();
1444
+
1445
+ if (!selection || selection.rangeCount === 0) {
1446
+ return;
1447
+ }
1448
+
1449
+ const range = selection.getRangeAt(0);
1450
+
1451
+ range.deleteContents();
1452
+
1453
+ const fragment = document.createDocumentFragment();
1454
+ const wrapper = document.createElement('div');
1455
+
1456
+ wrapper.innerHTML = html;
1457
+
1458
+ Array.from(wrapper.childNodes).forEach((child) => fragment.appendChild(child));
1459
+
1460
+ if (fragment.childNodes.length === 0) {
1461
+ fragment.appendChild(new Text());
1462
+ }
1463
+
1464
+ const lastChild = fragment.lastChild as ChildNode;
1465
+
1466
+ range.insertNode(fragment);
1467
+
1468
+ const newRange = document.createRange();
1469
+ const nodeToSetCaret = lastChild.nodeType === Node.TEXT_NODE ? lastChild : lastChild.firstChild;
1470
+
1471
+ if (nodeToSetCaret !== null && nodeToSetCaret.textContent !== null) {
1472
+ newRange.setStart(nodeToSetCaret, nodeToSetCaret.textContent.length);
1473
+ }
1474
+
1475
+ selection.removeAllRanges();
1476
+ selection.addRange(newRange);
1477
+ }
1478
+
1409
1479
  private pastePayloadIntoCells(
1410
1480
  gridEl: HTMLElement,
1411
1481
  payload: TableCellsClipboard,
@@ -144,8 +144,8 @@ export class TableCellBlocks {
144
144
  return;
145
145
  }
146
146
 
147
- // ArrowDown at last row -> exit table
148
- if (event.key === 'ArrowDown' && position.row === this.getRowCount() - 1) {
147
+ // ArrowDown at last row -> exit table (skip if already handled by block-level navigation)
148
+ if (event.key === 'ArrowDown' && !event.defaultPrevented && position.row === this.getRowCount() - 1) {
149
149
  event.preventDefault();
150
150
  this.exitTableForward();
151
151
  }
@@ -492,7 +492,19 @@ export class TableCellBlocks {
492
492
  return;
493
493
  }
494
494
 
495
- container.appendChild(block.holder);
495
+ // Insert at the correct DOM position based on the flat array order,
496
+ // so that pressing Enter on a non-last paragraph inserts the new block
497
+ // right after the current one instead of always at the end of the cell.
498
+ const blocksCount = this.api.blocks.getBlocksCount();
499
+ const nextSiblingHolder = Array.from(
500
+ { length: blocksCount - index - 1 },
501
+ (_, offset) => this.api.blocks.getBlockByIndex(index + 1 + offset)
502
+ ).find(
503
+ candidate => candidate?.holder.parentElement === container
504
+ )?.holder ?? null;
505
+
506
+ // insertBefore(el, null) is equivalent to appendChild
507
+ container.insertBefore(block.holder, nextSiblingHolder);
496
508
  this.api.blocks.setBlockParent(blockId, this.tableBlockId);
497
509
  this.stripPlaceholders(container);
498
510
  }
@@ -79,12 +79,12 @@ export function serializeCellsToClipboard(entries: CellEntry[]): TableCellsClipb
79
79
  }
80
80
 
81
81
  /**
82
- * Extract a plain-text representation from a single block's data.
82
+ * Extract the raw HTML content from a single block's data.
83
83
  *
84
84
  * Looks for `data.text` (string), then `data.items` (array of strings),
85
85
  * and falls back to an empty string.
86
86
  */
87
- function extractBlockText(block: ClipboardBlockData): string {
87
+ function extractBlockHtml(block: ClipboardBlockData): string {
88
88
  const { data } = block;
89
89
 
90
90
  if (typeof data.text === 'string') {
@@ -100,6 +100,25 @@ function extractBlockText(block: ClipboardBlockData): string {
100
100
  return '';
101
101
  }
102
102
 
103
+ /**
104
+ * Strip HTML tags from a string, returning only the visible text content.
105
+ */
106
+ function stripHtmlTags(html: string): string {
107
+ const div = document.createElement('div');
108
+
109
+ div.innerHTML = html;
110
+
111
+ return div.textContent ?? '';
112
+ }
113
+
114
+ /**
115
+ * Extract a plain-text representation from a single block's data.
116
+ * HTML tags are stripped so only visible text remains.
117
+ */
118
+ function extractBlockPlainText(block: ClipboardBlockData): string {
119
+ return stripHtmlTags(extractBlockHtml(block));
120
+ }
121
+
103
122
  /**
104
123
  * Build an HTML `<table>` string that carries the clipboard payload in a
105
124
  * `data-blok-table-cells` attribute.
@@ -116,7 +135,7 @@ export function buildClipboardHtml(payload: TableCellsClipboard): string {
116
135
  .map((row) => {
117
136
  const cellsHtml = row
118
137
  .map((cell) => {
119
- const text = cell.blocks.map(extractBlockText).join(' ');
138
+ const text = cell.blocks.map(extractBlockHtml).join(' ');
120
139
 
121
140
  const styles = [
122
141
  cell.color ? `background-color: ${cell.color}` : '',
@@ -145,7 +164,7 @@ export function buildClipboardHtml(payload: TableCellsClipboard): string {
145
164
  export function buildClipboardPlainText(payload: TableCellsClipboard): string {
146
165
  return payload.cells
147
166
  .map((row) =>
148
- row.map((cell) => cell.blocks.map(extractBlockText).join(' ')).join('\t')
167
+ row.map((cell) => cell.blocks.map(extractBlockPlainText).join(' ')).join('\t')
149
168
  )
150
169
  .join('\n');
151
170
  }
@@ -406,7 +425,15 @@ export function parseClipboardHtml(html: string): TableCellsClipboard | null {
406
425
  return null;
407
426
  }
408
427
 
409
- const jsonStr = raw.replace(/&#39;/g, "'").replace(/&quot;/g, '"');
428
+ // Browsers re-serialize clipboard HTML, encoding special characters inside
429
+ // attribute values as HTML entities. Decode them back before JSON.parse.
430
+ // Order matters: &amp; must be last to avoid double-decoding (e.g. &amp;lt; → &lt; → <).
431
+ const jsonStr = raw
432
+ .replace(/&#39;/g, "'")
433
+ .replace(/&quot;/g, '"')
434
+ .replace(/&lt;/g, '<')
435
+ .replace(/&gt;/g, '>')
436
+ .replace(/&amp;/g, '&');
410
437
 
411
438
  return JSON.parse(jsonStr) as TableCellsClipboard;
412
439
  } catch {