@jackuait/blok 0.7.1 → 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.
- package/dist/blok.mjs +2 -2
- package/dist/chunks/{blok-D-7DpjTs.mjs → blok-3RuPZd3G.mjs} +1205 -1172
- package/dist/chunks/{constants-DXYRzX7f.mjs → constants-BkelccB1.mjs} +185 -164
- package/dist/chunks/{tools-Chd7Auwx.mjs → tools-rsbC2UUN.mjs} +31 -12
- package/dist/full.mjs +3 -3
- package/dist/react.mjs +2 -2
- package/dist/tools.mjs +2 -2
- package/package.json +1 -1
- package/src/components/inline-tools/inline-tool-marker.ts +11 -0
- package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +1 -9
- package/src/components/modules/caret.ts +13 -1
- package/src/components/modules/paste/google-docs-preprocessor.ts +96 -38
- package/src/components/modules/toolbar/blockSettings.ts +1 -1
- package/src/components/modules/toolbar/index.ts +10 -1
- package/src/components/modules/toolbar/inline/index.ts +24 -2
- package/src/components/selection/cursor.ts +7 -0
- package/src/components/ui/toolbox.ts +14 -0
- package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +1 -1
- package/src/components/utils/popover/components/search-input/search-input.const.ts +1 -0
- package/src/components/utils/popover/components/search-input/search-input.ts +32 -1
- package/src/components/utils/popover/popover-desktop.ts +9 -1
- package/src/components/utils/popover/popover-inline.ts +8 -0
- package/src/styles/main.css +39 -11
- package/src/tools/paragraph/index.ts +3 -5
- package/src/tools/table/index.ts +70 -0
- package/src/tools/table/table-cell-blocks.ts +15 -3
- package/src/tools/table/table-cell-clipboard.ts +32 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as e, A as t, At as n, B as r, D as i, Dn as a, Dt as o, En as s, F as c, Ft as l, G as u, H as d, I as f, J as p, K as m, L as h, M as g, N as ee, P as te, Pt as ne, Q as re, R as ie, Tt as ae, U as oe, V as se, W as ce, X as le, Xt as ue, _t as de, a as fe, bn as pe, bt as me, c as he, ct as ge, d as _e, dt as ve, et as ye, f as be, ft as xe, g as Se, gt as Ce, h as we, ht as Te, i as Ee, it as De, j as Oe, jt as _, k as ke, l as Ae, lt as je, mt as Me, n as Ne, nt as Pe, o as Fe, p as Ie, pt as Le, q as Re, r as ze, rt as Be, s as Ve, t as He, tt as Ue, u as v, ut as We, v as Ge, wt as Ke, z as qe } from "./constants-
|
|
1
|
+
import { $ as e, A as t, At as n, B as r, D as i, Dn as a, Dt as o, En as s, F as c, Ft as l, G as u, H as d, I as f, J as p, K as m, L as h, M as g, N as ee, P as te, Pt as ne, Q as re, R as ie, Tt as ae, U as oe, V as se, W as ce, X as le, Xt as ue, _t as de, a as fe, bn as pe, bt as me, c as he, ct as ge, d as _e, dt as ve, et as ye, f as be, ft as xe, g as Se, gt as Ce, h as we, ht as Te, i as Ee, it as De, j as Oe, jt as _, k as ke, l as Ae, lt as je, mt as Me, n as Ne, nt as Pe, o as Fe, p as Ie, pt as Le, q as Re, r as ze, rt as Be, s as Ve, t as He, tt as Ue, u as v, ut as We, v as Ge, wt as Ke, z as qe } from "./constants-BkelccB1.mjs";
|
|
2
2
|
import { t as y } from "./objectSpread2-CyPxu8-u.mjs";
|
|
3
3
|
import { n as b } from "./tw-DmW6-pCY.mjs";
|
|
4
4
|
//#region src/components/utils/html.ts
|
|
@@ -106,9 +106,7 @@ var Je = (e) => {
|
|
|
106
106
|
}
|
|
107
107
|
onPaste(e) {
|
|
108
108
|
let t = e.detail;
|
|
109
|
-
"data" in t && (this._data = { text: t.data.innerHTML },
|
|
110
|
-
this._element && (this._element.innerHTML = this._data.text || "");
|
|
111
|
-
}));
|
|
109
|
+
"data" in t && (this._data = { text: t.data.innerHTML }, this._element && (this._element.innerHTML = this._data.text || ""));
|
|
112
110
|
}
|
|
113
111
|
static get conversionConfig() {
|
|
114
112
|
return {
|
|
@@ -1956,7 +1954,7 @@ var vt = "outline-hidden py-[3px] mt-[2px] mb-px", yt = "outline-hidden pl-0.5 l
|
|
|
1956
1954
|
e.preventDefault(), this.handleShiftTabNavigation(t);
|
|
1957
1955
|
return;
|
|
1958
1956
|
}
|
|
1959
|
-
e.key === "ArrowDown" && t.row === this.getRowCount() - 1 && (e.preventDefault(), this.exitTableForward());
|
|
1957
|
+
e.key === "ArrowDown" && !e.defaultPrevented && t.row === this.getRowCount() - 1 && (e.preventDefault(), this.exitTableForward());
|
|
1960
1958
|
}
|
|
1961
1959
|
handleTabNavigation(e) {
|
|
1962
1960
|
let t = e.col + 1;
|
|
@@ -2090,12 +2088,15 @@ var vt = "outline-hidden py-[3px] mt-[2px] mb-px", yt = "outline-hidden pl-0.5 l
|
|
|
2090
2088
|
return n;
|
|
2091
2089
|
}
|
|
2092
2090
|
claimBlockForCell(e, t) {
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2091
|
+
var n, r;
|
|
2092
|
+
let i = e.querySelector(`[${k}]`);
|
|
2093
|
+
if (!i) return;
|
|
2094
|
+
let a = this.api.blocks.getBlockIndex(t);
|
|
2095
|
+
if (a === void 0) return;
|
|
2096
|
+
let o = this.api.blocks.getBlockByIndex(a);
|
|
2097
|
+
if (!o || o.holder.contains(i)) return;
|
|
2098
|
+
let s = this.api.blocks.getBlocksCount(), c = (n = (r = Array.from({ length: s - a - 1 }, (e, t) => this.api.blocks.getBlockByIndex(a + 1 + t)).find((e) => (e == null ? void 0 : e.holder.parentElement) === i)) == null ? void 0 : r.holder) == null ? null : n;
|
|
2099
|
+
i.insertBefore(o.holder, c), this.api.blocks.setBlockParent(t, this.tableBlockId), this.stripPlaceholders(i);
|
|
2099
2100
|
}
|
|
2100
2101
|
findCellForNewBlock(e) {
|
|
2101
2102
|
return this.findCellForAdjacentBlock(e - 1) || this.findCellForAdjacentBlock(e + 1);
|
|
@@ -4489,10 +4490,28 @@ var er = (e) => ({ element: $n({
|
|
|
4489
4490
|
if (!s || !t.contains(s)) return;
|
|
4490
4491
|
let c = s.closest(`[${T}]`);
|
|
4491
4492
|
if (!c) return;
|
|
4493
|
+
if (a.rows === 1 && a.cols === 1) {
|
|
4494
|
+
e.preventDefault(), e.stopPropagation(), this.insertSingleCellPayloadInline(a.cells[0][0]);
|
|
4495
|
+
return;
|
|
4496
|
+
}
|
|
4492
4497
|
e.preventDefault(), e.stopPropagation();
|
|
4493
4498
|
let l = Array.from(t.querySelectorAll(`[${T}]`)).indexOf(c), u = Array.from(c.querySelectorAll(`[${E}]`)).indexOf(s);
|
|
4494
4499
|
this.pastePayloadIntoCells(t, a, l, u);
|
|
4495
4500
|
}
|
|
4501
|
+
insertSingleCellPayloadInline(e) {
|
|
4502
|
+
let t = e.blocks.map((e) => typeof e.data.text == "string" ? e.data.text : "").filter(Boolean).join("<br>");
|
|
4503
|
+
if (!t) return;
|
|
4504
|
+
let n = window.getSelection();
|
|
4505
|
+
if (!n || n.rangeCount === 0) return;
|
|
4506
|
+
let r = n.getRangeAt(0);
|
|
4507
|
+
r.deleteContents();
|
|
4508
|
+
let i = document.createDocumentFragment(), a = document.createElement("div");
|
|
4509
|
+
a.innerHTML = t, Array.from(a.childNodes).forEach((e) => i.appendChild(e)), i.childNodes.length === 0 && i.appendChild(new Text());
|
|
4510
|
+
let o = i.lastChild;
|
|
4511
|
+
r.insertNode(i);
|
|
4512
|
+
let s = document.createRange(), c = o.nodeType === Node.TEXT_NODE ? o : o.firstChild;
|
|
4513
|
+
c !== null && c.textContent !== null && s.setStart(c, c.textContent.length), n.removeAllRanges(), n.addRange(s);
|
|
4514
|
+
}
|
|
4496
4515
|
pastePayloadIntoCells(e, t, n, r) {
|
|
4497
4516
|
this.runTransactedStructuralOp(() => {
|
|
4498
4517
|
var i, a;
|
|
@@ -5767,7 +5786,7 @@ var Q = (e) => e.tagName === "MARK", Ji = (e) => q(e, Q), $, Yi = {
|
|
|
5767
5786
|
return { mark: (t) => {
|
|
5768
5787
|
let n = t.style, r = Array.from({ length: n.length }, (e, t) => n.item(t));
|
|
5769
5788
|
for (let t of r) e.ALLOWED_STYLE_PROPS.has(t) || n.removeProperty(t);
|
|
5770
|
-
return n.length > 0 ? { style: !0 } : {};
|
|
5789
|
+
return n.getPropertyValue("color") && !n.getPropertyValue("background-color") && n.setProperty("background-color", "transparent"), n.length > 0 ? { style: !0 } : {};
|
|
5771
5790
|
} };
|
|
5772
5791
|
}
|
|
5773
5792
|
constructor({ api: e }) {
|
package/dist/full.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { En as e } from "./chunks/constants-
|
|
2
|
-
import { n as t, t as n } from "./chunks/blok-
|
|
1
|
+
import { En as e } from "./chunks/constants-BkelccB1.mjs";
|
|
2
|
+
import { n as t, t as n } from "./chunks/blok-3RuPZd3G.mjs";
|
|
3
3
|
import { t as r } from "./chunks/objectSpread2-CyPxu8-u.mjs";
|
|
4
|
-
import { a as i, d as a, i as o, l as s, n as c, o as l, r as u, s as d, t as f, u as p } from "./chunks/tools-
|
|
4
|
+
import { a as i, d as a, i as o, l as s, n as c, o as l, r as u, s as d, t as f, u as p } from "./chunks/tools-rsbC2UUN.mjs";
|
|
5
5
|
//#region src/full.ts
|
|
6
6
|
var m = {
|
|
7
7
|
paragraph: {
|
package/dist/react.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import "./chunks/constants-
|
|
2
|
-
import { i as e, t } from "./chunks/blok-
|
|
1
|
+
import "./chunks/constants-BkelccB1.mjs";
|
|
2
|
+
import { i as e, t } from "./chunks/blok-3RuPZd3G.mjs";
|
|
3
3
|
import { t as n } from "./chunks/objectSpread2-CyPxu8-u.mjs";
|
|
4
4
|
import { forwardRef as r, useEffect as i, useMemo as a, useRef as o, useState as s } from "react";
|
|
5
5
|
import { jsx as c } from "react/jsx-runtime";
|
package/dist/tools.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { m as e } from "./chunks/constants-
|
|
2
|
-
import { a as t, c as n, d as r, i, l as a, n as o, o as s, r as c, s as l, t as u, u as d } from "./chunks/tools-
|
|
1
|
+
import { m as e } from "./chunks/constants-BkelccB1.mjs";
|
|
2
|
+
import { a as t, c as n, d as r, i, l as a, n as o, o as s, r as c, s as l, t as u, u as d } from "./chunks/tools-rsbC2UUN.mjs";
|
|
3
3
|
export { s as Bold, e as Convert, d as Header, t as Italic, i as Link, a as List, c as Marker, r as Paragraph, n as Table, l as Toggle, u as defaultBlockTools, o as defaultInlineTools };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackuait/blok",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Blok — headless, highly extensible rich text editor built for developers who need to implement a block-based editing experience (similar to Notion) without building it from scratch",
|
|
5
5
|
"module": "dist/blok.mjs",
|
|
6
6
|
"types": "./types/index.d.ts",
|
|
@@ -86,6 +86,17 @@ export class MarkerInlineTool implements InlineTool {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* When text color is set without an explicit background-color,
|
|
91
|
+
* add transparent background to override the browser's default
|
|
92
|
+
* <mark> background (yellow/Mark system color). This handles
|
|
93
|
+
* pasted content where the browser may have dropped the
|
|
94
|
+
* transparent value during clipboard serialization.
|
|
95
|
+
*/
|
|
96
|
+
if (style.getPropertyValue('color') && !style.getPropertyValue('background-color')) {
|
|
97
|
+
style.setProperty('background-color', 'transparent');
|
|
98
|
+
}
|
|
99
|
+
|
|
89
100
|
return style.length > 0 ? { style: true } : {};
|
|
90
101
|
},
|
|
91
102
|
} as SanitizerConfig;
|
|
@@ -249,7 +249,7 @@ export class KeyboardNavigation extends BlockEventComposer {
|
|
|
249
249
|
|
|
250
250
|
if (toggleWrapper?.getAttribute('data-blok-toggle-open') === 'true') {
|
|
251
251
|
this.Blok.BlockManager.setBlockParent(newBlock, currentBlock.id);
|
|
252
|
-
} else if (currentBlock.parentId !== null) {
|
|
252
|
+
} else if (currentBlock.parentId !== null && newBlock.parentId !== currentBlock.parentId) {
|
|
253
253
|
this.Blok.BlockManager.setBlockParent(newBlock, currentBlock.parentId);
|
|
254
254
|
}
|
|
255
255
|
|
|
@@ -600,14 +600,6 @@ export class KeyboardNavigation extends BlockEventComposer {
|
|
|
600
600
|
const isDownKey = keyCode === keyCodes.DOWN;
|
|
601
601
|
const isRightKey = keyCode === keyCodes.RIGHT && !this.isRtl;
|
|
602
602
|
|
|
603
|
-
/**
|
|
604
|
-
* Skip ArrowDown handling for blocks inside table cells — the table's own
|
|
605
|
-
* grid-level handler manages row-based navigation and table exit.
|
|
606
|
-
*/
|
|
607
|
-
if (isDownKey && this.isCurrentBlockInsideTableCell) {
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
603
|
const isNavigated = (() => {
|
|
612
604
|
if (isDownKey) {
|
|
613
605
|
/**
|
|
@@ -681,10 +681,22 @@ export class Caret extends Module {
|
|
|
681
681
|
return true;
|
|
682
682
|
}
|
|
683
683
|
|
|
684
|
+
/**
|
|
685
|
+
* If both blocks share the same DOM container (e.g., same table cell),
|
|
686
|
+
* navigate directly to the next block instead of exiting the table.
|
|
687
|
+
*/
|
|
688
|
+
if (nextBlock !== null && currentBlock.parentId !== null &&
|
|
689
|
+
currentBlock.holder.parentElement !== null &&
|
|
690
|
+
currentBlock.holder.parentElement === nextBlock.holder.parentElement) {
|
|
691
|
+
this.setToBlockAtXPosition(nextBlock, caretX, true);
|
|
692
|
+
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
|
|
684
696
|
/**
|
|
685
697
|
* If current block is inside a table cell (has parentId), check if we should
|
|
686
698
|
* exit the table. This handles two cases:
|
|
687
|
-
* 1. nextBlock is
|
|
699
|
+
* 1. nextBlock is in a different cell of the same table → skip to block after table
|
|
688
700
|
* 2. nextBlock is null (last block in flat list) → table is at the end
|
|
689
701
|
*/
|
|
690
702
|
const shouldExitParent = currentBlock.parentId !== null && (
|
|
@@ -17,9 +17,13 @@ export function preprocessGoogleDocsHtml(html: string): string {
|
|
|
17
17
|
|
|
18
18
|
wrapper.innerHTML = html;
|
|
19
19
|
|
|
20
|
-
unwrapGoogleDocsContent(wrapper);
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const isGoogleDocs = unwrapGoogleDocsContent(wrapper);
|
|
21
|
+
|
|
22
|
+
convertGoogleDocsStyles(wrapper, isGoogleDocs);
|
|
23
|
+
|
|
24
|
+
if (isGoogleDocs) {
|
|
25
|
+
convertTableCellParagraphs(wrapper);
|
|
26
|
+
}
|
|
23
27
|
|
|
24
28
|
return wrapper.innerHTML;
|
|
25
29
|
}
|
|
@@ -29,12 +33,14 @@ export function preprocessGoogleDocsHtml(html: string): string {
|
|
|
29
33
|
* Google Docs wraps clipboard HTML in `<b id="docs-internal-guid-...">`.
|
|
30
34
|
* Content may be split across multiple child `<div>` elements (e.g. one
|
|
31
35
|
* per table), so all children are moved out of the wrapper.
|
|
36
|
+
*
|
|
37
|
+
* @returns true if Google Docs content was detected
|
|
32
38
|
*/
|
|
33
|
-
function unwrapGoogleDocsContent(wrapper: HTMLElement):
|
|
39
|
+
function unwrapGoogleDocsContent(wrapper: HTMLElement): boolean {
|
|
34
40
|
const googleDocsWrapper = wrapper.querySelector<HTMLElement>('b[id^="docs-internal-guid-"]');
|
|
35
41
|
|
|
36
42
|
if (!googleDocsWrapper) {
|
|
37
|
-
return;
|
|
43
|
+
return false;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
const fragment = document.createDocumentFragment();
|
|
@@ -44,6 +50,8 @@ function unwrapGoogleDocsContent(wrapper: HTMLElement): void {
|
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
googleDocsWrapper.replaceWith(fragment);
|
|
53
|
+
|
|
54
|
+
return true;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/**
|
|
@@ -76,6 +84,82 @@ function isDefaultBlack(color: string): boolean {
|
|
|
76
84
|
return normalized === 'rgb(0,0,0)' || normalized === '#000000';
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Check whether a CSS background-color value is the default white page background.
|
|
89
|
+
* When the browser natively copies from a contenteditable, it adds computed styles
|
|
90
|
+
* including `background-color: rgb(255, 255, 255)` — the resolved page background.
|
|
91
|
+
* These should not be treated as intentional marker formatting.
|
|
92
|
+
*/
|
|
93
|
+
function isDefaultWhiteBackground(bgColor: string): boolean {
|
|
94
|
+
const normalized = bgColor.replace(/\s/g, '').toLowerCase();
|
|
95
|
+
|
|
96
|
+
return normalized === 'rgb(255,255,255)' || normalized === '#ffffff' || normalized === 'white';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Optionally wrap innerHTML in a `<mark>` with mapped color styles.
|
|
101
|
+
* Returns the original content unchanged when no color formatting is needed.
|
|
102
|
+
*/
|
|
103
|
+
function buildMarkWrapper(
|
|
104
|
+
innerHTML: string,
|
|
105
|
+
hasColor: boolean,
|
|
106
|
+
hasBgColor: boolean,
|
|
107
|
+
color: string | undefined,
|
|
108
|
+
bgColor: string | undefined
|
|
109
|
+
): string {
|
|
110
|
+
if (!hasColor && !hasBgColor) {
|
|
111
|
+
return innerHTML;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const mappedColor = hasColor && color !== undefined ? mapToNearestPresetColor(color, 'text') : '';
|
|
115
|
+
const mappedBg = hasBgColor && bgColor !== undefined ? mapToNearestPresetColor(bgColor, 'bg') : '';
|
|
116
|
+
|
|
117
|
+
const colorStyles = [
|
|
118
|
+
hasColor ? `color: ${mappedColor}` : '',
|
|
119
|
+
resolveBackgroundStyle(hasBgColor, hasColor, mappedBg),
|
|
120
|
+
].filter(Boolean).join('; ');
|
|
121
|
+
|
|
122
|
+
return colorStyles
|
|
123
|
+
? `<mark style="${colorStyles};">${innerHTML}</mark>`
|
|
124
|
+
: innerHTML;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Convert a single style `<span>` to semantic HTML.
|
|
129
|
+
*
|
|
130
|
+
* For Google Docs content, all non-transparent backgrounds are treated as
|
|
131
|
+
* intentional formatting. For browser-native clipboard content, default
|
|
132
|
+
* page values (black text, white background) are filtered out so computed
|
|
133
|
+
* styles on plain text don't produce spurious `<mark>` elements.
|
|
134
|
+
*
|
|
135
|
+
* @returns replacement HTML string, or `null` if the span should be left as-is
|
|
136
|
+
*/
|
|
137
|
+
function convertSpanToSemanticHtml(span: Element, isGoogleDocs: boolean): string | null {
|
|
138
|
+
const style = span.getAttribute('style') ?? '';
|
|
139
|
+
const isBold = /font-weight\s*:\s*(700|bold)/i.test(style);
|
|
140
|
+
const isItalic = /font-style\s*:\s*italic/i.test(style);
|
|
141
|
+
|
|
142
|
+
const colorMatch = /(?<![a-z-])color\s*:\s*([^;]+)/i.exec(style);
|
|
143
|
+
const bgMatch = /background-color\s*:\s*([^;]+)/i.exec(style);
|
|
144
|
+
|
|
145
|
+
const color = colorMatch?.[1]?.trim();
|
|
146
|
+
const bgColor = bgMatch?.[1]?.trim();
|
|
147
|
+
|
|
148
|
+
const hasColor = color !== undefined && !isDefaultBlack(color);
|
|
149
|
+
const hasBgColor = isGoogleDocs
|
|
150
|
+
? bgColor !== undefined && bgColor !== 'transparent'
|
|
151
|
+
: bgColor !== undefined && bgColor !== 'transparent' && !isDefaultWhiteBackground(bgColor);
|
|
152
|
+
|
|
153
|
+
if (!isBold && !isItalic && !hasColor && !hasBgColor) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const inner = buildMarkWrapper(span.innerHTML, hasColor, hasBgColor, color, bgColor);
|
|
158
|
+
const italic = isItalic ? `<i>${inner}</i>` : inner;
|
|
159
|
+
|
|
160
|
+
return isBold ? `<b>${italic}</b>` : italic;
|
|
161
|
+
}
|
|
162
|
+
|
|
79
163
|
/**
|
|
80
164
|
* Convert Google Docs style-based `<span>` elements to semantic HTML tags.
|
|
81
165
|
*
|
|
@@ -86,44 +170,18 @@ function isDefaultBlack(color: string): boolean {
|
|
|
86
170
|
*
|
|
87
171
|
* Color and bold/italic can combine: a bold red span becomes `<b><mark style="color: red;">text</mark></b>`.
|
|
88
172
|
*/
|
|
89
|
-
function convertGoogleDocsStyles(wrapper: HTMLElement): void {
|
|
173
|
+
function convertGoogleDocsStyles(wrapper: HTMLElement, isGoogleDocs: boolean): void {
|
|
90
174
|
for (const span of Array.from(wrapper.querySelectorAll('span[style]'))) {
|
|
91
|
-
const
|
|
92
|
-
const isBold = /font-weight\s*:\s*(700|bold)/i.test(style);
|
|
93
|
-
const isItalic = /font-style\s*:\s*italic/i.test(style);
|
|
175
|
+
const replacement = convertSpanToSemanticHtml(span, isGoogleDocs);
|
|
94
176
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const color = colorMatch?.[1]?.trim();
|
|
99
|
-
const bgColor = bgMatch?.[1]?.trim();
|
|
100
|
-
|
|
101
|
-
const hasColor = color !== undefined && !isDefaultBlack(color);
|
|
102
|
-
const hasBgColor = bgColor !== undefined && bgColor !== 'transparent';
|
|
103
|
-
|
|
104
|
-
if (!isBold && !isItalic && !hasColor && !hasBgColor) {
|
|
105
|
-
continue;
|
|
177
|
+
if (replacement !== null) {
|
|
178
|
+
span.replaceWith(document.createRange().createContextualFragment(replacement));
|
|
106
179
|
}
|
|
107
|
-
|
|
108
|
-
const mappedColor = hasColor ? mapToNearestPresetColor(color, 'text') : '';
|
|
109
|
-
const mappedBg = hasBgColor ? mapToNearestPresetColor(bgColor, 'bg') : '';
|
|
110
|
-
|
|
111
|
-
const colorStyles = [
|
|
112
|
-
hasColor ? `color: ${mappedColor}` : '',
|
|
113
|
-
resolveBackgroundStyle(hasBgColor, hasColor, mappedBg),
|
|
114
|
-
].filter(Boolean).join('; ');
|
|
115
|
-
|
|
116
|
-
const inner = colorStyles
|
|
117
|
-
? `<mark style="${colorStyles};">${span.innerHTML}</mark>`
|
|
118
|
-
: span.innerHTML;
|
|
119
|
-
|
|
120
|
-
const italic = isItalic ? `<i>${inner}</i>` : inner;
|
|
121
|
-
const wrapped = isBold ? `<b>${italic}</b>` : italic;
|
|
122
|
-
|
|
123
|
-
span.replaceWith(document.createRange().createContextualFragment(wrapped));
|
|
124
180
|
}
|
|
125
181
|
|
|
126
|
-
|
|
182
|
+
if (isGoogleDocs) {
|
|
183
|
+
convertAnchorColorStyles(wrapper);
|
|
184
|
+
}
|
|
127
185
|
}
|
|
128
186
|
|
|
129
187
|
/**
|
|
@@ -188,7 +188,7 @@ export class BlockSettings extends Module<BlockSettingsNodes> {
|
|
|
188
188
|
|
|
189
189
|
const PopoverClass = isMobileScreen() ? PopoverMobile : PopoverDesktop;
|
|
190
190
|
const popoverParams: PopoverParams & { flipper?: Flipper } = {
|
|
191
|
-
searchable:
|
|
191
|
+
searchable: true,
|
|
192
192
|
trigger: trigger || this.nodes.wrapper,
|
|
193
193
|
items: await this.getTunesItems(block, commonTunes, toolTunes),
|
|
194
194
|
scopeElement: this.Blok.API.methods.ui.nodes.redactor,
|
|
@@ -428,6 +428,16 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
428
428
|
targetBlock.setupDraggable(settingsToggler, this.Blok.DragManager);
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Update toolbox left alignment to the block's content element so the popover
|
|
433
|
+
* aligns with the actual visible content, not the toolbar's internal wrapper.
|
|
434
|
+
*/
|
|
435
|
+
const blockContentElement = targetBlockHolder.querySelector<HTMLElement>(`[${DATA_ATTR.elementContent}]`);
|
|
436
|
+
|
|
437
|
+
if (blockContentElement) {
|
|
438
|
+
this.toolboxInstance.updateLeftAlignElement(blockContentElement);
|
|
439
|
+
}
|
|
440
|
+
|
|
431
441
|
/**
|
|
432
442
|
* Apply content offset for nested elements (e.g., nested list items)
|
|
433
443
|
*/
|
|
@@ -836,7 +846,6 @@ export class Toolbar extends Module<ToolbarNodes> {
|
|
|
836
846
|
},
|
|
837
847
|
i18n: this.Blok.I18n,
|
|
838
848
|
triggerElement: this.nodes.plusButton,
|
|
839
|
-
leftAlignElement: this.nodes.content,
|
|
840
849
|
});
|
|
841
850
|
|
|
842
851
|
this.toolboxInstance.on(ToolboxEvent.Opened, () => {
|
|
@@ -81,6 +81,11 @@ export class InlineToolbar extends Module<InlineToolbarNodes> {
|
|
|
81
81
|
private keyboardHandler: InlineKeyboardHandler;
|
|
82
82
|
private lifecycleManager: InlineLifecycleManager;
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* SelectionUtils instance for fake background cleanup
|
|
86
|
+
*/
|
|
87
|
+
private selection: SelectionUtils = new SelectionUtils();
|
|
88
|
+
|
|
84
89
|
/**
|
|
85
90
|
* Currently visible tools instances
|
|
86
91
|
*/
|
|
@@ -185,8 +190,22 @@ export class InlineToolbar extends Module<InlineToolbarNodes> {
|
|
|
185
190
|
}
|
|
186
191
|
|
|
187
192
|
this.openingPromise = this.open();
|
|
188
|
-
|
|
189
|
-
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
await this.openingPromise;
|
|
196
|
+
} catch {
|
|
197
|
+
this.opened = false;
|
|
198
|
+
|
|
199
|
+
if (this.popover) {
|
|
200
|
+
this.popover.hide?.();
|
|
201
|
+
this.popover.destroy?.();
|
|
202
|
+
this.popover = null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return;
|
|
206
|
+
} finally {
|
|
207
|
+
this.openingPromise = null;
|
|
208
|
+
}
|
|
190
209
|
|
|
191
210
|
this.Blok.Toolbar.hideBlockActions();
|
|
192
211
|
}
|
|
@@ -203,6 +222,9 @@ export class InlineToolbar extends Module<InlineToolbarNodes> {
|
|
|
203
222
|
this.opened = false;
|
|
204
223
|
this.openingPromise = null;
|
|
205
224
|
|
|
225
|
+
// Clean up any orphaned fake background elements left by inline tools (Link, Marker, Convert)
|
|
226
|
+
this.selection.clearFakeBackground();
|
|
227
|
+
|
|
206
228
|
// Hide and destroy popover
|
|
207
229
|
if (this.popover) {
|
|
208
230
|
this.popover.hide?.();
|
|
@@ -43,6 +43,13 @@ export class SelectionCursor {
|
|
|
43
43
|
selection.removeAllRanges();
|
|
44
44
|
selection.addRange(range);
|
|
45
45
|
|
|
46
|
+
// Focus contenteditable elements explicitly after setting the selection range.
|
|
47
|
+
// Placed after addRange() so the selection is preserved when focus transfers —
|
|
48
|
+
// calling focus() before addRange() can reset the caret during arrow navigation.
|
|
49
|
+
if ($.isContentEditable(element) && document.activeElement !== element) {
|
|
50
|
+
element.focus();
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
return range.getBoundingClientRect();
|
|
47
54
|
}
|
|
48
55
|
|
|
@@ -205,6 +205,20 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
|
|
|
205
205
|
this.api.events.on(BlokMobileLayoutToggled, this.handleMobileLayoutToggle);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Updates the element used for horizontal popover alignment.
|
|
210
|
+
* Called when the toolbar moves to a new block so the popover
|
|
211
|
+
* aligns with the block's content element rather than the toolbar's own wrapper.
|
|
212
|
+
* @param element - block content element to align against
|
|
213
|
+
*/
|
|
214
|
+
public updateLeftAlignElement(element: HTMLElement | undefined): void {
|
|
215
|
+
this.leftAlignElement = element;
|
|
216
|
+
|
|
217
|
+
if (this.popover !== null && 'setLeftAlignElement' in this.popover) {
|
|
218
|
+
(this.popover as { setLeftAlignElement: (el: HTMLElement | undefined) => void }).setLeftAlignElement(element);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
208
222
|
/**
|
|
209
223
|
* Returns root block settings element
|
|
210
224
|
*/
|
|
@@ -339,7 +339,7 @@ export class PopoverItemDefault extends PopoverItem {
|
|
|
339
339
|
private getIconClass(iconWithGap: boolean, isInline: boolean, isNestedInline: boolean, isWobbling: boolean): string {
|
|
340
340
|
return twMerge(
|
|
341
341
|
css.icon,
|
|
342
|
-
isInline && 'w-auto h-auto [&_svg]:w-icon [&_svg]:h-icon mobile:[&_svg]:w-icon-mobile mobile:[&_svg]:h-icon-mobile',
|
|
342
|
+
isInline && 'w-auto h-auto bg-transparent [&_svg]:w-icon [&_svg]:h-icon mobile:[&_svg]:w-icon-mobile mobile:[&_svg]:h-icon-mobile',
|
|
343
343
|
isNestedInline && 'w-toolbox-btn h-toolbox-btn',
|
|
344
344
|
iconWithGap && 'mr-3',
|
|
345
345
|
iconWithGap && isInline && 'shadow-none mr-0!',
|
|
@@ -5,4 +5,5 @@ export const css = {
|
|
|
5
5
|
wrapper: 'bg-search-input-bg border border-search-input-border rounded-lg p-1 grid grid-cols-[auto_auto_1fr] grid-rows-[auto] transition-all duration-200 focus-within:bg-popover-bg focus-within:border-search-input-focus-border focus-within:shadow-[0_0_0_2px_rgba(35,131,226,0.08)]',
|
|
6
6
|
icon: 'w-toolbox-btn h-toolbox-btn flex items-center justify-center mr-2 [&_svg]:w-icon [&_svg]:h-icon [&_svg]:text-gray-text',
|
|
7
7
|
input: "text-sm outline-hidden font-medium font-inherit border-0 bg-transparent m-0 p-0 leading-[22px] min-w-[calc(100%-(--spacing(6))-10px)] placeholder:text-gray-text/60 placeholder:font-normal",
|
|
8
|
+
clearButton: 'flex items-center justify-center w-toolbox-btn h-toolbox-btn cursor-pointer border-0 bg-transparent rounded p-0 opacity-0 pointer-events-none transition-opacity duration-150 [&_svg]:w-3 [&_svg]:h-3 [&_svg]:text-gray-text can-hover:hover:[&_svg]:text-dark',
|
|
8
9
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Dom } from '../../../../dom';
|
|
2
|
-
import { IconSearch } from '../../../../icons';
|
|
2
|
+
import { IconCross, IconSearch } from '../../../../icons';
|
|
3
3
|
import { EventsDispatcher } from '../../../events';
|
|
4
4
|
import { Listeners } from '../../../listeners';
|
|
5
5
|
|
|
@@ -23,6 +23,11 @@ export class SearchInput extends EventsDispatcher<SearchInputEventMap> {
|
|
|
23
23
|
*/
|
|
24
24
|
private input: HTMLInputElement;
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Clear button element
|
|
28
|
+
*/
|
|
29
|
+
private clearButton: HTMLButtonElement;
|
|
30
|
+
|
|
26
31
|
/**
|
|
27
32
|
* The instance of the Listeners util
|
|
28
33
|
*/
|
|
@@ -73,8 +78,15 @@ export class SearchInput extends EventsDispatcher<SearchInputEventMap> {
|
|
|
73
78
|
this.input.setAttribute('data-blok-flipper-navigation-target', 'true');
|
|
74
79
|
this.input.setAttribute('data-blok-testid', 'popover-search-input');
|
|
75
80
|
|
|
81
|
+
this.clearButton = Dom.make('button', css.clearButton, {
|
|
82
|
+
type: 'button',
|
|
83
|
+
innerHTML: IconCross,
|
|
84
|
+
}) as HTMLButtonElement;
|
|
85
|
+
this.clearButton.setAttribute('aria-label', 'Clear search');
|
|
86
|
+
|
|
76
87
|
this.wrapper.appendChild(iconWrapper);
|
|
77
88
|
this.wrapper.appendChild(this.input);
|
|
89
|
+
this.wrapper.appendChild(this.clearButton);
|
|
78
90
|
|
|
79
91
|
this.overrideValueProperty();
|
|
80
92
|
|
|
@@ -83,6 +95,11 @@ export class SearchInput extends EventsDispatcher<SearchInputEventMap> {
|
|
|
83
95
|
eventsToHandle.forEach((eventName) => {
|
|
84
96
|
this.listeners.on(this.input, eventName, this.handleValueChange);
|
|
85
97
|
});
|
|
98
|
+
|
|
99
|
+
this.listeners.on(this.clearButton, 'click', () => {
|
|
100
|
+
this.clear();
|
|
101
|
+
this.input.focus();
|
|
102
|
+
});
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
/**
|
|
@@ -104,12 +121,14 @@ export class SearchInput extends EventsDispatcher<SearchInputEventMap> {
|
|
|
104
121
|
*/
|
|
105
122
|
public clear(): void {
|
|
106
123
|
this.input.value = '';
|
|
124
|
+
this.updateClearButtonVisibility();
|
|
107
125
|
}
|
|
108
126
|
|
|
109
127
|
/**
|
|
110
128
|
* Handles value changes for the input element
|
|
111
129
|
*/
|
|
112
130
|
private handleValueChange = (): void => {
|
|
131
|
+
this.updateClearButtonVisibility();
|
|
113
132
|
this.applySearch(this.input.value);
|
|
114
133
|
};
|
|
115
134
|
|
|
@@ -159,6 +178,18 @@ export class SearchInput extends EventsDispatcher<SearchInputEventMap> {
|
|
|
159
178
|
});
|
|
160
179
|
}
|
|
161
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Shows or hides the clear button based on whether the input has a value
|
|
183
|
+
*/
|
|
184
|
+
private updateClearButtonVisibility(): void {
|
|
185
|
+
const visible = this.input.value.length > 0;
|
|
186
|
+
|
|
187
|
+
this.clearButton.classList.toggle('opacity-0', !visible);
|
|
188
|
+
this.clearButton.classList.toggle('pointer-events-none', !visible);
|
|
189
|
+
this.clearButton.classList.toggle('opacity-100', visible);
|
|
190
|
+
this.clearButton.classList.toggle('pointer-events-auto', visible);
|
|
191
|
+
}
|
|
192
|
+
|
|
162
193
|
/**
|
|
163
194
|
* Clears memory
|
|
164
195
|
*/
|
|
@@ -67,6 +67,14 @@ export class PopoverDesktop extends PopoverAbstract {
|
|
|
67
67
|
*/
|
|
68
68
|
private leftAlignElement: HTMLElement | undefined;
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Updates the element whose left edge is used for horizontal positioning.
|
|
72
|
+
* @param element - new element to align against, or undefined to fall back to trigger
|
|
73
|
+
*/
|
|
74
|
+
public setLeftAlignElement(element: HTMLElement | undefined): void {
|
|
75
|
+
this.leftAlignElement = element;
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
/**
|
|
71
79
|
* Popover size cache
|
|
72
80
|
*/
|
|
@@ -405,7 +413,7 @@ export class PopoverDesktop extends PopoverAbstract {
|
|
|
405
413
|
* Destroys nested popover unless the mouse moved into it.
|
|
406
414
|
* @param event - mouseleave event
|
|
407
415
|
*/
|
|
408
|
-
|
|
416
|
+
protected handleMouseLeave(event: Event): void {
|
|
409
417
|
const mouseEvent = event as MouseEvent;
|
|
410
418
|
const relatedTarget = mouseEvent.relatedTarget;
|
|
411
419
|
|
|
@@ -240,6 +240,14 @@ export class PopoverInline extends PopoverDesktop {
|
|
|
240
240
|
return;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Disable mouse-leave event handling.
|
|
245
|
+
* Inline toolbar uses click-to-toggle for nested popovers, not hover.
|
|
246
|
+
*/
|
|
247
|
+
protected override handleMouseLeave(): void {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
243
251
|
/**
|
|
244
252
|
* Sets CSS variable with position of item near which nested popover should be displayed.
|
|
245
253
|
* Is used to position nested popover right below clicked item
|