upfront-editable
Version:
Friendly contenteditable API
445 lines (392 loc) • 16 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _rangy = _interopRequireDefault(require("rangy"));
var viewport = _interopRequireWildcard(require("./util/viewport"));
var content = _interopRequireWildcard(require("./content"));
var parser = _interopRequireWildcard(require("./parser"));
var string = _interopRequireWildcard(require("./util/string"));
var _nodeType = require("./node-type");
var _error = _interopRequireDefault(require("./util/error"));
var rangeSaveRestore = _interopRequireWildcard(require("./range-save-restore"));
var _nodeIterator = _interopRequireDefault(require("./node-iterator"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
// import printRange from './util/print_range'
/**
* The Cursor module provides a cross-browser abstraction layer for cursor.
*
* @module core
* @submodule cursor
*/
var Cursor = /*#__PURE__*/function () {
/**
* Class for the Cursor module.
*
* @class Cursor
* @constructor
*/
function Cursor(editableHost, rangyRange) {
(0, _classCallCheck2["default"])(this, Cursor);
this.setHost(editableHost);
this.range = rangyRange;
this.isCursor = true;
} // Get all tags that affect the current selection. Optionally pass a
// method to filter the returned elements.
//
// @param {Function filter(node)} [Optional] Method to filter the returned
// DOM Nodes.
// @return {Array of DOM Nodes}
(0, _createClass2["default"])(Cursor, [{
key: "getTags",
value: function getTags(filterFunc) {
return content.getTags(this.host, this.range, filterFunc);
} // Get the names of all tags that affect the current selection. Optionally
// pass a method to filter the returned elements.
//
// @param {Function filter(node)} [Optional] Method to filter the DOM
// Nodes whose names are returned.
// @return {Array<String> of tag names}
}, {
key: "getTagNames",
value: function getTagNames(filterFunc) {
return content.getTagNames(this.getTags(filterFunc));
} // Get all tags of the specified type that affect the current selection.
//
// @method getTagsByName
// @param {String} tagName. E.g. 'a' to get all links.
// @return {Array of DOM Nodes}
}, {
key: "getTagsByName",
value: function getTagsByName(tagName) {
return content.getTagsByName(this.host, this.range, tagName);
}
}, {
key: "isAtEnd",
value: function isAtEnd() {
return parser.isEndOfHost(this.host, this.range.endContainer, this.range.endOffset);
}
}, {
key: "isAtTextEnd",
value: function isAtTextEnd() {
return parser.isTextEndOfHost(this.host, this.range.endContainer, this.range.endOffset);
}
}, {
key: "isAtLastLine",
value: function isAtLastLine() {
var hostRange = this.win.document.createRange();
hostRange.selectNodeContents(this.host);
var hostCoords = hostRange.getBoundingClientRect();
var cursorCoords;
if (this.range.nativeRange.startContainer.nodeType === _nodeType.elementNode) {
var container = this.range.nativeRange.startContainer;
if (container.children.length - 1 >= this.range.nativeRange.startOffset) {
var elem = container.children[this.range.nativeRange.startOffset];
var iterator = new _nodeIterator["default"](elem);
var textNode = iterator.getNextTextNode();
if (textNode) {
var cursorRange = this.win.document.createRange();
cursorRange.setStart(textNode, 0);
cursorRange.collapse(true);
cursorCoords = cursorRange.getBoundingClientRect();
} else {
cursorCoords = hostCoords;
}
} else {
cursorCoords = hostCoords;
}
} else {
cursorCoords = this.getBoundingClientRect();
}
return hostCoords.bottom === cursorCoords.bottom;
}
}, {
key: "isAtFirstLine",
value: function isAtFirstLine() {
var hostRange = this.win.document.createRange();
hostRange.selectNodeContents(this.host);
var hostCoords = hostRange.getBoundingClientRect();
var cursorCoords;
if (this.range.nativeRange.startContainer.nodeType === _nodeType.elementNode) {
var container = this.range.nativeRange.startContainer;
if (container.children.length - 1 >= this.range.nativeRange.startOffset) {
var elem = container.children[this.range.nativeRange.startOffset];
var iterator = new _nodeIterator["default"](elem);
var textNode = iterator.getPreviousTextNode();
if (textNode) {
var cursorRange = this.win.document.createRange();
cursorRange.setStart(textNode, 0);
cursorRange.collapse(true);
cursorCoords = cursorRange.getBoundingClientRect();
} else {
cursorCoords = hostCoords;
}
} else {
cursorCoords = hostCoords;
}
} else {
cursorCoords = this.range.nativeRange.getBoundingClientRect();
}
return hostCoords.top === cursorCoords.top;
}
}, {
key: "isAtBeginning",
value: function isAtBeginning() {
return parser.isBeginningOfHost(this.host, this.range.startContainer, this.range.startOffset);
} // Insert content before the cursor
//
// @param {String, DOM node or document fragment}
}, {
key: "insertBefore",
value: function insertBefore(element) {
if (string.isString(element)) element = content.createFragmentFromString(element);
if (parser.isDocumentFragmentWithoutChildren(element)) return;
element = this.adoptElement(element);
var preceedingElement = element;
if (element.nodeType === _nodeType.documentFragmentNode) {
var lastIndex = element.childNodes.length - 1;
preceedingElement = element.childNodes[lastIndex];
}
this.range.insertNode(element);
this.range.setStartAfter(preceedingElement);
this.range.setEndAfter(preceedingElement);
} // Insert content after the cursor
//
// @param {String, DOM node or document fragment}
}, {
key: "insertAfter",
value: function insertAfter(element) {
if (string.isString(element)) element = content.createFragmentFromString(element);
if (parser.isDocumentFragmentWithoutChildren(element)) return;
element = this.adoptElement(element);
this.range.insertNode(element);
} // Alias for #setVisibleSelection()
}, {
key: "setSelection",
value: function setSelection() {
this.setVisibleSelection();
}
}, {
key: "setVisibleSelection",
value: function setVisibleSelection() {
if (this.win.document.activeElement !== this.host) {
var _viewport$getScrollPo = viewport.getScrollPosition(this.win),
x = _viewport$getScrollPo.x,
y = _viewport$getScrollPo.y;
this.win.scrollTo(x, y);
}
_rangy["default"].getSelection(this.win).setSingleRange(this.range);
} // Take the following example:
// (The character '|' represents the cursor position)
//
// <div contenteditable="true">fo|o</div>
// before() will return a document fragment containing a text node 'fo'.
//
// @returns {Document Fragment} content before the cursor or selection.
}, {
key: "before",
value: function before() {
var range = this.range.cloneRange();
range.collapse(true);
range.setStartBefore(this.host);
return content.cloneRangeContents(range);
}
}, {
key: "textBefore",
value: function textBefore() {
var range = this.range.cloneRange();
range.collapse(true);
range.setStartBefore(this.host);
return range.toString();
} // Same as before() but returns a string.
}, {
key: "beforeHtml",
value: function beforeHtml() {
return content.getInnerHtmlOfFragment(this.before());
} // Take the following example:
// (The character '|' represents the cursor position)
//
// <div contenteditable="true">fo|o</div>
// after() will return a document fragment containing a text node 'o'.
//
// @returns {Document Fragment} content after the cursor or selection.
}, {
key: "after",
value: function after() {
var range = this.range.cloneRange();
range.collapse(false);
range.setEndAfter(this.host);
return content.cloneRangeContents(range);
}
}, {
key: "textAfter",
value: function textAfter() {
var range = this.range.cloneRange();
range.collapse(false);
range.setEndAfter(this.host);
return range.toString();
} // Same as after() but returns a string.
}, {
key: "afterHtml",
value: function afterHtml() {
return content.getInnerHtmlOfFragment(this.after());
}
}, {
key: "getBoundingClientRect",
value: function getBoundingClientRect() {
return this.range.nativeRange.getBoundingClientRect();
} // Get the BoundingClientRect of the cursor.
// The returned values are transformed to be absolute
// (relative to the document).
}, {
key: "getCoordinates",
value: function getCoordinates() {
var positioning = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'absolute';
var coords = this.range.nativeRange.getBoundingClientRect();
if (positioning === 'fixed') return coords; // translate into absolute positions
var _viewport$getScrollPo2 = viewport.getScrollPosition(this.win),
x = _viewport$getScrollPo2.x,
y = _viewport$getScrollPo2.y;
return {
top: coords.top + y,
bottom: coords.bottom + y,
left: coords.left + x,
right: coords.right + x,
height: coords.height,
width: coords.width
};
}
}, {
key: "moveBefore",
value: function moveBefore(element) {
this.updateHost(element);
this.range.setStartBefore(element);
this.range.setEndBefore(element);
if (this.isSelection) return new Cursor(this.host, this.range);
}
}, {
key: "moveAfter",
value: function moveAfter(element) {
this.updateHost(element);
this.range.setStartAfter(element);
this.range.setEndAfter(element);
if (this.isSelection) return new Cursor(this.host, this.range);
} // Move the cursor to the beginning of the host.
}, {
key: "moveAtBeginning",
value: function moveAtBeginning() {
var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.host;
this.updateHost(element);
this.range.selectNodeContents(element);
this.range.collapse(true);
if (this.isSelection) return new Cursor(this.host, this.range);
} // Move the cursor to the end of the host.
}, {
key: "moveAtEnd",
value: function moveAtEnd() {
var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.host;
this.updateHost(element);
this.range.selectNodeContents(element);
this.range.collapse(false);
if (this.isSelection) return new Cursor(this.host, this.range);
} // Move the cursor after the last visible character of the host.
}, {
key: "moveAtTextEnd",
value: function moveAtTextEnd(element) {
return this.moveAtEnd(parser.latestChild(element));
}
}, {
key: "setHost",
value: function setHost(element) {
if (element.jquery) element = element[0];
this.host = element;
this.win = element === undefined || element === null ? window : element.ownerDocument.defaultView;
}
}, {
key: "updateHost",
value: function updateHost(element) {
var host = parser.getHost(element);
if (!host) (0, _error["default"])('Can not set cursor outside of an editable block');
this.setHost(host);
}
}, {
key: "retainVisibleSelection",
value: function retainVisibleSelection(callback) {
this.save();
callback();
this.restore();
this.setVisibleSelection();
}
}, {
key: "save",
value: function save() {
this.savedRangeInfo = rangeSaveRestore.save(this.range);
this.savedRangeInfo.host = this.host;
}
}, {
key: "restore",
value: function restore() {
if (!this.savedRangeInfo) (0, _error["default"])('Could not restore selection');
this.host = this.savedRangeInfo.host;
this.range = rangeSaveRestore.restore(this.host, this.savedRangeInfo);
this.savedRangeInfo = undefined;
}
}, {
key: "equals",
value: function equals(cursor) {
if (!cursor) return false;
if (!cursor.host) return false;
if (!cursor.host.isEqualNode(this.host)) return false;
if (!cursor.range) return false;
if (!cursor.range.equals(this.range)) return false;
return true;
} // Create an element with the correct ownerWindow
// (see: http://www.w3.org/DOM/faq.html#ownerdoc)
}, {
key: "createElement",
value: function createElement(tagName) {
var attributes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var element = this.win.document.createElement(tagName);
for (var attributeName in attributes) {
var attributeValue = attributes[attributeName];
element.setAttribute(attributeName, attributeValue);
}
return element;
}
}, {
key: "createTextNode",
value: function createTextNode(text) {
return this.win.document.createTextNode(text);
} // Make sure a node has the correct ownerWindow
// (see: https://developer.mozilla.org/en-US/docs/Web/API/Document/importNode)
}, {
key: "adoptElement",
value: function adoptElement(node) {
return content.adoptElement(node, this.win.document);
} // Currently we call triggerChange manually after format changes.
// This is to prevent excessive triggering of the change event during
// merge or split operations or other manipulations by scripts.
}, {
key: "triggerChange",
value: function triggerChange() {
var event = document.createEvent('HTMLEvents');
event.initEvent('formatEditable', true, false);
this.host.dispatchEvent(event);
}
}], [{
key: "findHost",
value: function findHost(elem, selector) {
if (!elem.closest) elem = elem.parentNode;
return elem.closest(selector);
}
}]);
return Cursor;
}();
exports["default"] = Cursor;
module.exports = exports.default;