mirror of
https://github.com/codex-team/editor.js
synced 2024-06-10 09:52:36 +02:00
3d512d66ff
* hotfix * update
327 lines
7.9 KiB
TypeScript
327 lines
7.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;
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|