Skip to content

Commit c9cfe8d

Browse files
committed
feat: improve link editing UX and inline toolbar
- Add direct link editing on click (Webtools integration) When clicking an existing link, the Webtools Link Picker opens directly for editing instead of requiring click on link + click on toolbar button - Add Bold and Italic inline tools with proper EditorJS 2.31 support - Fix inline toolbar CSS for horizontal layout - Add safety guards for getBlocksCount API calls - Improve editor initialization stability
1 parent db2d354 commit c9cfe8d

3 files changed

Lines changed: 401 additions & 46 deletions

File tree

admin/src/components/EditorJS/index.jsx

Lines changed: 260 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,120 @@ const FullscreenGlobalStyle = createGlobalStyle`
5555
}
5656
`;
5757

58-
/* Global z-index fixes for Editor.js popovers (may render at body level) */
58+
/* Global z-index and visibility fixes for Editor.js */
5959
const EditorJSGlobalStyles = createGlobalStyle`
60-
/* Popover rendered at document body */
61-
body > .ce-popover,
62-
body > .ce-popover--opened,
63-
body > .ce-popover__container,
64-
body > .ce-settings,
65-
body > .ce-conversion-toolbar,
66-
body > .ce-inline-toolbar {
60+
/* ============================================
61+
INLINE TOOLBAR - EditorJS 2.31
62+
Structure: .ce-inline-toolbar > .ce-popover--inline > .ce-popover__items > .ce-popover-item-html > .ce-inline-tool
63+
============================================ */
64+
65+
/* Hide "Nothing found" message when inline tools ARE present */
66+
.ce-popover--inline .ce-popover__nothing-found-message {
67+
display: none !important;
68+
}
69+
70+
/* Inline Toolbar Popover - horizontal layout for tool buttons */
71+
.ce-popover--inline.ce-popover--opened {
72+
display: block !important;
73+
opacity: 1 !important;
74+
visibility: visible !important;
6775
z-index: 99999 !important;
76+
background: white !important;
77+
border: 1px solid #e2e8f0 !important;
78+
border-radius: 8px !important;
79+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12) !important;
80+
padding: 4px !important;
6881
}
6982
70-
/* Ensure popovers are visible above Strapi modals */
71-
.ce-popover,
72-
.ce-popover--opened {
83+
.ce-popover--inline .ce-popover__container {
84+
display: block !important;
85+
}
86+
87+
/* Items container - HORIZONTAL layout for inline tools */
88+
.ce-popover--inline .ce-popover__items {
89+
display: flex !important;
90+
flex-direction: row !important;
91+
flex-wrap: wrap !important;
92+
align-items: center !important;
93+
gap: 2px !important;
94+
opacity: 1 !important;
95+
visibility: visible !important;
96+
}
97+
98+
/* Custom HTML wrapper for inline tools */
99+
.ce-popover--inline .ce-popover-item-html {
100+
display: flex !important;
101+
opacity: 1 !important;
102+
visibility: visible !important;
103+
}
104+
105+
/* The actual inline tool buttons (Bold, Italic, etc.) */
106+
.ce-popover--inline .ce-inline-tool {
107+
display: flex !important;
108+
align-items: center !important;
109+
justify-content: center !important;
110+
width: 32px !important;
111+
height: 32px !important;
112+
opacity: 1 !important;
113+
visibility: visible !important;
114+
background: transparent !important;
115+
border: none !important;
116+
border-radius: 6px !important;
117+
cursor: pointer !important;
118+
color: #64748b !important;
119+
transition: background 0.15s ease, color 0.15s ease !important;
120+
}
121+
122+
.ce-popover--inline .ce-inline-tool:hover {
123+
background: #f1f5f9 !important;
124+
color: #334155 !important;
125+
}
126+
127+
.ce-popover--inline .ce-inline-tool--active {
128+
background: #ede9fe !important;
129+
color: #7C3AED !important;
130+
}
131+
132+
.ce-popover--inline .ce-inline-tool svg {
133+
width: 18px !important;
134+
height: 18px !important;
135+
}
136+
137+
/* Convert-to button (block type changer) */
138+
.ce-popover--inline .ce-popover-item[data-item-name="convert-to"] {
139+
display: flex !important;
140+
align-items: center !important;
141+
padding: 4px 8px !important;
142+
border-radius: 6px !important;
143+
cursor: pointer !important;
144+
}
145+
146+
.ce-popover--inline .ce-popover-item[data-item-name="convert-to"]:hover {
147+
background: #f1f5f9 !important;
148+
}
149+
150+
/* Separator line between convert-to and inline tools */
151+
.ce-popover--inline .ce-popover-item-separator {
152+
width: 1px !important;
153+
height: 24px !important;
154+
background: #e2e8f0 !important;
155+
margin: 0 4px !important;
156+
}
157+
158+
.ce-popover--inline .ce-popover-item-separator__line {
159+
display: none !important;
160+
}
161+
162+
/* ============================================
163+
GLOBAL Z-INDEX FOR ALL EDITOR POPOVERS
164+
============================================ */
165+
166+
body > .ce-popover,
167+
body > .ce-inline-toolbar,
168+
.ce-popover--opened,
169+
.ce-inline-toolbar,
170+
.ce-settings,
171+
.ce-conversion-toolbar {
73172
z-index: 99999 !important;
74173
}
75174
@@ -940,19 +1039,29 @@ const EditorWrapper = styled.div`
9401039
TOOLBAR INSIDE EDITOR - Position Fix
9411040
============================================ */
9421041
943-
/* Centered content area */
1042+
/* Content area - full container width */
9441043
.codex-editor__redactor {
9451044
padding-bottom: 100px !important;
9461045
padding-left: 0 !important;
947-
margin: 0 auto !important;
948-
max-width: 800px !important;
1046+
padding-right: 0 !important;
1047+
margin: 0 !important;
1048+
max-width: 100% !important;
1049+
width: 100% !important;
9491050
}
9501051
951-
/* Content blocks - centered */
1052+
/* Content blocks - full width, no centering */
9521053
.ce-block__content {
953-
max-width: 100%;
954-
margin: 0 auto;
955-
padding: 0 16px;
1054+
max-width: 100% !important;
1055+
margin: 0 !important;
1056+
padding: 0 16px !important;
1057+
}
1058+
1059+
/* Paragraph and other editable elements - full width */
1060+
.ce-paragraph,
1061+
.ce-header,
1062+
.cdx-block {
1063+
max-width: 100% !important;
1064+
width: 100% !important;
9561065
}
9571066
9581067
/* ============================================
@@ -1001,18 +1110,20 @@ const EditorWrapper = styled.div`
10011110
border-radius: 6px;
10021111
}
10031112
1004-
/* Toolbar positioning - centered with content */
1113+
/* Toolbar positioning - full width */
10051114
.ce-toolbar__content {
1006-
max-width: 800px;
1007-
margin: 0 auto;
1008-
padding: 0 16px;
1115+
max-width: 100% !important;
1116+
margin: 0 !important;
1117+
padding: 0 16px !important;
10091118
}
10101119
10111120
.ce-toolbar {
1012-
left: 50% !important;
1013-
transform: translateX(-50%) !important;
1121+
left: 0 !important;
1122+
right: 0 !important;
1123+
transform: none !important;
10141124
width: 100% !important;
1015-
max-width: 832px !important;
1125+
max-width: 100% !important;
1126+
padding-left: 8px !important;
10161127
}
10171128
10181129
.ce-toolbar__plus {
@@ -2152,6 +2263,8 @@ const Editor = forwardRef(({
21522263
if (!collabEnabled || !editorInstanceRef.current || !yTextMap) return;
21532264

21542265
const editor = editorInstanceRef.current;
2266+
if (!editor.blocks || typeof editor.blocks.getBlocksCount !== 'function') return;
2267+
21552268
const blockCount = editor.blocks.getBlocksCount();
21562269

21572270
console.log('[Magic Editor X] [CHAR-SYNC] Binding', blockCount, 'blocks to Y.Text');
@@ -2720,6 +2833,13 @@ const Editor = forwardRef(({
27202833
pendingRenderRef.current = pendingRenderRef.current || true;
27212834
return;
27222835
}
2836+
2837+
// Ensure blocks API is available
2838+
if (!editor.blocks || typeof editor.blocks.getBlocksCount !== 'function') {
2839+
console.warn('[Magic Editor X] Editor blocks API not ready for renderFromYDoc');
2840+
pendingRenderRef.current = pendingRenderRef.current || true;
2841+
return;
2842+
}
27232843

27242844
// Prevent echo loops
27252845
if (isApplyingRemoteRef.current) {
@@ -3075,6 +3195,8 @@ const Editor = forwardRef(({
30753195
}
30763196

30773197
const editor = editorInstanceRef.current;
3198+
if (!editor.blocks || typeof editor.blocks.getBlocksCount !== 'function') return;
3199+
30783200
const lastIndex = editor.blocks.getBlocksCount();
30793201

30803202
editor.blocks.insert(blockType, {}, {}, lastIndex, true);
@@ -3145,6 +3267,23 @@ const Editor = forwardRef(({
31453267
}
31463268
}
31473269

3270+
// Debug: Log registered tools to verify inline tools are loaded
3271+
console.log('[Magic Editor X] Registered tools:', Object.keys(tools));
3272+
3273+
// Check each tool for isInline property
3274+
const inlineTools = Object.entries(tools).filter(([name, config]) => {
3275+
const toolClass = config.class || config;
3276+
const isInline = toolClass?.isInline === true;
3277+
if (isInline) {
3278+
console.log(`[Magic Editor X] Found inline tool: ${name}`, toolClass);
3279+
}
3280+
return isInline;
3281+
}).map(([name]) => name);
3282+
3283+
console.log('[Magic Editor X] Inline tools found:', inlineTools);
3284+
console.log('[Magic Editor X] Marker isInline:', tools.marker?.class?.isInline);
3285+
console.log('[Magic Editor X] Bold isInline:', tools.bold?.class?.isInline);
3286+
31483287
const editor = new EditorJS({
31493288
holder: editorRef.current,
31503289
tools,
@@ -3153,28 +3292,40 @@ const Editor = forwardRef(({
31533292
placeholder: customPlaceholder,
31543293
minHeight: 200,
31553294
autofocus: false,
3295+
// Note: Do NOT set inlineToolbar here - each block tool controls its own inline toolbar
3296+
// The inline tools (bold, italic, marker, etc.) are automatically available when block tools have inlineToolbar: true
31563297

31573298
onReady: async () => {
31583299
isReadyRef.current = true;
31593300
setIsReady(true);
31603301
console.log('[Magic Editor X] [READY] Editor onReady fired');
31613302
console.log('[Magic Editor X] [READY] Editor holder:', editorRef.current?.id);
31623303

3163-
// Initialize Undo/Redo plugin
3164-
try {
3165-
initUndoRedo(editor);
3166-
console.log('[Magic Editor X] [SUCCESS] Undo/Redo initialized');
3167-
} catch (e) {
3168-
console.warn('[Magic Editor X] Could not initialize Undo/Redo:', e);
3169-
}
3170-
3171-
// Initialize Drag & Drop plugin
3172-
try {
3173-
initDragDrop(editor);
3174-
console.log('[Magic Editor X] [SUCCESS] Drag & Drop initialized');
3175-
} catch (e) {
3176-
console.warn('[Magic Editor X] Could not initialize Drag & Drop:', e);
3177-
}
3304+
// Initialize Undo/Redo and Drag & Drop plugins with longer delay
3305+
// These plugins require the editor to be fully ready with blocks API available
3306+
setTimeout(() => {
3307+
try {
3308+
if (editor && editor.blocks && typeof editor.blocks.getBlocksCount === 'function') {
3309+
initUndoRedo(editor);
3310+
console.log('[Magic Editor X] [SUCCESS] Undo/Redo initialized');
3311+
} else {
3312+
console.warn('[Magic Editor X] Editor blocks API not ready for Undo/Redo');
3313+
}
3314+
} catch (e) {
3315+
console.warn('[Magic Editor X] Could not initialize Undo/Redo:', e);
3316+
}
3317+
3318+
try {
3319+
if (editor && editor.blocks && typeof editor.blocks.getBlocksCount === 'function') {
3320+
initDragDrop(editor);
3321+
console.log('[Magic Editor X] [SUCCESS] Drag & Drop initialized');
3322+
} else {
3323+
console.warn('[Magic Editor X] Editor blocks API not ready for Drag & Drop');
3324+
}
3325+
} catch (e) {
3326+
console.warn('[Magic Editor X] Could not initialize Drag & Drop:', e);
3327+
}
3328+
}, 500);
31783329

31793330
if (pendingRenderRef.current) {
31803331
try {
@@ -3206,6 +3357,68 @@ const Editor = forwardRef(({
32063357
bindAllBlocksToYText();
32073358
}, 100);
32083359
}
3360+
3361+
// WEBTOOLS LINK CLICK HANDLER
3362+
// When clicking on an existing link, open the link picker directly for editing
3363+
if (isWebtoolsAvailable && webtoolsOpenLinkPicker && editorRef.current) {
3364+
const handleLinkClick = async (e) => {
3365+
// Find if click was on an anchor tag or inside one
3366+
const anchor = e.target.closest('a');
3367+
3368+
if (anchor && editorRef.current?.contains(anchor)) {
3369+
// Prevent default link navigation
3370+
e.preventDefault();
3371+
e.stopPropagation();
3372+
3373+
const existingHref = anchor.href || '';
3374+
const existingText = anchor.textContent || '';
3375+
3376+
console.log('[Magic Editor X] Link clicked, opening editor:', existingHref);
3377+
3378+
try {
3379+
// Select the link text for editing
3380+
const selection = window.getSelection();
3381+
const range = document.createRange();
3382+
range.selectNodeContents(anchor);
3383+
selection.removeAllRanges();
3384+
selection.addRange(range);
3385+
3386+
// Open the Webtools Link Picker with existing values
3387+
const result = await webtoolsOpenLinkPicker({
3388+
initialText: existingText,
3389+
initialHref: existingHref,
3390+
});
3391+
3392+
if (result && result.href) {
3393+
// Update the link
3394+
anchor.href = result.href;
3395+
if (result.label && result.label !== existingText) {
3396+
anchor.textContent = result.label;
3397+
}
3398+
console.log('[Magic Editor X] Link updated:', result.href);
3399+
} else if (result === null) {
3400+
// User cancelled - keep the link as-is
3401+
console.log('[Magic Editor X] Link edit cancelled');
3402+
}
3403+
} catch (err) {
3404+
console.error('[Magic Editor X] Error editing link:', err);
3405+
}
3406+
}
3407+
};
3408+
3409+
// Add click listener to the editor container
3410+
editorRef.current.addEventListener('click', handleLinkClick);
3411+
3412+
// Store cleanup function
3413+
const cleanup = () => {
3414+
editorRef.current?.removeEventListener('click', handleLinkClick);
3415+
};
3416+
3417+
// Store for cleanup on unmount
3418+
if (!editorRef.current._linkClickCleanup) {
3419+
editorRef.current._linkClickCleanup = cleanup;
3420+
}
3421+
}
32093422
},
32103423

32113424
onChange: async (api) => {
@@ -3255,6 +3468,13 @@ const Editor = forwardRef(({
32553468
console.log('[Magic Editor X] [CLEANUP] Editor component unmounting, destroying editor');
32563469
isReadyRef.current = false;
32573470
setIsReady(false);
3471+
3472+
// Cleanup link click handler
3473+
if (editorRef.current?._linkClickCleanup) {
3474+
editorRef.current._linkClickCleanup();
3475+
delete editorRef.current._linkClickCleanup;
3476+
}
3477+
32583478
if (editorInstanceRef.current && editorInstanceRef.current.destroy) {
32593479
try {
32603480
editorInstanceRef.current.destroy();

0 commit comments

Comments
 (0)