"use strict"; (function () { 'use strict'; /** * @ngdoc directive * @name myitsmApp:emailWindow * @restrict E * @desription Email windows are used for user to send email based on context of current ticket. It's a draggable window which contains * email controls bar. From email window one can search and include email recipients from ITSM DB . Once email is sent, it will also be logged to instance timeline */ angular.module('myitsmApp') //TODO: remove not use dependency .directive('emailWindow', ['$window', '$filter', '$timeout', '$state', 'ticketModel', 'emailModel', 'systemAlertService', 'events', 'accelerators', 'i18nService', 'relationModel', 'browser', function ($window, $filter, $timeout, $state, ticketModel, emailModel, systemAlertService, events, accelerators, i18nService, relationModel, browser) { return { restrict: 'E', templateUrl: 'components/email/email-window.html', replace: true, scope: { emailInstance: '=', isPopupWindow: '@' }, link: function (scope, $element) { var $textArea = $element.find('textarea.email__message-editor'), typeAheadMode = false, gotFullMatch = false, actualTypeAheadText, expressionStart = 0, expressionEnd = 0; var keyCodes = { enter: 13, escape: 27, tab: 9, leftArrow: 37, rightArrow: 39, upArrow: 38, downArrow: 40, space: 32 }; var preventDefaultForKeys = [keyCodes.leftArrow, keyCodes.rightArrow, keyCodes.enter, keyCodes.upArrow, keyCodes.downArrow, keyCodes.tab]; var popupMode = !!scope.isPopupWindow; if (popupMode) { $window.document.body.style.minWidth = 0; $window.document.body.style.overflow = 'hidden'; scope.emailContext = scope.emailInstance.parentEmailContext; } else { $element.draggable({ containment: 'parent', scroll: false, handle: '.drag-handle' }); scope.emailContext = { headerText: '', recipientInputText: '', senderList: scope.emailInstance.senderList || [], subjectText: '', messageBody: '' }; if (scope.emailInstance.ticketData.length > 1) { scope.emailContext.ticketList = _.map(scope.emailInstance.ticketData, function (ticket) { return { id: ticket.id, type: ticket.type }; }); } else { scope.emailContext.ticketType = scope.emailInstance.ticketData[0].type; scope.emailContext.ticketId = scope.emailInstance.ticketData[0].id; } } scope.sendingEmail = false; scope.showArticles = false; scope.articlesLoading = false; scope.emailActions = {}; scope.accelerators = { expression: '', showAcceleratorsList: false, availableExpressions: [] }; if (!scope.emailContext.ticketList) { scope.accelerators.availableExpressions = _.filter(accelerators.ticket, function (accelerator) { if (accelerator.onlyFor && accelerator.onlyFor.indexOf(scope.emailInstance.ticketData[0].type) < 0) { return false; } accelerator.expr = '!' + i18nService.getLocalizedString(accelerator.key); accelerator.text = scope.$eval(accelerator.valueExpr, { exprContext: scope.emailInstance.ticketData[0] }); return accelerator.text ? accelerator.text.trim() : false; }); } /*List of available email window actions*/ scope.emailControls = [ { iconClass: 'icon-trash', name: 'deleteEmail', inactive: false }, { iconClass: 'icon-pop_up', name: 'popoutEmail', inactive: !!scope.isPopupWindow } ]; /*Email controls click event transmitter. Uses controls item, to execute applicable click handler */ scope.handleEmailControlsClick = function ($e, action) { return scope.emailActions[action.name](); }; //Backend allows 255 bytes, not 255 characters, so checking input length in bytes. scope.checkByteLength = function (maxSize) { if (scope.emailContext.subjectText.length > maxSize) { scope.emailContext.subjectText = scope.emailContext.subjectText.substring(0, maxSize); } var byteLength = new Blob([scope.emailContext.subjectText]).size, strLength = scope.emailContext.subjectText.length; while (byteLength > maxSize) { strLength = strLength - Math.ceil((byteLength - strLength) / 4); //Max length of a character in UTF-8 is 4 byte currently scope.emailContext.subjectText = scope.emailContext.subjectText.substring(0, strLength); byteLength = new Blob([scope.emailContext.subjectText]).size; } }; /** * @ngdoc method * @name myitsmApp.emailWindow#leaveEmail * @methodOf myitsmApp.emailWindow * * @description * Action can be triggered from email controls, by clicking leave email button. It prompts * confirmation modal to ensure user is intended to delete the email. * */ scope.emailActions.deleteEmail = function () { var modalSettings = { title: $filter('i18n')('email.deleteEmailModal.title'), text: $filter('i18n')('email.deleteEmailModal.label'), buttons: [ { text: $filter('i18n')('email.button.keepEmail') }, { text: $filter('i18n')('email.button.deleteEmail'), data: { delete: true } } ] }; var modalInstance = systemAlertService.modal(modalSettings); modalInstance.result.then(function (data) { if (!_.isEmpty(data)) { if (data.delete) { if (scope.isPopupWindow) { $window.close(); } emailModel.deleteEmailWindow(scope.emailInstance); } } }); }; scope.emailActions.popoutEmail = function () { scope.emailInstance.parentEmailContext = scope.emailContext; $window.popupDetails = { emailWindow: scope.emailInstance }; var width = Math.round((screen.width / 3) / 10) * 10, height = Math.round(screen.height * 70 / 100); $window.open('#/emailPopupWindow', '', 'location=no, scrollbars=no, resizable=yes, width=' + width + ', height=' + height); emailModel.deleteEmailWindow(scope.emailInstance); }; scope.sendEmail = function () { var inputValue = $element.find('.email__recipient-list span').text(), rootScope = scope.$root, validEmailInput = false; if (inputValue) { validEmailInput = validateEmail(inputValue); } var internalRecip = _.filter(scope.emailContext.senderList, { internal: true }), externalRecip = _.filter(scope.emailContext.senderList, { internal: false }); if (validEmailInput) { externalRecip.push({ email: inputValue, internal: false }); } try { rootScope = window.opener.angular.element('body').scope(); } catch (e) { } scope.sendingEmail = true; emailModel.sendEmail(scope.emailContext, internalRecip, externalRecip) .finally(function () { _.delay(function () { rootScope.$broadcast(events.REFRESH_ACTIVITY_FEED); }, 400); emailModel.deleteEmailWindow(scope.emailInstance); if (scope.isPopupWindow) { window.close(); } }); }; scope.handleEmailBodyKeyDown = function ($event) { // disable for bulk email if (scope.emailContext.ticketList) { return; } if (($event.which === 49 || $event.keyCode === 49) && $event.shiftKey) { scope.accelerators.showAcceleratorsList = true; expressionStart = $event.target.selectionStart; typeAheadMode = true; // Tere is a open defect on IE 11, for incorrect definition of selectionStart propery after Enter click // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14692189/ // Workaround for IE if (browser.isIE11 && $textArea[0].value.length < expressionStart) { expressionStart = $textArea[0].value.length; } } if (scope.accelerators.showAcceleratorsList && (preventDefaultForKeys.indexOf($event.keyCode) >= 0 || ($event.keyCode == keyCodes.space && gotFullMatch))) { $event.preventDefault(); } }; scope.handleEmailBodyChange = function ($event) { // disable for bulk email if (scope.emailContext.ticketList) { return; } expressionEnd = $event.target.selectionEnd; $textArea = $element.find('textarea.email__message-editor'); var textArea = $textArea[0]; if (textArea.value.length > 1 && typeAheadMode && (expressionStart > expressionEnd)) { scope.accelerators.showAcceleratorsList = false; return; } // 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(); } if (scope.accelerators.showAcceleratorsList) { if (gotFullMatch && [keyCodes.tab, keyCodes.rightArrow, keyCodes.space].indexOf($event.keyCode) >= 0) { scope.insertAcceleratorText(scope.acceleratorsList[0]); gotFullMatch = false; return; } if (typeAheadMode && ($event.keyCode == keyCodes.tab)) { var emailText = textArea.value; (scope.typeAheadListPos == scope.acceleratorsList.length - 1) ? scope.typeAheadListPos = 0 : scope.typeAheadListPos++; var selectedItem = scope.acceleratorsList[scope.typeAheadListPos] ? scope.acceleratorsList[scope.typeAheadListPos].expr : ''; var updatedText = [emailText.slice(0, expressionStart), selectedItem, emailText.slice(expressionEnd)].join(''); expressionEnd = parseInt(expressionStart + selectedItem.length); actualTypeAheadText = updatedText.substring(expressionStart, expressionEnd); scope.emailContext.messageBody = updatedText; $timeout(function () { textArea.setSelectionRange(expressionStart, expressionEnd); textArea.selectionStart = expressionStart; textArea.selectionEnd = expressionEnd; }); return; } // ignore if control or alternate key was pressed if ($event.ctrlKey === true || $event.altKey === true || $event.keyCode === keyCodes.leftArrow || $event.keyCode === keyCodes.rightArrow) { return; } else if ($event.keyCode === keyCodes.upArrow) { // up key if (scope.typeAheadListPos > 0) { scope.typeAheadListPos--; } return; } else if ($event.keyCode === keyCodes.downArrow) { // down key if (scope.typeAheadListPos < (scope.acceleratorsList.length - 1)) { scope.typeAheadListPos++; } return; } else if ($event.keyCode === keyCodes.enter) { // enter key scope.accelerators.expression = actualTypeAheadText; scope.insertAcceleratorText(); return; } else if ($event.keyCode === keyCodes.escape) { scope.disableTypeAheadMode(); return; } } if (scope.emailContext.messageBody.length > 0) { if (typeAheadMode) { actualTypeAheadText = textArea.value.substring(expressionStart, expressionEnd); scope.typeAheadListPos = 0; } } else if (typeAheadMode) { scope.hideTypeAheadPopup(); } if (typeAheadMode) { if (actualTypeAheadText.length > 0 && actualTypeAheadText != scope.accelerators.expression) { scope.acceleratorsList = $filter('filter')(scope.accelerators.availableExpressions, { expr: actualTypeAheadText.substring(1) }); if (scope.acceleratorsList.length > 0 || scope.accelerators.expression.length == 0) { (scope.acceleratorsList.length > 0) ? scope.accelerators.showAcceleratorsList = true : angular.noop(); scope.accelerators.expression = actualTypeAheadText; scope.popupLeftPosition = 0; scope.popupTopPosition = 0; if (scope.acceleratorsList.length === 1) { var accelerator = scope.acceleratorsList[0], regExpr = new RegExp(accelerator.expr, 'gi'); gotFullMatch = regExpr.test(actualTypeAheadText); if (gotFullMatch) { scope.emailContext.messageBody.replace(regExpr, accelerator.expr); textArea.setSelectionRange(expressionStart, expressionEnd); textArea.selectionStart = expressionStart; textArea.selectionEnd = expressionEnd; } } } else if (scope.acceleratorsList.length === 0 && scope.accelerators.expression.length >= 1) { scope.hideTypeAheadPopup(); scope.accelerators.expression = ""; } } } }; scope.handleEmailBodyClick = function ($event) { expressionEnd = $event.target.selectionEnd; $textArea = $element.find('textarea.email__message-editor'); if ($textArea[0] && $textArea[0].value.length > 1 && scope.accelerators.showAcceleratorsList && expressionStart > expressionEnd) { scope.accelerators.showAcceleratorsList = false; return; } }; scope.handleTextPaste = function ($event) { $textArea = $element.find('textarea.email__message-editor'); var content; $event.preventDefault(); $event.stopImmediatePropagation(); 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 } var parsedContentVersions = [content]; _(scope.accelerators.availableExpressions).forEach(function (accelerator, index) { var updatedText = parsedContentVersions[index]; var regExpr = new RegExp(accelerator.expr, 'gi'), replaceText = accelerator.text; parsedContentVersions.push(updatedText.replace(regExpr, replaceText)); }); var emailText = $textArea[0].value, start = 0, end = 0; if ($event.target.selectionStart == $event.target.selectionEnd && $event.target.selectionStart > 0) { start = end = $event.target.selectionStart; } else if ($event.target.selectionStart != $event.target.selectionEnd) { if ($event.target.selectionStart > $event.target.selectionEnd) { start = $event.target.selectionEnd; end = $event.target.selectionStart; } else { start = $event.target.selectionStart; end = $event.target.selectionEnd; } } var pasteText = parsedContentVersions.pop(); scope.emailContext.messageBody = emailText.substring(0, start) + pasteText + emailText.substring(end, emailText.length); }; scope.acceleratorMouseover = function ($index) { scope.typeAheadListPos = $index; }; scope.insertAcceleratorText = function (selectedItem) { var emailText = $textArea[0].value, accelerator = selectedItem || scope.acceleratorsList[scope.typeAheadListPos], insertText = scope.$eval(accelerator.valueExpr, { exprContext: scope.emailInstance.ticketData[0] }), updatedText = emailText; if (scope.accelerators.expression) { if (expressionStart === 0) { updatedText = insertText + ' '; } else { insertText += ' '; updatedText = [emailText.slice(0, expressionStart), insertText, emailText.slice(expressionEnd)].join(''); } } else { if (emailText.length === 0) { updatedText = insertText; } else { if (expressionEnd > 0) { updatedText = [emailText.slice(0, expressionEnd), ' ' + insertText + ' ', emailText.slice(expressionEnd)].join(''); } else { updatedText += ' ' + insertText; } } } scope.disableTypeAheadMode(); scope.emailContext.messageBody = updatedText; }; scope.hideTypeAheadPopup = function () { scope.accelerators.showAcceleratorsList = false; }; scope.disableTypeAheadMode = function () { actualTypeAheadText = ''; scope.accelerators.expression = ''; scope.accelerators.showAcceleratorsList = false; scope.typeAheadListPos = 0; scope.acceleratorsList = null; typeAheadMode = false; expressionStart = 0; expressionEnd = 0; }; scope.acceleratorsHelp = function () { actualTypeAheadText = '!'; scope.accelerators.expression = ''; scope.accelerators.showAcceleratorsList = true; scope.typeAheadListPos = 0; scope.acceleratorsList = scope.accelerators.availableExpressions; expressionStart = scope.emailContext.messageBody.length; $textArea = $element.find('textarea.email__message-editor'); }; scope.addEmailAttachment = function (fileInput) { if (scope.emailContext.attachment) { return false; } scope.emailContext.attachment = emailModel.addAttachment(fileInput); }; scope.openPinnedArticles = function () { //scope.articlesLoading = true; scope.showArticles = true; }; scope.selectResource = function ($event, resource) { $event.stopPropagation(); scope.emailContext.kba = (scope.emailContext.kba === resource) ? null : resource; }; scope.isResourceSelected = function (resource) { return (scope.emailContext.kba && scope.emailContext.kba.displayId === resource.displayId); }; scope.attachKBA = function () { scope.showArticles = false; }; scope.showEmailBody = function () { scope.emailContext.kba = null; scope.showArticles = false; }; scope.removeKBAttachment = function () { scope.emailContext.kba = null; }; scope.removeEmailAttachment = function () { if (scope.emailContext.attachment) { scope.emailContext.attachment = null; } }; scope.handleAttachClick = function () { setTimeout(function () { document.getElementById('email-ka-attach-btn').click(); }, 0); }; function init() { scope.showPopOver = false; var idField, summaryField; if (scope.emailInstance.ticketData) { if (scope.emailInstance.ticketData.length === 1 && scope.emailInstance.ticketData[0].type === EntityVO.TYPE_INCIDENT) { scope.showPopOver = true; relationModel.getRelations(scope.emailInstance.ticketData[0].id, scope.emailInstance.ticketData[0].type).then(function () { scope.pinnedKBA = _.filter(relationModel.cache[scope.emailInstance.ticketData[0].id], function (item) { return item.type === EntityVO.TYPE_KNOWLEDGE && item.realObject.internalUse === false && !item.isDecisionTree(); }); scope.disableKBAttachment = (scope.pinnedKBA.length) ? false : true; scope.showArticles = false; }); } if (scope.emailInstance.ticketData[0].type === EntityVO.TYPE_KNOWLEDGE) { idField = 'articleId'; summaryField = 'title'; } else if (scope.emailInstance.ticketData[0].type === EntityVO.TYPE_SBEREQUEST) { idField = 'id'; summaryField = 'serviceName'; } else { idField = 'displayId'; summaryField = 'summary'; } if (scope.emailInstance.ticketData.length > 1) { scope.emailContext.headerText = _.map(scope.emailInstance.ticketData, function (ticket) { return ticket[idField]; }).join('; '); } else { scope.emailContext.headerText = scope.emailInstance.ticketData[0][idField] + ' : ' + scope.emailInstance.ticketData[0][summaryField]; } //scope.emailContext.headerText = scope.emailInstance.ticketData.displayId + ': ' + scope.emailInstance.ticketData.summary; scope.emailContext.subjectText = scope.emailContext.headerText; } } function validateEmail(email) { var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return re.test(email); } init(); } }; }]); }());