upfront-editable
Version:
Friendly contenteditable API
382 lines (337 loc) • 14.4 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 _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;