upfront-editable
Version:
Friendly contenteditable API
386 lines (323 loc) • 12.7 kB
JavaScript
'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;