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

Reply via email to