mirror of
https://github.com/codex-team/editor.js
synced 2026-03-14 22:55:47 +01:00
fix link in safari
This commit is contained in:
parent
339e772b92
commit
6aef9adbc9
8 changed files with 242 additions and 36 deletions
79
index.html
79
index.html
|
|
@ -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}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 ];
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
50
src/components/utils/selection.ts
Normal file
50
src/components/utils/selection.ts
Normal 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);
|
||||
// });
|
||||
};
|
||||
}
|
||||
|
|
@ -106,3 +106,10 @@
|
|||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ce-fake-selection {
|
||||
display: inline;
|
||||
line-height: inherit;
|
||||
background-color: var(--inlineSelectionColor);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue