1006 lines
62 KiB
JavaScript
1006 lines
62 KiB
JavaScript
"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',
|
|
function ($http, $templateCache, $compile, $window, events, attachmentService, $filter, smartRecorderModel, $timeout, metadataModel, systemAlertService, feedModel, userModel, i18nService, configurationModel) {
|
|
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: '=?'
|
|
},
|
|
link: function (scope, $elem) {
|
|
var textAreaElement;
|
|
var defaultWorknoteTypeName = 'General Information';
|
|
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: !_.contains([EntityVO.TYPE_KNOWLEDGE, EntityVO.TYPE_SERVICEREQUEST], scope.type),
|
|
isAttachEnabled: !scope.attachmentDisabled,
|
|
isPostButtonVisible: !scope.isDraft,
|
|
noteFormIsActive: false,
|
|
savingNote: false,
|
|
access: false,
|
|
shareWithVendor: 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.state.isMultipleVendorTickets = _.isArray(vendorInfo) && vendorInfo.length > 1;
|
|
scope.updateShareWithVendorFlag();
|
|
}
|
|
});
|
|
scope.updateVendorTicketList = function (filterOption) {
|
|
filterOption.selected = !filterOption.selected;
|
|
scope.updateShareWithVendorFlag();
|
|
};
|
|
scope.updateShareWithVendorFlag = function () {
|
|
scope.state.shareWithVendor = _.some(scope.parentContext.vendorInfo, { 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.selectedWorknoteType = _.find(scope.worknoteTypes, { name: defaultWorknoteTypeName }) || {};
|
|
scope.attachments = [];
|
|
scope.addFlagNote = true;
|
|
scope.flag = data.flag;
|
|
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.selectedWorknoteType = _.find(scope.worknoteTypes, { name: defaultWorknoteTypeName }) || {};
|
|
scope.toggleNoteForm();
|
|
scope.$apply();
|
|
}
|
|
function keyDownKeyPressHandler(event) {
|
|
if (event.which === 13) {
|
|
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) {
|
|
if (elem && elem.target && (elem.target.innerText === $filter('i18n')('timeline.note.post.label') || elem.target.innerText === $filter('i18n')('timeline.note.cancel'))) {
|
|
textAreaElement && textAreaElement.empty();
|
|
scope.commentFormNode.hide();
|
|
scope.state.noteFormIsActive = false;
|
|
scope.inputText = '';
|
|
}
|
|
typeAheadMode = false;
|
|
scope.dismissPopup();
|
|
scope.state.shareWithVendor = false;
|
|
if (scope.parentContext && scope.parentContext.vendorInfo && _.isArray(scope.parentContext.vendorInfo)) {
|
|
_.forEach(scope.parentContext.vendorInfo, function (vendorTicket) {
|
|
vendorTicket.selected = false;
|
|
});
|
|
}
|
|
scope.state.unflagging = false;
|
|
scope.attachments = [];
|
|
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
|
|
angular.element('#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);
|
|
}).catch(function (error) {
|
|
if (error) {
|
|
systemAlertService.error({
|
|
text: error.data.detailMessage || error.data.error || error,
|
|
clear: false
|
|
});
|
|
}
|
|
})
|
|
.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(/<p>/g, '').replace(/<\/p>/g, '<br><br>');
|
|
}
|
|
textAreaElm[0].innerHTML = textAreaElm[0].innerHTML.replace(/<br>/g, '\n');
|
|
return cleanUpTextBeforePost(textAreaElm.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.parentContext.vendorInfo)) {
|
|
if (scope.parentContext.vendorInfo.length === 1) {
|
|
noteData.vendorTicketId = _.map(scope.parentContext.vendorInfo, mapVendorUrl).join() || '';
|
|
}
|
|
else if (scope.parentContext.vendorInfo.length > 1) {
|
|
noteData.vendorTicketId = _(scope.parentContext.vendorInfo)
|
|
.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;
|
|
}
|
|
return noteData;
|
|
}
|
|
else {
|
|
return {};
|
|
}
|
|
}
|
|
///////////////////////////// @ MENTION
|
|
var typeAheadMode = false;
|
|
var highlightSelected = false;
|
|
var typeAheadSearchString = '';
|
|
var pauseInterval = 2000; //2 sec pause
|
|
var wordCountToStartSmartSearch = 8;
|
|
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 <div> instead of <br> on enter key within contenteditible div which cause a
|
|
// lot of issues with the highlighted span. Here we force it to be <br> for all browsers.
|
|
// IE has similar issue, it insert <p> instead of <br>
|
|
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('insertText')) {
|
|
document.execCommand('insertText', false, content);
|
|
}
|
|
else if (document.queryCommandSupported('paste')) {
|
|
document.execCommand('paste', false, content);
|
|
}
|
|
else {
|
|
return; // not support, let the default handle it
|
|
}
|
|
}
|
|
scope.handleSmartInputChange();
|
|
};
|
|
scope.handleSmartInputChange = function ($event) {
|
|
var typeAheadStartPos;
|
|
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();
|
|
}
|
|
scope.inputTextHtml = textAreaElement.html();
|
|
scope.inputText = getInputText();
|
|
// ensure there is already a child <br> 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 or alternate key was pressed
|
|
if ($event.ctrlKey === true || $event.altKey === true) {
|
|
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.indexOf(' @');
|
|
if (typeAheadStartPos === -1) {
|
|
typeAheadStartPos = scope.inputText.indexOf('\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';
|
|
}
|
|
}
|
|
if (typeAheadStartPos > -1) {
|
|
scope.actualTypeAheadText = scope.inputText.substring(typeAheadStartPos);
|
|
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 newTypeAheadWords = scope.actualTypeAheadText.split(' ');
|
|
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') {
|
|
smartRecorderModel.getListOfPersons(typeAheadSearchString, searchChunkSize).then(onListOfPersonLoaded);
|
|
smartRecorderModel.getListOfAssets(typeAheadSearchString, searchChunkSize).then(onListOfAssetLoaded);
|
|
}
|
|
return;
|
|
}
|
|
if (personTotalMatches > searchChunkSize) {
|
|
typeAheadSearchString = scope.actualTypeAheadText;
|
|
scope.typeAheadText = scope.actualTypeAheadText;
|
|
if (scope.showPopup && scope.popupType === 'profile') {
|
|
smartRecorderModel.getListOfPersons(typeAheadSearchString, searchChunkSize).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 = jQuery.trim(scope.inputText).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);
|
|
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('<span>' + displayName + '</span>');
|
|
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('<span>' + displayName + '</span>');
|
|
elementToInsert
|
|
.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'
|
|
});
|
|
// 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 = $('<span>' + displayName + '</span>');
|
|
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 >= searchChunkSize && scope.actualTypeAheadText !== smartRecorderModel.personSearchString) || (scope.actualTypeAheadText.length < smartRecorderModel.personSearchString.length)) && scope.actualTypeAheadText.length >= 3) {
|
|
typeAheadSearchString = scope.actualTypeAheadText;
|
|
scope.typeAheadText = scope.actualTypeAheadText;
|
|
smartRecorderModel.getListOfPersons(typeAheadSearchString, searchChunkSize).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;
|
|
}
|
|
}
|
|
};
|
|
}]);
|
|
})();
|