diff --git a/app/css/app.css b/app/css/app.css index 9a07986a..e21fdbea 100644 --- a/app/css/app.css +++ b/app/css/app.css @@ -1915,53 +1915,7 @@ img.img_fullsize { -/* Emoji area */ -.emoji-wysiwyg-editor:empty:before { - content: attr(placeholder); - color: #9aa2ab; -} -.emoji-wysiwyg-editor:active:before, -.emoji-wysiwyg-editor:focus:before { - content: none; -} - -.emoji-wysiwyg-editor { - box-sizing: content-box; - -moz-box-sizing: content-box; - font-size: 12px; - margin-bottom: 10px; - padding: 6px; - min-height: 38px; - height: auto; - width: auto; - max-height: 284px; - overflow: auto; - line-height: 17px; - - border: 1px solid #d2dbe3; - border-radius: 2px; - -webkit-box-shadow: none; - box-shadow: none; - -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; - transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; - - -webkit-user-select: text; - word-wrap: break-word; - - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; -} -.emoji-wysiwyg-editor img { - width: 20px; - height: 20px; - vertical-align: middle; - margin: -3px 0 0 0; -} - - - +/* Message composer */ .composer_emoji_insert_btn { display: block; position: absolute; @@ -2001,6 +1955,18 @@ img.img_fullsize { background: #FFF; padding: 5px 2px 5px 5px; } +.composer_emoji_tooltip_shown { + display: block; +} +.composer_emoji_tooltip_tail { + position: absolute; + bottom: -14px; + left: 50%; + margin-left: -13px; + overflow: hidden; + width: 26px; + height: 14px; +} .icon-tooltip-tail { background: #FFF; width: 18px; @@ -2019,10 +1985,10 @@ img.img_fullsize { -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; - position: absolute; - bottom: -10px; - left: 50%; - margin-left: -9px; + position: relative; + top: -8px; + left: 4px; + } .composer_emoji_tooltip_tabs { @@ -2089,8 +2055,6 @@ a.composer_emoji_btn:hover { background-color: #edf2f5; } - - .emoji { display: -moz-inline-box; -moz-box-orient: vertical; @@ -2128,7 +2092,6 @@ a.composer_emoji_btn:hover { background-image: url('../img/emojisprite_4.png'); } - .emoji-w20 { width: 20px; height: 20px; @@ -2141,6 +2104,18 @@ a.composer_emoji_btn:hover { .emoji-w20.emoji-spritesheet-3 {background-size: 680px 60px;} .emoji-w20.emoji-spritesheet-4 {background-size: 680px 140px;} +.emoji-w26 { + width: 26px; + height: 26px; + vertical-align: middle; + display: inline-block; +} +.emoji-w26.emoji-spritesheet-0 {background-size: 702px 182px;} +.emoji-w26.emoji-spritesheet-1 {background-size: 754px 104px;} +.emoji-w26.emoji-spritesheet-2 {background-size: 858px 182px;} +.emoji-w26.emoji-spritesheet-3 {background-size: 884px 78px;} +.emoji-w26.emoji-spritesheet-4 {background-size: 884px 182px;} + .composer_dropdown { display: none; /*max-width: 100%;*/ @@ -2186,6 +2161,55 @@ a.composer_emoji_btn:hover { max-height: 64px; } +.composer_rich_textarea { + box-sizing: content-box; + -moz-box-sizing: content-box; + font-size: 12px; + margin-bottom: 10px; + padding: 6px; + min-height: 38px; + height: auto; + width: auto; + max-height: 284px; + overflow: auto; + line-height: 17px; + + border: 1px solid #d2dbe3; + border-radius: 2px; + -webkit-box-shadow: none; + box-shadow: none; + -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + + -webkit-user-select: text; + word-wrap: break-word; + + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.composer_rich_textarea:empty:before { + content: attr(placeholder); + color: #9aa2ab; +} +.composer_rich_textarea:active:before, +.composer_rich_textarea:focus:before { + content: none; +} +.composer_rich_textarea:empty:active:before, +.composer_rich_textarea:empty:focus:before { + content: attr(placeholder); + color: #9aa2ab; +} + +.composer_rich_textarea img { + width: 20px; + height: 20px; + vertical-align: middle; + margin: -3px 0 0 0; +} .error_modal_window .modal-dialog { diff --git a/app/css/desktop.css b/app/css/desktop.css index 5bbb607f..6a7d3d4d 100644 --- a/app/css/desktop.css +++ b/app/css/desktop.css @@ -963,7 +963,7 @@ a.im_panel_peer_photo .peer_initials { opacity: 1; } .composer_emoji_insert_btn:active .icon-emoji, -.composer_emoji_insert_btn.on .icon-emoji { +.composer_emoji_insert_btn.composer_emoji_insert_btn_on .icon-emoji { background-position: -9px -367px; opacity: 1; } @@ -986,8 +986,8 @@ a.im_panel_peer_photo .peer_initials { .im_send_field_wrap { margin-bottom: 15px; } -.emoji-wysiwyg-editor, -.form-control.im_message_field { +.composer_rich_textarea, +.composer_textarea { border-radius: 0; border: 0; box-shadow: none; @@ -999,8 +999,8 @@ a.im_panel_peer_photo .peer_initials { line-height: 20px; height: auto; } -.emoji-wysiwyg-editor:focus, -.form-control.im_message_field:focus { +.composer_rich_textarea:focus, +.composer_textarea:focus { border: 0; box-shadow: none; outline: none; diff --git a/app/css/mobile.css b/app/css/mobile.css index e02939ec..1e84e566 100644 --- a/app/css/mobile.css +++ b/app/css/mobile.css @@ -1105,7 +1105,7 @@ a.mobile_modal_action .tg_checkbox_label { display: block; } -.im_emoji_btn { +.composer_emoji_insert_btn { position: absolute; left: 0; top: 0; @@ -1127,10 +1127,10 @@ a.mobile_modal_action .tg_checkbox_label { .is_1x .icon-emoji { background-image: url(../img/icons/IconsetW_1x.png); } -.im_emoji_btn:active .icon-emoji, -.is_1x .im_emoji_btn:active .icon-emoji, -.im_emoji_btn.on .icon-emoji, -.is_1x .im_emoji_btn.on .icon-emoji { +.composer_emoji_insert_btn:active .icon-emoji, +.is_1x .composer_emoji_insert_btn:active .icon-emoji, +.composer_emoji_insert_btn.on .icon-emoji, +.is_1x .composer_emoji_insert_btn.on .icon-emoji { background-position: -10px -803px; } @@ -1141,21 +1141,21 @@ a.mobile_modal_action .tg_checkbox_label { .im_send_field_wrap .form-control { border-radius: 4px; } -.emoji-wysiwyg-editor { +.composer_rich_textarea { min-height: 18px; max-height: 136px; margin-bottom: 0; font-size: 15px; } -.emoji-wysiwyg-editor:empty { +.composer_rich_textarea:empty { background: #f1f1f1; border-color: #f1f1f1; } -.emoji-wysiwyg-editor:empty:before { +.composer_rich_textarea:empty:before { color: #999; } -.emoji-wysiwyg-editor:active, -.emoji-wysiwyg-editor:focus { +.composer_rich_textarea:active, +.composer_rich_textarea:focus { box-shadow: none; outline: 0; background: #FFF; @@ -1164,17 +1164,33 @@ a.mobile_modal_action .tg_checkbox_label { } -.emoji-menu { - margin-left: -20px; - margin-top: -202px; +.composer_emoji_tooltip { + margin-left: 10px; + margin-top: -175px; width: 262px; } -.emoji-menu .emoji-items-wrap { +.composer_emoji_tooltip .composer_emoji_tooltip_content { height: 106px; } -.emoji-menu .emoji-items a { +.composer_emoji_tooltip .composer_emoji_tooltip_content .composer_emoji_btn { padding: 5px; } +.icon-tooltip-tail { + display: none; +} +.composer_emoji_tooltip_tab { + margin: 0 5px; +} + +.composer_sticker_btn { + width: 80px; + height: 80px; + padding: 3px; +} +.composer_sticker_image { + max-width: 64px; + max-height: 64px; +} .contacts_modal_search { diff --git a/app/index.html b/app/index.html index e98f1f7c..1164326e 100644 --- a/app/index.html +++ b/app/index.html @@ -48,7 +48,6 @@ - @@ -83,8 +82,8 @@ PRODUCTION_ONLY_END--> - + diff --git a/app/js/controllers.js b/app/js/controllers.js index 333ee3e7..278efae4 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -1448,10 +1448,10 @@ angular.module('myApp.controllers', ['myApp.i18n']) var text = $scope.draftMessage.text; if (angular.isString(text) && text.length > 0) { - text = text.replace(/:([a-z0-9\-\+\*_]+?):/gi, function (all, name) { - var utfChar = $.emojiarea.reverseIcons[name]; - if (utfChar !== undefined) { - return utfChar; + text = text.replace(/:([a-z0-9\-\+\*_]+?):/gi, function (all, shortcut) { + var emojiCode = EmojiHelper.shortcuts[shortcut]; + if (emojiCode !== undefined) { + return EmojiHelper.emojis[emojiCode][0]; } return all; }); @@ -1506,10 +1506,10 @@ angular.module('myApp.controllers', ['myApp.i18n']) var backupDraftObj = {}; backupDraftObj['draft' + $scope.curDialog.peerID] = newVal; Storage.set(backupDraftObj); - // console.log('draft save', backupDraftObj); + // console.log(dT(), 'draft save', backupDraftObj); } else { Storage.remove('draft' + $scope.curDialog.peerID); - // console.log('draft delete', 'draft' + $scope.curDialog.peerID); + // console.log(dT(), 'draft delete', 'draft' + $scope.curDialog.peerID); } } diff --git a/app/js/directives.js b/app/js/directives.js index 0824928b..6899fc1d 100755 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -1072,7 +1072,15 @@ angular.module('myApp.directives', ['myApp.filters']) function link ($scope, element, attrs) { + var messageField = $('textarea', element)[0]; var emojiButton = $('.composer_emoji_insert_btn', element)[0]; + var emojiPanel = $('.composer_emoji_panel', element)[0]; + var fileSelects = $('input', element); + var dropbox = $('.im_send_dropbox_wrap', element)[0]; + var messageFieldWrap = $('.im_send_field_wrap', element)[0]; + var dragStarted, dragTimeout; + var submitBtn = $('.im_submit', element)[0]; + new EmojiTooltip(emojiButton, { getStickers: function (callback) { AppStickersManager.getStickers().then(function () { @@ -1093,153 +1101,77 @@ angular.module('myApp.directives', ['myApp.filters']) } }); - var emojiPanel = $('.composer_emoji_panel', element)[0]; - new EmojiPanel(emojiPanel, { - onEmojiSelected: function (code) { - composer.onEmojiSelected(code); - } - }); - - - var messageField = $('textarea', element)[0]; - var composer = new MessageComposer(messageField, {}); - - return; - + var composerEmojiPanel; + if (emojiPanel) { + composerEmojiPanel = new EmojiPanel(emojiPanel, { + onEmojiSelected: function (code) { + composer.onEmojiSelected(code); + } + }); + } - var messageField = $('textarea', element)[0], - fileSelects = $('input', element), - dropbox = $('.im_send_dropbox_wrap', element)[0], - emojiButton = $('.im_emoji_btn', element)[0], - emojiQuickSelect = !Config.Mobile ? $('.im_emoji_quick_select_area', element)[0] : false, - editorElement = messageField, - dragStarted, dragTimeout, - emojiArea = $(messageField).emojiarea({button: emojiButton, norealTime: true, quickSelect: emojiQuickSelect}), - emojiMenu = $('.emoji-menu', element)[0], - submitBtn = $('.im_submit', element)[0], - richTextarea = $('.emoji-wysiwyg-editor', element)[0]; + var composer = new MessageComposer(messageField, { + onTyping: function () { + $scope.$emit('ui_typing'); + }, + getSendOnEnter: function () { + return sendOnEnter; + }, + onMessageSubmit: onMessageSubmit, + onFilesPaste: onFilesPaste + }); + var richTextarea = composer.richTextareaEl[0]; if (richTextarea) { - editorElement = richTextarea; - $(richTextarea).addClass('form-control'); - $(richTextarea).attr('placeholder', $interpolate($(messageField).attr('placeholder'))($scope)); - - var updatePromise; $(richTextarea) - .on('DOMNodeInserted', onPastedImageEvent) - .on('keyup', function (e) { - updateHeight(); - - if (!sendAwaiting) { - $scope.$apply(function () { - $scope.draftMessage.text = richTextarea.textContent; - }); - } - - $timeout.cancel(updatePromise); - updatePromise = $timeout(updateValue, 1000); - }); + .attr('placeholder', $interpolate($(messageField).attr('placeholder'))($scope)) + .on('keydown keyup', updateHeight); } - // Head is sometimes slower - $timeout(function () { - fileSelects - .on('change', function () { - var self = this; - $scope.$apply(function () { - $scope.draftMessage.files = Array.prototype.slice.call(self.files); - $scope.draftMessage.isMedia = $(self).hasClass('im_media_attach_input') || Config.Mobile; - setTimeout(function () { - try { - self.value = ''; - } catch (e) {}; - }, 1000); - }); - }); - }, 1000); - - var sendOnEnter = true, - updateSendSettings = function () { - Storage.get('send_ctrlenter').then(function (sendOnCtrl) { - sendOnEnter = !sendOnCtrl; - }); - }; + fileSelects.on('change', function () { + var self = this; + $scope.$apply(function () { + $scope.draftMessage.files = Array.prototype.slice.call(self.files); + $scope.draftMessage.isMedia = $(self).hasClass('im_media_attach_input') || Config.Mobile; + setTimeout(function () { + try { + self.value = ''; + } catch (e) {}; + }, 1000); + }); + }); + var sendOnEnter = true; + function updateSendSettings () { + Storage.get('send_ctrlenter').then(function (sendOnCtrl) { + sendOnEnter = !sendOnCtrl; + }); + }; $scope.$on('settings_changed', updateSendSettings); updateSendSettings(); - $(editorElement).on('keydown', function (e) { - if (richTextarea) { - updateHeight(); - } + $(submitBtn).on('mousedown touchstart', onMessageSubmit); - if (e.keyCode == 13) { - var submit = false; - if (sendOnEnter && !e.shiftKey) { - submit = true; - } else if (!sendOnEnter && (e.ctrlKey || e.metaKey)) { - submit = true; - } - - if (submit) { - $timeout.cancel(updatePromise); - updateValue(); - $scope.draftMessage.send(); - $(element).trigger('message_send'); - resetTyping(); - return cancelEvent(e); + function onMessageSubmit (e) { + $scope.$apply(function () { + updateValue(); + $scope.draftMessage.send(); + composer.resetTyping(); + if (composerEmojiPanel) { + composerEmojiPanel.update(); } - } - - }); - - $(submitBtn).on('mousedown touchstart', function (e) { - $timeout.cancel(updatePromise); - updateValue(); - $scope.draftMessage.send(); - $(element).trigger('message_send'); - resetTyping(); + }); return cancelEvent(e); - }); - - var lastTyping = 0, - lastLength; - $(editorElement).on('keyup', function (e) { - var now = tsNow(), - length = (editorElement[richTextarea ? 'textContent' : 'value']).length; - - - if (now - lastTyping > 5000 && length != lastLength) { - lastTyping = now; - lastLength = length; - $scope.$emit('ui_typing'); - } - }); - - function resetTyping () { - lastTyping = 0; - lastLength = 0; - }; - - function updateRichTextarea () { - if (richTextarea) { - $timeout.cancel(updatePromise); - var html = $('
').text($scope.draftMessage.text || '').html(); - html = html.replace(/\n/g, '
'); - $(richTextarea).html(html); - lastLength = html.length; - updateHeight(); - } } function updateValue () { if (richTextarea) { - $(richTextarea).trigger('change'); + composer.onChange(); updateHeight(); } } - var height = richTextarea.offsetHeight; + var height = richTextarea && richTextarea.offsetHeight; function updateHeight () { var newHeight = richTextarea.offsetHeight; if (height != newHeight) { @@ -1250,7 +1182,7 @@ angular.module('myApp.directives', ['myApp.filters']) function onKeyDown(e) { if (e.keyCode == 9 && !e.shiftKey && !e.ctrlKey && !e.metaKey && !$modalStack.getTop()) { // TAB - editorElement.focus(); + composer.focus(); return cancelEvent(e); } } @@ -1265,13 +1197,18 @@ angular.module('myApp.directives', ['myApp.filters']) $scope.$on('ui_history_change', focusField); } - $scope.$on('ui_peer_change', resetTyping); - $scope.$on('ui_peer_draft', updateRichTextarea); + $scope.$on('ui_peer_change', composer.resetTyping.bind(composer)); + $scope.$on('ui_peer_draft', function () { + if (richTextarea) { + composer.setValue($scope.draftMessage.text || ''); + updateHeight(); + } + composer.focus(); + }); var sendAwaiting = false; $scope.$on('ui_message_before_send', function () { sendAwaiting = true; - $timeout.cancel(updatePromise); updateValue(); }); $scope.$on('ui_message_send', function () { @@ -1279,35 +1216,17 @@ angular.module('myApp.directives', ['myApp.filters']) focusField(); }); - function focusField () { onContentLoaded(function () { - editorElement.focus(); + composer.focus(); }); } - function onPastedImageEvent (e) { - var element = (e.originalEvent || e).target, - src = (element || {}).src || '', - remove = false; - - if (src.substr(0, 5) == 'data:') { - remove = true; - var blob = dataUrlToBlob(src); - ErrorService.confirm({type: 'FILE_CLIPBOARD_PASTE'}).then(function () { - $scope.draftMessage.files = [blob]; - $scope.draftMessage.isMedia = true; - }); - setZeroTimeout(function () { - element.parentNode.removeChild(element); - }) - } - else if (src && !src.match(/img\/blank\.gif/)) { - var replacementNode = document.createTextNode(' ' + src + ' '); - setTimeout(function () { - element.parentNode.replaceChild(replacementNode, element); - }, 100); - } + function onFilesPaste (blobs) { + ErrorService.confirm({type: 'FILE_CLIPBOARD_PASTE'}).then(function () { + $scope.draftMessage.files = blobs; + $scope.draftMessage.isMedia = true; + }); }; function onPasteEvent (e) { @@ -1348,7 +1267,7 @@ angular.module('myApp.directives', ['myApp.filters']) if (e.type == 'dragenter' || e.type == 'dragover') { if (dragStateChanged) { $(dropbox) - .css({height: editorElement.offsetHeight + 2, width: editorElement.offsetWidth}) + .css({height: messageFieldWrap.offsetHeight + 2, width: messageFieldWrap.offsetWidth}) .show(); } } else { @@ -1371,15 +1290,11 @@ angular.module('myApp.directives', ['myApp.filters']) $scope.$on('$destroy', function cleanup() { - $('body').off('dragenter dragleave dragover drop', onDragDropEvent); $(document).off('paste', onPasteEvent); $(document).off('keydown', onKeyDown); - $(submitBtn).off('mousedown') + $('body').off('dragenter dragleave dragover drop', onDragDropEvent); + $(submitBtn).off('mousedown touchstart'); fileSelects.off('change'); - if (richTextarea) { - $(richTextarea).off('DOMNodeInserted keyup', onPastedImageEvent); - } - $(editorElement).off('keydown'); }); if (!Config.Navigator.touch) { diff --git a/app/js/lib/utils.js b/app/js/lib/utils.js index 77a25605..dea19f5d 100644 --- a/app/js/lib/utils.js +++ b/app/js/lib/utils.js @@ -112,25 +112,63 @@ function getFieldSelection (field) { return len; } -function getFieldValue(field) { +function getRichValue(field) { if (!field) { return ''; } - if (field.tagName == 'INPUT' || field.tagName == 'TEXTAREA') { - return field.value; - } var lines = []; var line = []; - getFieldElementValue(field, lines, line); + + getRichElementValue(field, lines, line); if (line.length) { lines.push(line.join('')); } + return lines.join('\n'); } -function getFieldElementValue(node, lines, line) { +function getRichValueWithCaret(field) { + if (!field) { + return []; + } + var lines = []; + var line = []; + + var sel = window.getSelection ? window.getSelection() : false; + var selNode, selOffset; + if (sel && sel.rangeCount) { + var range = sel.getRangeAt(0); + if (range.startContainer && + range.startContainer == range.endContainer && + range.startOffset == range.endOffset) { + selNode = range.startContainer; + selOffset = range.startOffset; + } + } + + getRichElementValue(field, lines, line, selNode, selOffset); + + if (line.length) { + lines.push(line.join('')); + } + + var value = lines.join('\n'); + var caretPos = value.indexOf('\001'); + if (caretPos != -1) { + value = value.substr(0, caretPos) + value.substr(caretPos + 1); + } + + return [value, caretPos]; +} + +function getRichElementValue(node, lines, line, selNode, selOffset) { if (node.nodeType == 3) { // TEXT - line.push(node.nodeValue); + if (selNode === node) { + var value = node.nodeValue; + line.push(value.substr(0, selOffset) + '\001' + value.substr(selOffset)); + } else { + line.push(node.nodeValue); + } return; } if (node.nodeType != 1) { // NON-ELEMENT @@ -147,9 +185,12 @@ function getFieldElementValue(node, lines, line) { line.push(node.alt); } } + if (selNode === node) { + line.push('\001'); + } var curChild = node.firstChild; while (curChild) { - getFieldElementValue(curChild, lines, line); + getRichElementValue(curChild, lines, line, selNode, selOffset); curChild = curChild.nextSibling; } if (isBlock && line.length) { @@ -158,8 +199,31 @@ function getFieldElementValue(node, lines, line) { } } +function setRichFocus(field, selectNode) { + field.focus(); + if (window.getSelection && document.createRange) { + var range = document.createRange(); + if (selectNode) { + range.selectNode(selectNode); + } else { + range.selectNodeContents(field); + } + range.collapse(false); + + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + else if (document.body.createTextRange !== undefined) { + var textRange = document.body.createTextRange(); + textRange.moveToElementText(selectNode || field); + textRange.collapse(false); + textRange.select(); + } +} + function onContentLoaded (cb) { - setTimeout(cb, 0); + setZeroTimeout(cb); } function tsNow (seconds) { diff --git a/app/js/message_composer.js b/app/js/message_composer.js index bd01a19d..e1064baa 100644 --- a/app/js/message_composer.js +++ b/app/js/message_composer.js @@ -147,6 +147,17 @@ function EmojiTooltip (btnEl, options) { self.onMouseLeave(true); } }); + $(this.btnEl).on('mousedown', function (e) { + if (!self.shown) { + clearTimeout(self.showTimeout); + delete self.showTimeout; + self.show(); + } else { + clearTimeout(self.hideTimeout); + delete self.hideTimeout; + self.hide(); + } + }); } EmojiTooltip.prototype.onMouseEnter = function (triggerShow) { @@ -180,7 +191,7 @@ EmojiTooltip.prototype.createTooltip = function () { } var self = this; - this.tooltipEl = $('
').appendTo(document.body); + this.tooltipEl = $('
').appendTo(document.body); this.tabsEl = $('.composer_emoji_tooltip_tabs', this.tooltip); this.contentEl = $('.composer_emoji_tooltip_content', this.tooltip); @@ -278,7 +289,7 @@ EmojiTooltip.prototype.updateTabContents = function (tab) { emoticonData = Config.Emoji[emoticonCode]; x = iconSize * (i % totalColumns); y = iconSize * Math.floor(i / totalColumns); - html.push(''); + html.push(''); } this.contentEl.html(html.join('')); } @@ -296,7 +307,7 @@ EmojiTooltip.prototype.updateTabContents = function (tab) { pos = spritesheet[1]; x = iconSize * spritesheet[3]; y = iconSize * spritesheet[2]; - html.push(''); + html.push(''); } } self.contentEl.html(html.join('')); @@ -311,13 +322,17 @@ EmojiTooltip.prototype.updatePosition = function () { EmojiTooltip.prototype.show = function () { this.updatePosition(); - this.tooltipEl.show(); + this.tooltipEl.addClass('composer_emoji_tooltip_shown'); + this.btnEl.addClass('composer_emoji_insert_btn_on'); delete this.showTimeout; + this.shown = true; }; EmojiTooltip.prototype.hide = function () { - this.tooltipEl.hide(); + this.tooltipEl.removeClass('composer_emoji_tooltip_shown'); + this.btnEl.removeClass('composer_emoji_insert_btn_on'); delete this.hideTimeout; + delete this.shown; }; @@ -381,8 +396,6 @@ function MessageComposer (textarea, options) { this.textareaEl = $(textarea); this.setUpInput(); - // this.textareaEl.on('keyup keydown', this.onKeyEvent.bind(this)); - // this.textareaEl.on('focus blur', this.onFocusBlur.bind(this)); this.autoCompleteEl = $('').appendTo(document.body); @@ -395,7 +408,7 @@ function MessageComposer (textarea, options) { } if (code = target.attr('data-code')) { if (self.onEmojiSelected) { - self.onEmojiSelected(code); + self.onEmojiSelected(code, true); } EmojiHelper.pushPopularEmoji(code); } @@ -403,24 +416,32 @@ function MessageComposer (textarea, options) { }); this.isActive = false; + + this.onTyping = options.onTyping; + this.onMessageSubmit = options.onMessageSubmit; + this.getSendOnEnter = options.getSendOnEnter; } MessageComposer.prototype.setUpInput = function () { if ('contentEditable' in document.body) { - this.setUpContenteditable(); + this.setUpRich(); } else { this.setUpPlaintext(); } } -MessageComposer.prototype.setUpContenteditable = function () { +MessageComposer.prototype.setUpRich = function () { this.textareaEl.hide(); - this.contentEditableEl = $('
'); + this.richTextareaEl = $('
'); - this.textareaEl[0].parentNode.insertBefore(this.contentEditableEl[0], this.textareaEl[0]); + this.textareaEl[0].parentNode.insertBefore(this.richTextareaEl[0], this.textareaEl[0]); - this.contentEditableEl.on('keyup keydown', this.onKeyEvent.bind(this)); - this.contentEditableEl.on('focus blur', this.onFocusBlur.bind(this)); + this.richTextareaEl.on('keyup keydown', this.onKeyEvent.bind(this)); + this.richTextareaEl.on('focus blur', this.onFocusBlur.bind(this)); + this.richTextareaEl.on('paste', this.onRichPaste.bind(this)); + this.richTextareaEl.on('DOMNodeInserted', this.onRichPasteNode.bind(this)); + + $(document.body).on('keydown', this.backupSelection.bind(this)); } MessageComposer.prototype.setUpPlaintext = function () { @@ -432,51 +453,145 @@ MessageComposer.prototype.onKeyEvent = function (e) { var self = this; if (e.type == 'keyup') { this.checkAutocomplete(); - } - if (e.type == 'keydown' && this.autocompleteShown) { - if (e.keyCode == 38 || e.keyCode == 40) { // UP / DOWN - var next = e.keyCode == 40; - var currentSelected = $(this.autoCompleteEl).find('.composer_emoji_option_active'); - - if (currentSelected.length) { - var currentSelectedWrap = currentSelected[0].parentNode; - var nextWrap = currentSelectedWrap[next ? 'nextSibling' : 'previousSibling']; - currentSelected.removeClass('composer_emoji_option_active'); - if (nextWrap) { - $(nextWrap).find('a').addClass('composer_emoji_option_active'); - return cancelEvent(e); + + if (this.onTyping) { + var now = tsNow(); + if (now - this.lastTyping > 5000) { + var length = (this.richTextareaEl ? this.richTextareaEl[0].textContent : this.textareaEl[0].value).length; + + if (length != this.lastLength) { + this.lastTyping = now; + this.lastLength = length; + this.onTyping(); } } + } + + if (this.richTextareaEl) { + clearTimeout(this.updateValueTO); + var now = tsNow(); + if (this.keyupStarted === undefined) { + this.keyupStarted = now; + } + if (now - this.keyupStarted > 10000) { + this.onChange(); + } + else { + this.updateValueTO = setTimeout(this.onChange.bind(this), 1000); + } + } + + } + if (e.type == 'keydown') { + if (this.autocompleteShown) { + if (e.keyCode == 38 || e.keyCode == 40) { // UP / DOWN + var next = e.keyCode == 40; + var currentSelected = $(this.autoCompleteEl).find('.composer_emoji_option_active'); + + if (currentSelected.length) { + var currentSelectedWrap = currentSelected[0].parentNode; + var nextWrap = currentSelectedWrap[next ? 'nextSibling' : 'previousSibling']; + currentSelected.removeClass('composer_emoji_option_active'); + if (nextWrap) { + $(nextWrap).find('a').addClass('composer_emoji_option_active'); + return cancelEvent(e); + } + } + + var childNodes = this.autoCompleteEl[0].childNodes; + var nextWrap = childNodes[next ? 0 : childNodes.length - 1]; + $(nextWrap).find('a').addClass('composer_emoji_option_active'); - var childNodes = this.autoCompleteEl[0].childNodes; - var nextWrap = childNodes[next ? 0 : childNodes.length - 1]; - $(nextWrap).find('a').addClass('composer_emoji_option_active'); + return cancelEvent(e); + } - return cancelEvent(e); + if (e.keyCode == 13) { // ENTER + var currentSelected = $(this.autoCompleteEl).find('.composer_emoji_option_active') || + $(this.autoCompleteEl).childNodes[0].find('a'); + var code = currentSelected.attr('data-code'); + if (code) { + this.onEmojiSelected(code, true); + EmojiHelper.pushPopularEmoji(code); + } + return cancelEvent(e); + } } - if (e.keyCode == 13) { // ENTER - var currentSelected = $(this.autoCompleteEl).find('.composer_emoji_option_active') || - $(this.autoCompleteEl).childNodes[0].find('a'); - var code = currentSelected.attr('data-code'); - if (code) { - this.onEmojiSelected(code); - EmojiHelper.pushPopularEmoji(code); + else if (e.keyCode == 13) { + var submit = false; + var sendOnEnter = true; + if (this.getSendOnEnter && !this.getSendOnEnter()) { + sendOnEnter = false; + } + if (sendOnEnter && !e.shiftKey) { + submit = true; + } else if (!sendOnEnter && (e.ctrlKey || e.metaKey)) { + submit = true; + } + + if (submit) { + this.onMessageSubmit(e); + return cancelEvent(e); } - return cancelEvent(e); } } } +MessageComposer.prototype.backupSelection = function () { + delete this.selection; + + if (!this.isActive) { + return; + } + if (window.getSelection) { + var sel = window.getSelection(); + if (sel.getRangeAt && sel.rangeCount) { + this.selection = sel.getRangeAt(0); + } + } else if (document.selection && document.selection.createRange) { + this.selection = document.selection.createRange(); + } +} + +MessageComposer.prototype.restoreSelection = function () { + if (!this.selection) { + return false; + } + var result = false; + if (window.getSelection) { + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(this.selection); + result = true; + } + else if (document.selection && this.selection.select) { + this.selection.select(); + result = true; + } + delete this.selection; + + return result; +} + + + MessageComposer.prototype.checkAutocomplete = function () { - var textarea = this.contentEditableEl ? this.contentEditableEl[0] : this.textareaEl; - var pos = getFieldSelection(textarea); - var value = getFieldValue(textarea).substr(0, pos); + var pos, value; + if (this.richTextareaEl) { + var textarea = this.richTextareaEl[0]; + var valueCaret = getRichValueWithCaret(textarea); + var value = valueCaret[0]; + var pos = valueCaret[1] >= 0 ? valueCaret[1] : value.length; + } else { + var textarea = this.textareaEl[0]; + var pos = getFieldSelection(textarea); + var value = textarea.value; + } - console.log(pos, value); + value = value.substr(0, pos); var matches = value.match(/:([A-Za-z_0-z\+-]*)$/); - if (matches) { + if (matches/* && !this.richTextareaEl*/) { if (this.previousQuery == matches[0]) { return; } @@ -509,31 +624,157 @@ MessageComposer.prototype.onFocusBlur = function (e) { } else { setTimeout(this.checkAutocomplete.bind(this), 100); } + if (this.richTextareaEl) { + document.execCommand('enableObjectResizing', !this.isActive, !this.isActive); + } } -MessageComposer.prototype.onEmojiSelected = function (code) { - console.log('emoji selected', code); +MessageComposer.prototype.onRichPaste = function (e) { + var cData = (e.originalEvent || e).clipboardData, + items = cData && cData.items || [], + i; + for (i = 0; i < items.length; i++) { + if (items[i].kind == 'file') { + e.preventDefault(); + return true; + } + } - var emoji = EmojiHelper.emojis[code]; + var text = (e.originalEvent || e).clipboardData.getData('text/plain'); + setZeroTimeout(this.onChange.bind(this), 0); + if (text.length) { + document.execCommand('insertText', false, text); + return cancelEvent(e); + } + return true; +} - var textarea = this.textareaEl[0]; - var fullValue = textarea.value; - var pos = this.isActive ? getFieldSelection(textarea) : fullValue.length; - var suffix = fullValue.substr(pos); - var prefix = fullValue.substr(0, pos); - var matches = prefix.match(/:([A-Za-z_0-z\+-]*)$/); +MessageComposer.prototype.onRichPasteNode = function (e) { + var element = (e.originalEvent || e).target, + src = (element || {}).src || '', + remove = false; + + if (src.substr(0, 5) == 'data:') { + remove = true; + var blob = dataUrlToBlob(src); + this.onFilePaste(blob); + setZeroTimeout(function () { + element.parentNode.removeChild(element); + }) + } + else if (src && !src.match(/img\/blank\.gif/)) { + var replacementNode = document.createTextNode(' ' + src + ' '); + setTimeout(function () { + element.parentNode.replaceChild(replacementNode, element); + }, 100); + } +} - if (matches && matches[0]) { - var newValue = prefix.substr(0, matches.index) + ':' + emoji[1] + ': ' + suffix; - var newPos = matches.index + emoji[1].length + 3; - } else { - var newValue = prefix + ':' + emoji[1] + ': ' + suffix; - var newPos = prefix.length + emoji[1].length + 3; + + +MessageComposer.prototype.onEmojiSelected = function (code, autocomplete) { + if (this.richTextareaEl) { + var textarea = this.richTextareaEl[0]; + if (!this.isActive) { + if (!this.restoreSelection()) { + setRichFocus(textarea); + } + } + if (autocomplete) { + var valueCaret = getRichValueWithCaret(textarea); + var fullValue = valueCaret[0]; + var pos = valueCaret[1] >= 0 ? valueCaret[1] : fullValue.length; + var suffix = fullValue.substr(pos); + var prefix = fullValue.substr(0, pos); + var matches = prefix.match(/:([A-Za-z0-9\-\+\*_]*)$/); + var emoji = EmojiHelper.emojis[code]; + + var newValuePrefix; + if (matches && matches[0]) { + newValuePrefix = prefix.substr(0, matches.index) + ':' + emoji[1] + ':'; + } else { + newValuePrefix = prefix + ':' + emoji[1] + ':'; + } + textarea.value = newValue; + + this.selId = (this.selId || 0) + 1; + var html = this.getRichHtml(newValuePrefix) + ' ' + this.getRichHtml(suffix); + + this.richTextareaEl.html(html); + setRichFocus(textarea, $('#composer_sel' + this.selId)[0]); + } else { + document.execCommand('insertHTML', false, this.getEmojiHtml(code)); + } + } + else { + var textarea = this.textareaEl[0]; + var fullValue = textarea.value; + var pos = this.isActive ? getFieldSelection(textarea) : fullValue.length; + var suffix = fullValue.substr(pos); + var prefix = fullValue.substr(0, pos); + var matches = autocomplete && prefix.match(/:([A-Za-z0-9\-\+\*_]*)$/); + var emoji = EmojiHelper.emojis[code]; + + if (matches && matches[0]) { + var newValue = prefix.substr(0, matches.index) + ':' + emoji[1] + ': ' + suffix; + var newPos = matches.index + emoji[1].length + 3; + } else { + var newValue = prefix + ':' + emoji[1] + ': ' + suffix; + var newPos = prefix.length + emoji[1].length + 3; + } + textarea.value = newValue; + setFieldSelection(textarea, newPos); } - textarea.value = newValue; - setFieldSelection(textarea, newPos); this.hideSuggestions(); + this.onChange(); +} + +MessageComposer.prototype.onChange = function (e) { + if (this.richTextareaEl) { + delete this.keyupStarted; + this.textareaEl.val(getRichValue(this.richTextareaEl[0])).trigger('change'); + } +} + +MessageComposer.prototype.getEmojiHtml = function (code, emoji) { + emoji = emoji || EmojiHelper.emojis[code]; + var iconSize = 20; + var spritesheet = EmojiHelper.spritesheetPositions[code]; + var categoryIndex = spritesheet[0]; + var pos = spritesheet[1]; + var x = iconSize * spritesheet[3]; + var y = iconSize * spritesheet[2]; + + return ':' + encodeEntities(emoji[1]) + ':'; +} + +MessageComposer.prototype.setValue = function (text) { + if (this.richTextareaEl) { + this.richTextareaEl.html(this.getRichHtml(text)); + this.lastLength = text.length; + } else { + this.textareaEl.val(text); + } +} + +MessageComposer.prototype.getRichHtml = function (text) { + return $('
').text(text).html().replace(/:([A-Za-z0-9\-\+\*_]+?):/gi, (function (all, shortcut) { + var code = EmojiHelper.shortcuts[shortcut]; + if (code !== undefined) { + return this.getEmojiHtml(code); + } + return all; + }).bind(this)); +} + + +MessageComposer.prototype.focus = function () { + if (this.richTextareaEl) { + setRichFocus(this.richTextareaEl[0]); + } else { + setFieldSelection(this.textareaEl[0]); + } } @@ -567,7 +808,7 @@ MessageComposer.prototype.showEmojiSuggestions = function (codes) { } MessageComposer.prototype.updatePosition = function () { - var offset = this.textareaEl.offset(); + var offset = (this.richTextareaEl || this.textareaEl).offset(); var height = this.autoCompleteEl.outerHeight(); this.autoCompleteEl.css({top: offset.top - height, left: offset.left}); } @@ -576,3 +817,9 @@ MessageComposer.prototype.hideSuggestions = function () { this.autoCompleteEl.hide(); delete this.autocompleteShown; } + +MessageComposer.prototype.resetTyping = function () { + this.lastTyping = 0; + this.lastLength = 0; +} + diff --git a/app/partials/desktop/im.html b/app/partials/desktop/im.html index c152e5fc..bb632a7f 100644 --- a/app/partials/desktop/im.html +++ b/app/partials/desktop/im.html @@ -171,13 +171,13 @@ -
+
- +
@@ -193,7 +193,7 @@
-
+
diff --git a/app/partials/mobile/im.html b/app/partials/mobile/im.html index 5bd132b6..a67e8749 100644 --- a/app/partials/mobile/im.html +++ b/app/partials/mobile/im.html @@ -138,9 +138,7 @@
-
- -
+ diff --git a/app/webogram.appcache b/app/webogram.appcache index 9802e602..83172322 100644 --- a/app/webogram.appcache +++ b/app/webogram.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 57 +# 59 NETWORK: *