mirror of
https://github.com/codex-team/editor.js
synced 2024-05-19 06:47:16 +02:00
ac93017c70
* 2.16.0 * [Refactor] Separate internal and external settings (#845) * Enable flipping tools via standalone class (#830) * Enable flipping tools via standalone class * use flipper to refactor (#842) * use flipper to refactor * save changes * update * fix flipper on inline toolbar * ready for testing * requested changes * update doc * updates * destroy flippers * some requested changes * update * update * ready * update * last changes * update docs * Hghl active button of CT, simplify activate/deactivate * separate dom iterator * unhardcode directions * fixed a link in readme.md (#856) * Fix Block selection via CMD+A (#829) * Fix Block selection via CMD+A * Delete editor.js.map * update * update * Update CHANGELOG.md * Improve style of selected blocks (#858) * Cross-block-selection style improved * Update CHANGELOG.md * Fix case when property 'observer' in modificationObserver is not defined (#866) * Bump lodash.template from 4.4.0 to 4.5.0 (#885) Bumps [lodash.template](https://github.com/lodash/lodash) from 4.4.0 to 4.5.0. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.4.0...4.5.0) Signed-off-by: dependabot[bot] <support@github.com> * Bump eslint-utils from 1.3.1 to 1.4.2 (#886) Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.3.1 to 1.4.2. - [Release notes](https://github.com/mysticatea/eslint-utils/releases) - [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.3.1...v1.4.2) Signed-off-by: dependabot[bot] <support@github.com> * Bump mixin-deep from 1.3.1 to 1.3.2 (#887) Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2. - [Release notes](https://github.com/jonschlinkert/mixin-deep/releases) - [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2) Signed-off-by: dependabot[bot] <support@github.com> * update bundle and readme * Update README.md * upd codeowners, fix funding * Minor Docs Fix according to main Readme (#916) * Inline Toolbar now contains Conversion Toolbar (#932) * Block lifecycle hooks (#906) * [Fix] Arrow selection (#964) * Fix arrow selection * Add docs * [issue-926]: fix dom iterator leafing when items are empty (#958) * [issue-926]: fix dom iterator leafing when items are empty * update Changelog * Issue 869 (#963) * Fix issue 943 (#965) * [Draft] Feature/tooltip enhancements (#907) * initial * update * make module standalone * use tooltips as external module * update * build via prod mode * add tooltips as external module * add declaration file and options param * add api tooltip * update * removed submodule * removed due to the incorrect setip * setup tooltips again * wip * update tooltip module * toolbox, inline toolbar * Tooltips in block tunes not uses shorthand * shorthand in a plus and block settings * fix doc * Update tools-inline.md * Delete tooltip.css * Update CHANGELOG.md * Update codex.tooltips * Update api.md * [issue-779]: Grammarly conflicts (#956) * grammarly conflicts * update * upd bundle * Submodule Header now on master * Submodule Marker now on master * Submodule Paragraph now on master * Submodule InlineCode now on master * Submodule Simple Image now on master * [issue-868]: Deleting multiple blocks triggers back button in Firefox (#967) * Deleting multiple blocks triggers back button in Firefox @evgenusov * Update editor.js * Update CHANGELOG.md * pass options on removeEventListener (#904) * pass options on removeEventListener by removeAll * rebuild * Merge branch 'release/2.16' into pr/904 * Update CHANGELOG.md * Update inline.ts * [Fix] Selection rangecount (#968) * Fix #952 (#969) * Update codex.tooltips * Selection bugfix (#970) * Selection bugfix * fix cross block selection * close inline toolbar when blocks selected via shift * remove inline toolbar closing on cross block selection mouse up due to the bug (#972) * [Feature] Log levels (#971) * Decrease margins (#973) * Decrease margins * Update editor.licenses.txt * Update src/components/domIterator.ts Co-Authored-By: Murod Khaydarov <murod.haydarov@gmail.com> * [Fix] Fix delete blocks api method (#974) * Update docs/usage.md Co-Authored-By: Murod Khaydarov <murod.haydarov@gmail.com> * rm unused * Update yarn.lock file * upd bundle, changelog
384 lines
8.8 KiB
TypeScript
384 lines
8.8 KiB
TypeScript
import SelectionUtils from '../selection';
|
|
|
|
import $ from '../dom';
|
|
import * as _ from '../utils';
|
|
import {API, InlineTool, SanitizerConfig} from '../../../types';
|
|
import {Notifier, Toolbar} from '../../../types/api';
|
|
|
|
/**
|
|
* Link Tool
|
|
*
|
|
* Inline Toolbar Tool
|
|
*
|
|
* Wrap selected text with <a> tag
|
|
*/
|
|
export default class LinkInlineTool implements InlineTool {
|
|
|
|
/**
|
|
* Specifies Tool as Inline Toolbar Tool
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
public static isInline = true;
|
|
|
|
/**
|
|
* Title for hover-tooltip
|
|
*/
|
|
public static title: string = 'Link';
|
|
|
|
/**
|
|
* Sanitizer Rule
|
|
* Leave <a> tags
|
|
* @return {object}
|
|
*/
|
|
static get sanitize(): SanitizerConfig {
|
|
return {
|
|
a: {
|
|
href: true,
|
|
target: '_blank',
|
|
rel: 'nofollow',
|
|
},
|
|
} as SanitizerConfig;
|
|
}
|
|
|
|
/**
|
|
* Native Document's commands for link/unlink
|
|
*/
|
|
private readonly commandLink: string = 'createLink';
|
|
private readonly commandUnlink: string = 'unlink';
|
|
|
|
/**
|
|
* Enter key code
|
|
*/
|
|
private readonly ENTER_KEY: number = 13;
|
|
|
|
/**
|
|
* Styles
|
|
*/
|
|
private readonly CSS = {
|
|
button: 'ce-inline-tool',
|
|
buttonActive: 'ce-inline-tool--active',
|
|
buttonModifier: 'ce-inline-tool--link',
|
|
buttonUnlink: 'ce-inline-tool--unlink',
|
|
input: 'ce-inline-tool-input',
|
|
inputShowed: 'ce-inline-tool-input--showed',
|
|
};
|
|
|
|
/**
|
|
* Elements
|
|
*/
|
|
private nodes: {
|
|
button: HTMLButtonElement;
|
|
input: HTMLInputElement;
|
|
} = {
|
|
button: null,
|
|
input: null,
|
|
};
|
|
|
|
/**
|
|
* SelectionUtils instance
|
|
*/
|
|
private selection: SelectionUtils;
|
|
|
|
/**
|
|
* Input opening state
|
|
*/
|
|
private inputOpened: boolean = false;
|
|
|
|
/**
|
|
* Available Toolbar methods (open/close)
|
|
*/
|
|
private toolbar: Toolbar;
|
|
|
|
/**
|
|
* Available inline toolbar methods (open/close)
|
|
*/
|
|
private inlineToolbar: Toolbar;
|
|
|
|
/**
|
|
* Notifier API methods
|
|
*/
|
|
private notifier: Notifier;
|
|
|
|
/**
|
|
* @param {{api: API}} - Editor.js API
|
|
*/
|
|
constructor({api}) {
|
|
this.toolbar = api.toolbar;
|
|
this.inlineToolbar = api.inlineToolbar;
|
|
this.notifier = api.notifier;
|
|
this.selection = new SelectionUtils();
|
|
}
|
|
|
|
/**
|
|
* Create button for Inline Toolbar
|
|
*/
|
|
public render(): HTMLElement {
|
|
this.nodes.button = document.createElement('button') as HTMLButtonElement;
|
|
this.nodes.button.type = 'button';
|
|
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
|
|
this.nodes.button.appendChild($.svg('link', 14, 10));
|
|
this.nodes.button.appendChild($.svg('unlink', 15, 11));
|
|
return this.nodes.button;
|
|
}
|
|
|
|
/**
|
|
* Input for the link
|
|
*/
|
|
public renderActions(): HTMLElement {
|
|
this.nodes.input = document.createElement('input') as HTMLInputElement;
|
|
this.nodes.input.placeholder = 'Add a link';
|
|
this.nodes.input.classList.add(this.CSS.input);
|
|
this.nodes.input.addEventListener('keydown', (event: KeyboardEvent) => {
|
|
if (event.keyCode === this.ENTER_KEY) {
|
|
this.enterPressed(event);
|
|
}
|
|
});
|
|
return this.nodes.input;
|
|
}
|
|
|
|
/**
|
|
* Handle clicks on the Inline Toolbar icon
|
|
* @param {Range} range
|
|
*/
|
|
public surround(range: Range): void {
|
|
/**
|
|
* Range will be null when user makes second click on the 'link icon' to close opened input
|
|
*/
|
|
if (range) {
|
|
/**
|
|
* Save selection before change focus to the input
|
|
*/
|
|
if (!this.inputOpened) {
|
|
/** Create blue background instead of selection */
|
|
this.selection.setFakeBackground();
|
|
this.selection.save();
|
|
} else {
|
|
this.selection.restore();
|
|
this.selection.removeFakeBackground();
|
|
}
|
|
const parentAnchor = this.selection.findParentTag('A');
|
|
|
|
/**
|
|
* Unlink icon pressed
|
|
*/
|
|
if (parentAnchor) {
|
|
this.selection.expandToTag(parentAnchor);
|
|
this.unlink();
|
|
this.closeActions();
|
|
this.checkState();
|
|
this.toolbar.close();
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.toggleActions();
|
|
}
|
|
|
|
/**
|
|
* Check selection and set activated state to button if there are <a> tag
|
|
* @param {Selection} selection
|
|
*/
|
|
public checkState(selection?: Selection): boolean {
|
|
const anchorTag = this.selection.findParentTag('A');
|
|
|
|
if (anchorTag) {
|
|
this.nodes.button.classList.add(this.CSS.buttonUnlink);
|
|
this.nodes.button.classList.add(this.CSS.buttonActive);
|
|
this.openActions();
|
|
|
|
/**
|
|
* Fill input value with link href
|
|
*/
|
|
const hrefAttr = anchorTag.getAttribute('href');
|
|
this.nodes.input.value = hrefAttr !== 'null' ? hrefAttr : '';
|
|
|
|
this.selection.save();
|
|
} else {
|
|
this.nodes.button.classList.remove(this.CSS.buttonUnlink);
|
|
this.nodes.button.classList.remove(this.CSS.buttonActive);
|
|
}
|
|
|
|
return !!anchorTag;
|
|
}
|
|
|
|
/**
|
|
* Function called with Inline Toolbar closing
|
|
*/
|
|
public clear(): void {
|
|
this.closeActions();
|
|
}
|
|
|
|
/**
|
|
* Set a shortcut
|
|
*/
|
|
public get shortcut(): string {
|
|
return 'CMD+K';
|
|
}
|
|
|
|
private toggleActions(): void {
|
|
if (!this.inputOpened) {
|
|
this.openActions(true);
|
|
} else {
|
|
this.closeActions(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} needFocus - on link creation we need to focus input. On editing - nope.
|
|
*/
|
|
private openActions(needFocus: boolean = false): void {
|
|
this.nodes.input.classList.add(this.CSS.inputShowed);
|
|
if (needFocus) {
|
|
this.nodes.input.focus();
|
|
}
|
|
this.inputOpened = true;
|
|
}
|
|
|
|
/**
|
|
* Close input
|
|
* @param {boolean} clearSavedSelection — we don't need to clear saved selection
|
|
* on toggle-clicks on the icon of opened Toolbar
|
|
*/
|
|
private closeActions(clearSavedSelection: boolean = true): void {
|
|
if (this.selection.isFakeBackgroundEnabled) {
|
|
// if actions is broken by other selection We need to save new selection
|
|
const currentSelection = new SelectionUtils();
|
|
currentSelection.save();
|
|
|
|
this.selection.restore();
|
|
this.selection.removeFakeBackground();
|
|
|
|
// and recover new selection after removing fake background
|
|
currentSelection.restore();
|
|
}
|
|
|
|
this.nodes.input.classList.remove(this.CSS.inputShowed);
|
|
this.nodes.input.value = '';
|
|
if (clearSavedSelection) {
|
|
this.selection.clearSaved();
|
|
}
|
|
this.inputOpened = false;
|
|
}
|
|
|
|
/**
|
|
* Enter pressed on input
|
|
* @param {KeyboardEvent} event
|
|
*/
|
|
private enterPressed(event: KeyboardEvent): void {
|
|
let value = this.nodes.input.value || '';
|
|
|
|
if (!value.trim()) {
|
|
this.selection.restore();
|
|
this.unlink();
|
|
event.preventDefault();
|
|
this.closeActions();
|
|
}
|
|
|
|
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
|
|
*/
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
event.stopImmediatePropagation();
|
|
this.selection.collapseToEnd();
|
|
this.inlineToolbar.close();
|
|
}
|
|
|
|
/**
|
|
* Detects if passed string is URL
|
|
* @param {string} str
|
|
* @return {Boolean}
|
|
*/
|
|
private validateURL(str: string): boolean {
|
|
/**
|
|
* Don't allow spaces
|
|
*/
|
|
return !/\s/.test(str);
|
|
}
|
|
|
|
/**
|
|
* Process link before injection
|
|
* - sanitize
|
|
* - add protocol for links like 'google.com'
|
|
* @param {string} link - raw user input
|
|
*/
|
|
private prepareLink(link: string): string {
|
|
link = link.trim();
|
|
link = this.addProtocol(link);
|
|
return link;
|
|
}
|
|
|
|
/**
|
|
* Add 'http' protocol to the links like 'vc.ru', 'google.com'
|
|
* @param {String} link
|
|
*/
|
|
private addProtocol(link: string): string {
|
|
/**
|
|
* If protocol already exists, do nothing
|
|
*/
|
|
if (/^(\w+):(\/\/)?/.test(link)) {
|
|
return link;
|
|
}
|
|
|
|
/**
|
|
* We need to add missed HTTP protocol to the link, but skip 2 cases:
|
|
* 1) Internal links like "/general"
|
|
* 2) Anchors looks like "#results"
|
|
* 3) Protocol-relative URLs like "//google.com"
|
|
*/
|
|
const isInternal = /^\/[^\/\s]/.test(link),
|
|
isAnchor = link.substring(0, 1) === '#',
|
|
isProtocolRelative = /^\/\/[^\/\s]/.test(link);
|
|
|
|
if (!isInternal && !isAnchor && !isProtocolRelative) {
|
|
link = 'http://' + link;
|
|
}
|
|
|
|
return link;
|
|
}
|
|
|
|
/**
|
|
* Inserts <a> tag with "href"
|
|
* @param {string} link - "href" value
|
|
*/
|
|
private insertLink(link: string): void {
|
|
|
|
/**
|
|
* Edit all link, not selected part
|
|
*/
|
|
const anchorTag = this.selection.findParentTag('A');
|
|
|
|
if (anchorTag) {
|
|
this.selection.expandToTag(anchorTag);
|
|
}
|
|
|
|
document.execCommand(this.commandLink, false, link);
|
|
}
|
|
|
|
/**
|
|
* Removes <a> tag
|
|
*/
|
|
private unlink(): void {
|
|
document.execCommand(this.commandUnlink);
|
|
}
|
|
}
|