Skip to content

Commit b35f22e

Browse files
committed
fix(inline-tools): fix issue with not being able to unlink
1 parent 530ec56 commit b35f22e

File tree

3 files changed

+87
-77
lines changed

3 files changed

+87
-77
lines changed

src/components/inline-tools/inline-tool-link.ts

Lines changed: 60 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ export default class LinkInlineTool implements InlineTool {
6767
* Elements
6868
*/
6969
private nodes: {
70-
button: HTMLButtonElement;
71-
input: HTMLInputElement;
70+
button: HTMLButtonElement | null;
71+
input: HTMLInputElement | null;
7272
} = {
7373
button: null,
7474
input: null,
@@ -147,49 +147,35 @@ export default class LinkInlineTool implements InlineTool {
147147

148148
/**
149149
* Handle clicks on the Inline Toolbar icon
150-
*
151-
* @param {Range} range - range to wrap with link
152150
*/
153-
public surround(range: Range): void {
151+
public surround(): void {
154152
/**
155153
* Range will be null when user makes second click on the 'link icon' to close opened input
156154
*/
157-
if (range) {
158-
/**
159-
* Save selection before change focus to the input
160-
*/
161-
if (!this.inputOpened) {
162-
/** Create blue background instead of selection */
163-
this.selection.setFakeBackground();
164-
this.selection.save();
165-
} else {
166-
this.selection.restore();
167-
this.selection.removeFakeBackground();
168-
}
169-
const parentAnchor = this.selection.findParentTag('A');
155+
/**
156+
* Save selection before change focus to the input
157+
*/
158+
if (!this.inputOpened) {
159+
/** Create blue background instead of selection */
160+
this.selection.setFakeBackground();
161+
this.selection.save();
162+
} else {
163+
this.selection.restore();
164+
this.selection.removeFakeBackground();
165+
}
166+
const parentAnchor = this.selection.findParentTag('A');
170167

171-
/**
172-
* Unlink icon pressed
173-
*/
174-
if (parentAnchor) {
175-
/**
176-
* If input is not opened, treat click as explicit unlink action.
177-
* If input is opened (e.g., programmatic close when switching tools), avoid unlinking.
178-
*/
179-
if (!this.inputOpened) {
180-
this.selection.expandToTag(parentAnchor);
181-
this.unlink();
182-
this.closeActions();
183-
this.checkState();
184-
this.toolbar.close();
185-
} else {
186-
/** Only close actions without clearing saved selection to preserve user state */
187-
this.closeActions(false);
188-
this.checkState();
189-
}
190-
191-
return;
192-
}
168+
/**
169+
* Unlink icon pressed
170+
*/
171+
if (parentAnchor) {
172+
this.selection.expandToTag(parentAnchor);
173+
this.unlink();
174+
this.closeActions();
175+
this.checkState();
176+
this.toolbar.close();
177+
178+
return;
193179
}
194180

195181
this.toggleActions();
@@ -202,23 +188,30 @@ export default class LinkInlineTool implements InlineTool {
202188
const anchorTag = this.selection.findParentTag('A');
203189

204190
if (anchorTag) {
205-
this.nodes.button.innerHTML = IconUnlink;
206-
this.nodes.button.classList.add(this.CSS.buttonUnlink);
207-
this.nodes.button.classList.add(this.CSS.buttonActive);
191+
if (this.nodes.button) {
192+
this.nodes.button.innerHTML = IconUnlink;
193+
this.nodes.button.classList.add(this.CSS.buttonUnlink);
194+
this.nodes.button.classList.add(this.CSS.buttonActive);
195+
}
208196
this.openActions();
209197

210198
/**
211199
* Fill input value with link href
212200
*/
213201
const hrefAttr = anchorTag.getAttribute('href');
214202

215-
this.nodes.input.defaultValue = hrefAttr !== 'null' ? hrefAttr : '';
203+
if (this.nodes.input) {
204+
this.nodes.input.defaultValue =
205+
hrefAttr !== null && hrefAttr !== 'null' ? hrefAttr : '';
206+
}
216207

217208
this.selection.save();
218209
} else {
219-
this.nodes.button.innerHTML = IconLink;
220-
this.nodes.button.classList.remove(this.CSS.buttonUnlink);
221-
this.nodes.button.classList.remove(this.CSS.buttonActive);
210+
if (this.nodes.button) {
211+
this.nodes.button.innerHTML = IconLink;
212+
this.nodes.button.classList.remove(this.CSS.buttonUnlink);
213+
this.nodes.button.classList.remove(this.CSS.buttonActive);
214+
}
222215
}
223216

224217
return !!anchorTag;
@@ -228,6 +221,16 @@ export default class LinkInlineTool implements InlineTool {
228221
* Function called with Inline Toolbar closing
229222
*/
230223
public clear(): void {
224+
/**
225+
* Restore the original text selection if fake background was set
226+
* (e.g. when user was typing a URL and switched to another tool).
227+
* This must happen before closeActions() so the browser selection
228+
* is on the text, not stuck in the input field.
229+
*/
230+
if (this.selection.isFakeBackgroundEnabled) {
231+
this.selection.restore();
232+
this.selection.removeFakeBackground();
233+
}
231234
this.closeActions();
232235
}
233236

@@ -253,9 +256,11 @@ export default class LinkInlineTool implements InlineTool {
253256
* @param {boolean} needFocus - on link creation we need to focus input. On editing - nope.
254257
*/
255258
private openActions(needFocus = false): void {
256-
this.nodes.input.classList.add(this.CSS.inputShowed);
257-
if (needFocus) {
258-
this.nodes.input.focus();
259+
if (this.nodes.input) {
260+
this.nodes.input.classList.add(this.CSS.inputShowed);
261+
if (needFocus) {
262+
this.nodes.input.focus();
263+
}
259264
}
260265
this.inputOpened = true;
261266
}
@@ -280,8 +285,10 @@ export default class LinkInlineTool implements InlineTool {
280285
currentSelection.restore();
281286
}
282287

283-
this.nodes.input.classList.remove(this.CSS.inputShowed);
284-
this.nodes.input.value = '';
288+
if (this.nodes.input) {
289+
this.nodes.input.classList.remove(this.CSS.inputShowed);
290+
this.nodes.input.value = '';
291+
}
285292
if (clearSavedSelection) {
286293
this.selection.clearSaved();
287294
}
@@ -294,7 +301,7 @@ export default class LinkInlineTool implements InlineTool {
294301
* @param {KeyboardEvent} event - enter keydown event
295302
*/
296303
private enterPressed(event: KeyboardEvent): void {
297-
let value = this.nodes.input.value || '';
304+
let value = (this.nodes.input && this.nodes.input.value) || '';
298305

299306
if (!value.trim()) {
300307
this.selection.restore();

src/components/modules/toolbar/inline.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,12 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
6868
eventsDispatcher,
6969
});
7070

71-
window.requestIdleCallback(() => {
72-
this.make();
73-
}, { timeout: 2000 });
71+
window.requestIdleCallback(
72+
() => {
73+
this.make();
74+
},
75+
{ timeout: 2000 }
76+
);
7477
}
7578

7679
/**
@@ -233,7 +236,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
233236
* Prevent InlineToolbar from overflowing the content zone on the right side
234237
*/
235238
if (realRightCoord > this.Editor.UI.contentRect.right) {
236-
newCoords.x = this.Editor.UI.contentRect.right -popoverWidth - wrapperOffset.x;
239+
newCoords.x = this.Editor.UI.contentRect.right - popoverWidth - wrapperOffset.x;
237240
}
238241

239242
this.nodes.wrapper!.style.left = Math.floor(newCoords.x) + 'px';
@@ -415,7 +418,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
415418
const actions = instance.renderActions();
416419

417420
(popoverItem as WithChildren<PopoverItemHtmlParams>).children = {
418-
isOpen: instance.checkState?.(SelectionUtils.get()),
421+
isOpen: instance.checkState?.(SelectionUtils.get()!),
419422
/** Disable keyboard navigation in actions, as it might conflict with enter press handling */
420423
isFlippable: false,
421424
items: [
@@ -424,12 +427,17 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
424427
element: actions,
425428
},
426429
],
430+
onClose: () => {
431+
if (_.isFunction(instance.clear)) {
432+
instance.clear();
433+
}
434+
},
427435
};
428436
} else {
429437
/**
430438
* Legacy inline tools might perform some UI mutating logic in checkState method, so, call it just in case
431439
*/
432-
instance.checkState?.(SelectionUtils.get());
440+
instance.checkState?.(SelectionUtils.get()!);
433441
}
434442

435443
popoverItems.push(popoverItem);
@@ -541,7 +549,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
541549
*/
542550
// if (SelectionUtils.isCollapsed) return;
543551

544-
if (!currentBlock.tool.enabledInlineTools) {
552+
if (currentBlock.tool.enabledInlineTools === false) {
545553
return;
546554
}
547555

@@ -573,7 +581,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
573581
*/
574582
private checkToolsState(): void {
575583
this.tools?.forEach((toolInstance) => {
576-
toolInstance.checkState?.(SelectionUtils.get());
584+
toolInstance.checkState?.(SelectionUtils.get()!);
577585
});
578586
}
579587

src/components/utils/popover/popover-inline.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,15 @@ export class PopoverInline extends PopoverDesktop {
5353
* once you select <a> tag content in text
5454
*/
5555
this.items
56-
.forEach((item) => {
57-
if (!(item instanceof PopoverItemDefault) && !(item instanceof PopoverItemHtml)) {
58-
return;
59-
}
60-
61-
if (item.hasChildren && item.isChildrenOpen) {
62-
this.showNestedItems(item);
63-
}
64-
});
56+
.forEach((item) => {
57+
if (!(item instanceof PopoverItemDefault) && !(item instanceof PopoverItemHtml)) {
58+
return;
59+
}
60+
61+
if (item.hasChildren && item.isChildrenOpen) {
62+
this.showNestedItems(item);
63+
}
64+
});
6565
}
6666

6767
/**
@@ -166,13 +166,8 @@ export class PopoverInline extends PopoverDesktop {
166166
protected override handleItemClick(item: PopoverItem): void {
167167
if (item !== this.nestedPopoverTriggerItem) {
168168
/**
169-
* In case tool had special handling for toggling button (like link tool which modifies selection)
170-
* we need to call handleClick on nested popover trigger item
171-
*/
172-
this.nestedPopoverTriggerItem?.handleClick();
173-
174-
/**
175-
* Then close the nested popover
169+
* Close the nested popover without triggering the tool's action.
170+
* The onChildrenClose callback will handle any necessary UI cleanup.
176171
*/
177172
super.destroyNestedPopoverIfExists();
178173
}

0 commit comments

Comments
 (0)