@jackuait/blok 0.10.0-beta.8 → 0.10.0
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-CRvF-xVm.mjs → blok-BfcBwAfE.mjs} +1211 -1159
- package/dist/chunks/{constants-BOZ5plBi.mjs → constants-QNVyXALL.mjs} +49 -48
- package/dist/chunks/{tools-CnqCfv2L.mjs → tools-DHtzbrxy.mjs} +1411 -1220
- package/dist/full.mjs +3 -3
- package/dist/react.mjs +2 -2
- package/dist/tools.mjs +2 -2
- package/package.json +3 -5
- package/src/cli/commands/convert-gdocs/index.ts +26 -0
- package/src/cli/commands/convert-html/block-builder.ts +392 -0
- package/src/cli/commands/convert-html/id-generator.ts +11 -0
- package/src/cli/commands/convert-html/index.ts +23 -0
- package/src/cli/commands/convert-html/preprocessor.ts +422 -0
- package/src/cli/commands/convert-html/sanitizer.ts +93 -0
- package/src/cli/commands/convert-html/types.ts +15 -0
- package/src/cli/index.ts +56 -5
- package/src/components/block/index.ts +44 -10
- package/src/components/constants/data-attributes.ts +10 -0
- package/src/components/icons/index.ts +16 -0
- package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +18 -0
- package/src/components/modules/blockManager/hierarchy.ts +4 -1
- package/src/components/modules/readonly.ts +46 -0
- package/src/components/modules/rectangleSelection.ts +25 -5
- package/src/components/modules/toolbar/index.ts +96 -19
- package/src/components/modules/toolbar/styles.ts +0 -2
- package/src/components/modules/uiControllers/controllers/blockHover.ts +44 -1
- package/src/components/tools/block.ts +10 -0
- package/src/components/utils/placeholder.ts +9 -2
- package/src/styles/main.css +16 -0
- package/src/tools/callout/constants.ts +2 -1
- package/src/tools/callout/dom-builder.ts +13 -1
- package/src/tools/callout/index.ts +21 -7
- package/src/tools/code/constants.ts +9 -1
- package/src/tools/code/dom-builder.ts +90 -54
- package/src/tools/code/index.ts +73 -31
- package/src/tools/divider/index.ts +5 -0
- package/src/tools/header/index.ts +47 -1
- package/src/tools/list/dom-builder.ts +3 -1
- package/src/tools/list/index.ts +55 -3
- package/src/tools/list/list-helpers.ts +2 -2
- package/src/tools/nested-blocks.ts +25 -0
- package/src/tools/paragraph/index.ts +47 -6
- package/src/tools/quote/index.ts +43 -8
- package/src/tools/stub/index.ts +10 -0
- package/src/tools/table/index.ts +238 -6
- package/src/tools/table/table-add-controls.ts +37 -5
- package/src/tools/table/table-cell-blocks.ts +87 -18
- package/src/tools/table/table-core.ts +2 -0
- package/src/tools/table/table-corner-drag.ts +247 -0
- package/src/tools/table/table-operations.ts +45 -9
- package/src/tools/toggle/dom-builder.ts +1 -0
- package/src/tools/toggle/index.ts +25 -0
- package/src/tools/toggle/toggle-lifecycle.ts +5 -4
- package/src/types-internal/jsdom.d.ts +9 -0
- package/types/tools/adapters/block-tool-adapter.d.ts +6 -0
- package/types/tools/block-tool.d.ts +10 -0
- package/bin/blok.mjs +0 -10
- package/dist/cli.mjs +0 -37
- package/src/tools/code/language-picker.ts +0 -241
|
@@ -16,6 +16,7 @@ import type { CalloutData, CalloutConfig } from './types';
|
|
|
16
16
|
import { buildCalloutDOM, type CalloutDOMRefs } from './dom-builder';
|
|
17
17
|
import { saveCallout } from './block-operations';
|
|
18
18
|
import { handleCalloutFirstChildBackspace } from './callout-keyboard';
|
|
19
|
+
import { mountChildBlocks } from '../nested-blocks';
|
|
19
20
|
import { createColorPicker, type ColorPickerHandle } from '../../components/shared/color-picker';
|
|
20
21
|
import { colorVarName } from '../../components/shared/color-presets';
|
|
21
22
|
import { mapToNearestPresetName } from '../../components/utils/color-mapping';
|
|
@@ -60,11 +61,12 @@ const VARIANT_TO_BG_PRESET: Record<string, string | null> = {
|
|
|
60
61
|
|
|
61
62
|
export class CalloutTool implements BlockTool {
|
|
62
63
|
private readonly api: API;
|
|
63
|
-
private
|
|
64
|
+
private readOnly: boolean;
|
|
64
65
|
private _data: CalloutData;
|
|
65
66
|
private _dom: CalloutDOMRefs | null = null;
|
|
66
67
|
private _emojiPicker: EmojiPicker | null = null;
|
|
67
68
|
private _colorPicker: ColorPickerHandle | null = null;
|
|
69
|
+
private _dragZone: HTMLElement | null = null;
|
|
68
70
|
private blockId?: string;
|
|
69
71
|
|
|
70
72
|
constructor({ data, api, readOnly, block }: BlockToolConstructorOptions<CalloutData, CalloutConfig>) {
|
|
@@ -108,6 +110,10 @@ export class CalloutTool implements BlockTool {
|
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
public render(): HTMLElement {
|
|
113
|
+
if (this._dom) {
|
|
114
|
+
return this._dom.wrapper;
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
const dom = buildCalloutDOM({
|
|
112
118
|
emoji: this._data.emoji,
|
|
113
119
|
readOnly: this.readOnly,
|
|
@@ -115,6 +121,7 @@ export class CalloutTool implements BlockTool {
|
|
|
115
121
|
});
|
|
116
122
|
|
|
117
123
|
this._dom = dom;
|
|
124
|
+
this._dragZone = dom.dragZone;
|
|
118
125
|
this.applyColors();
|
|
119
126
|
|
|
120
127
|
if (!this.readOnly) {
|
|
@@ -144,12 +151,7 @@ export class CalloutTool implements BlockTool {
|
|
|
144
151
|
|
|
145
152
|
const children = this.api.blocks.getChildren(this.blockId);
|
|
146
153
|
|
|
147
|
-
|
|
148
|
-
for (const child of children) {
|
|
149
|
-
if (child.holder.parentElement !== this._dom.childContainer) {
|
|
150
|
-
this._dom.childContainer.appendChild(child.holder);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
154
|
+
mountChildBlocks(this._dom.childContainer, children);
|
|
153
155
|
|
|
154
156
|
// Auto-create initial paragraph child when callout has no children
|
|
155
157
|
if (children.length === 0) {
|
|
@@ -252,6 +254,18 @@ export class CalloutTool implements BlockTool {
|
|
|
252
254
|
// No-op — no subscriptions to clean up
|
|
253
255
|
}
|
|
254
256
|
|
|
257
|
+
public setReadOnly(state: boolean): void {
|
|
258
|
+
this.readOnly = state;
|
|
259
|
+
|
|
260
|
+
if (this._dom) {
|
|
261
|
+
this._dom.emojiButton.disabled = state;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public get dragZone(): HTMLElement | null {
|
|
266
|
+
return this._dragZone;
|
|
267
|
+
}
|
|
268
|
+
|
|
255
269
|
private syncPickerActiveColors(): void {
|
|
256
270
|
if (this._colorPicker === null) {
|
|
257
271
|
return;
|
|
@@ -53,7 +53,7 @@ export const LANGUAGES: LanguageEntry[] = [
|
|
|
53
53
|
];
|
|
54
54
|
|
|
55
55
|
// CSS — Tailwind classes
|
|
56
|
-
export const WRAPPER_STYLES = 'flex flex-col rounded-lg bg-bg-secondary overflow-hidden my-1';
|
|
56
|
+
export const WRAPPER_STYLES = 'flex flex-col rounded-lg border border-border-secondary bg-bg-secondary overflow-hidden my-1';
|
|
57
57
|
export const HEADER_STYLES = 'flex items-center gap-1 px-3 py-1.5 border-b border-border-primary text-xs text-gray-text';
|
|
58
58
|
export const LANGUAGE_BUTTON_STYLES = 'px-1.5 py-0.5 rounded cursor-pointer bg-transparent border-0 text-xs text-gray-text font-medium transition-colors can-hover:hover:bg-item-hover-bg select-none';
|
|
59
59
|
export const HEADER_BUTTON_STYLES = 'p-1 rounded cursor-pointer bg-transparent border-0 text-gray-text transition-colors can-hover:hover:bg-item-hover-bg flex items-center justify-center';
|
|
@@ -73,6 +73,14 @@ export const TAB_ACTIVE_STYLES = 'bg-blue-500 text-white';
|
|
|
73
73
|
export const TAB_INACTIVE_STYLES = 'bg-transparent text-gray-text can-hover:hover:bg-item-hover-bg';
|
|
74
74
|
export const PREVIEW_AREA_STYLES = 'px-4 py-3 overflow-x-auto min-h-[1.5em] flex justify-center';
|
|
75
75
|
|
|
76
|
+
// i18n key — preview toggle
|
|
77
|
+
export const PREVIEW_TOGGLE_KEY = 'tools.code.previewToggle';
|
|
78
|
+
|
|
79
|
+
// CSS — more menu dropdown
|
|
80
|
+
export const MORE_MENU_STYLES = 'absolute right-0 top-full mt-1 min-w-[10rem] rounded-lg bg-bg-secondary border border-border-secondary shadow-lg p-1 z-10';
|
|
81
|
+
export const MORE_MENU_ITEM_STYLES = 'flex items-center gap-2 w-full px-2.5 py-1.5 rounded text-xs text-gray-text cursor-pointer bg-transparent border-0 transition-colors can-hover:hover:bg-item-hover-bg select-none';
|
|
82
|
+
export const MORE_MENU_ITEM_ACTIVE_STYLES = 'text-blue-500';
|
|
83
|
+
|
|
76
84
|
// Shiki theme names for syntax highlighting
|
|
77
85
|
export const SHIKI_LIGHT_THEME = 'one-light';
|
|
78
86
|
export const SHIKI_DARK_THEME = 'vitesse-dark';
|
|
@@ -4,15 +4,14 @@ import {
|
|
|
4
4
|
LANGUAGE_BUTTON_STYLES,
|
|
5
5
|
HEADER_BUTTON_STYLES,
|
|
6
6
|
CODE_AREA_STYLES,
|
|
7
|
-
TAB_STYLES,
|
|
8
|
-
TAB_ACTIVE_STYLES,
|
|
9
|
-
TAB_INACTIVE_STYLES,
|
|
10
7
|
PREVIEW_AREA_STYLES,
|
|
11
8
|
CODE_BODY_STYLES,
|
|
12
9
|
GUTTER_STYLES,
|
|
13
10
|
GUTTER_LINE_STYLES,
|
|
11
|
+
MORE_MENU_STYLES,
|
|
12
|
+
MORE_MENU_ITEM_STYLES,
|
|
14
13
|
} from './constants';
|
|
15
|
-
import { IconCopy, IconWrap, IconLineNumbers } from '../../components/icons';
|
|
14
|
+
import { IconCopy, IconCode, IconChevronDown, IconEllipsis, IconWrap, IconLineNumbers } from '../../components/icons';
|
|
16
15
|
|
|
17
16
|
export interface CodeDOMRefs {
|
|
18
17
|
wrapper: HTMLElement;
|
|
@@ -23,9 +22,10 @@ export interface CodeDOMRefs {
|
|
|
23
22
|
preElement: HTMLPreElement;
|
|
24
23
|
codeElement: HTMLElement;
|
|
25
24
|
gutterElement: HTMLElement;
|
|
26
|
-
|
|
27
|
-
previewTab: HTMLButtonElement | null;
|
|
25
|
+
previewToggleButton: HTMLButtonElement | null;
|
|
28
26
|
previewElement: HTMLDivElement | null;
|
|
27
|
+
moreButton: HTMLButtonElement;
|
|
28
|
+
moreMenu: HTMLElement;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface BuildCodeDOMOptions {
|
|
@@ -36,38 +36,30 @@ export interface BuildCodeDOMOptions {
|
|
|
36
36
|
wrapLabel: string;
|
|
37
37
|
lineNumbersLabel?: string;
|
|
38
38
|
previewable?: boolean;
|
|
39
|
-
|
|
40
|
-
previewTabLabel?: string;
|
|
39
|
+
previewToggleLabel?: string;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
function buildPreviewElements(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const codeTab = document.createElement('button');
|
|
43
|
+
previewToggleLabel?: string,
|
|
44
|
+
): { previewToggleButton: HTMLButtonElement; previewElement: HTMLDivElement } {
|
|
45
|
+
const previewToggleButton = document.createElement('button');
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const previewTab = document.createElement('button');
|
|
55
|
-
|
|
56
|
-
previewTab.type = 'button';
|
|
57
|
-
previewTab.className = `${TAB_STYLES} ${TAB_ACTIVE_STYLES}`;
|
|
58
|
-
previewTab.textContent = previewTabLabel ?? 'Preview';
|
|
59
|
-
previewTab.setAttribute('data-blok-testid', 'code-preview-tab');
|
|
47
|
+
previewToggleButton.type = 'button';
|
|
48
|
+
previewToggleButton.className = HEADER_BUTTON_STYLES;
|
|
49
|
+
previewToggleButton.innerHTML = IconCode;
|
|
50
|
+
previewToggleButton.setAttribute('aria-label', previewToggleLabel ?? 'Preview');
|
|
51
|
+
previewToggleButton.setAttribute('data-blok-testid', 'code-preview-toggle-btn');
|
|
60
52
|
|
|
61
53
|
const previewElement = document.createElement('div');
|
|
62
54
|
|
|
63
55
|
previewElement.className = PREVIEW_AREA_STYLES;
|
|
64
56
|
previewElement.setAttribute('data-blok-testid', 'code-preview');
|
|
65
57
|
|
|
66
|
-
return {
|
|
58
|
+
return { previewToggleButton, previewElement };
|
|
67
59
|
}
|
|
68
60
|
|
|
69
61
|
export function buildCodeDOM(options: BuildCodeDOMOptions): CodeDOMRefs {
|
|
70
|
-
const { code, languageName, readOnly, copyLabel, wrapLabel, lineNumbersLabel, previewable,
|
|
62
|
+
const { code, languageName, readOnly, copyLabel, wrapLabel, lineNumbersLabel, previewable, previewToggleLabel } = options;
|
|
71
63
|
|
|
72
64
|
// Wrapper
|
|
73
65
|
const wrapper = document.createElement('div');
|
|
@@ -77,38 +69,30 @@ export function buildCodeDOM(options: BuildCodeDOMOptions): CodeDOMRefs {
|
|
|
77
69
|
const header = document.createElement('div');
|
|
78
70
|
header.className = HEADER_STYLES;
|
|
79
71
|
|
|
80
|
-
// Language button (opens language picker)
|
|
72
|
+
// Language button (opens language picker) — includes text + chevron icon
|
|
81
73
|
const languageButton = document.createElement('button');
|
|
82
74
|
languageButton.type = 'button';
|
|
83
75
|
languageButton.className = LANGUAGE_BUTTON_STYLES;
|
|
84
|
-
languageButton.textContent = languageName;
|
|
85
76
|
languageButton.setAttribute('aria-haspopup', 'listbox');
|
|
86
77
|
languageButton.setAttribute('data-blok-testid', 'code-language-btn');
|
|
87
78
|
|
|
79
|
+
const langText = document.createElement('span');
|
|
80
|
+
langText.textContent = languageName;
|
|
81
|
+
languageButton.appendChild(langText);
|
|
82
|
+
|
|
83
|
+
const chevronSpan = document.createElement('span');
|
|
84
|
+
chevronSpan.className = 'inline-flex items-center ml-0.5 -mr-0.5';
|
|
85
|
+
chevronSpan.innerHTML = IconChevronDown;
|
|
86
|
+
languageButton.appendChild(chevronSpan);
|
|
87
|
+
|
|
88
88
|
// Spacer
|
|
89
89
|
const spacer = document.createElement('div');
|
|
90
90
|
spacer.className = 'flex-1';
|
|
91
91
|
|
|
92
|
-
//
|
|
93
|
-
const {
|
|
94
|
-
? buildPreviewElements(
|
|
95
|
-
: {
|
|
96
|
-
|
|
97
|
-
// Wrap toggle button
|
|
98
|
-
const wrapButton = document.createElement('button');
|
|
99
|
-
wrapButton.type = 'button';
|
|
100
|
-
wrapButton.className = HEADER_BUTTON_STYLES;
|
|
101
|
-
wrapButton.innerHTML = IconWrap;
|
|
102
|
-
wrapButton.setAttribute('aria-label', wrapLabel);
|
|
103
|
-
wrapButton.setAttribute('data-blok-testid', 'code-wrap-btn');
|
|
104
|
-
|
|
105
|
-
// Line numbers toggle button
|
|
106
|
-
const lineNumbersButton = document.createElement('button');
|
|
107
|
-
lineNumbersButton.type = 'button';
|
|
108
|
-
lineNumbersButton.className = HEADER_BUTTON_STYLES;
|
|
109
|
-
lineNumbersButton.innerHTML = IconLineNumbers;
|
|
110
|
-
lineNumbersButton.setAttribute('aria-label', lineNumbersLabel ?? 'Line numbers');
|
|
111
|
-
lineNumbersButton.setAttribute('data-blok-testid', 'code-line-numbers-btn');
|
|
92
|
+
// Preview toggle button (only when previewable and not read-only)
|
|
93
|
+
const { previewToggleButton, previewElement } = previewable
|
|
94
|
+
? buildPreviewElements(previewToggleLabel)
|
|
95
|
+
: { previewToggleButton: null, previewElement: null };
|
|
112
96
|
|
|
113
97
|
// Copy button
|
|
114
98
|
const copyButton = document.createElement('button');
|
|
@@ -118,6 +102,54 @@ export function buildCodeDOM(options: BuildCodeDOMOptions): CodeDOMRefs {
|
|
|
118
102
|
copyButton.setAttribute('aria-label', copyLabel);
|
|
119
103
|
copyButton.setAttribute('data-blok-testid', 'code-copy-btn');
|
|
120
104
|
|
|
105
|
+
// More button (ellipsis)
|
|
106
|
+
const moreButton = document.createElement('button');
|
|
107
|
+
moreButton.type = 'button';
|
|
108
|
+
moreButton.className = HEADER_BUTTON_STYLES;
|
|
109
|
+
moreButton.innerHTML = IconEllipsis;
|
|
110
|
+
moreButton.setAttribute('aria-label', 'More');
|
|
111
|
+
moreButton.setAttribute('aria-haspopup', 'true');
|
|
112
|
+
moreButton.setAttribute('data-blok-testid', 'code-more-btn');
|
|
113
|
+
|
|
114
|
+
// More menu dropdown
|
|
115
|
+
const moreMenu = document.createElement('div');
|
|
116
|
+
moreMenu.className = MORE_MENU_STYLES;
|
|
117
|
+
moreMenu.hidden = true;
|
|
118
|
+
moreMenu.setAttribute('data-blok-testid', 'code-more-menu');
|
|
119
|
+
|
|
120
|
+
// Line numbers toggle (inside more menu)
|
|
121
|
+
const lineNumbersButton = document.createElement('button');
|
|
122
|
+
lineNumbersButton.type = 'button';
|
|
123
|
+
lineNumbersButton.className = MORE_MENU_ITEM_STYLES;
|
|
124
|
+
lineNumbersButton.setAttribute('data-blok-testid', 'code-line-numbers-btn');
|
|
125
|
+
|
|
126
|
+
const lineNumIconSpan = document.createElement('span');
|
|
127
|
+
lineNumIconSpan.className = 'flex items-center justify-center w-5 h-5';
|
|
128
|
+
lineNumIconSpan.innerHTML = IconLineNumbers;
|
|
129
|
+
lineNumbersButton.appendChild(lineNumIconSpan);
|
|
130
|
+
|
|
131
|
+
const lineNumText = document.createElement('span');
|
|
132
|
+
lineNumText.textContent = lineNumbersLabel ?? 'Line numbers';
|
|
133
|
+
lineNumbersButton.appendChild(lineNumText);
|
|
134
|
+
|
|
135
|
+
// Wrap toggle (inside more menu)
|
|
136
|
+
const wrapButton = document.createElement('button');
|
|
137
|
+
wrapButton.type = 'button';
|
|
138
|
+
wrapButton.className = MORE_MENU_ITEM_STYLES;
|
|
139
|
+
wrapButton.setAttribute('data-blok-testid', 'code-wrap-btn');
|
|
140
|
+
|
|
141
|
+
const wrapIconSpan = document.createElement('span');
|
|
142
|
+
wrapIconSpan.className = 'flex items-center justify-center w-5 h-5';
|
|
143
|
+
wrapIconSpan.innerHTML = IconWrap;
|
|
144
|
+
wrapButton.appendChild(wrapIconSpan);
|
|
145
|
+
|
|
146
|
+
const wrapText = document.createElement('span');
|
|
147
|
+
wrapText.textContent = wrapLabel;
|
|
148
|
+
wrapButton.appendChild(wrapText);
|
|
149
|
+
|
|
150
|
+
moreMenu.appendChild(lineNumbersButton);
|
|
151
|
+
moreMenu.appendChild(wrapButton);
|
|
152
|
+
|
|
121
153
|
// Code area
|
|
122
154
|
const codeElement = document.createElement('code');
|
|
123
155
|
codeElement.className = CODE_AREA_STYLES;
|
|
@@ -146,19 +178,23 @@ export function buildCodeDOM(options: BuildCodeDOMOptions): CodeDOMRefs {
|
|
|
146
178
|
gutterElement.appendChild(lineEl);
|
|
147
179
|
});
|
|
148
180
|
|
|
149
|
-
// Assemble header
|
|
181
|
+
// Assemble header: [language] [spacer] [preview toggle?] [copy] [more ▸ menu]
|
|
150
182
|
header.appendChild(languageButton);
|
|
151
183
|
header.appendChild(spacer);
|
|
152
184
|
|
|
153
|
-
if (
|
|
154
|
-
header.appendChild(
|
|
155
|
-
header.appendChild(previewTab);
|
|
185
|
+
if (previewToggleButton) {
|
|
186
|
+
header.appendChild(previewToggleButton);
|
|
156
187
|
}
|
|
157
188
|
|
|
158
|
-
header.appendChild(lineNumbersButton);
|
|
159
|
-
header.appendChild(wrapButton);
|
|
160
189
|
header.appendChild(copyButton);
|
|
161
190
|
|
|
191
|
+
// More wrapper (relative position anchor for absolute dropdown)
|
|
192
|
+
const moreWrapper = document.createElement('div');
|
|
193
|
+
moreWrapper.className = 'relative';
|
|
194
|
+
moreWrapper.appendChild(moreButton);
|
|
195
|
+
moreWrapper.appendChild(moreMenu);
|
|
196
|
+
header.appendChild(moreWrapper);
|
|
197
|
+
|
|
162
198
|
// Pre wrapper for semantic HTML
|
|
163
199
|
const preElement = document.createElement('pre');
|
|
164
200
|
preElement.appendChild(codeElement);
|
|
@@ -177,5 +213,5 @@ export function buildCodeDOM(options: BuildCodeDOMOptions): CodeDOMRefs {
|
|
|
177
213
|
wrapper.appendChild(previewElement);
|
|
178
214
|
}
|
|
179
215
|
|
|
180
|
-
return { wrapper, languageButton, lineNumbersButton, copyButton, wrapButton, preElement, codeElement, gutterElement,
|
|
216
|
+
return { wrapper, languageButton, lineNumbersButton, copyButton, wrapButton, preElement, codeElement, gutterElement, previewToggleButton, previewElement, moreButton, moreMenu };
|
|
181
217
|
}
|
package/src/tools/code/index.ts
CHANGED
|
@@ -14,7 +14,8 @@ import { IconCodeBlock } from '../../components/icons';
|
|
|
14
14
|
import { buildCodeDOM } from './dom-builder';
|
|
15
15
|
import type { CodeDOMRefs } from './dom-builder';
|
|
16
16
|
import { handleCodeKeydown } from './code-keyboard';
|
|
17
|
-
import {
|
|
17
|
+
import { PopoverDesktop } from '../../components/utils/popover';
|
|
18
|
+
import type { PopoverItemParams } from '@/types/utils/popover/popover-item';
|
|
18
19
|
import {
|
|
19
20
|
DEFAULT_LANGUAGE,
|
|
20
21
|
LANGUAGES,
|
|
@@ -24,13 +25,10 @@ import {
|
|
|
24
25
|
LINE_NUMBERS_KEY,
|
|
25
26
|
COPIED_KEY,
|
|
26
27
|
LANGUAGE_KEY,
|
|
28
|
+
SEARCH_LANGUAGE_KEY,
|
|
27
29
|
COPIED_FEEDBACK_STYLES,
|
|
28
30
|
PREVIEWABLE_LANGUAGES,
|
|
29
|
-
|
|
30
|
-
PREVIEW_TAB_KEY,
|
|
31
|
-
TAB_STYLES,
|
|
32
|
-
TAB_ACTIVE_STYLES,
|
|
33
|
-
TAB_INACTIVE_STYLES,
|
|
31
|
+
PREVIEW_TOGGLE_KEY,
|
|
34
32
|
PREVIEW_AREA_STYLES,
|
|
35
33
|
GUTTER_LINE_STYLES,
|
|
36
34
|
} from './constants';
|
|
@@ -48,7 +46,7 @@ export class CodeTool implements BlockTool {
|
|
|
48
46
|
private _dom: CodeDOMRefs | null = null;
|
|
49
47
|
private _wrapping = true;
|
|
50
48
|
private _lineNumbers = true;
|
|
51
|
-
private _picker:
|
|
49
|
+
private _picker: PopoverDesktop | null = null;
|
|
52
50
|
private _previewActive = false;
|
|
53
51
|
private _previewContainer: HTMLElement | null = null;
|
|
54
52
|
private _disposeHighlights: (() => void) | null = null;
|
|
@@ -76,8 +74,7 @@ export class CodeTool implements BlockTool {
|
|
|
76
74
|
wrapLabel: this.api.i18n.t(WRAP_LINES_KEY),
|
|
77
75
|
lineNumbersLabel: this.api.i18n.t(LINE_NUMBERS_KEY),
|
|
78
76
|
previewable: this.readOnly ? false : isPreviewable,
|
|
79
|
-
|
|
80
|
-
previewTabLabel: this.api.i18n.t(PREVIEW_TAB_KEY),
|
|
77
|
+
previewToggleLabel: this.api.i18n.t(PREVIEW_TOGGLE_KEY),
|
|
81
78
|
});
|
|
82
79
|
|
|
83
80
|
this._dom = dom;
|
|
@@ -86,7 +83,10 @@ export class CodeTool implements BlockTool {
|
|
|
86
83
|
dom.gutterElement.hidden = !this._lineNumbers;
|
|
87
84
|
dom.lineNumbersButton.addEventListener('click', () => this.toggleLineNumbers());
|
|
88
85
|
|
|
89
|
-
//
|
|
86
|
+
// More menu toggle
|
|
87
|
+
dom.moreButton.addEventListener('click', () => this.toggleMoreMenu());
|
|
88
|
+
|
|
89
|
+
// Read-only + previewable: show preview only, hide code, no toggle
|
|
90
90
|
if (this.readOnly && isPreviewable) {
|
|
91
91
|
const previewEl = document.createElement('div');
|
|
92
92
|
|
|
@@ -99,8 +99,8 @@ export class CodeTool implements BlockTool {
|
|
|
99
99
|
void this.renderPreview();
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
// Edit mode + previewable: show
|
|
103
|
-
if (!this.readOnly && isPreviewable && dom.
|
|
102
|
+
// Edit mode + previewable: show preview toggle, default to preview
|
|
103
|
+
if (!this.readOnly && isPreviewable && dom.previewToggleButton && dom.previewElement) {
|
|
104
104
|
this._previewActive = true;
|
|
105
105
|
dom.preElement.hidden = true;
|
|
106
106
|
dom.gutterElement.hidden = true;
|
|
@@ -108,8 +108,7 @@ export class CodeTool implements BlockTool {
|
|
|
108
108
|
this._previewContainer = dom.previewElement;
|
|
109
109
|
void this.renderPreview();
|
|
110
110
|
|
|
111
|
-
dom.
|
|
112
|
-
dom.previewTab.addEventListener('click', () => this.showPreview());
|
|
111
|
+
dom.previewToggleButton.addEventListener('click', () => this.togglePreview());
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
if (!this.readOnly) {
|
|
@@ -135,17 +134,28 @@ export class CodeTool implements BlockTool {
|
|
|
135
134
|
dom.wrapButton.addEventListener('click', () => this.toggleWrap());
|
|
136
135
|
|
|
137
136
|
if (!this.readOnly) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
const languageItems: PopoverItemParams[] = LANGUAGES.map((lang) => ({
|
|
138
|
+
title: lang.name,
|
|
139
|
+
name: lang.id,
|
|
140
|
+
toggle: 'language',
|
|
141
|
+
isActive: (): boolean => this._data.language === lang.id,
|
|
142
|
+
closeOnActivate: true,
|
|
143
|
+
onActivate: (): void => this.setLanguage(lang.id),
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
this._picker = new PopoverDesktop({
|
|
147
|
+
items: languageItems,
|
|
148
|
+
trigger: dom.languageButton,
|
|
149
|
+
leftAlignElement: dom.wrapper,
|
|
150
|
+
searchable: true,
|
|
151
|
+
width: '200px',
|
|
152
|
+
messages: {
|
|
153
|
+
search: this.api.i18n.t(SEARCH_LANGUAGE_KEY),
|
|
154
|
+
},
|
|
143
155
|
});
|
|
144
156
|
|
|
145
|
-
document.body.appendChild(this._picker.getElement());
|
|
146
|
-
|
|
147
157
|
dom.languageButton.addEventListener('click', () => {
|
|
148
|
-
this._picker?.
|
|
158
|
+
this._picker?.show();
|
|
149
159
|
});
|
|
150
160
|
}
|
|
151
161
|
|
|
@@ -156,8 +166,16 @@ export class CodeTool implements BlockTool {
|
|
|
156
166
|
void this.highlightCode();
|
|
157
167
|
}
|
|
158
168
|
|
|
169
|
+
private togglePreview(): void {
|
|
170
|
+
if (this._previewActive) {
|
|
171
|
+
this.showCode();
|
|
172
|
+
} else {
|
|
173
|
+
this.showPreview();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
159
177
|
private showCode(): void {
|
|
160
|
-
if (!this._dom?.previewElement || !this._dom.
|
|
178
|
+
if (!this._dom?.previewElement || !this._dom.previewToggleButton) {
|
|
161
179
|
return;
|
|
162
180
|
}
|
|
163
181
|
|
|
@@ -165,12 +183,10 @@ export class CodeTool implements BlockTool {
|
|
|
165
183
|
this._dom.preElement.hidden = false;
|
|
166
184
|
this._dom.gutterElement.hidden = !this._lineNumbers;
|
|
167
185
|
this._dom.previewElement.hidden = true;
|
|
168
|
-
this._dom.codeTab.className = `${TAB_STYLES} ${TAB_ACTIVE_STYLES}`;
|
|
169
|
-
this._dom.previewTab.className = `${TAB_STYLES} ${TAB_INACTIVE_STYLES}`;
|
|
170
186
|
}
|
|
171
187
|
|
|
172
188
|
private showPreview(): void {
|
|
173
|
-
if (!this._dom?.previewElement || !this._dom.
|
|
189
|
+
if (!this._dom?.previewElement || !this._dom.previewToggleButton) {
|
|
174
190
|
return;
|
|
175
191
|
}
|
|
176
192
|
|
|
@@ -178,13 +194,19 @@ export class CodeTool implements BlockTool {
|
|
|
178
194
|
this._dom.preElement.hidden = true;
|
|
179
195
|
this._dom.gutterElement.hidden = true;
|
|
180
196
|
this._dom.previewElement.hidden = false;
|
|
181
|
-
this._dom.codeTab.className = `${TAB_STYLES} ${TAB_INACTIVE_STYLES}`;
|
|
182
|
-
this._dom.previewTab.className = `${TAB_STYLES} ${TAB_ACTIVE_STYLES}`;
|
|
183
197
|
|
|
184
198
|
// Re-render preview with current code content
|
|
185
199
|
void this.renderPreview();
|
|
186
200
|
}
|
|
187
201
|
|
|
202
|
+
private toggleMoreMenu(): void {
|
|
203
|
+
if (!this._dom) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this._dom.moreMenu.hidden = !this._dom.moreMenu.hidden;
|
|
208
|
+
}
|
|
209
|
+
|
|
188
210
|
private async renderPreview(): Promise<void> {
|
|
189
211
|
if (!this._previewContainer) {
|
|
190
212
|
return;
|
|
@@ -198,6 +220,22 @@ export class CodeTool implements BlockTool {
|
|
|
198
220
|
this._previewContainer.innerHTML = rendered;
|
|
199
221
|
}
|
|
200
222
|
|
|
223
|
+
public setReadOnly(state: boolean): void {
|
|
224
|
+
this.readOnly = state;
|
|
225
|
+
|
|
226
|
+
if (!this._dom) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (state) {
|
|
231
|
+
this._dom.codeElement.setAttribute('contenteditable', 'false');
|
|
232
|
+
this._dom.codeElement.removeAttribute('spellcheck');
|
|
233
|
+
} else {
|
|
234
|
+
this._dom.codeElement.setAttribute('contenteditable', 'plaintext-only');
|
|
235
|
+
this._dom.codeElement.setAttribute('spellcheck', 'false');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
201
239
|
public save(_blockContent: HTMLElement): CodeData {
|
|
202
240
|
return {
|
|
203
241
|
code: this._dom?.codeElement.textContent ?? '',
|
|
@@ -265,10 +303,14 @@ export class CodeTool implements BlockTool {
|
|
|
265
303
|
this._data.language = id;
|
|
266
304
|
|
|
267
305
|
if (this._dom) {
|
|
268
|
-
|
|
306
|
+
// Update the text span inside the language button (first child)
|
|
307
|
+
const textSpan = this._dom.languageButton.querySelector('span');
|
|
308
|
+
|
|
309
|
+
if (textSpan) {
|
|
310
|
+
textSpan.textContent = this.getLanguageName(id);
|
|
311
|
+
}
|
|
269
312
|
}
|
|
270
313
|
|
|
271
|
-
this._picker?.setActiveLanguage(id);
|
|
272
314
|
void this.highlightCode();
|
|
273
315
|
}
|
|
274
316
|
|
|
@@ -419,7 +461,7 @@ export class CodeTool implements BlockTool {
|
|
|
419
461
|
}
|
|
420
462
|
|
|
421
463
|
if (this._picker) {
|
|
422
|
-
this._picker.
|
|
464
|
+
this._picker.destroy();
|
|
423
465
|
this._picker = null;
|
|
424
466
|
}
|
|
425
467
|
}
|
|
@@ -96,6 +96,11 @@ export class DividerTool implements BlockTool {
|
|
|
96
96
|
*/
|
|
97
97
|
public onPaste(_event: PasteEvent): void {}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Toggle read-only mode in place. Divider is purely presentational — no-op.
|
|
101
|
+
*/
|
|
102
|
+
public setReadOnly(_state: boolean): void {}
|
|
103
|
+
|
|
99
104
|
/**
|
|
100
105
|
* Nothing to sanitize — no HTML content
|
|
101
106
|
*/
|
|
@@ -111,6 +111,12 @@ export class Header implements BlockTool {
|
|
|
111
111
|
*/
|
|
112
112
|
private readOnly: boolean;
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Cleanup function for the placeholder, returned by setupPlaceholder().
|
|
116
|
+
* Stored so we can tear down the placeholder when entering read-only mode.
|
|
117
|
+
*/
|
|
118
|
+
private placeholderCleanup: (() => void) | null = null;
|
|
119
|
+
|
|
114
120
|
/**
|
|
115
121
|
* Tool's settings passed from Editor
|
|
116
122
|
*/
|
|
@@ -302,6 +308,45 @@ export class Header implements BlockTool {
|
|
|
302
308
|
this._element.removeEventListener('keydown', this.handleKeyDown);
|
|
303
309
|
}
|
|
304
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Toggle read-only mode in-place without re-rendering the DOM element.
|
|
313
|
+
* Manages contentEditable, keydown listener (for toggle headings),
|
|
314
|
+
* placeholder setup/teardown, and body placeholder click handler.
|
|
315
|
+
*
|
|
316
|
+
* @param state - true to enter read-only mode, false to exit
|
|
317
|
+
*/
|
|
318
|
+
public setReadOnly(state: boolean): void {
|
|
319
|
+
if (!this._element) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
this.readOnly = state;
|
|
324
|
+
|
|
325
|
+
if (state) {
|
|
326
|
+
this._element.contentEditable = 'false';
|
|
327
|
+
|
|
328
|
+
if (this._data.isToggleable) {
|
|
329
|
+
this._element.removeEventListener('keydown', this.handleKeyDown);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (this.placeholderCleanup) {
|
|
333
|
+
this.placeholderCleanup();
|
|
334
|
+
this.placeholderCleanup = null;
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
this._element.contentEditable = 'true';
|
|
338
|
+
|
|
339
|
+
if (this._data.isToggleable) {
|
|
340
|
+
this._element.addEventListener('keydown', this.handleKeyDown);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const translatedName = this.api.i18n.t(this.currentLevel.nameKey);
|
|
344
|
+
const placeholderText = this.resolvePlaceholderText(translatedName);
|
|
345
|
+
|
|
346
|
+
this.placeholderCleanup = setupPlaceholder(this._element, placeholderText);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
305
350
|
/**
|
|
306
351
|
* Expand the toggle heading (no-op if not toggleable or already expanded).
|
|
307
352
|
* Can be called externally via block.call('expand').
|
|
@@ -679,7 +724,7 @@ export class Header implements BlockTool {
|
|
|
679
724
|
const placeholderText = this.resolvePlaceholderText(translatedName);
|
|
680
725
|
|
|
681
726
|
if (!this.readOnly) {
|
|
682
|
-
setupPlaceholder(tag, placeholderText);
|
|
727
|
+
this.placeholderCleanup = setupPlaceholder(tag, placeholderText);
|
|
683
728
|
} else {
|
|
684
729
|
tag.setAttribute('data-placeholder', placeholderText);
|
|
685
730
|
}
|
|
@@ -750,6 +795,7 @@ export class Header implements BlockTool {
|
|
|
750
795
|
// pl-8 (32px) matches the heading's left padding so children align with the title text start.
|
|
751
796
|
childContainer.className = 'pl-8';
|
|
752
797
|
childContainer.setAttribute(TOGGLE_ATTR.toggleChildren, '');
|
|
798
|
+
childContainer.setAttribute(DATA_ATTR.nestedBlocks, '');
|
|
753
799
|
// Block DOM mutations inside the children container from triggering the header tool's
|
|
754
800
|
// didMutated → syncBlockDataToYjs path (same rationale as the toggle list tool).
|
|
755
801
|
childContainer.setAttribute('data-blok-mutation-free', 'true');
|
|
@@ -111,7 +111,9 @@ export const buildListItem = (context: DOMBuilderContext): BuildResult => {
|
|
|
111
111
|
// Extract element references for the result
|
|
112
112
|
const markerElement = itemContent.querySelector<HTMLElement>('[data-list-marker]');
|
|
113
113
|
const checkboxElement = itemContent.querySelector<HTMLInputElement>('input[type="checkbox"]');
|
|
114
|
-
const contentElement =
|
|
114
|
+
const contentElement = data.style === 'checklist'
|
|
115
|
+
? itemContent.querySelector<HTMLElement>(`[data-blok-testid="${LIST_TEST_IDS.checklistContent}"]`)
|
|
116
|
+
: itemContent.querySelector<HTMLElement>(`[data-blok-testid="${LIST_TEST_IDS.contentContainer}"]`);
|
|
115
117
|
|
|
116
118
|
return {
|
|
117
119
|
wrapper,
|