Commit 973065b1304451710f72e92be67d3fda31384276:
add comments to the parts page
Branch: refs/heads/secmail
Author: Sam Ruby <[email protected]>
Committer: Sam Ruby <[email protected]>
Pusher: rubys <[email protected]>
------------------------------------------------------------
www/secmail/public/secmail.css | ++++
www/secmail/views/context-menu.js.rb | ++++++++
www/secmail/views/parts.js.rb | +++++++ -----
------------------------------------------------------------
299 changes: 230 additions, 69 deletions.
------------------------------------------------------------
diff --git a/www/secmail/public/secmail.css b/www/secmail/public/secmail.css
index 1a7c559..657ce65 100644
--- a/www/secmail/public/secmail.css
+++ b/www/secmail/public/secmail.css
@@ -63,6 +63,10 @@ form .btn {
margin-top: 0.5em;
}
+#attachments li {
+ width: 100%
+}
+
.contextMenu {
position: absolute;
z-index: 9999999;
diff --git a/www/secmail/views/context-menu.js.rb
b/www/secmail/views/context-menu.js.rb
new file mode 100644
index 0000000..b030127
--- /dev/null
+++ b/www/secmail/views/context-menu.js.rb
@@ -0,0 +1,126 @@
+#
+# Context menu with actions to apply to an attachment
+#
+
+class ContextMenu < React
+ def render
+ # context menu that displays when you 'right click' an attachment
+ _ul.contextMenu do
+ _li "\u2704 burst", onMouseDown: self.burst
+ _li.divider
+ _li "\u21B7 right", onMouseDown: self.rotate_attachment
+ _li "\u21c5 flip", onMouseDown: self.rotate_attachment
+ _li "\u21B6 left", onMouseDown: self.rotate_attachment
+ _li.divider
+ _li "\u2716 delete", onMouseDown: self.delete_attachment
+ end
+ end
+
+ # disable context menu
+ def componentDidMount()
+ document.querySelector('.contextMenu').style.display = :none
+ end
+
+ # position and show context menu
+ def self.show(event)
+ menu = document.querySelector('.contextMenu')
+ menu.style.position = :absolute
+ menu.style.display = :block
+
+ bodyRect = document.body.getBoundingClientRect()
+ menuRect = menu.getBoundingClientRect()
+ position = {x: event.clientX, y: event.clientY}
+
+ if position.x + menuRect.width > bodyRect.width
+ position.x -= menuRect.width if position.x >= menuRect.width
+ end
+
+ if position.y + menuRect.height > bodyRect.height
+ position.y -= menuRect.height if position.y >= menuRect.height
+ end
+
+ menu.style.left = position.x + 'px'
+ menu.style.top = position.y + 'px'
+ event.preventDefault()
+ end
+
+ # hide context menu whenever a click is received outside the menu
+ def self.hide(event)
+ target = event && event.target
+ while target
+ return if target.class == 'contextMenu'
+ target = target.parentNode
+ end
+ document.querySelector('.contextMenu').style.display = :none
+ end
+
+ # burst a PDF into individual pages
+ def burst(event)
+ data = {
+ selected: @@parts.state.menu,
+ message: window.parent.location.pathname
+ }
+
+ @@parts.setState busy: true
+ HTTP.post('../../actions/burst', data).then {|response|
+ @@parts.setState attachments: response.attachments,
+ selected: response.selected, busy: false, menu: nil
+ window.parent.frames.content.location.href=response.selected
+ ContextMenu.hide()
+ }.catch {|error|
+ alert error
+ @@parts.setState busy: false, menu: nil
+ ContextMenu.hide()
+ }
+ end
+
+ # burst a PDF into individual pages
+ def delete_attachment(event)
+ data = {
+ selected: @@parts.state.menu,
+ message: window.parent.location.pathname
+ }
+
+ @@parts.setState busy: true
+ HTTP.post('../../actions/delete-attachment', data).then {|response|
+ if response.attachments and not response.attachments.empty?
+ @@parts.setState attachments: response.attachments, busy: false,
+ menu: nil
+ window.parent.frames.content.location.href='_body_'
+ ContextMenu.hide()
+ else
+ window.parent.location.href = '../..'
+ end
+ }.catch {|error|
+ alert error
+ @@parts.setState busy: false, menu: nil
+ ContextMenu.hide()
+ }
+ end
+
+ # rotate an attachment
+ def rotate_attachment(event)
+ message = window.parent.location.pathname
+
+ data = {
+ selected: @@parts.state.menu,
+ message: message,
+ direction: event.currentTarget.textContent
+ }
+
+ @@parts.setState busy: true
+ HTTP.post('../../actions/rotate-attachment', data).then {|response|
+ @@parts.setState attachments: response.attachments,
+ selected: response.selected, busy: false, menu: nil
+
+ # reload attachment in content pane
+ window.parent.frames.content.location.href = response.selected
+
+ ContextMenu.hide()
+ }.catch {|error|
+ alert error
+ @@parts.setState busy: false, menu: nil
+ ContextMenu.hide()
+ }
+ end
+end
diff --git a/www/secmail/views/parts.js.rb b/www/secmail/views/parts.js.rb
index 3fb5881..365d4c8 100644
--- a/www/secmail/views/parts.js.rb
+++ b/www/secmail/views/parts.js.rb
@@ -1,3 +1,8 @@
+#
+# Parts list for a message: shows attachments, handles context
+# menus and drag and drop, and hosts forms.
+#
+
class Parts < React
def initialize
@selected = nil
@@ -5,8 +10,13 @@ def initialize
@attachments = []
@drag = nil
@form = nil
+ @menu = nil
end
+ ########################################################################
+ # HTML rendering of this frame #
+ ########################################################################
+
def render
# common options for all list items
options = {
@@ -17,7 +27,7 @@ def render
onDragLeave: self.dragLeave,
onDragEnd: self.dragEnd,
onDrop: self.drop,
- onContextMenu: self.menu,
+ onContextMenu: self.showMenu,
onClick: self.select
}
@@ -47,8 +57,9 @@ def render
_li "\u2716 delete", onMouseDown: self.delete_attachment
end
- # filing options
- if @selected
+ if @selected and not @menu
+
+ # filing options
_table.doctype do
_tr do
_td do
@@ -94,15 +105,19 @@ def render
_img.spinner src: '../../rotatingclock-slow2.gif' if @busy
end
- # initialize attachments list with the data from the server
+ ########################################################################
+ # React lifecycle #
+ ########################################################################
+
+ # initial list of attachments comes from the server; may be updated
+ # by context menu actions.
def componentWillMount()
@attachments = @@attachments
end
- # disable context menu and register mouse and keyboard handlers
+ # register mouse and keyboard handlers, hide context menu
def componentDidMount()
- document.querySelector('.contextMenu').style.display = :none
- window.onmousedown = self.window_click
+ window.onmousedown = self.hideMenu
# register keyboard handler on parent window and all frames
window.parent.onkeydown = self.keydown
@@ -110,11 +125,17 @@ def componentDidMount()
for i in 0...frames.length
frames[i].onkeydown=self.keydown
end
+
+ self.hideMenu()
end
+ ########################################################################
+ # Context menu #
+ ########################################################################
+
# position and show context menu
- def menu(event)
- @selected = event.currentTarget.textContent
+ def showMenu(event)
+ @menu = event.currentTarget.textContent
menu = document.querySelector('.contextMenu')
menu.style.position = :absolute
menu.style.display = :block
@@ -136,64 +157,24 @@ def menu(event)
event.preventDefault()
end
- # form submission - handles all forms
- def submit(event)
- event.preventDefault()
- form = event.currentTarget
-
- data = {}
- Array(form.querySelectorAll('input')).each do |field|
- data[field.name] = field.value if field.name
- end
-
- @busy = true
- HTTP(post form.action, data).then {|response|
- @busy = false
- alert response.result
- }.catch {|error|
- alert error
- @busy = false
- }
- end
-
# hide context menu whenever a click is received outside the menu
- def window_click(event)
- target = event.target
+ def hideMenu(event)
+ target = event && event.target
while target
return if target.class == 'contextMenu'
target = target.parentNode
end
- document.querySelector('.contextMenu').style.display = :none
- end
- # clicking on an attachment selects it
- def select(event)
- @selected = event.currentTarget.querySelector('a').getAttribute('href')
- end
-
- # handle keyboard events
- def keydown(event)
- if event.keyCode == 8 or event.keyCode == 46 # backspace or delete
- if event.metaKey or event.ctrlKey
- @busy = true
- event.stopPropagation()
+ document.querySelector('.contextMenu').style.display = :none
- pathname = window.parent.location.pathname
- HTTP.delete(pathname).then {
- Status.pushDeleted pathname
- window.parent.location.href = '../..'
- }.catch {|error|
- alert error
- @busy = false
- }
- end
- end
+ @menu = nil
+ @busy = false
end
# burst a PDF into individual pages
def burst(event)
data = {
- selected: @selected,
+ selected: @menu,
message: window.parent.location.pathname
}
@@ -201,18 +182,18 @@ def burst(event)
HTTP.post('../../actions/burst', data).then {|response|
@attachments = response.attachments
@selected = response.selected
- @busy = false
+ self.hideMenu()
window.parent.frames.content.location.href=response.selected
}.catch {|error|
alert error
- @busy = false
+ self.hideMenu()
}
end
# burst a PDF into individual pages
def delete_attachment(event)
data = {
- selected: @selected,
+ selected: @menu,
message: window.parent.location.pathname
}
@@ -220,14 +201,14 @@ def delete_attachment(event)
HTTP.post('../../actions/delete-attachment', data).then {|response|
if response.attachments and not response.attachments.empty?
@attachments = response.attachments
- @busy = false
+ self.hideMenu()
window.parent.frames.content.location.href='_body_'
else
window.parent.location.href = '../..'
end
}.catch {|error|
alert error
- @busy = false
+ self.hideMenu()
}
end
@@ -236,7 +217,7 @@ def rotate_attachment(event)
message = window.parent.location.pathname
data = {
- selected: @selected,
+ selected: @menu,
message: message,
direction: event.currentTarget.textContent
}
@@ -245,24 +226,74 @@ def rotate_attachment(event)
HTTP.post('../../actions/rotate-attachment', data).then {|response|
@attachments = response.attachments
@selected = response.selected
- @busy = false
+ self.hideMenu()
# reload attachment in content pane
window.parent.frames.content.location.href = response.selected
}.catch {|error|
alert error
+ self.hideMenu()
+ }
+ end
+
+ ########################################################################
+ # Miscellaneous #
+ ########################################################################
+
+ # form submission - handles all forms
+ def submit(event)
+ event.preventDefault()
+ form = event.currentTarget
+
+ data = {}
+ Array(form.querySelectorAll('input')).each do |field|
+ data[field.name] = field.value if field.name
+ end
+
+ @busy = true
+ HTTP(post form.action, data).then {|response|
+ @busy = false
+ alert response.result
+ }.catch {|error|
+ alert error
@busy = false
}
end
+ # clicking on an attachment selects it
+ def select(event)
+ @selected = event.currentTarget.querySelector('a').getAttribute('href')
+ end
+
+ # handle keyboard events
+ def keydown(event)
+ if event.keyCode == 8 or event.keyCode == 46 # backspace or delete
+ if event.metaKey or event.ctrlKey
+ @busy = true
+ event.stopPropagation()
+
+ pathname = window.parent.location.pathname
+ HTTP.delete(pathname).then {
+ Status.pushDeleted pathname
+ window.parent.location.href = '../..'
+ }.catch {|error|
+ alert error
+ @busy = false
+ }
+ end
+ end
+ end
+
+ ########################################################################
+ # drag/drop support #
+ ########################################################################
#
- # drag/drop support. Note: support varies by browser (in particular,
- # when events are called and whether or not a particular event has
- # access to dataTransfer data.) Accordingly, the below is coded in
- # a way that is mildly redundant and uses React.js state data in lieu of
- # dataTransfer. Oddly, with some browsers, drag and drop isn't possible
- # without setting something in dataTransfer, so that data is set too, even
- # though it is not used.
+ # Note: support varies by browser (in particular, when events are called
+ # and whether or not a particular event has access to dataTransfer data.)
+ # Accordingly, the below is coded in a way that is mildly redundant and
+ # uses React.js state data in lieu of dataTransfer. Oddly, with some
+ # browsers, drag and drop isn't possible without setting something in
+ # dataTransfer, so that data is set too, even though it is not used.
#
# start by capturing the 'href' attribute