upfront-editable
Version:
Friendly contenteditable API
218 lines (177 loc) • 7.69 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 _rangy = require('rangy');
var _rangy2 = _interopRequireDefault(_rangy);
var _featureDetection = require('./feature-detection');
var _nodeType = require('./node-type');
var nodeType = _interopRequireWildcard(_nodeType);
var _eventable = require('./eventable');
var _eventable2 = _interopRequireDefault(_eventable);
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 Keyboard module defines an event API for key events.
*/
var Keyboard = function () {
function Keyboard(selectionWatcher) {
(0, _classCallCheck3.default)(this, Keyboard);
(0, _eventable2.default)(this);
this.selectionWatcher = selectionWatcher;
}
(0, _createClass3.default)(Keyboard, [{
key: 'dispatchKeyEvent',
value: function dispatchKeyEvent(event, target, notifyCharacterEvent) {
switch (event.keyCode) {
case this.key.left:
return this.notify(target, 'left', event);
case this.key.right:
return this.notify(target, 'right', event);
case this.key.up:
return this.notify(target, 'up', event);
case this.key.down:
return this.notify(target, 'down', event);
case this.key.tab:
if (event.shiftKey) return this.notify(target, 'shiftTab', event);
return this.notify(target, 'tab', event);
case this.key.esc:
return this.notify(target, 'esc', event);
case this.key.backspace:
this.preventContenteditableBug(target, event);
return this.notify(target, 'backspace', event);
case this.key.delete:
this.preventContenteditableBug(target, event);
return this.notify(target, 'delete', event);
case this.key.enter:
if (event.shiftKey) return this.notify(target, 'shiftEnter', event);
return this.notify(target, 'enter', event);
case this.key.ctrl:
case this.key.shift:
case this.key.alt:
return;
// Metakey
case 224: // Firefox: 224
case 17: // Opera: 17
case 91: // Chrome/Safari: 91 (Left)
case 93:
// Chrome/Safari: 93 (Right)
return;
default:
this.preventContenteditableBug(target, event);
if (!notifyCharacterEvent) return;
// Don't notify character events as long as either the ctrl or
// meta key are pressed.
// see: https://github.com/livingdocsIO/editable.js/pull/125
if (!event.ctrlKey && !event.metaKey) return this.notify(target, 'character', event);
}
}
}, {
key: 'preventContenteditableBug',
value: function preventContenteditableBug(target, event) {
if (!_featureDetection.contenteditableSpanBug) return;
if (event.ctrlKey || event.metaKey) return;
// This fixes a strange webkit bug that can be reproduced as follows:
//
// 1. A node used within a contenteditable has some style, e.g through the
// following CSS:
//
// strong {
// color: red
// }
//
// 2. A selection starts with the first character of a styled node and ends
// outside of that node, e.g: "big beautiful" is selected in the folloing
// html:
//
// <p contenteditable="true">
// Hello <strong>big</strong> beautiful world
// </p>
//
// 3. The user types a letter character to replace "big beautiful", e.g. "x"
//
// Result: Webkits adds <font> and <b> tags:
//
// <p contenteditable="true">
// Hello
// <font color="#ff0000">
// <b>f</b>
// </font>
// world
// </p>
//
// This bug ONLY happens, if the first character of the node is selected and
// the selection goes further than the node.
//
// Solution:
//
// Manually remove the element that would be removed anyway before inserting
// the new letter.
var rangyInstance = this.selectionWatcher.getFreshRange();
if (!rangyInstance.isSelection) return;
var nodeToRemove = Keyboard.getNodeToRemove(rangyInstance.range, target);
if (nodeToRemove) nodeToRemove.remove();
}
}], [{
key: 'getNodeToRemove',
value: function getNodeToRemove(selectionRange, target) {
// This function is only used by preventContenteditableBug. It is exposed on
// the Keyboard constructor for testing purpose only.
// Let's make sure we are in the edge-case, in which the bug happens.
// The selection does not start at the beginning of a node. We have
// nothing to do.
if (selectionRange.startOffset !== 0) return;
var startNodeElement = selectionRange.startContainer;
// If the node is a textNode, we select its parent.
if (startNodeElement.nodeType === nodeType.textNode) startNodeElement = startNodeElement.parentNode;
// The target is the contenteditable element, which we do not want to replace
if (startNodeElement === target) return;
// We get a range that contains everything within the sartNodeElement to test
// if the selectionRange is within the startNode, we have nothing to do.
var startNodeRange = _rangy2.default.createRange();
startNodeRange.setStartBefore(startNodeElement.firstChild);
startNodeRange.setEndAfter(startNodeElement.lastChild);
if (startNodeRange.containsRange(selectionRange)) return;
// If the selectionRange.startContainer was a textNode, we have to make sure
// that its parent's content starts with this node. Content is either a
// text node or an element. This is done to avoid false positives like the
// following one:
// <strong>foo<em>bar</em>|baz</strong>quux|
if (selectionRange.startContainer.nodeType === nodeType.textNode) {
var contentNodeTypes = [nodeType.textNode, nodeType.elementNode];
var firstContentNode = startNodeElement.firstChild;
do {
if (contentNodeTypes.indexOf(firstContentNode.nodeType) !== -1) break;
} while (firstContentNode = firstContentNode.nextSibling);
if (firstContentNode !== selectionRange.startContainer) return;
}
// Now we know, that we have to return at lease the startNodeElement for
// removal. But it could be, that we also need to remove its parent, e.g.
// we need to remove <strong> in the following example:
// <strong><em>|foo</em>bar</strong>baz|
var rangeStatingBeforeCurrentElement = selectionRange.cloneRange();
rangeStatingBeforeCurrentElement.setStartBefore(startNodeElement);
return Keyboard.getNodeToRemove(rangeStatingBeforeCurrentElement, target) || startNodeElement;
}
}]);
return Keyboard;
}();
exports.default = Keyboard;
Keyboard.key = Keyboard.prototype.key = {
left: 37,
up: 38,
right: 39,
down: 40,
tab: 9,
esc: 27,
backspace: 8,
delete: 46,
enter: 13,
shift: 16,
ctrl: 17,
alt: 18
};