"use strict"; (function () { 'use strict'; angular.module('myitsmApp') .directive('feedCommentThread', ['$http', '$templateCache', '$compile', '$window', 'events', 'attachmentService', '$filter', 'smartRecorderModel', '$timeout', 'metadataModel', 'systemAlertService', 'feedModel', 'userModel', 'i18nService', 'configurationModel', 'browser', 'utilityFunctions', function ($http, $templateCache, $compile, $window, events, attachmentService, $filter, smartRecorderModel, $timeout, metadataModel, systemAlertService, feedModel, userModel, i18nService, configurationModel, browser, utilityFunctions) { return { restrict: 'A', priority: 99, template: angular.noop, replace: false, scope: { nestingLevel: '@', runSaveNoteHandler: '&savenote', opened: '@', isClosed: '=', data: '@', parentContext: '=', type: '=', isDraft: '=', timelineItem: '=', attachmentDisabled: '=', isUnflagEditAllowed: '=', inputDisabled: '=', inputText: '=?', isRequired: '=?' }, link: function (scope, $elem) { var textAreaElement; var defaultWorknoteTypeName = 'General Information'; var allowSearchByCompany = configurationModel.smartRecorderSearchByCompany; var defaultPlaceHolder = (scope.timelineItem && scope.timelineItem.isFlag()) ? $filter('i18n')('timeline.note.respondFlag.placeholder') : $filter('i18n')('timeline.note.addNote.placeholder'); function handleSaveTicketDraftRequest() { console.log('handling SAVE_DRAFT event in feed-comment-thread '); var workNote = getWorkNoteData(); if (workNote.noteText || (workNote.attachments && workNote.attachments.length)) { feedModel.pendingWorkNote = workNote; console.log('initialized pending note'); // block ui to prevent changes if (!(scope.type === EntityVO.TYPE_CHANGE || scope.type === EntityVO.TYPE_RELEASE)) { scope.state.savingNote = true; } } } scope.$on(events.SAVE_TICKET_DRAFT, handleSaveTicketDraftRequest); scope.$on(events.SAVE_COPY_CHANGE_ACTIVITY, handleSaveTicketDraftRequest); function handleTicketDraftMentionedNote() { scope.selectedWorknoteType = _.find(scope.worknoteTypes, { name: defaultWorknoteTypeName }) || {}; scope.addNoteForm(true); } function showMentionedNote() { var textArea = textAreaElement[0]; var mentionedPersons = _.filter(smartRecorderModel.smartRecorderData.confirmedItems, { relationship: 'mentioned' }); if (mentionedPersons && mentionedPersons.length) { var mentionedPerson; for (var i = 0; i < mentionedPersons.length; i++) { mentionedPerson = mentionedPersons[i]; if (i > 0) { textArea.lastChild.data = ', '; } textArea.appendChild(document.createTextNode(mentionedPerson.originalName)); highlightConfirmItem(mentionedPerson.originalName, mentionedPerson.content, mentionedPerson.type, mentionedPerson.displayName, mentionedPerson.id); } var mentionedText = mentionedPersons.length === 1 ? i18nService.getLocalizedString('createNew.ticket.mentionedPerson') : i18nService.getLocalizedString('createNew.ticket.mentionedPersons'); textArea.appendChild(document.createTextNode(mentionedText)); var range = document.createRange(); var sel = window.getSelection(); var lastChild = textArea.lastChild; range.setStart(lastChild, lastChild.length); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); textAreaElement.focus(); } } scope.$on(events.SET_TICKET_DRAFT_MENTIONED, handleTicketDraftMentionedNote); scope.state = { isPublicEnabled: !_.includes([EntityVO.TYPE_KNOWLEDGE, EntityVO.TYPE_SERVICEREQUEST], scope.type), isAttachEnabled: !scope.attachmentDisabled, isPostButtonVisible: !scope.isDraft, noteFormIsActive: false, savingNote: false, access: false, shareWithVendor: false, atLeastOneShareableVendorTickets: false }; if (scope.timelineItem) { scope.state.isThreadUnflagged = scope.timelineItem.hasUnflaggingResponse(); scope.isFlagThread = scope.timelineItem.isFlag(); } scope.$watch('parentContext.brokerVendorName', function (brokerVendorName) { scope.state.isVendorEnabled = !!brokerVendorName; }); scope.$watch('parentContext.vendorInfo', function (vendorInfo) { if (_.isArray(vendorInfo)) { scope.shareableVendorTickets = _.filter(vendorInfo, function (vendorTicket) { return vendorTicket.vendor.doNotShareWorkLogToVendor !== true; }); scope.state.atLeastOneShareableVendorTickets = _.isArray(scope.shareableVendorTickets) && scope.shareableVendorTickets.length > 0; scope.state.isMultipleVendorTickets = _.isArray(scope.shareableVendorTickets) && scope.shareableVendorTickets.length > 1; scope.updateShareWithVendorFlag(); } }); scope.updateVendorTicketList = function (filterOption) { filterOption.selected = !filterOption.selected; scope.updateShareWithVendorFlag(); }; scope.updateShareWithVendorFlag = function () { scope.state.shareWithVendor = _.some(scope.shareableVendorTickets, { selected: true }); }; scope.commentFormNode = angular.element(''); scope.attachments = []; scope.selectedWorknoteType = {}; scope.placeholderText = defaultPlaceHolder; if (EntityVO.hasMetadata(scope.type)) { metadataModel.getMetadataByType(scope.type).then(function (metadata) { scope.worknoteTypes = metadata.workinfoTypes; }); } scope.$on(events.SET_FOCUS_TO_ACTIVITY_INPUT, function () { scope.selectedWorknoteType = _.find(scope.worknoteTypes, { name: defaultWorknoteTypeName }) || {}; scope.addFlagNote = false; scope.placeholderText = defaultPlaceHolder; scope.addNoteForm(); }); if (_.isUndefined(scope.timelineItem)) { scope.$on(events.ADD_FLAG_NOTE, function (event, data) { scope.isNeedAttentionFlag = !!data.isNeedAttentionFlag; scope.selectedWorknoteType = _.find(scope.worknoteTypes, { name: defaultWorknoteTypeName }) || {}; scope.attachments = []; scope.addFlagNote = true; scope.flag = data.flag; if (scope.isNeedAttentionFlag) { scope.placeholderText = data.flag ? $filter('i18n')('ticket.needsAttention.flag.inputPlaceholder') : $filter('i18n')('ticket.needsAttention.unflag.inputPlaceholder'); } else { scope.placeholderText = data.flag ? $filter('i18n')('timeline.note.flag.placeholder') : $filter('i18n')('timeline.note.removeFlag.placeholder'); } scope.addNoteForm(); }); } scope.$on(events.DISMISS_NOTE_FORM, function () { scope.dismissNoteForm(); }); function clickFocusInHandler() { scope.isNeedAttentionFlag = false; scope.selectedWorknoteType = _.find(scope.worknoteTypes, { name: defaultWorknoteTypeName }) || {}; scope.toggleNoteForm(); scope.$apply(); } function keyDownKeyPressHandler(event) { if (event.which === 13) { scope.isNeedAttentionFlag = false; scope.selectedWorknoteType = _.find(scope.worknoteTypes, { name: defaultWorknoteTypeName }) || {}; scope.toggleNoteForm(); scope.$apply(); } } $elem.on('click focusin', clickFocusInHandler); $elem.on('keydown keypress', keyDownKeyPressHandler); scope.$on("$destroy", function () { console.log("feedCommentThread: unbind events"); $elem.off('click', clickFocusInHandler); $elem.off('focusin', clickFocusInHandler); $elem.off('keydown', keyDownKeyPressHandler); $elem.off('keypress', keyDownKeyPressHandler); $elem.off(); }); scope.handleKeydown = function ($event, type) { if ($event.keyCode === 32) { // space key scope.selectWorknoteType(type); $event.preventDefault(); $event.stopPropagation(); } }; scope.handleKeydownOnUpdate = function ($event, option) { if ($event.keyCode === 32) { // space key scope.updateVendorTicketList(option); $event.preventDefault(); $event.stopPropagation(); } }; scope.selectWorknoteType = function (type) { if (scope.selectedWorknoteType.index !== type.index) { scope.selectedWorknoteType = type; } }; scope.expandWorknoteTypeSection = function ($event, worknoteType) { if ($event) { $event.preventDefault(); $event.stopPropagation(); } if (worknoteType.expanded) { return; } _.forEach(scope.worknoteTypes, function (item) { item.expanded = false; }); worknoteType.expanded = true; }; scope.addNoteForm = function (mentionedFlag) { $http.get('views/feed/feed-add-note-form.html', { cache: $templateCache }).then(function (template) { scope.state.noteFormIsActive = true; scope.editMode = true; scope.state.unflagging = false; if (scope.type === EntityVO.TYPE_SERVICEREQUEST) { scope.state.access = true; } else if (scope.type !== EntityVO.TYPE_KNOWLEDGE && configurationModel.socialWorklogAccessSetting) { scope.state.access = configurationModel.socialWorklogAccessSetting; } else { scope.state.access = false; } if (scope.commentFormNode[0]) { scope.commentFormNode.show(); if (textAreaElement !== undefined) { $('.resource-preview__body').scrollTop($(textAreaElement).offset().top); textAreaElement.focus(); } return; } var nestingLevels = scope.nestingLevel, appendAfterDOM = $elem, formTemplate; if (template.data) { formTemplate = angular.element(template.data); scope.commentFormNode = $compile(formTemplate)(scope); } while (nestingLevels--) { appendAfterDOM = appendAfterDOM.parent(); } appendAfterDOM.after(scope.commentFormNode); textAreaElement = scope.commentFormNode.find('.timeline-note__text'); textAreaElement.focus(); if (mentionedFlag) { showMentionedNote(); } }); }; scope.dismissNoteForm = function (elem) { scope.attachments = []; typeAheadMode = false; scope.dismissPopup(); textAreaElement && textAreaElement.empty(); scope.commentFormNode.hide(); scope.state.noteFormIsActive = false; scope.inputText = ''; scope.state.shareWithVendor = false; if (scope.shareableVendorTickets && _.isArray(scope.shareableVendorTickets)) { _.forEach(scope.shareableVendorTickets, function (vendorTicket) { vendorTicket.selected = false; }); } scope.state.unflagging = false; scope.addFlagNote = false; scope.placeholderText = defaultPlaceHolder; if (scope.opened) { scope.isClosed = false; } }; scope.toggleNoteForm = function () { scope.state.noteFormIsActive ? scope.dismissNoteForm() : scope.addNoteForm(); }; scope.handleFileChange = function (fileInput) { var newAttachment = attachmentService.prepareFileToUpload({ fileInput: fileInput }); if (newAttachment && newAttachment.size <= 0) { systemAlertService.warning({ text: $filter('i18n')('attachment.file_empty'), icon: 'icon-exclamation_triangle', clear: true, hide: 5000 }); return; } if (newAttachment) { scope.attachments ? scope.attachments.push(newAttachment) : scope.attachments = [newAttachment]; //clear the file input value, otherwise the onChange event will not trigger, // if you are attaching the same file again in the same or next activity $elem.parent().find('#uploadAttachment').val(''); } }; scope.dismissAttachment = function ($e, attachment) { var index = scope.attachments.indexOf(attachment); if (index >= 0) { scope.attachments.splice(index, 1); } $e.stopImmediatePropagation(); }; scope.submitNote = function (elem) { if (!textAreaElement.text() && !scope.attachments.length) { //TODO: handle case when no text is entered and no attachments return false; } scope.state.savingNote = true; scope.noteData = getWorkNoteData(); scope.runSaveNoteHandler({ noteData: scope.noteData }) .then(function () { scope.dismissNoteForm(elem); }) .finally(function () { scope.state.savingNote = false; }); }; var cleanUpTextBeforePost = function (text) { return text .replace(/\n/g, ' ') // replace line breaks with special character .replace(/\s/g, ' ') // replace any space with regular one .replace(/ /g, '\n') // replace special line-break character with normal line-break .replace(/(\n{2})\n+/g, '$1'); // replace more that two line-breaks with just two }; var processWorknoteText = function (textAreaElm) { textAreaElm.find('.smart-recorder-highlightPerfectMatch').each(function () { var span = $(this); var textToReplace = '@[' + span.data('displayName') + ']|' + span.data('id') + '|(' + span.data('type') + ')'; // if mention is not at the start of text, put a space before it so that Angular's linky filter would not be confused if (this.previousSibling) { textToReplace = ' ' + textToReplace; } span.replaceWith(textToReplace); }); if (browserIsMSIE) { textAreaElm[0].innerHTML = textAreaElm[0].innerHTML.replace(/

/g, '').replace(/<\/p>/g, '

'); } textAreaElm[0].innerHTML = textAreaElm[0].innerHTML.replace(/
/g, '\n'); textAreaElm[0].innerHTML = textAreaElm[0].innerHTML.trimRight(); //Logic to delete the
added at the end of note for non IE. var text; if (browser.isSafari) { //There is no \n after converting it to innerText in safari if data present in different divs. // Replacing div with \n. This use case is applied while pasting data in activity note. text = $('

').html(textAreaElm[0].innerHTML.replace(/
/ig, "\n").replace(/<\/div>/ig, "")).text(); } else { text = textAreaElm.text(); } return cleanUpTextBeforePost(text); }; /** * Get note data * @returns {} worknote descriptor */ function mapVendorUrl(vendorInfo) { if (vendorInfo.id && vendorInfo.vendorTicketUrl) { return '@[' + vendorInfo.id + ']|' + vendorInfo.vendorTicketUrl + '|(url)'; } else { return vendorInfo.id || ''; } } function getWorkNoteData() { if (textAreaElement) { var noteData = { noteText: processWorknoteText(textAreaElement.clone(true)), attachments: scope.attachments, access: !scope.state.access, workInfoType: scope.selectedWorknoteType.index, addFlagNote: scope.addFlagNote, flag: scope.flag }; if (scope.state.shareWithVendor && scope.parentContext && scope.parentContext.brokerVendorName) { if (scope.type === EntityVO.TYPE_INCIDENT) { noteData.shareWithVendor = true; } else if (_.isArray(scope.shareableVendorTickets)) { if (scope.shareableVendorTickets.length === 1) { noteData.vendorTicketId = _.map(scope.shareableVendorTickets, mapVendorUrl).join() || ''; } else if (scope.shareableVendorTickets.length > 1) { noteData.vendorTicketId = _(scope.shareableVendorTickets) .filter({ selected: true }) .map(mapVendorUrl) .join(', '); } } noteData.brokerVendorName = scope.parentContext.brokerVendorName; } if (scope.isFlagThread && scope.state.isThreadUnflagged) { noteData.addFlagNote = true; noteData.flag = false; noteData.isCommentOnly = true; noteData.workNoteGuid = scope.timelineItem.note.workNoteGuid; } else if (scope.isFlagThread) { noteData.addFlagNote = true; noteData.flag = !scope.state.unflagging; noteData.isCommentOnly = !scope.state.unflagging; noteData.workNoteGuid = scope.timelineItem.note.workNoteGuid; } if (scope.isNeedAttentionFlag) { noteData.isNeedAttentionFlag = scope.isNeedAttentionFlag; } return noteData; } else { return {}; } } ///////////////////////////// @ MENTION var typeAheadMode = false; var highlightSelected = false; var typeAheadSearchString = ''; var pauseInterval = 2000; //2 sec pause var wordCountToStartSmartSearch = 8; var personChunkSize = configurationModel.personChunkSize; var searchChunkSize = 20; var assetTotalMatches = 0; var personTotalMatches = 0; var timerPromise; var selectedElement = null; var browserIsMSIE = ($window.navigator.userAgent.indexOf('MSIE') > -1 || $window.navigator.userAgent.indexOf('Edge') > -1) || ($window.navigator.userAgent.indexOf('Trident\/7.0') > -1); // once again MSIE is opposite to the rest in terms of contenteditable setting var contenteditableSetting = browserIsMSIE ? 'true' : 'false'; var keyCodes = { enter: 13, leftArrow: 37, rightArrow: 39, upArrow: 38, downArrow: 40 }; scope.personProfileList = []; scope.assetProfileList = []; scope.smartResultItems = []; // This list stores items that were highlighted at the text input area scope.personProfileListFilteredLength = 0; scope.assetProfileListFilteredLength = 0; scope.popupType = ''; scope.selectedText = ''; scope.typeAheadListPos = 0; scope.typeAheadText = ''; scope.actualTypeAheadText = ''; scope.showPopup = false; scope.smartRecorderModel = smartRecorderModel; scope.showAssetCount = true; scope.showPopupHeader = false; scope.toggleMentioning = function () { var edtiElm = scope.commentFormNode.find('.timeline-note__text'); if (!scope.showPopup) { pasteHtmlAtCaret('@', edtiElm[0]); scope.handleSmartInputChange($.Event('keyup')); } else { scope.dismissPopup(); } }; function setEndOfContenteditable(contentEditableElement) { var range, selection; if (document.createRange) { //Firefox, Chrome, Opera, Safari, IE 9+ range = document.createRange(); //Create a range (a range is a like the selection but invisible) range.selectNodeContents(contentEditableElement); //Select the entire contents of the element with the range range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start selection = window.getSelection(); //get the selection object (allows you to change selection) selection.removeAllRanges(); //remove any selections already made selection.addRange(range); //make the range you have just created the visible selection } else if (document.selection) { //IE 8 and lower range = document.body.createTextRange(); //Create a range (a range is a like the selection but invisible) range.moveToElementText(contentEditableElement); //Select the entire contents of the element with the range range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start range.select(); //Select the range (make it the visible selection } } function elementContainsSelection(el) { var sel; if (window.getSelection) { sel = window.getSelection(); if (sel.rangeCount > 0) { for (var i = 0; i < sel.rangeCount; ++i) { if (!isOrContains(sel.getRangeAt(i).commonAncestorContainer, el)) { return false; } } return true; } } else if ((sel = document.selection) && sel.type !== 'Control') { return isOrContains(sel.createRange().parentElement(), el); } return false; } function isOrContains(node, container) { while (node) { if (node === container) { return true; } node = node.parentNode; } return false; } function pasteHtmlAtCaret(html, el) { var sel, range; if (window.getSelection) { // IE9 and non-IE sel = window.getSelection(); if (elementContainsSelection(el)) { if (sel.getRangeAt && sel.rangeCount) { range = sel.getRangeAt(0); range.deleteContents(); // Range.createContextualFragment() would be useful here but is // non-standard and not supported in all browsers (IE9, for one) el = document.createElement('div'); el.innerHTML = html; var frag = document.createDocumentFragment(), node, lastNode; 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); } } else if (document.selection && document.selection.type !== 'Control') { // IE < 9 document.selection.createRange().pasteHTML(html); } } else { setEndOfContenteditable(el); pasteHtmlAtCaret(html, el); } } } scope.handleSmartInputKeyDown = function ($event) { // prevent the cursor from moving on the text input field if // type-ahead popup is active. The up, down and enter keys will be used exclusive for selection purposes if (($event.keyCode === keyCodes.upArrow || $event.keyCode === keyCodes.downArrow || $event.keyCode === keyCodes.enter) && scope.showPopup === true) { $event.preventDefault(); } // Chrome and Safari insert
instead of
on enter key within contenteditible div which cause a // lot of issues with the highlighted span. Here we force it to be
for all browsers. // IE has similar issue, it insert

instead of
else if ($event.keyCode === keyCodes.enter && !scope.showPopup && window.getSelection) { var selection = window.getSelection(), range = selection.getRangeAt(0), br = document.createElement('br'); range.deleteContents(); range.insertNode(br); range.setStartAfter(br); range.setEndAfter(br); range.collapse(false); selection.removeAllRanges(); selection.addRange(range); $event.preventDefault(); } }; /*Unused function*/ scope.hidePopupHeader = function () { scope.showPopupHeader = false; }; scope.handleSmartInputPaste = function ($event) { $event.preventDefault(); var content; if ($event.originalEvent && $event.originalEvent.clipboardData) { content = $event.originalEvent.clipboardData.getData('text/plain'); } else if ($window.clipboardData) { content = $window.clipboardData.getData('Text'); } else { return; // not support, let the default handle it } if (content) { if (document.queryCommandSupported('insertHTML')) { content = utilityFunctions.escapeHTML(content); content = content.replace(/(?:\r\n|\r|\n)/g, '
'); document.execCommand('insertHTML', false, content); textAreaElement.find('div').each(function (index, item) { if (item.innerHTML.trim().length === 0) { $(item).addClass('empty-div'); } }); } else if (document.queryCommandSupported('paste')) { document.execCommand('paste', false, content); } else { return; // not support, let the default handle it } } scope.handleSmartInputChange($event); }; function getTextWithLineBreak(htmlNode) { var textAreaText = ''; $.each(htmlNode.childNodes, function (nodeIndex, node) { // check every text node for matching if (node.nodeName.toLowerCase() === 'br') { textAreaText += '\n'; } else { textAreaText += node.innerText || node.textContent; } if (browserIsMSIE) { if (node.nodeName.toLowerCase() === 'p') { textAreaText += '\n'; } } }); return textAreaText.replace(/[\r\n]+$/, ''); } scope.getCaretPosition = function ($event) { var caretOffset = 0; var activityWindow = $event.currentTarget.ownerDocument.defaultView; var selection = activityWindow.getSelection(); if (selection.rangeCount > 0) { var range = selection.getRangeAt(0); var preCaretRange = range.cloneRange(); preCaretRange.selectNodeContents($event.currentTarget); preCaretRange.setEnd(range.endContainer, range.endOffset); var dummyContent = $("

"); dummyContent.append(preCaretRange.cloneContents()); caretOffset = getTextWithLineBreak(dummyContent[0]).length; } return caretOffset; }; scope.handleSmartInputChange = function ($event) { var typeAheadStartPos; var typeAheadEndPos = 0; var textArea = textAreaElement[0]; var typeaheadTextWords; // When text blocks were cut and paste to the text area, individual adjacent text block could be formed // Need to normalize textArea to merge adjacent text nodes before processing if (textArea.normalize) { textArea.normalize(); } scope.inputTextHtml = textAreaElement.html(); scope.inputText = getInputText(); // ensure there is already a child
at the end to make non IE work properly if (!browserIsMSIE) { if (!textArea.lastChild || textArea.lastChild.nodeName.toLowerCase() !== 'br') { textArea.appendChild(document.createElement('br')); } } if ($event) { // ignore if control (17) or alternate key (18) was pressed if ($event.ctrlKey === true || $event.keyCode === 17 || $event.altKey === true || $event.keyCode === 18) { return; } else if ($event.keyCode === keyCodes.upArrow && scope.showPopup === true) { if (scope.typeAheadListPos > 0) { scope.typeAheadListPos--; } return; } else if ($event.keyCode === keyCodes.downArrow && scope.showPopup === true) { if (scope.typeAheadListPos < (scope.personProfileListFilteredLength + scope.assetProfileListFilteredLength - 1)) { scope.typeAheadListPos++; } return; } else if ($event.keyCode === keyCodes.enter) { scope.handleSmartInputEnter(); return; } } // check @ or ! character if (scope.inputText.length > 0) { typeAheadStartPos = scope.inputText.lastIndexOf(' @'); if (typeAheadStartPos === -1) { typeAheadStartPos = scope.inputText.lastIndexOf('\n@'); } if (typeAheadStartPos > -1) { typeAheadStartPos += 2; scope.popupType = 'profile'; } else { // this is for Chrome and IE typeAheadStartPos = scope.inputText.indexOf(String.fromCharCode(160, 64)); if (typeAheadStartPos > -1) { typeAheadStartPos += 2; scope.popupType = 'profile'; } else if (scope.inputText[0] === '@') { typeAheadStartPos = 1; scope.popupType = 'profile'; } } typeAheadEndPos = scope.getCaretPosition($event); if (typeAheadStartPos > -1 && typeAheadEndPos > typeAheadStartPos) { scope.fullTypeaheadText = scope.inputText.substring(typeAheadStartPos); scope.actualTypeAheadText = scope.inputText.substring(typeAheadStartPos, typeAheadEndPos); scope.actualTypeAheadText = scope.actualTypeAheadText.trim().substring(0, 128); //Accept only 128 characters scope.actualTypeAheadText = scope.actualTypeAheadText.split("\n", 1)[0]; //Do not accept characters after line break if (scope.actualTypeAheadText.split(' ').length > 3) { //Accept maximum 3 words typeaheadTextWords = scope.actualTypeAheadText.split(' '); scope.actualTypeAheadText = typeaheadTextWords[0] + ' ' + typeaheadTextWords[1] + ' ' + typeaheadTextWords[2]; } if (typeAheadMode === false) { typeAheadMode = true; scope.typeAheadListPos = 0; } } else if (typeAheadMode === true) { typeAheadMode = false; scope.dismissPopup(); } } else if (typeAheadMode === true) { typeAheadMode = false; scope.dismissPopup(); } if (typeAheadMode === true) { if (scope.actualTypeAheadText.length >= 3 && scope.actualTypeAheadText !== scope.typeAheadText) { var personProfileListFilteredLength = $filter('filter')(scope.personProfileList, scope.actualTypeAheadText).length; var assetProfileListFilteredLength = $filter('filter')(scope.assetProfileList, scope.actualTypeAheadText).length; scope.showPopup = true; scope.showPopupHeader = true; if ((personProfileListFilteredLength + assetProfileListFilteredLength) > 0 || scope.typeAheadText.length === 0) { scope.typeAheadText = scope.actualTypeAheadText; scope.personProfileListFilteredLength = personProfileListFilteredLength; scope.assetProfileListFilteredLength = assetProfileListFilteredLength; } // If scope.actualTypeAheadText result in no match, keep the last non-empty list on the screen instead of showing nothing // This is the prefer behaviour else { // also check if more than 3 words has already been typed after the @ sign var textUptoCursor = scope.inputText.substring(typeAheadStartPos, typeAheadEndPos); var newTypeAheadWords = textUptoCursor.split(' '); if (newTypeAheadWords.length > 3 && newTypeAheadWords[newTypeAheadWords.length - 1] === "") { //If space in the end then don't consider it as word newTypeAheadWords.pop(); } if (newTypeAheadWords.length > 3) { // check any potential match before quiting. In the ideal world, we shouldn't need to do this check. // The NLP suppose to pick it up and highlight item accordingly. But we don't have that yet, so this // is the workaround. // check potential full name match first var name = newTypeAheadWords[0] + ' ' + newTypeAheadWords[1]; personProfileListFilteredLength = $filter('filter')(scope.personProfileList, name).length; if ((personProfileListFilteredLength) > 0) { highlightPotentialItem('@' + name, name); } // if full name not match, try lower the qualification with first or last name else { name = newTypeAheadWords[0]; personProfileListFilteredLength = $filter('filter')(scope.personProfileList, name).length; if ((personProfileListFilteredLength) > 0) { highlightPotentialItem('@' + name, name); } else { // check if there is any asset match assetProfileListFilteredLength = $filter('filter')(scope.assetProfileList, newTypeAheadWords[0]).length; if ((assetProfileListFilteredLength) > 0) { highlightPotentialItem('@' + newTypeAheadWords[0], newTypeAheadWords[0]); } } } // three words typed so far and user not selecting anything, automatically quit typeahead typeAheadMode = false; scope.dismissPopup(); return; } } scope.typeAheadListPos = 0; if ((typeAheadSearchString.length === 0) || (scope.actualTypeAheadText.indexOf(typeAheadSearchString) !== 0)) { typeAheadSearchString = scope.actualTypeAheadText; scope.typeAheadText = scope.actualTypeAheadText; if (scope.showPopup && scope.popupType === 'profile') { if (allowSearchByCompany) { smartRecorderModel.companySelectedByAgent = false; } smartRecorderModel.getListOfPersons(typeAheadSearchString, personChunkSize).then(onListOfPersonLoaded); //DRSMX-79936:skip asset search in Smart Recorder and Add comment section when skipAssetSearchInSmartRecorder is true if (!configurationModel.skipAssetSearchInSmartRecorder) { smartRecorderModel.getListOfAssets(typeAheadSearchString, searchChunkSize).then(onListOfAssetLoaded); } } return; } if (personTotalMatches > personChunkSize) { typeAheadSearchString = scope.actualTypeAheadText; scope.typeAheadText = scope.actualTypeAheadText; if (scope.showPopup && scope.popupType === 'profile') { if (allowSearchByCompany) { smartRecorderModel.companySelectedByAgent = false; } smartRecorderModel.getListOfPersons(typeAheadSearchString, personChunkSize).then(onListOfPersonLoaded); } } if (assetTotalMatches > searchChunkSize) { typeAheadSearchString = scope.actualTypeAheadText; scope.typeAheadText = scope.actualTypeAheadText; if (scope.showPopup && scope.popupType === 'profile') { smartRecorderModel.getListOfAssets(typeAheadSearchString, searchChunkSize).then(onListOfAssetLoaded); } } } else if (scope.actualTypeAheadText.length < 3) { scope.personProfileList = []; scope.assetProfileList = []; scope.typeAheadListPos = 0; scope.personProfileListFilteredLength = 0; scope.assetProfileListFilteredLength = 0; scope.typeAheadText = ''; scope.showPopupHeader = false; scope.actualTypeAheadText = ''; typeAheadSearchString = ''; assetTotalMatches = 0; personTotalMatches = 0; } } else if (scope.inputText.length === 0) { scope.dismissPopup(); } else if (scope.customerName) { $timeout.cancel(timerPromise); // the replace('\u00a0',' ') call is used to replace the ' ' character in Chrome and Safari var numWords = scope.inputText.trim().replace('\u00a0', ' ').split(' ').length - scope.customerName.split(' ').length; if (numWords > 0) { if (numWords % wordCountToStartSmartSearch === 0) { scope.smartSearch(); } else { timerPromise = $timeout(function () { if (typeAheadMode === false) { scope.smartSearch(); } }, pauseInterval); } } } }; scope.handleSmartInputEnter = function () { if (scope.showPopup === true) { if (scope.typeAheadListPos < scope.personProfileListFilteredLength && scope.personProfileListFilteredLength > 0) { var personProfileFilteredList = $filter('filter')(scope.personProfileList, scope.typeAheadText); var personProfile = personProfileFilteredList[scope.typeAheadListPos]; if (scope.popupType === 'profile') { scope.profileSelected(personProfile, 'person', personProfile.fullName, personProfile.id, 'customer', 'common.label.customer'); } else { scope.templateSelected(personProfile, 'incidentTemplate'); } } else if (scope.typeAheadListPos >= scope.personProfileListFilteredLength && scope.assetProfileListFilteredLength > 0) { var assetProfileFilteredList = $filter('filter')(scope.assetProfileList, scope.typeAheadText); var assetProfile = assetProfileFilteredList[scope.typeAheadListPos - scope.personProfileListFilteredLength]; if (scope.popupType === 'profile') { scope.profileSelected(assetProfile, 'asset', assetProfile.name, assetProfile.reconciliationId, 'affectedasset', 'common.label.asset'); } else { scope.templateSelected(assetProfile, 'servicerequestTemplate'); } } } }; scope.handleSmartInputHighlightSelected = function ($event) { if (typeAheadMode) { return; } // dismiss popup if it already opened highlightSelected = true; if (scope.showPopup) { scope.dismissPopup(); } var itemName = $event.currentTarget.firstChild.data; selectedElement = $event.currentTarget; scope.selectedText = itemName; scope.personProfileList = []; scope.assetProfileList = []; scope.showPopup = true; // get the list of potential match people records smartRecorderModel.getListOfPersons(itemName).then(onListOfPersonLoaded); if (!configurationModel.skipAssetSearchInSmartRecorder) { smartRecorderModel.getListOfAssets(itemName).then(onListOfAssetLoaded); } }; scope.profileSelected = function (profile, type, displayName, id) { if (typeAheadMode) { scope.selectedText = '@' + scope.actualTypeAheadText; highlightConfirmItem(scope.selectedText, profile, type, displayName, id); typeAheadMode = false; } else if (selectedElement) { highlightConfirmElement(selectedElement, profile, type, displayName, id); } scope.dismissPopup(); }; scope.dismissPopup = function () { if (highlightSelected || typeAheadMode) { return; } scope.showPopup = false; scope.personProfileList = []; scope.assetProfileList = []; scope.typeAheadText = ''; scope.typeAheadListPos = 0; scope.personProfileListFilteredLength = 0; scope.assetProfileListFilteredLength = 0; scope.showPopupHeader = false; typeAheadSearchString = ''; assetTotalMatches = 0; personTotalMatches = 0; }; //Unused function scope.personProfileMouseover = function ($index) { scope.typeAheadListPos = $index; }; //Unused function scope.assetProfileMouseover = function ($index) { scope.typeAheadListPos = scope.personProfileListFilteredLength + $index; }; function highlightPotentialItem(originalName, displayName) { var textArea = textAreaElement[0]; $.each(textArea.childNodes, function (nodeIndex, node) { // check every text node for matching NLP result in smartResultItem if (node.nodeName.toLowerCase() === '#text') { if (node.data.length > 1) { var startIndex = node.data.indexOf(originalName); if (startIndex > -1) { // found match, prepare range object for DOM update var rangeObj = document.createRange(); rangeObj.setStart(node, startIndex); rangeObj.setEnd(node, startIndex + originalName.length); // prepare span object to highlight matching object var elementToInsert = angular.element('' + displayName + ''); elementToInsert.addClass('smart-recorder-highlight'); elementToInsert.attr('ng-click', 'handleSmartInputHighlightSelected($event)'); elementToInsert.attr('contenteditable', contenteditableSetting); // compile the element to register the ng-click callback var linkFn = $compile(elementToInsert); var element = linkFn(scope); // delete the existing text rangeObj.deleteContents(); // replace it with the span object rangeObj.insertNode(element[0]); } } } }); } function highlightConfirmItem(originalName, profile, type, displayName, id) { var textArea = textAreaElement[0]; // When text blocks were cut and paste to the text area, individual adjacent text block could be formed // Need to normalize textArea to merge adjacent text nodes before processing if (textArea.normalize) { textArea.normalize(); } $.each(textArea.childNodes, function (nodeIndex, node) { // check every text node for matching if (node.nodeName.toLowerCase() === '#text') { if (node.data.length > 1) { var startIndex = node.data.indexOf(originalName); if (startIndex > -1) { // found match, prepare range object for DOM update var rangeObj = document.createRange(); rangeObj.setStart(node, startIndex); rangeObj.setEnd(node, startIndex + originalName.length); // prepare span object to highlight matching object var elementToInsert = angular.element('' + displayName + ''); elementToInsert .addClass('smart-recorder-highlightPerfectMatch') .attr('contenteditable', contenteditableSetting) .attr('tabindex', '-1') .data({ displayName: displayName, id: id + ((type === EntityVO.TYPE_ASSET) ? '*' + profile.classId : ''), type: (type === EntityVO.TYPE_ASSET) ? type : 'user' }); // compile the element to register the ng directive var linkFn = $compile(elementToInsert); var element = linkFn(scope); // delete the existing matching text rangeObj.deleteContents(); // and replace it with the span object with correct style rangeObj.insertNode(element[0]); scope.inputText = getInputText(); scope.inputTextHtml = textAreaElement.html(); // set focus var range = document.createRange(); var sel = window.getSelection(); var lastChild = textArea.lastChild; if (lastChild.nodeName.toLowerCase() === 'br') { lastChild = lastChild.previousSibling; } // (SW00467508) To fix issue with FF and IE, insert a ' ' character at the end. else if (lastChild.nodeName.toLowerCase() === '#text' && lastChild.data === '') { lastChild.data = '\u00a0'; } else if (lastChild.nodeName.toLowerCase() === 'span') { lastChild = document.createTextNode('\u00a0'); textArea.appendChild(lastChild); } range.setStart(lastChild, lastChild.length); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); textAreaElement.focus(); } } } }); } function highlightConfirmElement(selectedElement, profile, type, displayName, id) { var elmToReplace = $('' + displayName + ''); elmToReplace .addClass('smart-recorder-highlightPerfectMatch') .attr('contenteditable', contenteditableSetting) .data({ displayName: displayName, id: id + ((type === EntityVO.TYPE_ASSET) ? '*' + profile.classId : ''), type: (type === EntityVO.TYPE_ASSET) ? type : 'user' }); $(selectedElement).replaceWith(elmToReplace); scope.inputTextHtml = textAreaElement.html(); } function getInputText() { var textArea = textAreaElement[0]; var textAreaText = ''; $.each(textArea.childNodes, function (nodeIndex, node) { // check every text node for matching if (node.nodeName.toLowerCase() === 'br') { textAreaText += '\n'; } else { textAreaText += node.innerText || node.textContent; } if (browserIsMSIE) { if (node.nodeName.toLowerCase() === 'p') { textAreaText += '\n'; } } }); return textAreaText.replace(/[\r\n]+$/, ''); } function onListOfPersonLoaded(response) { var data = response.list; // Todo: backend currently not returning total match count, set it based on length of array for now if (((data.length >= personChunkSize && scope.actualTypeAheadText !== smartRecorderModel.personSearchString) || (scope.actualTypeAheadText.length < smartRecorderModel.personSearchString.length)) && scope.actualTypeAheadText.length >= 3) { typeAheadSearchString = scope.actualTypeAheadText; scope.typeAheadText = scope.actualTypeAheadText; smartRecorderModel.getListOfPersons(typeAheadSearchString, personChunkSize).then(onListOfPersonLoaded); } else { scope.personProfileList = data; personTotalMatches = data.length + 1; scope.personProfileListFilteredLength = $filter('filter')(scope.personProfileList, scope.typeAheadText).length; scope.typeAheadListPos = 0; highlightSelected = false; if (scope.personProfileList.length > 0 && typeAheadMode) { //back track the last type ahead text that show non-empty result var i = scope.actualTypeAheadText.length; var endPosition = scope.typeAheadText.length; while (i > 3 || i > endPosition) { var longestTypeAheadTextWithNonEmptyList = scope.actualTypeAheadText.substr(0, i); var filteredLength = $filter('filter')(scope.personProfileList, longestTypeAheadTextWithNonEmptyList).length; if (filteredLength > 0) { scope.personProfileListFilteredLength = filteredLength; scope.typeAheadText = longestTypeAheadTextWithNonEmptyList; break; } i--; } } } } function onListOfAssetLoaded(data) { if (((data.totalMatches > searchChunkSize && scope.actualTypeAheadText !== smartRecorderModel.assetSearchString) || (scope.actualTypeAheadText.length < smartRecorderModel.assetSearchString.length)) && scope.actualTypeAheadText.length >= 3) { typeAheadSearchString = scope.actualTypeAheadText; scope.typeAheadText = scope.actualTypeAheadText; smartRecorderModel.getListOfAssets(typeAheadSearchString, searchChunkSize).then(onListOfAssetLoaded); } else { scope.assetProfileList = data.objects; assetTotalMatches = data.totalMatches; scope.assetProfileListFilteredLength = $filter('filter')(scope.assetProfileList, scope.typeAheadText).length; scope.typeAheadListPos = 0; highlightSelected = false; if (scope.assetProfileList.length > 0 && typeAheadMode) { //back track the last type ahead text that show non-empty result var i = scope.actualTypeAheadText.length; var endPosition = scope.typeAheadText.length; while (i > 3 || i > endPosition) { var longestTypeAheadTextWithNonEmptyList = scope.actualTypeAheadText.substr(0, i); var filteredLength = $filter('filter')(scope.assetProfileList, longestTypeAheadTextWithNonEmptyList).length; if (filteredLength > 0) { scope.assetProfileListFilteredLength = filteredLength; scope.typeAheadText = longestTypeAheadTextWithNonEmptyList; break; } i--; } } } } scope.$on(events.HIDE_TICKET_DRAFT_LOADER, hideTicketDraftLoader); function hideTicketDraftLoader() { scope.state.savingNote = false; } } }; }]); })();