Skip to content
Snippets Groups Projects
Commit a830464f authored by Itamar Schen's avatar Itamar Schen
Browse files

New message editing features

- Allows cursor movement across wrapped lines when editing messages
- Saves drafts when editing messages, so switching between them doesn't
  discard changes made so far.
- ESC key discards draft for current message if exists or goes out of
  edit mode if doesn't exist.
- ALT+UP/DOWN to travel whole messages
- Emptying message deletes it
parent bd154842
No related branches found
No related tags found
No related merge requests found
......@@ -111,28 +111,8 @@ Meteor.startup ->
message = @_arguments[1]
msg = $(event.currentTarget).closest('.message')[0]
$("\##{msg.id} .message-dropdown").hide()
return if msg.classList.contains("system")
swal {
title: t('Are_you_sure')
text: t('You_will_not_be_able_to_recover')
type: 'warning'
showCancelButton: true
confirmButtonColor: '#DD6B55'
confirmButtonText: t('Yes_delete_it')
cancelButtonText: t('Cancel')
closeOnConfirm: false
html: false
}, ->
swal
title: t('Deleted')
text: t('Your_entry_has_been_deleted')
type: 'success'
timer: 1000
showConfirmButton: false
if chatMessages[Session.get('openedRoom')].editing.id is message._id
chatMessages[Session.get('openedRoom')].clearEditing(message)
chatMessages[Session.get('openedRoom')].deleteMsg(message)
chatMessages[Session.get('openedRoom')].confirmDeleteMsg(message)
validation: (message) ->
hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', message.rid)
isDeleteAllowed = RocketChat.settings.get 'Message_AllowDeleting'
......
class @ChatMessages
init: (node) ->
this.editing = {}
this.records = {}
this.messageMaxSize = RocketChat.settings.get('Message_MaxAllowedSize')
this.wrapper = $(node).find(".wrapper")
this.input = $(node).find(".input-message").get(0)
this.$input = $(this.input)
this.hasValue = new ReactiveVar false
this.bindEvents()
return
......@@ -13,23 +15,6 @@ class @ChatMessages
$(".messages-box").css
height: "calc(100% - #{dif}px)"
toPrevMessage: ->
msgs = this.wrapper.get(0).querySelectorAll(".own:not(.system)")
if msgs.length
if this.editing.element
if msgs[this.editing.index - 1]
this.edit msgs[this.editing.index - 1], this.editing.index - 1
else
this.edit msgs[msgs.length - 1], msgs.length - 1
toNextMessage: ->
if this.editing.element
msgs = this.wrapper.get(0).querySelectorAll(".own:not(.system)")
if msgs[this.editing.index + 1]
this.edit msgs[this.editing.index + 1], this.editing.index + 1
else
this.clearEditing()
getEditingIndex: (element) ->
msgs = this.wrapper.get(0).querySelectorAll(".own:not(.system)")
index = 0
......@@ -39,9 +24,60 @@ class @ChatMessages
index++
return -1
recordInputAsDraft: () ->
console.log(this.editing)
id = this.editing.id
record = this.records[id] || {}
draft = this.input.value
if(draft is record.original)
this.clearCurrentDraft()
else
record.draft = draft
this.records[id] = record
recordOriginalMessage: (message) ->
record = this.records[message._id] || {}
record.original = message.msg
this.records[message._id] = record
getMessageDraft: (id) -> return this.records[id]
clearMessageDraft: (id) -> delete this.records[id]
clearCurrentDraft: () ->
this.clearMessageDraft this.editing.id
resetToDraft: (id) ->
this.input.value = this.records[id].original
getMessageOfElement: (element) -> return ChatMessage.findOne( { _id: element.getAttribute("id") } )
toPrevMessage: ->
index = this.editing.index
this.editByIndex if index? then index - 1 else undefined
toNextMessage: ->
index = this.editing.index
this.clearEditing() unless this.editByIndex index + 1
editByIndex: (index) ->
return false if not this.editing.element and index?
msgs = this.wrapper.get(0).querySelectorAll(".own:not(.system)")
index = msgs.length - 1 if not index?
return false unless msgs[index]
element = msgs[index]
this.edit element, index
return true
edit: (element, index) ->
id = element.getAttribute("id")
message = ChatMessage.findOne { _id: id }
index = this.getEditingIndex(element) if not index?
message = this.getMessageOfElement(element)
hasPermission = RocketChat.authz.hasAtLeastOnePermission('edit-message', message.rid)
editAllowed = RocketChat.settings.get 'Message_AllowEditing'
editOwn = message?.u?._id is Meteor.userId()
......@@ -56,31 +92,53 @@ class @ChatMessages
if currentTsDiff > blockEditInMinutes
return
msg = this.getMessageDraft(message._id)?.draft
msg = message.msg unless msg?
this.recordOriginalMessage message
editingNext = this.editing.index < index
old_input = this.input.value
this.clearEditing()
this.input.value = message.msg
this.hasValue.set true
this.editing.element = element
this.editing.index = index or this.getEditingIndex(element)
this.editing.id = id
this.editing.index = index
this.editing.id = message._id
element.classList.add("editing")
this.input.classList.add("editing")
$(this.input).closest('.message-form').addClass('editing');
this.$input.closest('.message-form').addClass('editing')
setTimeout =>
this.input.focus()
this.input.value = msg
cursor_pos = if editingNext then 0 else -1
this.$input.setCursorPosition(cursor_pos)
, 5
clearEditing: ->
if this.editing.element
this.recordInputAsDraft()
this.editing.element.classList.remove("editing")
this.input.classList.remove("editing")
$(this.input).closest('.message-form').removeClass('editing');
this.editing.id = null
this.editing.element = null
this.editing.index = null
this.$input.closest('.message-form').removeClass('editing')
delete this.editing.id
delete this.editing.element
delete this.editing.index
this.input.value = this.editing.saved or ""
cursor_pos = this.editing.savedCursor ? -1
this.$input.setCursorPosition(cursor_pos)
this.hasValue.set this.input.value isnt ''
else
this.editing.saved = this.input.value
this.editing.savedCursor = this.input.selectionEnd
send: (rid, input) ->
if _.trim(input.value) isnt ''
......@@ -98,6 +156,7 @@ class @ChatMessages
if this.isMessageTooLong(msgObject.msg)
return toastr.error t('Message_too_long')
this.clearCurrentDraft()
if this.editing.id
this.update(this.editing.id, rid, msgObject.msg)
return
......@@ -117,6 +176,41 @@ class @ChatMessages
return
Meteor.call 'sendMessage', msgObject
else if this.editing.element
element = this.editing.element
message = this.getMessageOfElement(element)
this.resetToDraft this.editing.id
this.confirmDeleteMsg message
confirmDeleteMsg: (message) ->
return if RocketChat.MessageTypes.isSystemMessage(message)
swal {
title: t('Are_you_sure')
text: t('You_will_not_be_able_to_recover')
type: 'warning'
showCancelButton: true
confirmButtonColor: '#DD6B55'
confirmButtonText: t('Yes_delete_it')
cancelButtonText: t('Cancel')
closeOnConfirm: false
html: false
}, =>
swal
title: t('Deleted')
text: t('Your_entry_has_been_deleted')
type: 'success'
timer: 1000
showConfirmButton: false
if this.editing.id is message._id
this.clearEditing message
this.deleteMsg message
this.$input.focus()
# In order to avoid issue "[Callback not called when still animating](https://github.com/t4t5/sweetalert/issues/528)"
$('.sweet-alert').addClass 'visible'
deleteMsg: (message) ->
blockDeleteInMinutes = RocketChat.settings.get 'Message_AllowDeleting_BlockDeleteInMinutes'
......@@ -129,7 +223,7 @@ class @ChatMessages
Meteor.call 'deleteMessage', message, (error, result) ->
if error
return handleError(error);
return handleError(error)
pinMsg: (message) ->
message.pinned = true
......@@ -205,39 +299,54 @@ class @ChatMessages
keydown: (rid, event) ->
input = event.currentTarget
$input = $(input)
k = event.which
this.resize(input)
if k is 13 and not event.shiftKey
if k is 13 and not event.shiftKey # Enter without shift
event.preventDefault()
event.stopPropagation()
this.send(rid, input)
return
if k is 9
if k is 9 # Tab
event.preventDefault()
event.stopPropagation()
@tryCompletion input
if k is 27
if this.editing.id
if k is 27 # Escape
if this.editing.index?
record = this.getMessageDraft(this.editing.id)
if this.input.value is record?.original
this.clearCurrentDraft()
this.clearEditing()
else
this.resetToDraft this.editing.id
event.preventDefault()
event.stopPropagation()
this.clearEditing()
return
else if k is 38 or k is 40 # Arrow Up or down
return true if event.shiftKey
return true if $(input).val().length and !this.editing?.id
if k is 38
return if input.value.slice(0, input.selectionStart).match(/[\n]/) isnt null
this.toPrevMessage()
else
return if input.value.slice(input.selectionEnd, input.value.length).match(/[\n]/) isnt null
this.toNextMessage()
event.preventDefault()
event.stopPropagation()
cursor_pos = input.selectionEnd
if k is 38 # Arrow Up
if cursor_pos is 0
this.toPrevMessage()
else if event.altKey
this.$input.setCursorPosition(0)
else
return true
else # Arrow Down
if cursor_pos is input.value.length
this.toNextMessage()
else if event.altKey
this.$input.setCursorPosition(-1)
else
return true
return false
# ctrl (command) + shift + k -> clear room messages
else if k is 75 and ((navigator?.platform?.indexOf('Mac') isnt -1 and event.metaKey and event.shiftKey) or (navigator?.platform?.indexOf('Mac') is -1 and event.ctrlKey and event.shiftKey))
......
// Adapted from http://stackoverflow.com/a/499158
$.fn.setCursorPosition = function(pos) {
this.each(function(index, elem) {
var p = pos < 0 ? elem.value.length - pos : pos;
if (elem.setSelectionRange) {
elem.setSelectionRange(p, p);
} else if (elem.createTextRange) {
var range = elem.createTextRange();
range.collapse(true);
range.moveEnd('character', p);
range.moveStart('character', p);
range.select();
}
});
return this;
};
......@@ -72,6 +72,9 @@ Package.onUse(function(api) {
// LIB CLIPBOARDJS
api.addFiles('lib/clipboardjs/clipboard.js', 'client');
// TEXTAREA CURSOR MANAGEMENT
api.addFiles('lib/textarea-cursor/set-cursor-position.js', 'client');
// TEMPLATE FILES
api.addFiles('views/cmsPage.html', 'client');
api.addFiles('views/fxos.html', 'client');
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment