editor.js/src/components/selection.ts
Peter Savchenko ac93017c70
Release 2.16 (#966)
* 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
2019-11-30 23:42:39 +03:00

403 lines
9.4 KiB
TypeScript

/**
* TextRange interface fot IE9-
*/
import * as _ from './utils';
import $ from './dom';
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 selected anchor element
* @return {Element|null}
*/
static get anchorElement(): Element | null {
const selection = window.getSelection();
if (!selection) {
return null;
}
const anchorNode = selection.anchorNode;
if (!anchorNode) {
return null;
}
if (!$.isElement(anchorNode)) {
return anchorNode.parentElement;
} else {
return anchorNode;
}
}
/**
* 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;
}
if (sel.rangeCount === 0) {
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);
}
}