editor.js/src/components/selection.ts
George Berezhnoy 79f8660637 Version 2.13 (#719)
* Do not start multi-block selection on UI elements (#662)

* Do not start multi-block selection on UI elements

* Do not prevent mousedown event on inline toolbar actions

* Remove log

* Add comment

* Add link to issue

closes #646

* Fix loss of pointer (#666)

* Fix loss of pointer when click is outside of the editor but selection is inside

* Remove log

* Update shortcuts module (#685)

* Fixed possible grammatical typo (#681)

Thanks

* Update shortcuts module

* update changelog

* update

* Remove margin top for inline-link icon (#690)

* Remove margin top for inline-link icon

resolves #674

* Update CHANGELOG.md

* Remove unused style

* Pull fresh tools

* Remove changelog contents from readme (#700)

* #665 API to open and close inline-toolbar (#711)

* API to open and close inline-toolbar

* Fixed documentation

* renamed inline -> inline-toolbar

* removed dist

* reset editor.js

* added editor.js bundle

* Fixed build error

* Null checks on toolbar/inline@open

* updated bundle

* Improve some comments

* Updatd api.md CHANGELOG.md

* Change feature to new instead of improvement

* Allow holderId work with ref on dom element (#710)

* done

* update types

* attempt to simplify code

* remove useless helper

* revert holderId logic and add holder property

* Apply suggestions from code review

Co-Authored-By: dimensi <eddimensi@gmail.com>

* update holder type on string | HTMLElement

* fix typo

* add deprecated notice and fix typos

* fix wrong compare

* fix comments

* swap console.log on _.log

* update types for editor config

* update examples

* update docs

* update build

* Activating Open Collective (#736)

Hi, I'm making updates for Open Collective. Either you or a supporter signed this repo up for Open Collective. This pull request adds backers and sponsors from your Open Collective https://opencollective.com/editorjs❤️

It adds two badges at the top to show the latest number of backers and sponsors. It also adds placeholders so that the avatar/logo of new backers/sponsors can automatically be shown without having to update your README.md. [more info](https://github.com/opencollective/opencollective/wiki/Github-banner). See how it looks on this [repo](https://github.com/apex/apex#backers).

You can also add a postinstall script to let people know after npm|yarn install that you are welcoming donations (optional). [More info](https://github.com/OpenCollective/opencollective-cli)
You can also add a "Donate" button to your website and automatically show your backers and sponsors there with our widgets. Have a look here: https://opencollective.com/widgets

P.S: As with any pull request, feel free to comment or suggest changes. The only thing "required" are the placeholders on the README because we believe it's important to acknowledge the people in your community that are contributing (financially or with code!).

Thank you for your great contribution to the open source community. You are awesome! 🙌
And welcome to the open collective community! 😊

Come chat with us in the #opensource channel on https://slack.opencollective.com - great place to ask questions and share best practices with other open source sustainers!

* Do not install editor.js as dev-dependency (#731)

Resolves #730

* Move codex-notifier to dependencies for typescript declarations (#728)

* Close inline toolbar after creating new link by pressing ENTER (#722)

* Method to clear current selection and close inline toolbar

* clearSelection with optional collapsed range

* refactored selection.ts

* removed experimental function

* Update src/components/selection.ts

Co-Authored-By: tanmayv <12tanmayvijay@gmail.com>

* update version, add changelog

* Link Logo Image to homepage (#738)

* Update README.md (#744)

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

Co-Authored-By: neSpecc <specc.dev@gmail.com>

* Config minHeight option that allows to customize bottom zone (#745)

* issue-739: allow Block's editable element selection (#747)

* issue-739: allow Block's input selection

* little improvements

* update Changelog and cache inputs

* small fix

* delete map file

* fix inputs.count condition

* Fix typo in example paragraph (#749)

* Fix typo

* Update example-dev.html

* minor release
2019-04-29 15:52:54 +03:00

374 lines
8.9 KiB
TypeScript

/**
* TextRange interface fot IE9-
*/
import _ from './utils';
interface TextRange {
boundingTop: number;
boundingLeft: number;
boundingBottom: number;
boundingRight: number;
boundingHeight: number;
boundingWidth: number;
}
/**
* Interface for object returned by document.selection in IE9-
*/
interface MSSelection {
createRange: () => TextRange;
type: string;
}
/**
* Extends Document interface for IE9-
*/
interface Document {
selection?: MSSelection;
}
/**
* Working with selection
* @typedef {SelectionUtils} SelectionUtils
*/
export default class SelectionUtils {
/**
* Editor styles
* @return {{editorWrapper: string, editorZone: string}}
*/
static get CSS(): { editorWrapper: string, editorZone: string } {
return {
editorWrapper: 'codex-editor',
editorZone: 'codex-editor__redactor',
};
}
/**
* Returns selected anchor
* {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorNode}
* @return {Node|null}
*/
static get anchorNode(): Node | null {
const selection = window.getSelection();
return selection ? selection.anchorNode : null;
}
/**
* Returns selection offset according to the anchor node
* {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset}
* @return {Number|null}
*/
static get anchorOffset(): number | null {
const selection = window.getSelection();
return selection ? selection.anchorOffset : null;
}
/**
* Is current selection range collapsed
* @return {boolean|null}
*/
static get isCollapsed(): boolean | null {
const selection = window.getSelection();
return selection ? selection.isCollapsed : null;
}
/**
* Check current selection if it is at Editor's zone
* @return {boolean}
*/
static get isAtEditor(): boolean {
const selection = SelectionUtils.get();
/**
* Something selected on document
*/
let selectedNode = (selection.anchorNode || selection.focusNode) as HTMLElement;
if (selectedNode && selectedNode.nodeType === Node.TEXT_NODE) {
selectedNode = selectedNode.parentNode as HTMLElement;
}
let editorZone = null;
if (selectedNode) {
editorZone = selectedNode.closest(`.${SelectionUtils.CSS.editorZone}`);
}
/**
* SelectionUtils is not out of Editor because Editor's wrapper was found
*/
return editorZone && editorZone.nodeType === Node.ELEMENT_NODE;
}
/**
* Return first range
* @return {Range|null}
*/
static get range(): Range {
const selection = window.getSelection();
return selection && selection.rangeCount ? selection.getRangeAt(0) : null;
}
/**
* Calculates position and size of selected text
* @return {{x, y, width, height, top?, left?, bottom?, right?}}
*/
static get rect(): DOMRect | ClientRect {
let sel: Selection | MSSelection = (document as Document).selection,
range: TextRange | Range;
let rect = {
x: 0,
y: 0,
width: 0,
height: 0,
} as DOMRect;
if (sel && sel.type !== 'Control') {
sel = sel as MSSelection;
range = sel.createRange() as TextRange;
rect.x = range.boundingLeft;
rect.y = range.boundingTop;
rect.width = range.boundingWidth;
rect.height = range.boundingHeight;
return rect;
}
if (!window.getSelection) {
_.log('Method window.getSelection is not supported', 'warn');
return rect;
}
sel = window.getSelection();
if (sel.rangeCount === null || isNaN(sel.rangeCount)) {
_.log('Method SelectionUtils.rangeCount is not supported', 'warn');
return rect;
}
range = sel.getRangeAt(0).cloneRange() as Range;
if (range.getBoundingClientRect) {
rect = range.getBoundingClientRect() as DOMRect;
}
// Fall back to inserting a temporary element
if (rect.x === 0 && rect.y === 0) {
const span = document.createElement('span');
if (span.getBoundingClientRect) {
// Ensure span has dimensions and position by
// adding a zero-width space character
span.appendChild(document.createTextNode('\u200b'));
range.insertNode(span);
rect = span.getBoundingClientRect() as DOMRect;
const spanParent = span.parentNode;
spanParent.removeChild(span);
// Glue any broken text nodes back together
spanParent.normalize();
}
}
return rect;
}
/**
* Returns selected text as String
* @returns {string}
*/
static get text(): string {
return window.getSelection ? window.getSelection().toString() : '';
}
/**
* Returns window SelectionUtils
* {@link https://developer.mozilla.org/ru/docs/Web/API/Window/getSelection}
* @return {Selection}
*/
public static get(): Selection {
return window.getSelection();
}
public instance: Selection = null;
public selection: Selection = null;
/**
* This property can store SelectionUtils's range for restoring later
* @type {Range|null}
*/
public savedSelectionRange: Range = null;
/**
* Fake background is active
*
* @return {boolean}
*/
public isFakeBackgroundEnabled = false;
/**
* Native Document's commands for fake background
*/
private readonly commandBackground: string = 'backColor';
private readonly commandRemoveFormat: string = 'removeFormat';
/**
* Removes fake background
*/
public removeFakeBackground() {
if (!this.isFakeBackgroundEnabled) {
return;
}
this.isFakeBackgroundEnabled = false;
document.execCommand(this.commandRemoveFormat);
}
/**
* Sets fake background
*/
public setFakeBackground() {
document.execCommand(this.commandBackground, false, '#a8d6ff');
this.isFakeBackgroundEnabled = true;
}
/**
* Save SelectionUtils's range
*/
public save(): void {
this.savedSelectionRange = SelectionUtils.range;
}
/**
* Restore saved SelectionUtils's range
*/
public restore(): void {
if (!this.savedSelectionRange) {
return;
}
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(this.savedSelectionRange);
}
/**
* Clears saved selection
*/
public clearSaved(): void {
this.savedSelectionRange = null;
}
/**
* Collapse current selection
*/
public collapseToEnd(): void {
const sel = window.getSelection();
const range = document.createRange();
range.selectNodeContents(sel.focusNode);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
}
/**
* Looks ahead to find passed tag from current selection
*
* @param {String} tagName - tag to found
* @param {String} [className] - tag's class name
* @param {Number} [searchDepth] - count of tags that can be included. For better performance.
* @return {HTMLElement|null}
*/
public findParentTag(tagName: string, className?: string, searchDepth = 10): HTMLElement | null {
const selection = window.getSelection();
let parentTag = null;
/**
* If selection is missing or no anchorNode or focusNode were found then return null
*/
if (!selection || !selection.anchorNode || !selection.focusNode) {
return null;
}
/**
* Define Nodes for start and end of selection
*/
const boundNodes = [
/** the Node in which the selection begins */
selection.anchorNode as HTMLElement,
/** the Node in which the selection ends */
selection.focusNode as HTMLElement,
];
/**
* For each selection parent Nodes we try to find target tag [with target class name]
* It would be saved in parentTag variable
*/
boundNodes.forEach((parent) => {
/** Reset tags limit */
let searchDepthIterable = searchDepth;
while (searchDepthIterable > 0 && parent.parentNode) {
/**
* Check tag's name
*/
if (parent.tagName === tagName) {
/**
* Save the result
*/
parentTag = parent;
/**
* Optional additional check for class-name mismatching
*/
if (className && parent.classList && !parent.classList.contains(className)) {
parentTag = null;
}
/**
* If we have found required tag with class then go out from the cycle
*/
if (parentTag) {
break;
}
}
/**
* Target tag was not found. Go up to the parent and check it
*/
parent = parent.parentNode as HTMLElement;
searchDepthIterable--;
}
});
/**
* Return found tag or null
*/
return parentTag;
}
/**
* Expands selection range to the passed parent node
*
* @param {HTMLElement} element
*/
public expandToTag(element: HTMLElement): void {
const selection = window.getSelection();
selection.removeAllRanges();
const range = document.createRange();
range.selectNodeContents(element);
selection.addRange(range);
}
}