UNPKG

upfront-editable

Version:
386 lines (323 loc) 12.7 kB
'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 _featureDetection = require('./feature-detection'); var _clipboard = require('./clipboard'); var clipboard = _interopRequireWildcard(_clipboard); var _eventable = require('./eventable'); var _eventable2 = _interopRequireDefault(_eventable); var _selectionWatcher = require('./selection-watcher'); var _selectionWatcher2 = _interopRequireDefault(_selectionWatcher); var _config = require('./config'); var config = _interopRequireWildcard(_config); var _keyboard = require('./keyboard'); var _keyboard2 = _interopRequireDefault(_keyboard); 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 }; } // 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 = function () { function Dispatcher(editable) { (0, _classCallCheck3.default)(this, Dispatcher); var win = editable.win; (0, _eventable2.default)(this, editable); this.supportsInputEvent = false; this.$document = (0, _jquery2.default)(win.document); this.config = editable.config; this.editable = editable; this.editableSelector = editable.editableSelector; this.selectionWatcher = new _selectionWatcher2.default(this, win); this.keyboard = new _keyboard2.default(this.selectionWatcher); this.setup(); } /** * Sets up all events that Editable.JS is catching. * * @method setup */ (0, _createClass3.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.pastingAttribute)) return; self.selectionWatcher.syncSelection(); self.notify('focus', this); }).on('blur.editable', selector, function (event) { if (this.getAttribute(config.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 notifiying 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) * * Preferrably 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.getSelection(); if (!cursor || cursor.isSelection) return; if (direction === 'up' && cursor.isAtFirstLine()) { event.preventDefault(); event.stopPropagation(); this.notify('switch', element, direction, cursor); } if (direction === 'down' && cursor.isAtLastLine()) { event.preventDefault(); event.stopPropagation(); 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('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(_jquery2.default.proxy(selectionWatcher, 'selectionChanged'), 0); } $document.on('mouseup.editableSelection', function (event) { $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(_jquery2.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;