123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- import {
- assign,
- bind,
- pick
- } from 'min-dash';
- import {
- domify,
- query as domQuery,
- event as domEvent,
- remove as domRemove
- } from 'min-dom';
- var min = Math.min,
- max = Math.max;
- function preventDefault(e) {
- e.preventDefault();
- }
- function stopPropagation(e) {
- e.stopPropagation();
- }
- function isTextNode(node) {
- return node.nodeType === Node.TEXT_NODE;
- }
- function toArray(nodeList) {
- return [].slice.call(nodeList);
- }
- /**
- * Initializes a container for a content editable div.
- *
- * Structure:
- *
- * container
- * parent
- * content
- * resize-handle
- *
- * @param {object} options
- * @param {DOMElement} options.container The DOM element to append the contentContainer to
- * @param {Function} options.keyHandler Handler for key events
- * @param {Function} options.resizeHandler Handler for resize events
- */
- export default function TextBox(options) {
- this.container = options.container;
- this.parent = domify(
- '<div class="djs-direct-editing-parent">' +
- '<div class="djs-direct-editing-content" contenteditable="true"></div>' +
- '</div>'
- );
- this.content = domQuery('[contenteditable]', this.parent);
- this.keyHandler = options.keyHandler || function() {};
- this.resizeHandler = options.resizeHandler || function() {};
- this.autoResize = bind(this.autoResize, this);
- this.handlePaste = bind(this.handlePaste, this);
- }
- /**
- * Create a text box with the given position, size, style and text content
- *
- * @param {Object} bounds
- * @param {Number} bounds.x absolute x position
- * @param {Number} bounds.y absolute y position
- * @param {Number} [bounds.width] fixed width value
- * @param {Number} [bounds.height] fixed height value
- * @param {Number} [bounds.maxWidth] maximum width value
- * @param {Number} [bounds.maxHeight] maximum height value
- * @param {Number} [bounds.minWidth] minimum width value
- * @param {Number} [bounds.minHeight] minimum height value
- * @param {Object} [style]
- * @param {String} value text content
- *
- * @return {DOMElement} The created content DOM element
- */
- TextBox.prototype.create = function(bounds, style, value, options) {
- var self = this;
- var parent = this.parent,
- content = this.content,
- container = this.container;
- options = this.options = options || {};
- style = this.style = style || {};
- var parentStyle = pick(style, [
- 'width',
- 'height',
- 'maxWidth',
- 'maxHeight',
- 'minWidth',
- 'minHeight',
- 'left',
- 'top',
- 'backgroundColor',
- 'position',
- 'overflow',
- 'border',
- 'wordWrap',
- 'textAlign',
- 'outline',
- 'transform'
- ]);
- assign(parent.style, {
- width: bounds.width + 'px',
- height: bounds.height + 'px',
- maxWidth: bounds.maxWidth + 'px',
- maxHeight: bounds.maxHeight + 'px',
- minWidth: bounds.minWidth + 'px',
- minHeight: bounds.minHeight + 'px',
- left: bounds.x + 'px',
- top: bounds.y + 'px',
- backgroundColor: '#ffffff',
- position: 'absolute',
- overflow: 'visible',
- border: '1px solid #ccc',
- boxSizing: 'border-box',
- wordWrap: 'normal',
- textAlign: 'center',
- outline: 'none'
- }, parentStyle);
- var contentStyle = pick(style, [
- 'fontFamily',
- 'fontSize',
- 'fontWeight',
- 'lineHeight',
- 'padding',
- 'paddingTop',
- 'paddingRight',
- 'paddingBottom',
- 'paddingLeft'
- ]);
- assign(content.style, {
- boxSizing: 'border-box',
- width: '100%',
- outline: 'none',
- wordWrap: 'break-word'
- }, contentStyle);
- if (options.centerVertically) {
- assign(content.style, {
- position: 'absolute',
- top: '50%',
- transform: 'translate(0, -50%)'
- }, contentStyle);
- }
- content.innerText = value;
- domEvent.bind(content, 'keydown', this.keyHandler);
- domEvent.bind(content, 'mousedown', stopPropagation);
- domEvent.bind(content, 'paste', self.handlePaste);
- if (options.autoResize) {
- domEvent.bind(content, 'input', this.autoResize);
- }
- if (options.resizable) {
- this.resizable(style);
- }
- container.appendChild(parent);
- // set selection to end of text
- this.setSelection(content.lastChild, content.lastChild && content.lastChild.length);
- return parent;
- };
- /**
- * Intercept paste events to remove formatting from pasted text.
- */
- TextBox.prototype.handlePaste = function(e) {
- var options = this.options,
- style = this.style;
- e.preventDefault();
- var text;
- if (e.clipboardData) {
- // Chrome, Firefox, Safari
- text = e.clipboardData.getData('text/plain');
- } else {
- // Internet Explorer
- text = window.clipboardData.getData('Text');
- }
- this.insertText(text);
- if (options.autoResize) {
- var hasResized = this.autoResize(style);
- if (hasResized) {
- this.resizeHandler(hasResized);
- }
- }
- };
- TextBox.prototype.insertText = function(text) {
- text = normalizeEndOfLineSequences(text);
- // insertText command not supported by Internet Explorer
- var success = document.execCommand('insertText', false, text);
- if (success) {
- return;
- }
- this._insertTextIE(text);
- };
- TextBox.prototype._insertTextIE = function(text) {
- // Internet Explorer
- var range = this.getSelection(),
- startContainer = range.startContainer,
- endContainer = range.endContainer,
- startOffset = range.startOffset,
- endOffset = range.endOffset,
- commonAncestorContainer = range.commonAncestorContainer;
- var childNodesArray = toArray(commonAncestorContainer.childNodes);
- var container,
- offset;
- if (isTextNode(commonAncestorContainer)) {
- var containerTextContent = startContainer.textContent;
- startContainer.textContent =
- containerTextContent.substring(0, startOffset)
- + text
- + containerTextContent.substring(endOffset);
- container = startContainer;
- offset = startOffset + text.length;
- } else if (startContainer === this.content && endContainer === this.content) {
- var textNode = document.createTextNode(text);
- this.content.insertBefore(textNode, childNodesArray[startOffset]);
- container = textNode;
- offset = textNode.textContent.length;
- } else {
- var startContainerChildIndex = childNodesArray.indexOf(startContainer),
- endContainerChildIndex = childNodesArray.indexOf(endContainer);
- childNodesArray.forEach(function(childNode, index) {
- if (index === startContainerChildIndex) {
- childNode.textContent =
- startContainer.textContent.substring(0, startOffset) +
- text +
- endContainer.textContent.substring(endOffset);
- } else if (index > startContainerChildIndex && index <= endContainerChildIndex) {
- domRemove(childNode);
- }
- });
- container = startContainer;
- offset = startOffset + text.length;
- }
- if (container && offset !== undefined) {
- // is necessary in Internet Explorer
- setTimeout(function() {
- self.setSelection(container, offset);
- });
- }
- };
- /**
- * Automatically resize element vertically to fit its content.
- */
- TextBox.prototype.autoResize = function() {
- var parent = this.parent,
- content = this.content;
- var fontSize = parseInt(this.style.fontSize) || 12;
- if (content.scrollHeight > parent.offsetHeight ||
- content.scrollHeight < parent.offsetHeight - fontSize) {
- var bounds = parent.getBoundingClientRect();
- var height = content.scrollHeight;
- parent.style.height = height + 'px';
- this.resizeHandler({
- width: bounds.width,
- height: bounds.height,
- dx: 0,
- dy: height - bounds.height
- });
- }
- };
- /**
- * Make an element resizable by adding a resize handle.
- */
- TextBox.prototype.resizable = function() {
- var self = this;
- var parent = this.parent,
- resizeHandle = this.resizeHandle;
- var minWidth = parseInt(this.style.minWidth) || 0,
- minHeight = parseInt(this.style.minHeight) || 0,
- maxWidth = parseInt(this.style.maxWidth) || Infinity,
- maxHeight = parseInt(this.style.maxHeight) || Infinity;
- if (!resizeHandle) {
- resizeHandle = this.resizeHandle = domify(
- '<div class="djs-direct-editing-resize-handle"></div>'
- );
- var startX, startY, startWidth, startHeight;
- var onMouseDown = function(e) {
- preventDefault(e);
- stopPropagation(e);
- startX = e.clientX;
- startY = e.clientY;
- var bounds = parent.getBoundingClientRect();
- startWidth = bounds.width;
- startHeight = bounds.height;
- domEvent.bind(document, 'mousemove', onMouseMove);
- domEvent.bind(document, 'mouseup', onMouseUp);
- };
- var onMouseMove = function(e) {
- preventDefault(e);
- stopPropagation(e);
- var newWidth = min(max(startWidth + e.clientX - startX, minWidth), maxWidth);
- var newHeight = min(max(startHeight + e.clientY - startY, minHeight), maxHeight);
- parent.style.width = newWidth + 'px';
- parent.style.height = newHeight + 'px';
- self.resizeHandler({
- width: startWidth,
- height: startHeight,
- dx: e.clientX - startX,
- dy: e.clientY - startY
- });
- };
- var onMouseUp = function(e) {
- preventDefault(e);
- stopPropagation(e);
- domEvent.unbind(document,'mousemove', onMouseMove, false);
- domEvent.unbind(document, 'mouseup', onMouseUp, false);
- };
- domEvent.bind(resizeHandle, 'mousedown', onMouseDown);
- }
- assign(resizeHandle.style, {
- position: 'absolute',
- bottom: '0px',
- right: '0px',
- cursor: 'nwse-resize',
- width: '0',
- height: '0',
- borderTop: (parseInt(this.style.fontSize) / 4 || 3) + 'px solid transparent',
- borderRight: (parseInt(this.style.fontSize) / 4 || 3) + 'px solid #ccc',
- borderBottom: (parseInt(this.style.fontSize) / 4 || 3) + 'px solid #ccc',
- borderLeft: (parseInt(this.style.fontSize) / 4 || 3) + 'px solid transparent'
- });
- parent.appendChild(resizeHandle);
- };
- /**
- * Clear content and style of the textbox, unbind listeners and
- * reset CSS style.
- */
- TextBox.prototype.destroy = function() {
- var parent = this.parent,
- content = this.content,
- resizeHandle = this.resizeHandle;
- // clear content
- content.innerText = '';
- // clear styles
- parent.removeAttribute('style');
- content.removeAttribute('style');
- domEvent.unbind(content, 'keydown', this.keyHandler);
- domEvent.unbind(content, 'mousedown', stopPropagation);
- domEvent.unbind(content, 'input', this.autoResize);
- domEvent.unbind(content, 'paste', this.handlePaste);
- if (resizeHandle) {
- resizeHandle.removeAttribute('style');
- domRemove(resizeHandle);
- }
- domRemove(parent);
- };
- TextBox.prototype.getValue = function() {
- return this.content.innerText.trim();
- };
- TextBox.prototype.getSelection = function() {
- var selection = window.getSelection(),
- range = selection.getRangeAt(0);
- return range;
- };
- TextBox.prototype.setSelection = function(container, offset) {
- var range = document.createRange();
- if (container === null) {
- range.selectNodeContents(this.content);
- } else {
- range.setStart(container, offset);
- range.setEnd(container, offset);
- }
- var selection = window.getSelection();
- selection.removeAllRanges();
- selection.addRange(range);
- };
- // helpers //////////
- function normalizeEndOfLineSequences(string) {
- return string.replace(/\r\n|\r|\n/g, '\n');
- }
|