UNPKG

upfront-editable

Version:
220 lines (179 loc) 8.96 kB
"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;