upfront-editable
Version:
Friendly contenteditable API
409 lines (331 loc) • 12 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _jquery = require('jquery');
var _jquery2 = _interopRequireDefault(_jquery);
var _rangy = require('rangy');
var _rangy2 = _interopRequireDefault(_rangy);
var _viewport = require('./util/viewport');
var viewport = _interopRequireWildcard(_viewport);
var _content = require('./content');
var content = _interopRequireWildcard(_content);
var _parser = require('./parser');
var parser = _interopRequireWildcard(_parser);
var _string = require('./util/string');
var string = _interopRequireWildcard(_string);
var _nodeType = require('./node-type');
var nodeType = _interopRequireWildcard(_nodeType);
var _error = require('./util/error');
var _error2 = _interopRequireDefault(_error);
var _rangeSaveRestore = require('./range-save-restore');
var rangeSaveRestore = _interopRequireWildcard(_rangeSaveRestore);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* The Cursor module provides a cross-browser abstraction layer for cursor.
*
* @module core
* @submodule cursor
*/
var Cursor = function () {
/**
* Class for the Cursor module.
*
* @class Cursor
* @constructor
*/
function Cursor(editableHost, rangyRange) {
(0, _classCallCheck3.default)(this, Cursor);
this.setHost(editableHost);
this.range = rangyRange;
this.isCursor = true;
}
(0, _createClass3.default)(Cursor, [{
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 range = _rangy2.default.createRange();
range.selectNodeContents(this.host);
var hostCoords = range.nativeRange.getBoundingClientRect();
var cursorCoords = this.range.nativeRange.getBoundingClientRect();
return hostCoords.bottom === cursorCoords.bottom;
}
}, {
key: 'isAtFirstLine',
value: function isAtFirstLine() {
var range = _rangy2.default.createRange();
range.selectNodeContents(this.host);
var hostCoords = range.nativeRange.getBoundingClientRect();
var 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);
}
_rangy2.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());
}
// 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, _error2.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, _error2.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() {
(0, _jquery2.default)(this.host).trigger('formatEditable');
}
}]);
return Cursor;
}();
exports.default = Cursor;