fix link in safari

This commit is contained in:
Peter Savchenko 2024-09-12 22:50:25 +03:00
commit 6aef9adbc9
8 changed files with 242 additions and 36 deletions

View file

@ -25,6 +25,76 @@
<a href="https://editorjs.io/creating-a-block-tool" target="_blank">API</a>
</div>
</div>
<!--
<br>
<br>
<br>
<p>
To implement this policy, Treasury, in consultation with the Department of State, has issued a new determination under Executive Order (E.O.) 14071, which prohibits the supply to any person in the Russian Federation of (1) IT consultancy and design services; and (2) IT support services and cloud-based services for enterprise management software and design and manufacturing software. The determination will take effect on September 12, 2024.
</p>
<br>
<br>
<br>
<script>
function createFakeSelection(){
const selection = window.getSelection();
const range = selection?.getRangeAt(0);
const span = document.createElement('span');
span.style.background = 'red';
range?.surroundContents(span);
return function restore() {
if (!span.parentNode) {
return;
}
// Unwrap the span, restoring the original content
const parent = span.parentNode;
while (span.firstChild) {
parent.insertBefore(span.firstChild, span);
}
parent.removeChild(span);
// Restore the selection range
const newRange = document.createRange();
newRange.setStart(range.startContainer, range.startOffset);
newRange.setEnd(range.endContainer, range.endOffset);
// Set the selection to the new range
selection.removeAllRanges();
selection.addRange(newRange);
};
}
</script>
<script>
document.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
const restore = createFakeSelection();
setTimeout(() => {
restore();
}, 1000);
}
});
</script>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
-->
<div class="ce-example__content">
<div id="editorjs"></div>
<div class="ce-example__button" id="saveButton">
@ -83,7 +153,7 @@
<script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest"></script><!-- Delimiter -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/nested-list@latest"></script><!-- List -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest"></script><!-- Checklist -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@latest"></script><!-- Quote -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/code@latest"></script><!-- Code -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script><!-- Embed -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/table@latest"></script><!-- Table -->
@ -97,6 +167,10 @@
<!-- Initialization -->
<script type="module">
import EditorJS from './src/codex.ts';
import Quote from './example/tools/quote'
import Underline from '../underline'
console.log('Underline', Underline);
window.EditorJS = EditorJS;
@ -121,12 +195,13 @@
* - if an array of tool names, this order will be used
*/
// inlineToolbar: ['link', 'marker', 'bold', 'italic'],
// inlineToolbar: true,
inlineToolbar: true,
/**
* Tools list
*/
tools: {
underline: Underline,
/**
* Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}
*/

View file

@ -403,7 +403,7 @@ export default class Dom {
/**
* Normalize node to merge several text nodes to one to reduce tree walker iterations
*/
node.normalize();
// node.normalize();
const treeWalker = [ node ];

View file

@ -3,6 +3,7 @@ import * as _ from '../utils';
import type { InlineTool, SanitizerConfig, API } from '../../../types';
import type { Notifier, Toolbar, I18n, InlineToolbar } from '../../../types/api';
import { IconLink, IconUnlink } from '@codexteam/icons';
import { createFakeSelection } from '../utils/selection';
/**
* Link Tool
@ -77,7 +78,7 @@ export default class LinkInlineTool implements InlineTool {
/**
* SelectionUtils instance
*/
private selection: SelectionUtils;
private selection = new SelectionUtils();
/**
* Input opening state
@ -104,6 +105,10 @@ export default class LinkInlineTool implements InlineTool {
*/
private i18n: I18n;
private fakeSelectionRestore: null | ((onlyUnwrap?: boolean) => void) = null;
private currentAnchorElement: HTMLAnchorElement | null = null;
/**
* @param api - Editor.js API
*/
@ -112,7 +117,6 @@ export default class LinkInlineTool implements InlineTool {
this.inlineToolbar = api.inlineToolbar;
this.notifier = api.notifier;
this.i18n = api.i18n;
this.selection = new SelectionUtils();
}
/**
@ -151,6 +155,8 @@ export default class LinkInlineTool implements InlineTool {
* @param {Range} range - range to wrap with link
*/
public surround(range: Range): void {
console.log('surround');
/**
* Range will be null when user makes second click on the 'link icon' to close opened input
*/
@ -160,11 +166,14 @@ export default class LinkInlineTool implements InlineTool {
*/
if (!this.inputOpened) {
/** Create blue background instead of selection */
this.selection.setFakeBackground();
this.selection.save();
// this.selection.setFakeBackground();
// this.selection.save();
this.fakeSelectionRestore = createFakeSelection();
} else {
this.selection.restore();
this.selection.removeFakeBackground();
this.fakeSelectionRestore?.();
// this.selection.restore();
// this.selection.removeFakeBackground();
}
const parentAnchor = this.selection.findParentTag('A');
@ -174,9 +183,9 @@ export default class LinkInlineTool implements InlineTool {
if (parentAnchor) {
this.selection.expandToTag(parentAnchor);
this.unlink();
this.closeActions();
this.checkState();
this.toolbar.close();
// this.closeActions();
// this.checkState();
// this.toolbar.close();
return;
}
@ -189,12 +198,18 @@ export default class LinkInlineTool implements InlineTool {
* Check selection and set activated state to button if there are <a> tag
*/
public checkState(): boolean {
const anchorTag = this.selection.findParentTag('A');
// console.trace('checkState');
const anchorTag = this.selection.findParentTag('A') as HTMLAnchorElement;
if (anchorTag) {
if (anchorTag !== null) {
this.nodes.button.innerHTML = IconUnlink;
this.nodes.button.classList.add(this.CSS.buttonUnlink);
this.nodes.button.classList.add(this.CSS.buttonActive);
this.selection.expandToTag(anchorTag);
// console.log('1');
this.fakeSelectionRestore = createFakeSelection();
this.openActions();
/**
@ -204,21 +219,26 @@ export default class LinkInlineTool implements InlineTool {
this.nodes.input.value = hrefAttr !== 'null' ? hrefAttr : '';
this.selection.save();
this.currentAnchorElement = anchorTag;
// this.selection.save();
} else {
this.nodes.button.innerHTML = IconLink;
this.nodes.button.classList.remove(this.CSS.buttonUnlink);
this.nodes.button.classList.remove(this.CSS.buttonActive);
}
return !!anchorTag;
return anchorTag !== null;
}
/**
* Function called with Inline Toolbar closing
*/
public clear(): void {
this.closeActions();
// this.fakeSelectionRestore?.(true);
if (this.inputOpened) {
this.closeActions();
}
}
/**
@ -286,33 +306,61 @@ export default class LinkInlineTool implements InlineTool {
private enterPressed(event: KeyboardEvent): void {
let value = this.nodes.input.value || '';
if (!value.trim()) {
this.selection.restore();
/**
* Removing a link
*/
if (this.currentAnchorElement && !value.trim()) {
// this.selection.expandToTag(this.currentAnchorElement);
this.unlink();
event.preventDefault();
console.log('this.fakeSelectionRestore', this.fakeSelectionRestore);
this.fakeSelectionRestore?.();
this.closeActions();
} else {
if (!this.validateURL(value)) {
this.notifier.show({
message: 'Pasted link is not valid.',
style: 'error',
});
return;
_.log('Incorrect Link pasted', 'warn', value);
return;
}
value = this.prepareLink(value);
/**
* Addind / Editing a link
*/
/**
* First, we need to resotore selection
* Case 1. Input was opened by Link icon click in InlineToolbar, here will be a fake selection
* Case 2. Input was opened by selecting exising link in text, here will be this.currentAnchorElement
*/
if (this.fakeSelectionRestore !== null) {
this.fakeSelectionRestore();
} else if (this.currentAnchorElement) {
this.selection.expandToTag(this.currentAnchorElement);
} else {
throw new Error('Cannot add a link: no selection or anchor element found');
}
// const selection = window.getSelection();
// const range = selection?.getRangeAt(0);
// range?.surroundContents(document.createElement('a'));
// this.selection.restore();
// this.selection.removeFakeBackground();
// requestAnimationFrame(() => {
this.insertLink(value);
// });
}
if (!this.validateURL(value)) {
this.notifier.show({
message: 'Pasted link is not valid.',
style: 'error',
});
_.log('Incorrect Link pasted', 'warn', value);
return;
}
value = this.prepareLink(value);
this.selection.restore();
this.selection.removeFakeBackground();
this.insertLink(value);
/**
* Preventing events that will be able to happen
*/
@ -320,7 +368,7 @@ export default class LinkInlineTool implements InlineTool {
event.stopPropagation();
event.stopImmediatePropagation();
this.selection.collapseToEnd();
this.inlineToolbar.close();
// this.inlineToolbar.close();
}
/**

View file

@ -95,6 +95,16 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
* Avoid to use it just for closing IT, better call .close() clearly.
*/
public async tryToShow(needToClose = false): Promise<void> {
/**
* @todo
* infinite loop when selecting a link fragment:
* - selection changed
* - tryToShow called with needToClose = true
* - close called
* - open called
* - Link Tool, checkState called
* - link found, selection expanded to link
*/
if (needToClose) {
this.close();
}
@ -190,6 +200,8 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
* Shows Inline Toolbar
*/
private async open(): Promise<void> {
console.trace('open', this.opened);
if (this.opened) {
return;
}

View file

@ -228,6 +228,8 @@ export default class UI extends Module<UINodes> {
* Close all Editor's toolbars
*/
public closeAllToolbars(): void {
console.log('closeAllToolbars');
const { Toolbar, BlockSettings, InlineToolbar } = this.Editor;
BlockSettings.close();
@ -819,12 +821,16 @@ export default class UI extends Module<UINodes> {
* Uses for showing the Inline Toolbar
*/
private selectionChanged(): void {
console.log('selectionChanged');
const { CrossBlockSelection, BlockSelection } = this.Editor;
const focusedElement = Selection.anchorElement;
if (CrossBlockSelection.isCrossBlockSelectionStarted) {
// Removes all ranges when any Block is selected
if (BlockSelection.anyBlockSelected) {
console.log('selectionChanged: remove all ranges');
Selection.get().removeAllRanges();
}
}
@ -839,6 +845,8 @@ export default class UI extends Module<UINodes> {
* @todo Make this method more straightforward
*/
if (!Selection.range) {
console.log('selectionChanged: close InlineToolbar');
this.Editor.InlineToolbar.close();
}
@ -856,6 +864,8 @@ export default class UI extends Module<UINodes> {
* If new selection is not on Inline Toolbar, we need to close it
*/
if (!this.Editor.InlineToolbar.containsNode(focusedElement)) {
console.log('selectionChanged: close InlineToolbar');
this.Editor.InlineToolbar.close();
}
@ -875,9 +885,13 @@ export default class UI extends Module<UINodes> {
* Set current block when entering to Editor.js by tab key
*/
if (!this.Editor.BlockManager.currentBlock) {
console.log('selectionChanged: set current block');
this.Editor.BlockManager.setCurrentBlockByChildNode(focusedElement);
}
console.log('selectionChanged: try to show InlineToolbar');
this.Editor.InlineToolbar.tryToShow(true);
}

View file

@ -0,0 +1,50 @@
/**
* Wraps current selection with span imitating selection.
* Return a method for restoring selection based on this span
*/
export function createFakeSelection(): () => void {
console.info('createFakeSelection');
const selection = window.getSelection();
const range = selection?.getRangeAt(0);
const span = document.createElement('span');
span.classList.add('ce-fake-selection');
range?.surroundContents(span);
return function restore(onlyUnwrap = false) {
if (!span.parentNode) {
return;
}
/**
* Unwrap the span, restoring the original content
*/
const parent = span.parentNode;
while (span.firstChild) {
parent.insertBefore(span.firstChild, span);
}
parent.removeChild(span);
if (onlyUnwrap) {
return;
}
/**
* Restore the selection range
*/
// requestAnimationFrame(() => {
const newRange = document.createRange();
newRange.setStart(range.startContainer, range.startOffset);
newRange.setEnd(range.endContainer, range.endOffset);
// Set the selection to the new range
selection.removeAllRanges();
selection.addRange(newRange);
// });
};
}

View file

@ -106,3 +106,10 @@
position: fixed;
width: 100%;
}
.ce-fake-selection {
display: inline;
line-height: inherit;
background-color: var(--inlineSelectionColor);
border-radius: 3px;
}

View file

@ -881,7 +881,7 @@ describe('Popover', () => {
.should('exist');
});
it.only('shoould support i18n in nested popover', () => {
it('shoould support i18n in nested popover', () => {
/**
*
*/
@ -1076,7 +1076,7 @@ describe('Popover', () => {
.should('exist');
});
it.only('should allow to reach nested popover via keyboard', () => {
it('should allow to reach nested popover via keyboard', () => {
cy.createEditor({
tools: {
header: {