UNPKG

upfront-editable

Version:
382 lines (337 loc) 14.4 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 _jquery = _interopRequireDefault(require("jquery")); var _featureDetection = require("./feature-detection"); var clipboard = _interopRequireWildcard(require("./clipboard")); var _eventable = _interopRequireDefault(require("./eventable")); var _selectionWatcher = _interopRequireDefault(require("./selection-watcher")); var _config = _interopRequireDefault(require("./config")); var _keyboard = _interopRequireDefault(require("./keyboard")); 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; } // This will be set to true once we detect the input event is working. // Input event description on MDN: // https://developer.mozilla.org/en-US/docs/Web/Reference/Events/input var isInputEventSupported = false; /** * The Dispatcher module is responsible for dealing with events and their handlers. * * @module core * @submodule dispatcher */ var Dispatcher = /*#__PURE__*/function () { function Dispatcher(editable) { (0, _classCallCheck2["default"])(this, Dispatcher); var win = editable.win; (0, _eventable["default"])(this, editable); this.supportsInputEvent = false; this.$document = (0, _jquery["default"])(win.document); this.config = editable.config; this.editable = editable; this.editableSelector = editable.editableSelector; this.selectionWatcher = new _selectionWatcher["default"](this, win); this.keyboard = new _keyboard["default"](this.selectionWatcher); this.setup(); } /** * Sets up all events that Editable.JS is catching. * * @method setup */ (0, _createClass2["default"])(Dispatcher, [{ key: "setup", value: function setup() { // setup all events listeners and keyboard handlers this.setupKeyboardEvents(); this.setupEventListeners(); } }, { key: "unload", value: function unload() { this.off(); this.$document.off('.editable'); } }, { key: "suspend", value: function suspend() { if (this.suspended) return; this.suspended = true; this.$document.off('.editable'); } }, { key: "continue", value: function _continue() { if (!this.suspended) return; this.suspended = false; this.setupEventListeners(); } }, { key: "setupEventListeners", value: function setupEventListeners() { this.setupElementListeners(); this.setupKeydownListener(); if (_featureDetection.selectionchange) { this.setupSelectionChangeListeners(); } else { this.setupSelectionChangeFallbackListeners(); } } /** * Sets up events that are triggered on modifying an element. * * @method setupElementListeners */ }, { key: "setupElementListeners", value: function setupElementListeners() { var self = this; var selector = this.editableSelector; this.$document.on('focus.editable', selector, function (event) { if (this.getAttribute(_config["default"].pastingAttribute)) return; self.selectionWatcher.syncSelection(); self.notify('focus', this); }).on('blur.editable', selector, function (event) { if (this.getAttribute(_config["default"].pastingAttribute)) return; self.notify('blur', this); }).on('copy.editable', selector, function (event) { var selection = self.selectionWatcher.getFreshSelection(); if (selection.isSelection) { self.notify('clipboard', this, 'copy', selection); } }).on('cut.editable', selector, function (event) { var selection = self.selectionWatcher.getFreshSelection(); if (selection.isSelection) { self.notify('clipboard', this, 'cut', selection); self.triggerChangeEvent(this); } }).on('paste.editable', selector, function (event) { var element = this; function afterPaste(blocks, cursor) { if (blocks.length) { self.notify('paste', element, blocks, cursor); // The input event does not fire when we process the content manually // and insert it via script self.notify('change', element); } else { cursor.setVisibleSelection(); } } var cursor = self.selectionWatcher.getFreshSelection(); clipboard.paste(this, cursor, afterPaste); }).on('input.editable', selector, function (event) { if (isInputEventSupported) { self.notify('change', this); } else { // Most likely the event was already handled manually by // triggerChangeEvent so the first time we just switch the // isInputEventSupported flag without notifying the change event. isInputEventSupported = true; } }).on('formatEditable.editable', selector, function (event) { self.notify('change', this); }); } /** * Trigger a change event * * This should be done in these cases: * - typing a letter * - delete (backspace and delete keys) * - cut * - paste * - copy and paste (not easily possible manually as far as I know) * * Preferably this is done using the input event. But the input event is not * supported on all browsers for contenteditable elements. * To make things worse it is not detectable either. So instead of detecting * we set 'isInputEventSupported' when the input event fires the first time. */ }, { key: "triggerChangeEvent", value: function triggerChangeEvent(target) { if (isInputEventSupported) return; this.notify('change', target); } }, { key: "dispatchSwitchEvent", value: function dispatchSwitchEvent(event, element, direction) { if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return; var cursor = this.selectionWatcher.getFreshSelection(); if (!cursor || cursor.isSelection) return; // store position if (!this.switchContext) { this.switchContext = { positionX: cursor.getBoundingClientRect().left, events: ['cursor'] }; } else { this.switchContext.events = ['cursor']; } if (direction === 'up' && cursor.isAtFirstLine()) { event.preventDefault(); event.stopPropagation(); this.switchContext.events = ['switch', 'blur', 'focus', 'cursor']; this.notify('switch', element, direction, cursor); } if (direction === 'down' && cursor.isAtLastLine()) { event.preventDefault(); event.stopPropagation(); this.switchContext.events = ['switch', 'blur', 'focus', 'cursor']; this.notify('switch', element, direction, cursor); } } /** * Sets up listener for keydown event which forwards events to * the Keyboard instance. * * @method setupKeydownListener */ }, { key: "setupKeydownListener", value: function setupKeydownListener() { var self = this; this.$document.on('keydown.editable', this.editableSelector, function (event) { var notifyCharacterEvent = !isInputEventSupported; self.keyboard.dispatchKeyEvent(event, this, notifyCharacterEvent); }); } /** * Sets up handlers for the keyboard events. * Keyboard definitions are in {{#crossLink "Keyboard"}}{{/crossLink}}. * * @method setupKeyboardEvents */ }, { key: "setupKeyboardEvents", value: function setupKeyboardEvents() { var self = this; this.keyboard.on('up', function (event) { self.dispatchSwitchEvent(event, this, 'up'); }).on('down', function (event) { self.dispatchSwitchEvent(event, this, 'down'); }).on('backspace', function (event) { var range = self.selectionWatcher.getFreshRange(); if (!range.isCursor) return self.triggerChangeEvent(this); var cursor = range.getCursor(); if (!cursor.isAtBeginning()) return self.triggerChangeEvent(this); event.preventDefault(); event.stopPropagation(); self.notify('merge', this, 'before', cursor); }).on('delete', function (event) { var range = self.selectionWatcher.getFreshRange(); if (!range.isCursor) return self.triggerChangeEvent(this); var cursor = range.getCursor(); if (!cursor.isAtTextEnd()) return self.triggerChangeEvent(this); event.preventDefault(); event.stopPropagation(); self.notify('merge', this, 'after', cursor); }).on('enter', function (event) { event.preventDefault(); event.stopPropagation(); var range = self.selectionWatcher.getFreshRange(); var cursor = range.forceCursor(); if (cursor.isAtTextEnd()) { self.notify('insert', this, 'after', cursor); } else if (cursor.isAtBeginning()) { self.notify('insert', this, 'before', cursor); } else { self.notify('split', this, cursor.before(), cursor.after(), cursor); } }).on('shiftEnter', function (event) { event.preventDefault(); event.stopPropagation(); var cursor = self.selectionWatcher.forceCursor(); self.notify('newline', this, cursor); }).on('bold', function (event) { event.preventDefault(); event.stopPropagation(); var selection = self.selectionWatcher.getFreshSelection(); if (selection.isSelection) { self.notify('toggleBold', selection); } }).on('italic', function (event) { event.preventDefault(); event.stopPropagation(); var selection = self.selectionWatcher.getFreshSelection(); if (selection.isSelection) { self.notify('toggleEmphasis', selection); } }).on('character', function (event) { self.notify('change', this); }); } /** * Sets up events that are triggered on a selection change. * * @method setupSelectionChangeListeners * @param {HTMLElement} $document: The document element. * @param {Function} notifier: The callback to be triggered when the event is caught. */ }, { key: "setupSelectionChangeListeners", value: function setupSelectionChangeListeners() { var _this = this; var selectionDirty = false; var suppressSelectionChanges = false; var $document = this.$document; var selectionWatcher = this.selectionWatcher; // fires on mousemove (thats probably a bit too much) // catches changes like 'select all' from context menu $document.on('selectionchange.editable', function (event) { if (suppressSelectionChanges) { selectionDirty = true; } else { selectionWatcher.selectionChanged(); } }); // listen for selection changes by mouse so we can // suppress the selectionchange event and only fire the // change event on mouseup $document.on('mousedown.editable', this.editableSelector, function (event) { if (_this.config.mouseMoveSelectionChanges === false) { suppressSelectionChanges = true; // Without this timeout the previous selection is active // until the mouseup event (no. not good). setTimeout(_jquery["default"].proxy(selectionWatcher, 'selectionChanged'), 0); } $document.on('mouseup.editableSelection', function (evt) { $document.off('.editableSelection'); suppressSelectionChanges = false; if (selectionDirty) { selectionDirty = false; selectionWatcher.selectionChanged(); } }); }); } /** * Fallback solution to support selection change events on browsers that don't * support selectionChange. * * @method setupSelectionChangeFallbackListeners */ }, { key: "setupSelectionChangeFallbackListeners", value: function setupSelectionChangeFallbackListeners() { var $document = this.$document; var selectionWatcher = this.selectionWatcher; // listen for selection changes by mouse $document.on('mouseup.editable', function (event) { // In Opera when clicking outside of a block // it does not update the selection as it should // without the timeout setTimeout(_jquery["default"].proxy(selectionWatcher, 'selectionChanged'), 0); }); // listen for selection changes by keys $document.on('keyup.editable', this.editableSelector, function (event) { // when pressing Command + Shift + Left for example the keyup is only triggered // after at least two keys are released. Strange. The culprit seems to be the // Command key. Do we need a workaround? selectionWatcher.selectionChanged(); }); } }]); return Dispatcher; }(); exports["default"] = Dispatcher; module.exports = exports.default;