UNPKG

upfront-editable

Version:
602 lines (494 loc) 18.3 kB
'use strict'; 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 _rangy = require('rangy'); var _rangy2 = _interopRequireDefault(_rangy); var _config = require('./config'); var config = _interopRequireWildcard(_config); var _error = require('./util/error'); var _error2 = _interopRequireDefault(_error); var _parser = require('./parser'); var parser = _interopRequireWildcard(_parser); var _block = require('./block'); var block = _interopRequireWildcard(_block); var _content = require('./content'); var content = _interopRequireWildcard(_content); var _clipboard = require('./clipboard'); var clipboard = _interopRequireWildcard(_clipboard); var _dispatcher = require('./dispatcher'); var _dispatcher2 = _interopRequireDefault(_dispatcher); var _cursor = require('./cursor'); var _cursor2 = _interopRequireDefault(_cursor); var _highlightSupport = require('./highlight-support'); var _highlightSupport2 = _interopRequireDefault(_highlightSupport); var _highlighting = require('./highlighting'); var _highlighting2 = _interopRequireDefault(_highlighting); var _createDefaultEvents = require('./create-default-events'); var _createDefaultEvents2 = _interopRequireDefault(_createDefaultEvents); var _bowser = require('bowser'); var _bowser2 = _interopRequireDefault(_bowser); 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 }; } /** * The Core module provides the Editable class that defines the Editable.JS * API and is the main entry point for Editable.JS. * It also provides the cursor module for cross-browser cursors, and the dom * submodule. * * @module core */ /** * Constructor for the Editable.JS API that is externally visible. * * @param {Object} configuration for this editable instance. * window: The window where to attach the editable events. * defaultBehavior: {Boolean} Load default-behavior.js. * mouseMoveSelectionChanges: {Boolean} Whether to get cursor and selection events on mousemove. * browserSpellcheck: {Boolean} Set the spellcheck attribute on editable elements * * @class Editable */ var Editable = module.exports = function () { function Editable(instanceConfig) { (0, _classCallCheck3.default)(this, Editable); var defaultInstanceConfig = { window: window, defaultBehavior: true, mouseMoveSelectionChanges: false, browserSpellcheck: true }; this.config = _jquery2.default.extend(defaultInstanceConfig, instanceConfig); this.win = this.config.window; this.editableSelector = '.' + config.editableClass; if (!_rangy2.default.initialized) { _rangy2.default.init(); } this.dispatcher = new _dispatcher2.default(this); if (this.config.defaultBehavior === true) { this.dispatcher.on((0, _createDefaultEvents2.default)(this)); } } /** * @returns the default Editable configs from config.js */ (0, _createClass3.default)(Editable, [{ key: 'add', /** * Adds the Editable.JS API to the given target elements. * Opposite of {{#crossLink "Editable/remove"}}{{/crossLink}}. * Calls dispatcher.setup to setup all event listeners. * * @method add * @param {HTMLElement|Array(HTMLElement)|String} target A HTMLElement, an * array of HTMLElement or a query selector representing the target where * the API should be added on. * @chainable */ value: function add(target) { this.enable((0, _jquery2.default)(target)); // todo: check css whitespace settings return this; } /** * Removes the Editable.JS API from the given target elements. * Opposite of {{#crossLink "Editable/add"}}{{/crossLink}}. * * @method remove * @param {HTMLElement|Array(HTMLElement)|String} target A HTMLElement, an * array of HTMLElement or a query selector representing the target where * the API should be removed from. * @chainable */ }, { key: 'remove', value: function remove(target) { var $target = (0, _jquery2.default)(target); this.disable($target); $target.removeClass(config.editableDisabledClass); return this; } /** * Removes the Editable.JS API from the given target elements. * The target elements are marked as disabled. * * @method disable * @param { jQuery element | undefined } target editable root element(s) * If no param is specified all editables are disabled. * @chainable */ }, { key: 'disable', value: function disable($elem) { var body = this.win.document.body; $elem = $elem || (0, _jquery2.default)('.' + config.editableClass, body); $elem.each(function (i, el) { return block.disable(el); }); return this; } /** * Adds the Editable.JS API to the given target elements. * * @method enable * @param { jQuery element | undefined } target editable root element(s) * If no param is specified all editables marked as disabled are enabled. * @chainable */ }, { key: 'enable', value: function enable($elem, normalize) { var _this = this; var body = this.win.document.body; $elem = $elem || (0, _jquery2.default)('.' + config.editableDisabledClass, body); var shouldSpellcheck = this.config.browserSpellcheck; $elem.each(function (i, el) { block.init(el, { normalize: normalize, shouldSpellcheck: shouldSpellcheck }); _this.dispatcher.notify('init', el); }); return this; } /** * Temporarily disable an editable. * Can be used to prevent text selction while dragging an element * for example. * * @method suspend * @param jQuery object */ }, { key: 'suspend', value: function suspend($elem) { var body = this.win.document.body; $elem = $elem || (0, _jquery2.default)('.' + config.editableClass, body); $elem.removeAttr('contenteditable'); this.dispatcher.suspend(); return this; } /** * Reverse the effects of suspend() * * @method continue * @param jQuery object */ }, { key: 'continue', value: function _continue($elem) { var body = this.win.document.body; $elem = $elem || (0, _jquery2.default)('.' + config.editableClass, body); $elem.attr('contenteditable', true); this.dispatcher.continue(); return this; } /** * Set the cursor inside of an editable block. * * @method createCursor * @param position 'beginning', 'end', 'before', 'after' */ }, { key: 'createCursor', value: function createCursor(element) { var position = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'beginning'; var $host = (0, _jquery2.default)(element).closest(this.editableSelector); if (!$host.length) return undefined; var range = _rangy2.default.createRange(); if (position === 'beginning' || position === 'end') { range.selectNodeContents(element); range.collapse(position === 'beginning'); } else if (element !== $host[0]) { if (position === 'before') { range.setStartBefore(element); range.setEndBefore(element); } else if (position === 'after') { range.setStartAfter(element); range.setEndAfter(element); } } else { (0, _error2.default)('EditableJS: cannot create cursor outside of an editable block.'); } return new _cursor2.default($host[0], range); } }, { key: 'createCursorAtBeginning', value: function createCursorAtBeginning(element) { return this.createCursor(element, 'beginning'); } }, { key: 'createCursorAtEnd', value: function createCursorAtEnd(element) { return this.createCursor(element, 'end'); } }, { key: 'createCursorBefore', value: function createCursorBefore(element) { return this.createCursor(element, 'before'); } }, { key: 'createCursorAfter', value: function createCursorAfter(element) { return this.createCursor(element, 'after'); } /** * Extract the content from an editable host or document fragment. * This method will remove all internal elements and ui-elements. * * @param {DOM node or Document Fragment} The innerHTML of this element or fragment will be extracted. * @returns {String} The cleaned innerHTML. */ }, { key: 'getContent', value: function getContent(element) { return content.extractContent(element); } /** * @param {String | DocumentFragment} content to append. * @returns {Cursor} A new Cursor object just before the inserted content. */ }, { key: 'appendTo', value: function appendTo(inputElement, contentToAppend) { var element = content.adoptElement(inputElement, this.win.document); var cursor = this.createCursor(element, 'end'); // todo: create content in the right window cursor.insertAfter(typeof contentToAppend === 'string' ? content.createFragmentFromString(contentToAppend) : contentToAppend); return cursor; } /** * @param {String | DocumentFragment} content to prepend * @returns {Cursor} A new Cursor object just after the inserted content. */ }, { key: 'prependTo', value: function prependTo(inputElement, contentToPrepend) { var element = content.adoptElement(inputElement, this.win.document); var cursor = this.createCursor(element, 'beginning'); // todo: create content in the right window cursor.insertBefore(typeof contentToPrepend === 'string' ? content.createFragmentFromString(contentToPrepend) : contentToPrepend); return cursor; } /** * Get the current selection. * Only returns something if the selection is within an editable element. * If you pass an editable host as param it only returns something if the selection is inside this * very editable element. * * @param {DOMNode} Optional. An editable host where the selection needs to be contained. * @returns A Cursor or Selection object or undefined. */ }, { key: 'getSelection', value: function getSelection(editableHost) { var selection = this.dispatcher.selectionWatcher.getFreshSelection(); if (!(editableHost && selection)) return selection; var range = selection.range; // Check if the selection is inside the editableHost // The try...catch is required if the editableHost was removed from the DOM. try { if (range.compareNode(editableHost) !== range.NODE_BEFORE_AND_AFTER) { selection = undefined; } } catch (e) { selection = undefined; } return selection; } /** * Enable spellchecking * * @chainable */ }, { key: 'setupHighlighting', value: function setupHighlighting(hightlightingConfig) { this.highlighting = new _highlighting2.default(this, hightlightingConfig); return this; } // For backwards compatibility }, { key: 'setupSpellcheck', value: function setupSpellcheck(config) { var _this2 = this; var marker = void 0; if (config.markerNode) { marker = config.markerNode.outerHTML; } this.setupHighlighting({ throttle: config.throttle, spellcheck: { marker: marker, spellcheckService: config.spellcheckService } }); this.spellcheck = { checkSpelling: function checkSpelling(elem) { _this2.highlighting.highlight(elem); } }; } /** * Highlight text within an editable. * * By default highlights all occurences of `text`. * Pass it a `textRange` object to highlight a * specific text portion. * * The markup used for the highlighting will be removed * from the final content. * * * @param {Object} options * @param {DOMNode} options.editableHost * @param {String} options.text * @param {String} options.highlightId Added to the highlight markups in the property `data-word-id` * @param {Object} [options.textRange] An optional range which gets used to set the markers. * @param {Number} options.textRange.start * @param {Number} options.textRange.end * @param {Boolean} options.raiseEvents do throw change events * @return {Number} The text-based start offset of the newly applied highlight or `-1` if the range was considered invalid. */ }, { key: 'highlight', value: function highlight(_ref) { var editableHost = _ref.editableHost, text = _ref.text, highlightId = _ref.highlightId, textRange = _ref.textRange, raiseEvents = _ref.raiseEvents; if (!textRange) { return _highlightSupport2.default.highlightText(editableHost, text, highlightId); } if (typeof textRange.start !== 'number' || typeof textRange.end !== 'number') { (0, _error2.default)('Error in Editable.highlight: You passed a textRange object with invalid keys. Expected shape: { start: Number, end: Number }'); return -1; } if (textRange.start === textRange.end) { (0, _error2.default)('Error in Editable.highlight: You passed a textRange object with equal start and end offsets, which is considered a cursor and therefore unfit to create a highlight.'); return -1; } return _highlightSupport2.default.highlightRange(editableHost, highlightId, textRange.start, textRange.end, raiseEvents ? this.dispatcher : undefined); } /** * Extracts positions of all DOMNodes that match `[data-word-id]`. * * Returns an object where the keys represent a highlight id and the value * a text range object of shape: * ``` * { start: number, end: number, text: string} * ``` * * @param {Object} options * @param {DOMNode} options.editableHos * @return {Object} ranges */ }, { key: 'getHighlightPositions', value: function getHighlightPositions(_ref2) { var editableHost = _ref2.editableHost; return _highlightSupport2.default.extractHighlightedRanges(editableHost); } }, { key: 'removeHighlight', value: function removeHighlight(_ref3) { var editableHost = _ref3.editableHost, highlightId = _ref3.highlightId, raiseEvents = _ref3.raiseEvents; _highlightSupport2.default.removeHighlight(editableHost, highlightId, raiseEvents ? this.dispatcher : undefined); } }, { key: 'decorateHighlight', value: function decorateHighlight(_ref4) { var editableHost = _ref4.editableHost, highlightId = _ref4.highlightId, addCssClass = _ref4.addCssClass, removeCssClass = _ref4.removeCssClass; _highlightSupport2.default.updateHighlight(editableHost, highlightId, addCssClass, removeCssClass); } /** * Subscribe a callback function to a custom event fired by the API. * * @param {String} event The name of the event. * @param {Function} handler The callback to execute in response to the * event. * * @chainable */ }, { key: 'on', value: function on(event, handler) { // TODO throw error if event is not one of EVENTS // TODO throw error if handler is not a function this.dispatcher.on(event, handler); return this; } /** * Unsubscribe a callback function from a custom event fired by the API. * Opposite of {{#crossLink "Editable/on"}}{{/crossLink}}. * * @param {String} event The name of the event. * @param {Function} handler The callback to remove from the * event or the special value false to remove all callbacks. * * @chainable */ }, { key: 'off', value: function off() { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } this.dispatcher.off.apply(this.dispatcher, args); return this; } /** * Unsubscribe all callbacks and event listeners. * * @chainable */ }, { key: 'unload', value: function unload() { this.dispatcher.unload(); return this; } }], [{ key: 'getGlobalConfig', value: function getGlobalConfig() { return config; } /** * Set configuration options that affect all editable * instances. * * @param {Object} global configuration options (defaults are defined in config.js) * log: {Boolean} * logErrors: {Boolean} * editableClass: {String} e.g. 'js-editable' * editableDisabledClass: {String} e.g. 'js-editable-disabled' * pastingAttribute: {String} default: e.g. 'data-editable-is-pasting' * boldTag: e.g. '<strong>' * italicTag: e.g. '<em>' */ }, { key: 'globalConfig', value: function globalConfig(_globalConfig) { _jquery2.default.extend(config, _globalConfig); clipboard.updateConfig(config); } }]); return Editable; }(); // Expose modules and editable Editable.parser = parser; Editable.content = content; Editable.browser = _bowser2.default // Set up callback functions for several events. ;['focus', 'blur', 'flow', 'selection', 'cursor', 'newline', 'insert', 'split', 'merge', 'empty', 'change', 'switch', 'move', 'clipboard', 'paste'].forEach(function (name) { // Generate a callback function to subscribe to an event. Editable.prototype[name] = function (handler) { return this.on(name, handler); }; });