upfront-editable
Version:
Friendly contenteditable API
220 lines (179 loc) • 8.96 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 _featureDetection = require("./feature-detection");
var nodeType = _interopRequireWildcard(require("./node-type"));
var _eventable = _interopRequireDefault(require("./eventable"));
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; }
/**
* The Keyboard module defines an event API for key events.
*/
var Keyboard = /*#__PURE__*/function () {
function Keyboard(selectionWatcher) {
(0, _classCallCheck2["default"])(this, Keyboard);
(0, _eventable["default"])(this);
this.selectionWatcher = selectionWatcher;
}
(0, _createClass2["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:
// Added here to avoid using fall-through in the switch
// when b or i are pressed without ctrlKey or metaKey
if (event.keyCode === this.key.b && (event.ctrlKey || event.metaKey)) {
return this.notify(target, 'bold', event);
}
if (event.keyCode === this.key.i && (event.ctrlKey || event.metaKey)) {
return this.notify(target, 'italic', event);
}
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 following
// 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 = _rangy["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,
b: 66,
i: 73
};
module.exports = exports.default;