'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } // Thanks to https://github.com/jeff-collins/ment.io var TributeRange = function () { function TributeRange(tribute) { _classCallCheck(this, TributeRange); this.tribute = tribute; this.tribute.range = this; } _createClass(TributeRange, [{ key: 'getDocument', value: function getDocument() { var iframe = void 0; if (this.tribute.current.collection) { iframe = this.tribute.current.collection.iframe; } if (!iframe) { return document; } return iframe.contentWindow.document; } }, { key: 'positionMenuAtCaret', value: function positionMenuAtCaret(scrollTo) { var _this = this; var context = this.tribute.current, coordinates = void 0; var info = this.getTriggerInfo(false, false, true, this.tribute.allowSpaces); if (typeof info !== 'undefined') { if (!this.tribute.positionMenu) { this.tribute.menu.style.cssText = 'display: block;'; return; } if (!this.isContentEditable(context.element)) { coordinates = this.getTextAreaOrInputUnderlinePosition(this.getDocument().activeElement, info.mentionPosition); } else { coordinates = this.getContentEditableCaretPosition(info.mentionPosition); } this.tribute.menu.style.cssText = 'top: ' + coordinates.top + 'px;\n left: ' + coordinates.left + 'px;\n right: ' + coordinates.right + 'px;\n bottom: ' + coordinates.bottom + 'px;\n position: absolute;\n zIndex: 10000;\n display: block;'; if (coordinates.left === 'auto') { this.tribute.menu.style.left = 'auto'; } if (coordinates.top === 'auto') { this.tribute.menu.style.top = 'auto'; } if (scrollTo) this.scrollIntoView(); window.setTimeout(function () { var menuDimensions = { width: _this.tribute.menu.offsetWidth, height: _this.tribute.menu.offsetHeight }; var menuIsOffScreen = _this.isMenuOffScreen(coordinates, menuDimensions); if (menuIsOffScreen.horizontally || menuIsOffScreen.vertically) { _this.tribute.menu.style.cssText = 'display: none'; _this.positionMenuAtCaret(scrollTo); } }, 0); } else { this.tribute.menu.style.cssText = 'display: none'; } } }, { key: 'selectElement', value: function selectElement(targetElement, path, offset) { var range = void 0; var elem = targetElement; if (path) { for (var i = 0; i < path.length; i++) { elem = elem.childNodes[path[i]]; if (elem === undefined) { return; } while (elem.length < offset) { offset -= elem.length; elem = elem.nextSibling; } if (elem.childNodes.length === 0 && !elem.length) { elem = elem.previousSibling; } } } var sel = this.getWindowSelection(); range = this.getDocument().createRange(); range.setStart(elem, offset); range.setEnd(elem, offset); range.collapse(true); try { sel.removeAllRanges(); } catch (error) {} sel.addRange(range); targetElement.focus(); } // TODO: this may not be necessary anymore as we are using mouseup instead of click }, { key: 'resetSelection', value: function resetSelection(targetElement, path, offset) { if (!this.isContentEditable(targetElement)) { if (targetElement !== this.getDocument().activeElement) { targetElement.focus(); } } else { this.selectElement(targetElement, path, offset); } } }, { key: 'replaceTriggerText', value: function replaceTriggerText(text, requireLeadingSpace, hasTrailingSpace, originalEvent, item) { var context = this.tribute.current; // TODO: this may not be necessary anymore as we are using mouseup instead of click // this.resetSelection(context.element, context.selectedPath, context.selectedOffset) var info = this.getTriggerInfo(true, hasTrailingSpace, requireLeadingSpace, this.tribute.allowSpaces); // Create the event var replaceEvent = new CustomEvent('tribute-replaced', { detail: { item: item, event: originalEvent } }); if (info !== undefined) { if (!this.isContentEditable(context.element)) { var myField = this.getDocument().activeElement; var textSuffix = typeof this.tribute.replaceTextSuffix == 'string' ? this.tribute.replaceTextSuffix : ' '; text += textSuffix; var startPos = info.mentionPosition; var endPos = info.mentionPosition + info.mentionText.length + textSuffix.length; myField.value = myField.value.substring(0, startPos) + text + myField.value.substring(endPos, myField.value.length); myField.selectionStart = startPos + text.length; myField.selectionEnd = startPos + text.length; } else { // add a space to the end of the pasted text var _textSuffix = typeof this.tribute.replaceTextSuffix == 'string' ? this.tribute.replaceTextSuffix : '\xA0'; text += _textSuffix; this.pasteHtml(text, info.mentionPosition, info.mentionPosition + info.mentionText.length + 1); } context.element.dispatchEvent(replaceEvent); } } }, { key: 'pasteHtml', value: function pasteHtml(html, startPos, endPos) { var range = void 0, sel = void 0; sel = this.getWindowSelection(); range = this.getDocument().createRange(); range.setStart(sel.anchorNode, startPos); range.setEnd(sel.anchorNode, endPos); range.deleteContents(); var el = this.getDocument().createElement('div'); el.innerHTML = html; var frag = this.getDocument().createDocumentFragment(), node = void 0, lastNode = void 0; while (node = el.firstChild) { lastNode = frag.appendChild(node); } range.insertNode(frag); // Preserve the selection if (lastNode) { range = range.cloneRange(); range.setStartAfter(lastNode); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } } }, { key: 'getWindowSelection', value: function getWindowSelection() { if (this.tribute.collection.iframe) { return this.tribute.collection.iframe.contentWindow.getSelection(); } return window.getSelection(); } }, { key: 'getNodePositionInParent', value: function getNodePositionInParent(element) { if (element.parentNode === null) { return 0; } for (var i = 0; i < element.parentNode.childNodes.length; i++) { var node = element.parentNode.childNodes[i]; if (node === element) { return i; } } } }, { key: 'getContentEditableSelectedPath', value: function getContentEditableSelectedPath(ctx) { var sel = this.getWindowSelection(); var selected = sel.anchorNode; var path = []; var offset = void 0; if (selected != null) { var i = void 0; var ce = selected.contentEditable; while (selected !== null && ce !== 'true') { i = this.getNodePositionInParent(selected); path.push(i); selected = selected.parentNode; if (selected !== null) { ce = selected.contentEditable; } } path.reverse(); // getRangeAt may not exist, need alternative offset = sel.getRangeAt(0).startOffset; return { selected: selected, path: path, offset: offset }; } } }, { key: 'getTextPrecedingCurrentSelection', value: function getTextPrecedingCurrentSelection() { var context = this.tribute.current, text = ''; if (!this.isContentEditable(context.element)) { var textComponent = this.tribute.current.element; if (textComponent) { var startPos = textComponent.selectionStart; if (textComponent.value && startPos >= 0) { text = textComponent.value.substring(0, startPos); } } } else { var selectedElem = this.getWindowSelection().anchorNode; if (selectedElem != null) { var workingNodeContent = selectedElem.textContent; var selectStartOffset = this.getWindowSelection().getRangeAt(0).startOffset; if (workingNodeContent && selectStartOffset >= 0) { text = workingNodeContent.substring(0, selectStartOffset); } } } return text; } }, { key: 'getTriggerInfo', value: function getTriggerInfo(menuAlreadyActive, hasTrailingSpace, requireLeadingSpace, allowSpaces) { var _this2 = this; var ctx = this.tribute.current; var selected = void 0, path = void 0, offset = void 0; if (!this.isContentEditable(ctx.element)) { selected = this.getDocument().activeElement; } else { var selectionInfo = this.getContentEditableSelectedPath(ctx); if (selectionInfo) { selected = selectionInfo.selected; path = selectionInfo.path; offset = selectionInfo.offset; } } var effectiveRange = this.getTextPrecedingCurrentSelection(); if (effectiveRange !== undefined && effectiveRange !== null) { var mostRecentTriggerCharPos = -1; var triggerChar = void 0; this.tribute.collection.forEach(function (config) { var c = config.trigger; var idx = config.requireLeadingSpace ? _this2.lastIndexWithLeadingSpace(effectiveRange, c) : effectiveRange.lastIndexOf(c); if (idx > mostRecentTriggerCharPos) { mostRecentTriggerCharPos = idx; triggerChar = c; requireLeadingSpace = config.requireLeadingSpace; } }); if (mostRecentTriggerCharPos >= 0 && (mostRecentTriggerCharPos === 0 || !requireLeadingSpace || /[\xA0\s]/g.test(effectiveRange.substring(mostRecentTriggerCharPos - 1, mostRecentTriggerCharPos)))) { var currentTriggerSnippet = effectiveRange.substring(mostRecentTriggerCharPos + 1, effectiveRange.length); triggerChar = effectiveRange.substring(mostRecentTriggerCharPos, mostRecentTriggerCharPos + 1); var firstSnippetChar = currentTriggerSnippet.substring(0, 1); var leadingSpace = currentTriggerSnippet.length > 0 && (firstSnippetChar === ' ' || firstSnippetChar === '\xA0'); if (hasTrailingSpace) { currentTriggerSnippet = currentTriggerSnippet.trim(); } var regex = allowSpaces ? /[^\S ]/g : /[\xA0\s]/g; if (!leadingSpace && (menuAlreadyActive || !regex.test(currentTriggerSnippet))) { return { mentionPosition: mostRecentTriggerCharPos, mentionText: currentTriggerSnippet, mentionSelectedElement: selected, mentionSelectedPath: path, mentionSelectedOffset: offset, mentionTriggerChar: triggerChar }; } } } } }, { key: 'lastIndexWithLeadingSpace', value: function lastIndexWithLeadingSpace(str, char) { var reversedStr = str.split('').reverse().join(''); var index = -1; for (var cidx = 0, len = str.length; cidx < len; cidx++) { var firstChar = cidx === str.length - 1; var leadingSpace = /\s/.test(reversedStr[cidx + 1]); var match = char === reversedStr[cidx]; if (match && (firstChar || leadingSpace)) { index = str.length - 1 - cidx; break; } } return index; } }, { key: 'isContentEditable', value: function isContentEditable(element) { return element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA'; } }, { key: 'isMenuOffScreen', value: function isMenuOffScreen(coordinates, menuDimensions) { var contentWidth = menuDimensions.width + coordinates.left; var contentHeight = menuDimensions.height + coordinates.top; var windowWidth = window.innerWidth; var windowHeight = window.innerHeight; var doc = document.documentElement; var windowLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); var windowTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); return { horizontally: Math.ceil(contentWidth - windowLeft) >= windowWidth, vertically: Math.ceil(contentHeight - windowTop) >= windowHeight }; } }, { key: 'getMenuDimensions', value: function getMenuDimensions() { // Width of the menu depends of its contents and position // We must check what its width would be without any obstruction // This way, we can achieve good positioning for flipping the menu var dimensions = { width: null, height: null }; this.tribute.menu.style.cssText = 'top: 0px;\n left: 0px;\n position: fixed;\n zIndex: 10000;\n display: block;\n visibility; hidden;'; dimensions.width = this.tribute.menu.offsetWidth; dimensions.height = this.tribute.menu.offsetHeight; this.tribute.menu.style.cssText = 'display: none;'; return dimensions; } }, { key: 'getTextAreaOrInputUnderlinePosition', value: function getTextAreaOrInputUnderlinePosition(element, position, flipped) { var properties = ['direction', 'boxSizing', 'width', 'height', 'overflowX', 'overflowY', 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', 'fontSizeAdjust', 'lineHeight', 'fontFamily', 'textAlign', 'textTransform', 'textIndent', 'textDecoration', 'letterSpacing', 'wordSpacing']; var isFirefox = window.mozInnerScreenX !== null; var div = this.getDocument().createElement('div'); div.id = 'input-textarea-caret-position-mirror-div'; this.getDocument().body.appendChild(div); var style = div.style; var computed = window.getComputedStyle ? getComputedStyle(element) : element.currentStyle; style.whiteSpace = 'pre-wrap'; if (element.nodeName !== 'INPUT') { style.wordWrap = 'break-word'; } // position off-screen style.position = 'absolute'; style.visibility = 'hidden'; // transfer the element's properties to the div properties.forEach(function (prop) { style[prop] = computed[prop]; }); if (isFirefox) { style.width = parseInt(computed.width) - 2 + 'px'; if (element.scrollHeight > parseInt(computed.height)) style.overflowY = 'scroll'; } else { style.overflow = 'hidden'; } div.textContent = element.value.substring(0, position); if (element.nodeName === 'INPUT') { div.textContent = div.textContent.replace(/\s/g, ' '); } var span = this.getDocument().createElement('span'); span.textContent = element.value.substring(position) || '.'; div.appendChild(span); var rect = element.getBoundingClientRect(); var doc = document.documentElement; var windowLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); var windowTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); var coordinates = { top: rect.top + windowTop + span.offsetTop + parseInt(computed.borderTopWidth) + parseInt(computed.fontSize) - element.scrollTop, left: rect.left + windowLeft + span.offsetLeft + parseInt(computed.borderLeftWidth) }; var windowWidth = window.innerWidth; var windowHeight = window.innerHeight; var menuDimensions = this.getMenuDimensions(); var menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); if (menuIsOffScreen.horizontally) { coordinates.right = windowWidth - coordinates.left; coordinates.left = 'auto'; } var parentHeight = this.tribute.menuContainer ? this.tribute.menuContainer.offsetHeight : this.getDocument().body.offsetHeight; if (menuIsOffScreen.vertically) { var parentRect = this.tribute.menuContainer ? this.tribute.menuContainer.getBoundingClientRect() : this.getDocument().body.getBoundingClientRect(); var scrollStillAvailable = parentHeight - (windowHeight - parentRect.top); coordinates.bottom = scrollStillAvailable + (windowHeight - rect.top - span.offsetTop); coordinates.top = 'auto'; } this.getDocument().body.removeChild(div); return coordinates; } }, { key: 'getContentEditableCaretPosition', value: function getContentEditableCaretPosition(selectedNodePosition) { var markerTextChar = ''; var markerEl = void 0, markerId = 'sel_' + new Date().getTime() + '_' + Math.random().toString().substr(2); var range = void 0; var sel = this.getWindowSelection(); var prevRange = sel.getRangeAt(0); range = this.getDocument().createRange(); range.setStart(sel.anchorNode, selectedNodePosition); range.setEnd(sel.anchorNode, selectedNodePosition); range.collapse(false); // Create the marker element containing a single invisible character using DOM methods and insert it markerEl = this.getDocument().createElement('span'); markerEl.id = markerId; markerEl.appendChild(this.getDocument().createTextNode(markerTextChar)); range.insertNode(markerEl); sel.removeAllRanges(); sel.addRange(prevRange); var rect = markerEl.getBoundingClientRect(); var doc = document.documentElement; var windowLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); var windowTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); var coordinates = { left: rect.left + windowLeft, top: rect.top + markerEl.offsetHeight + windowTop }; var windowWidth = window.innerWidth; var windowHeight = window.innerHeight; var menuDimensions = this.getMenuDimensions(); var menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); if (menuIsOffScreen.horizontally) { coordinates.left = 'auto'; coordinates.right = windowWidth - rect.left - windowLeft; } var parentHeight = this.tribute.menuContainer ? this.tribute.menuContainer.offsetHeight : this.getDocument().body.offsetHeight; if (menuIsOffScreen.vertically) { var parentRect = this.tribute.menuContainer ? this.tribute.menuContainer.getBoundingClientRect() : this.getDocument().body.getBoundingClientRect(); var scrollStillAvailable = parentHeight - (windowHeight - parentRect.top); windowLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); windowTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); coordinates.top = 'auto'; coordinates.bottom = scrollStillAvailable + (windowHeight - rect.top); } markerEl.parentNode.removeChild(markerEl); return coordinates; } }, { key: 'scrollIntoView', value: function scrollIntoView(elem) { var reasonableBuffer = 20, clientRect = void 0; var maxScrollDisplacement = 100; var e = this.menu; if (typeof e === 'undefined') return; while (clientRect === undefined || clientRect.height === 0) { clientRect = e.getBoundingClientRect(); if (clientRect.height === 0) { e = e.childNodes[0]; if (e === undefined || !e.getBoundingClientRect) { return; } } } var elemTop = clientRect.top; var elemBottom = elemTop + clientRect.height; if (elemTop < 0) { window.scrollTo(0, window.pageYOffset + clientRect.top - reasonableBuffer); } else if (elemBottom > window.innerHeight) { var maxY = window.pageYOffset + clientRect.top - reasonableBuffer; if (maxY - window.pageYOffset > maxScrollDisplacement) { maxY = window.pageYOffset + maxScrollDisplacement; } var targetY = window.pageYOffset - (window.innerHeight - elemBottom); if (targetY > maxY) { targetY = maxY; } window.scrollTo(0, targetY); } } }]); return TributeRange; }(); exports.default = TributeRange; module.exports = exports['default'];