// twitter-like mention input // For ment.io, refer to https://github.com/jeff-collins/ment.io commonModule .directive('contenteditable', ['$rootScope', '$translate', 'mentioUtil', function ($rootScope, $translate, mentioUtil) { return { restrict: 'A', // only activate on element attribute require: ['?ngModel', '^?mentionInput'], // get a hold of NgModelController and parent directive link: function (scope, element, attrs, ctrl) { if (!ctrl) return; var ngModel = ctrl[0]; var mentionInput = ctrl[1]; // do nothing if no ng-model or not in mention-input directive if (!ngModel || !mentionInput) { return; } var nameStr = $translate.instant('Name'); var descStr = $translate.instant('Description'); var keyCodeExp = /@[A-Za-z0-9\.]+?\s/g; // like @VAT.ZPXSE.17 var formulaExp = /[A-Za-z_]+[A-Za-z0-9_]*\([^\(\)]*\)/g; // like BB("VAT001", 1, 1, 0) var tagExp = /<[^>]+?>/g; // like <a>, <span style=''>, <br/> //var otherTagExp = /(?!<pre(>|<\/>|\s.*>))<[^>]+?>/g; // like <a>, <span style=''>, <br/> scope.isValid = true; var read = function () { if (mentioUtil.clearEmptyTextNode) { mentioUtil.clearEmptyTextNode(element[0]); } // If show-name is true, replace code with name; otherwise do nothing. var txt = element.text(); if (scope.showName) { var pres = element.find('pre').toArray(); pres.forEach(function (e) { if (angular.element(e).attr('data-type') === '0') { var code = angular.element(e).attr('data-code'); if (code) { txt = txt.replace(angular.element(e).text(), code + ' '); } } }); } if (txt) { // replace and \r, \n if exist txt = txt.replace(/\xA0/g, ' ').replace(/\n/g, '').replace(/\r/g, ''); txt = _.unescape(txt); } ngModel.$setViewValue(txt); }; var customValidator = function (ngModelValue) { if (_.isFunction(scope.customValidator)) { scope.isValid = scope.customValidator({ 'value': ngModelValue, 'ngModel': ngModel }); } return ngModelValue; }; ngModel.$parsers.unshift(customValidator); ngModel.$formatters.unshift(customValidator); // Specify how UI should be updated ngModel.$render = function () { // trans between $viewValue and element html // $viewValue sample: @CH.BQ.QCYE +@VAT.ZPXSE.17 // element html sample:<pre>@CH.BQ.QCYE </pre>+<pre>@VAT.ZPXSE.17 </pre> var viewValue = ngModel.$viewValue; viewValue = _.escape(viewValue); var matches = viewValue ? viewValue.match(keyCodeExp) : []; if (matches) { matches = _.uniq(matches); matches.forEach(function (m) { var patt = new RegExp(m, "g"); var item; if (!_.isEmpty(scope.keyValueList)) { item = _.findWhere(scope.keyValueList, { code: $.trim(m) }); } if (!_.isEmpty(item) && !_.isEmpty(item.code)) { if (scope.showName) { viewValue = viewValue.replace(patt, '<pre data-type="0" title="' + item.description + '" data-code="' + item.code + '" contenteditable="false">' + '@' + item.name + ' </pre>'); } else { viewValue = viewValue.replace(patt, '<pre data-type="0" title="' + nameStr + ':' + item.name + ' ' + descStr + ':' + item.description + '" contenteditable="false">' + item.code + ' </pre>'); } } else { viewValue = viewValue.replace(patt, '<pre data-type="0" contenteditable="false">' + $.trim(m) + ' </pre>'); } }); } matches = viewValue ? viewValue.match(formulaExp) : []; if (matches) { matches.forEach(function (m) { var firstBracketIdx = m.indexOf('('); var formulaName = m.substring(0, firstBracketIdx); var paramStr = m.substring(firstBracketIdx + 1); var item; if (!_.isEmpty(scope.formulaList)) { item = _.findWhere(scope.formulaList, { code: formulaName }); } if (!_.isEmpty(item) && !_.isEmpty(item.code)) { viewValue = viewValue.replace(m, '<pre data-type="1" data-description="' + item.description + '" data-name="' + item.name + '" data-code="' + item.code + '" contenteditable="inherit"><span contenteditable="false">' + item.code + '(</span>' + paramStr + '</pre>'); } }); } element.html(viewValue || ''); }; // Listen for change events to enable binding element.on('blur keyup change', function () { if (!$rootScope.$$phase) { scope.$apply(read); } else { read(); } }); } }; }]) .directive('mentionInput', ['$rootScope', '$log', '$translate', 'mentioUtil', 'enums', function ($rootScope, $log, $translate, mentioUtil, enums) { 'use strict'; $log.debug('mentionInput.ctor()...'); return { restrict: 'E', templateUrl: '/app/common/controls/mention-input/mention-input.html' + '?_=' + Math.random(), replace: true, scope: { inputClass: '@', inputId: '@', ngModelVal: '=ngModel', inputReadonly: '=', includeBtn: '=', showName: '=', keyValueList: '=*mentionList', formulaList: '=*', displayMode: '=?', // input or textarea mentionApi: '=?', firedName: '=?', firedTagType: '=?', firedFormulaParams: '=?', btnClick: '&', btn2Click: '&', customValidator: '&', doubleClick: '&', inputFocus: '&', inputBlur: '&' }, controller: 'mentionInputController', link: function (scope, element, attr) { // Register api functions in internalApi // If selectorApi passed is not undefined, we will use it as internalApi scope.internalApi = scope.mentionApi || {}; scope.rangeInfo = {}; var content = element.find('.mention-input-content'); var nameStr = $translate.instant('Name'); var descStr = $translate.instant('Description'); // Set input classes var classAttrs = scope.inputClass; if (!_.isEmpty(classAttrs)) { content.addClass(classAttrs); } scope.$watch('inputReadonly', function (newValue, oldValue) { if (!newValue) { content.attr('contenteditable', true); } else if (!oldValue) { content.attr('contenteditable', false); } }); scope.$watch('rangeInfo', function (newValue, oldValue) { if (newValue) { getFiredFormulaInfo(); } }); // Build tag for selected key value item var getKeyValueText = function (item) { var rtn; if (!_.isEmpty(item) && !_.isEmpty(item.code)) { if (scope.showName) { rtn = '<pre data-type="0" title="' + item.description + '" data-code="' + item.code + '" contenteditable="false">' + '@' + item.name + ' </pre>'; } else { rtn = '<pre data-type="0" title="' + nameStr + ':' + item.name + ' ' + descStr + ':' + item.description + '" contenteditable="false">' + item.code + ' </pre>'; } } else { rtn = '<pre contenteditable="false">' + (item.code || ('@' + item.label)) + ' </pre>'; } return rtn; }; // Build tag for selected formula item var getFormulaText = function (item) { var rtn; if (!_.isEmpty(item) && !_.isEmpty(item.code)) { rtn = '<pre data-type="1" data-description="' + item.description + '" data-name="' + item.name + '" data-code="' + item.code + '" contenteditable="inherit"><span contenteditable="false">' + item.code + '(</span>)</pre>'; } else { rtn = item.code || item.label; } return rtn; }; // multiple types of elements can be selected: // <pre contenteditable='false'>@QCYE</pre> // <pre contenteditable='inherit'><span contenteditable='false'>BB</span>(1,2,3,4)</pre> var getStartEndInfo = function (range) { var rtn = null; if (!range) { var selection = window.getSelection(); try { range = selection.getRangeAt(0); } catch (ex) { } if (!range) { return rtn; } } // Only workable with range objects. if (Object.prototype.toString.call(range).indexOf('Range') >= 0) { rtn = { startEle: range.startContainer, endEle: range.endContainer, startOffset: range.startOffset, // The selection offset in the startEle, -1 represents the whole element is selected. endOffset: range.endOffset // The selection offset in the endEle, -1 represents the whole element is selected. } var curNode = rtn.startEle; for (var i = 0; i < 3; i++) { if (!curNode || curNode === content[0]) { break; } curNode = curNode.parentNode; } if (curNode !== content[0]) { rtn.startEle = null; rtn.startOffset = -1; } curNode = rtn.endEle; for (var i = 0; i < 3; i++) { if (!curNode || curNode === content[0]) { break; } curNode = curNode.parentNode; } if (curNode !== content[0]) { rtn.endEle = null; rtn.endOffset = -1; } // End element in the range is the contenteditable div itself // That means the real end element is the last element on or before the endOffset. if (rtn.endEle === content[0]) { rtn.endEle = rtn.endOffset <= 0 ? null : rtn.endEle.childNodes[rtn.endOffset - 1]; rtn.endOffset = -1; } // Ignore empty #text while (rtn.endEle && rtn.endEle.nodeType === enums.domNodeType.text && rtn.endEle.nodeValue === '') { rtn.endEle = rtn.endEle.previousSibling; rtn.endOffset = -1; } // #text not empty , get its parent. If parent is not contenteditable div itself, // set endEle to the parent element. if (rtn.endEle && rtn.endEle.nodeType === enums.domNodeType.text) { var parentEle = rtn.endEle.parentNode; if (!parentEle.isContentEditable) { // For IE, the startContainer/endContainer are always #text // even if actually it is an element rtn.endEle = parentEle; } else if (rtn.endOffset === 0 && parentEle === content[0]) { // endOffset === 0 means the #text is next to the real endEle rtn.endEle = rtn.endEle.previousSibling; if (rtn.endEle) { if (rtn.endEle.nodeType === enums.domNodeType.text) { rtn.endOffset = rtn.endEle.length; } else { rtn.endOffset = -1; } } } else if (rtn.endOffset < 0) { // endOffset < 0 means the range.endOffset is actually the offset of #text in its parent // set endOffset to the end of #text rtn.endOffset = rtn.endEle.length; } } // Start element in the range is the contenteditable div itself // That means the real start element is the first element on or after the startOffset. if (rtn.startEle === content[0]) { rtn.startEle = rtn.startOffset >= rtn.startEle.childNodes.length ? null : rtn.startEle.childNodes[rtn.startOffset]; rtn.startOffset = -1; } // Ignore empty #text while (rtn.startEle && rtn.startEle.nodeType === enums.domNodeType.text && rtn.startEle.nodeValue === '') { rtn.startEle = rtn.startEle.nextSibling; rtn.startOffset = -1; } // #text not empty , get its parent. If parent is not contenteditable div itself, // set startEle to the parent element. if (rtn.startEle && rtn.startEle.nodeType === enums.domNodeType.text) { var parentEle = rtn.startEle.parentNode; if (!parentEle.isContentEditable) { // For IE, the startContainer/endContainer are always #text // even if actually it is an element rtn.startEle = parentEle; } else if (rtn.startOffset === rtn.startEle.length && parentEle === content[0]) { // startOffset === startEle.length means the #text is previous to the real startEle rtn.startEle = rtn.startEle.nextSibling; if (rtn.startEle) { if (rtn.startEle.nodeType === enums.domNodeType.text) { rtn.startOffset = 0; } else { rtn.startOffset = -1; } } } else if (rtn.startOffset < 0) { // startOffset < 0 means the range.startOffset is actually the offset of #text in its parent // set startOffset to the start of #text rtn.startOffset = 0; } } } return rtn; }; var getFiredFormulaInfo = function () { if (!attr.firedName || !attr.firedFormulaParams) { return; } var selection = window.getSelection(); var range; try { range = selection.getRangeAt(0); } catch (ex) { } if (!range) { getFiredFormulaByElement(null); return; } $log.debug(range); var startEndInfo = getStartEndInfo(range); $log.debug(startEndInfo); if (!startEndInfo) { getFiredFormulaByElement(null); return; } if (startEndInfo.startEle && startEndInfo.startEle.parentNode !== content[0]) { startEndInfo.startEle = startEndInfo.startEle.parentNode; } if (startEndInfo.endEle && startEndInfo.endEle.parentNode !== content[0]) { startEndInfo.endEle = startEndInfo.endEle.parentNode; } if (range.startContainer !== range.endContainer && startEndInfo.startEle !== startEndInfo.endEle) { getFiredFormulaByElement(null); } else if (range.startContainer === range.endContainer) { // caret between two elements if (startEndInfo.endEle && startEndInfo.endEle.nodeType !== enums.domNodeType.text) { getFiredFormulaByElement(startEndInfo.endEle); } else if (startEndInfo.startEle && startEndInfo.startEle.nodeType !== enums.domNodeType.text) { getFiredFormulaByElement(startEndInfo.startEle); } else { getFiredFormulaByElement(null); } } else if (startEndInfo.endEle && startEndInfo.endEle.nodeType !== enums.domNodeType.text) { // Same element, not #text getFiredFormulaByElement(startEndInfo.endEle); } else { getFiredFormulaByElement(null); } }; var getFiredFormulaByElement = function (ele) { if (!ele) { scope.firedName = ''; scope.firedFormulaParams = ''; scope.firedTag = null; } else { var wrappedEle = angular.element(ele); if (wrappedEle.attr('data-type') === '1') { var firedFormulaParams = wrappedEle.text(); var firstBracketPos = firedFormulaParams.indexOf('('); if (firstBracketPos > 0) { scope.firedName = wrappedEle.attr('data-code'); firedFormulaParams = firedFormulaParams.substring(firstBracketPos + 1, firedFormulaParams.length); scope.firedFormulaParams = firedFormulaParams; scope.firedTag = ele; scope.firedTagType = wrappedEle.attr('data-type'); } else { scope.firedName = ''; scope.firedFormulaParams = ''; scope.firedTag = null; } } else { var firedName = wrappedEle.attr('data-code'); if (firedName && firedName.length > 1 && firedName.charAt(0) === '@') { firedName = firedName.substring(1); scope.firedName = firedName; scope.firedFormulaParams = ''; scope.firedTag = ele; scope.firedTagType = wrappedEle.attr('data-type'); } } } }; var deleteRange = function (startEndInfo) { if (startEndInfo && startEndInfo.startEle && startEndInfo.endEle) { // If part of the range is out of the editor, block this operation. if (startEndInfo.startEle.parentNode !== content[0] && (!startEndInfo.startEle.parentNode || startEndInfo.startEle.parentNode.parentNode !== content[0]) || startEndInfo.endEle.parentNode !== content[0] && (!startEndInfo.endEle.parentNode || startEndInfo.endEle.parentNode.parentNode !== content[0])) { return; } var startEle = startEndInfo.startEle; var startOffset = startEndInfo.startOffset; var endEle = startEndInfo.endEle; var endOffset = startEndInfo.endOffset; // Delete the elements between start element and end element // If start element and end element are not in the same level or not in same parent, treat both children of content[0]. if (startEle.parentNode !== content[0] && endEle.parentNode !== content[0] && startEle.parentNode !== endEle.parentNode) { startEle = startEle.parentNode; endEle = endEle.parentNode; } else if (startEle.parentNode !== content[0] && endEle.parentNode === content[0]) { startEle = startEle.parentNode; } else if (endEle.parentNode !== content[0] && startEle.parentNode === content[0]) { endEle = endEle.parentNode; } var curEle = startEle; var toDeleteNodes = []; if (curEle !== endEle) { while (curEle.nextSibling !== endEle && curEle.nextSibling) { curEle = curEle.nextSibling; toDeleteNodes.push(curEle); } toDeleteNodes.forEach(function (n) { if (n.parentNode === content[0]) { n.parentNode.removeChild(n); } }); } // If selection is in a #text node, delete the selected content if (startEle.nodeType === enums.domNodeType.text && startEle === endEle) { startEle.deleteData(startOffset, endOffset - startOffset); } else { // If selection start or end is not #text, just delete the element directly; otherwise delete the selected content if (startEle.nodeType !== enums.domNodeType.text) { if (startEle.parentNode === content[0]) { content[0].removeChild(startEle); } else if (startEle.parentNode && startEle.parentNode.parentNode === content[0]) { content[0].removeChild(startEle.parentNode); } } else { startEle.deleteData(startOffset, startEle.length - startOffset); } if (endEle.nodeType !== enums.domNodeType.text) { if (endEle.parentNode === content[0]) { content[0].removeChild(endEle); } else if (endEle.parentNode && endEle.parentNode.parentNode === content[0]) { content[0].removeChild(endEle.parentNode); } } else { endEle.deleteData(0, endOffset); } } } }; // <div contenteditable>""|"text1"|""|<pre>"" "preText" "" "" </pre>|""|" "|"" </div> var handleBackspace = function (e) { e.preventDefault(); var selection = window.getSelection(); var range; try { range = selection.getRangeAt(0); } catch (ex) { } var startEndInfo = getStartEndInfo(range); if (startEndInfo) { // selection type is caret if (range.startContainer === range.endContainer && range.startOffset === range.endOffset && startEndInfo.endEle) { if (startEndInfo.endEle.nodeType === enums.domNodeType.text) { if (startEndInfo.endOffset > 0) { // Delete character before caret startEndInfo.endEle.deleteData(startEndInfo.endOffset - 1, 1); } } else { if (startEndInfo.endEle.parentNode === content[0]) { content[0].removeChild(startEndInfo.endEle); } else if (startEndInfo.endEle.parentNode && startEndInfo.endEle.parentNode.parentNode === content[0]) { content[0].removeChild(startEndInfo.endEle.parentNode); } } } else { // selection type is range deleteRange(startEndInfo); } } content.trigger('keyup'); }; var handleDelete = function (e) { e.preventDefault(); var selection = window.getSelection(); var range; try { range = selection.getRangeAt(0); } catch (ex) { } var startEndInfo = getStartEndInfo(range); if (startEndInfo) { // selection type is caret if (range.startContainer === range.endContainer && range.startOffset === range.endOffset && startEndInfo.startEle) { if (startEndInfo.startEle.nodeType === enums.domNodeType.text) { if (startEndInfo.startOffset < startEndInfo.startEle.length) { // Delete character after caret startEndInfo.startEle.deleteData(startEndInfo.startOffset, 1); } } else { if (startEndInfo.startEle.parentNode === content[0]) { content[0].removeChild(startEndInfo.startEle); } else if (startEndInfo.startEle.parentNode && startEndInfo.startEle.parentNode.parentNode === content[0]) { content[0].removeChild(startEndInfo.startEle.parentNode); } } } else { // selection type is range deleteRange(startEndInfo); } } content.trigger('keyup'); }; var isValid = function () { return scope.isValid; }; if (scope.internalApi) { scope.internalApi.isValid = isValid; scope.internalApi.triggerValidator = function (val, field) { if (_.isFunction(scope.customValidator)) { scope.isValid = scope.customValidator({ 'value': val, 'ngModel': field }); } }; } content.bind('keydown', function (e) { if (scope.inputReadonly) { return; } // Clear empty text node in contenteditable to make sure mentio work. // And after empty text been cleared, Enter does not work issue is fixed. Why??? mentioUtil.clearEmptyTextNode(content[0]); // Since backspace in IE11 will delete characters in contenteditable="false" elements, // we have to add custom backspace handler. switch (e.keyCode) { case enums.keyCode.enter: // prevent the default behaviour of return key pressed e.preventDefault(); break; case enums.keyCode.backspace: handleBackspace(e); break; case enums.keyCode.delete: handleDelete(e); break; default: break; } }); // Fix issue - <pre contenteditable="false"></pre> can be modified in IE11 content.bind('keypress', function (e) { if (scope.inputReadonly || e.ctrlKey || e.metaKey || e.altKey) { return; } var selection = window.getSelection(); var range; try { range = selection.getRangeAt(0); } catch (ex) { } var startEndInfo = getStartEndInfo(range); if (startEndInfo && startEndInfo.endEle) { // selection type is caret if (range.startContainer === range.endContainer && range.startOffset === range.endOffset) { // caret is not in #text, but after or in a <pre> node in contenteditable if (startEndInfo.endEle.nodeType !== enums.domNodeType.text && startEndInfo.endEle.parentNode === content[0]) { // caret after <span> node in <pre> node, only appear in IE. if (startEndInfo.endOffset === 1 && startEndInfo.childNodes && startEndInfo.childNodes[0] !== enums.domNodeType.text) { return; } // Find the offset of the node in contenteditable var offset = -1; for (offset = 0; offset < content[0].childNodes.length; offset++) { var n = content[0].childNodes[offset]; if (n === startEndInfo.endEle) { break; } } if (offset < 0) { // Cannot find the node offset, it may not be in the contenteditable return; } // caret is at the beginning of editable <pre> node, only appear in IE. if (startEndInfo.endOffset === 0 && offset === 0 && startEndInfo.endEle.childNodes.length > 0 && startEndInfo.endEle.childNodes[0] !== enums.domNodeType.text) { // Add the key to the left of the node. //e.preventDefault(); var txtNode = document.createTextNode('\xA0'); //var newRange = document.createRange(); var maxLength = content[0].childNodes ? content[0].childNodes.length : 0; range.setStart(content[0], 0); range.setEnd(content[0], 0); range.insertNode(txtNode); range.collapse(false); range = range.cloneRange(); range.setStart(txtNode, 0); range.setEnd(txtNode, txtNode.length); //range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } else { // Add the key to the right of the node. //e.preventDefault(); var txtNode = document.createTextNode('\xA0'); //var newRange = document.createRange(); var maxLength = content[0].childNodes ? content[0].childNodes.length : 0; range.setStart(content[0], maxLength > offset ? (offset + 1) : maxLength); range.setEnd(content[0], maxLength > offset ? (offset + 1) : maxLength); range.insertNode(txtNode); range.collapse(false); range = range.cloneRange(); range.setStart(txtNode, 0); range.setEnd(txtNode, txtNode.length); //range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } } // caret is in #text, but in the end of the text node in editable <pre> node in contenteditable else if (startEndInfo.endEle.nodeType === enums.domNodeType.text && startEndInfo.endEle.parentNode && startEndInfo.endEle.parentNode.parentNode === content[0] && startEndInfo.endEle.length === range.endOffset) { // Find the offset of the node in contenteditable var offset = -1; for (offset = 0; offset < content[0].childNodes.length; offset++) { var n = content[0].childNodes[offset]; if (n === startEndInfo.endEle.parentNode) { break; } } if (offset < 0) { // Cannot find the node offset, it may not be in the contenteditable return; } // Add the key to the right of the node. //e.preventDefault(); var txtNode = document.createTextNode('\xA0'); //var newRange = range.cloneRange(); var maxLength = content[0].childNodes ? content[0].childNodes.length : 0; range.setStart(content[0], maxLength > offset ? (offset + 1) : maxLength); range.setEnd(content[0], maxLength > offset ? (offset + 1) : maxLength); range.insertNode(txtNode); range.collapse(false); range = range.cloneRange(); range.setStart(txtNode, txtNode.length); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } // caret is at the beginning of <span> node in editable <pre>, only appear in IE. else if (startEndInfo.endEle.nodeType !== enums.domNodeType.text && startEndInfo.endEle.parentNode && startEndInfo.endEle.parentNode.parentNode === content[0] && 0 === range.endOffset) { // Find the offset of the node in contenteditable var offset = -1; for (offset = 0; offset < content[0].childNodes.length; offset++) { var n = content[0].childNodes[offset]; if (n === startEndInfo.endEle.parentNode) { break; } } if (offset < 0) { // Cannot find the node offset, it may not be in the contenteditable return; } // Add the key to the left of <pre> node. //e.preventDefault(); var txtNode = document.createTextNode('\xA0'); //var newRange = document.createRange(); range.setStart(content[0], offset); range.setEnd(content[0], offset); range.insertNode(txtNode); range.collapse(false); range = range.cloneRange(); range.setStart(txtNode, offset); range.setEnd(txtNode, offset + txtNode.length); //range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } } else if (startEndInfo.startEle.nodeType !== enums.domNodeType.text || startEndInfo.endEle.nodeType !== enums.domNodeType.text) { // selection type is range and startEle or EndEle is not #text, // then prevent the operation. e.preventDefault(); } } }); var onDoubleClick = function () { if (_.isFunction(scope.doubleClick)) { if (_.isEmpty(scope.firedTag)) { $log.debug('dblclick event: get fired formula'); getFiredFormulaInfo(); } scope.doubleClick({ '$event': { tagInfo: scope.firedTag, name: scope.firedName, formulaParams: scope.firedFormulaParams, tagType: scope.firedTagType } }); } }; // Block pasting elements content.bind('paste', function (e) { e.preventDefault(); if (scope.inputReadonly) { return; } var text = null; if (window.clipboardData && clipboardData.setData) { // IE text = window.clipboardData.getData('text'); } else { text = (e.originalEvent || e).clipboardData.getData('text/plain'); } text = text.replace(/\r/g, '').replace(/\n/g, ''); // For IE, only support 11+ var selection = window.getSelection(); var range; try { range = selection.getRangeAt(0); } catch (ex) { } var startEndInfo = getStartEndInfo(range); if (startEndInfo) { // Block paste operation while selection start or end is not #text node. if (startEndInfo.startEle && startEndInfo.endEle && (startEndInfo.startEle.nodeType !== enums.domNodeType.text || startEndInfo.endEle.nodeType !== enums.domNodeType.text)) { return; } // selection type is not caret, then delete the selected range first. if (range.startContainer !== range.endContainer || range.startOffset !== range.endOffset) { deleteRange(startEndInfo); } var txtNode = document.createTextNode(text); range.insertNode(txtNode); // Preserve the selection range = range.cloneRange(); range.setStart(txtNode, txtNode.length); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } content.trigger('keyup'); }); content.bind('focus', function (e) { if (_.isFunction(scope.inputFocus)) { scope.inputFocus({ '$event': e }); } }); content.bind('blur', function (e) { if (_.isFunction(scope.inputBlur)) { scope.inputBlur({ '$event': e }); } }); scope.getKeyValueText = getKeyValueText; scope.getFormulaText = getFormulaText; scope.getFiredFormulaInfo = getFiredFormulaInfo; scope.onDoubleClick = onDoubleClick; } }; } ]);