From 8f23b685878826fb5c932d36322a6c8526471fcf Mon Sep 17 00:00:00 2001
From: eikek <eike.kettner@posteo.de>
Date: Tue, 17 Aug 2021 02:15:14 +0200
Subject: [PATCH] Add a qr code to the link of an item or attachment

---
 .../webapp/src/main/elm/Comp/ItemDetail.elm   |   2 +-
 .../src/main/elm/Comp/ItemDetail/Model.elm    |  32 ++++-
 .../main/elm/Comp/ItemDetail/ShowQrCode.elm   | 117 ++++++++++++++++++
 .../elm/Comp/ItemDetail/SingleAttachment.elm  |  29 ++++-
 .../src/main/elm/Comp/ItemDetail/Update.elm   |  27 ++++
 .../src/main/elm/Comp/ItemDetail/View2.elm    |  55 +++++---
 .../webapp/src/main/elm/Comp/SourceManage.elm |   7 +-
 modules/webapp/src/main/elm/Data/Icons.elm    |  26 ++--
 .../src/main/elm/Messages/Comp/ItemDetail.elm |   6 +
 .../Comp/ItemDetail/SingleAttachment.elm      |   3 +
 .../src/main/elm/Page/ItemDetail/View2.elm    |   4 +-
 modules/webapp/src/main/elm/Ports.elm         |   7 ++
 modules/webapp/src/main/elm/Styles.elm        |   5 +
 modules/webapp/src/main/webjar/docspell.js    |  28 +++++
 14 files changed, 307 insertions(+), 41 deletions(-)
 create mode 100644 modules/webapp/src/main/elm/Comp/ItemDetail/ShowQrCode.elm

diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail.elm b/modules/webapp/src/main/elm/Comp/ItemDetail.elm
index ec9917f2..7e320da8 100644
--- a/modules/webapp/src/main/elm/Comp/ItemDetail.elm
+++ b/modules/webapp/src/main/elm/Comp/ItemDetail.elm
@@ -38,6 +38,6 @@ update =
     Comp.ItemDetail.Update.update
 
 
-view2 : Texts -> ItemNav -> UiSettings -> Model -> Html Msg
+view2 : Texts -> Flags -> ItemNav -> UiSettings -> Model -> Html Msg
 view2 =
     Comp.ItemDetail.View2.view
diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm
index de3affe7..2a9336e5 100644
--- a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm
+++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm
@@ -18,7 +18,10 @@ module Comp.ItemDetail.Model exposing
     , ViewMode(..)
     , emptyModel
     , initSelectViewModel
+    , initShowQrModel
     , isEditNotes
+    , isShowQrAttach
+    , isShowQrItem
     , personMatchesOrg
     , resultModel
     , resultModelCmd
@@ -40,7 +43,6 @@ import Api.Model.SentMails exposing (SentMails)
 import Api.Model.Tag exposing (Tag)
 import Api.Model.TagList exposing (TagList)
 import Comp.AttachmentMeta
-import Comp.ConfirmModal
 import Comp.CustomFieldMultiInput
 import Comp.DatePicker
 import Comp.DetailEdit
@@ -118,9 +120,33 @@ type alias Model =
     , attachmentDropdownOpen : Bool
     , editMenuTabsOpen : Set String
     , viewMode : ViewMode
+    , showQrModel : ShowQrModel
     }
 
 
+type alias ShowQrModel =
+    { item : Bool
+    , attach : Bool
+    }
+
+
+initShowQrModel : ShowQrModel
+initShowQrModel =
+    { item = False
+    , attach = False
+    }
+
+
+isShowQrItem : ShowQrModel -> Bool
+isShowQrItem model =
+    model.item
+
+
+isShowQrAttach : ShowQrModel -> Bool
+isShowQrAttach model =
+    model.attach
+
+
 type ConfirmModalValue
     = ConfirmModalReprocessItem Msg
     | ConfirmModalReprocessFile Msg
@@ -230,6 +256,7 @@ emptyModel =
     , attachmentDropdownOpen = False
     , editMenuTabsOpen = Set.empty
     , viewMode = SimpleView
+    , showQrModel = initShowQrModel
     }
 
 
@@ -340,6 +367,9 @@ type Msg
     | ReprocessItemConfirmed
     | ToggleSelectView
     | RestoreItem
+    | ToggleShowQrItem String
+    | ToggleShowQrAttach String
+    | PrintElement String
 
 
 type SaveNameState
diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/ShowQrCode.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/ShowQrCode.elm
new file mode 100644
index 00000000..2b649ed5
--- /dev/null
+++ b/modules/webapp/src/main/elm/Comp/ItemDetail/ShowQrCode.elm
@@ -0,0 +1,117 @@
+{-
+   Copyright 2020 Docspell Contributors
+
+   SPDX-License-Identifier: GPL-3.0-or-later
+-}
+
+
+module Comp.ItemDetail.ShowQrCode exposing (UrlId(..), qrCodeElementId, view, view1)
+
+import Api
+import Comp.Basic as B
+import Comp.ItemDetail.Model exposing (Model, Msg(..), isShowQrAttach, isShowQrItem)
+import Comp.MenuBar as MB
+import Data.Flags exposing (Flags)
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (onClick)
+import QRCode
+import Styles as S
+
+
+view : Flags -> String -> Model -> UrlId -> Html Msg
+view flags classes model urlId =
+    case urlId of
+        Attach _ ->
+            if isShowQrAttach model.showQrModel then
+                view1 flags classes urlId
+
+            else
+                span [ class "hidden" ] []
+
+        Item _ ->
+            if isShowQrItem model.showQrModel then
+                view1 flags classes urlId
+
+            else
+                span [ class "hidden" ] []
+
+
+view1 : Flags -> String -> UrlId -> Html Msg
+view1 flags classes urlId =
+    let
+        docUrl =
+            case urlId of
+                Attach str ->
+                    flags.config.baseUrl ++ Api.fileURL str
+
+                Item str ->
+                    flags.config.baseUrl ++ "/app/item/" ++ str
+
+        elementId =
+            qrCodeElementId urlId
+
+        toggleShowQr =
+            case urlId of
+                Attach id ->
+                    ToggleShowQrAttach id
+
+                Item id ->
+                    ToggleShowQrItem id
+    in
+    div
+        [ class "flex flex-col py-2 px-2 items-center"
+        , class classes
+        ]
+        [ MB.view
+            { start =
+                [ MB.PrimaryButton
+                    { tagger = PrintElement elementId
+                    , title = "Print this QR code"
+                    , icon = Just "fa fa-print"
+                    , label = "Print"
+                    }
+                ]
+            , end =
+                [ MB.SecondaryButton
+                    { tagger = toggleShowQr
+                    , title = "Close"
+                    , icon = Just "fa fa-times"
+                    , label = "Close"
+                    }
+                ]
+            , rootClasses = "w-full mt-2 mb-4"
+            }
+        , div [ class "flex flex-col sm:flex-row sm:space-x-2" ]
+            [ div
+                [ class S.border
+                , class S.qrCode
+                , id elementId
+                ]
+                [ qrCodeView docUrl
+                ]
+            ]
+        ]
+
+
+qrCodeElementId : UrlId -> String
+qrCodeElementId urlId =
+    case urlId of
+        Attach str ->
+            "qr-attach-" ++ str
+
+        Item str ->
+            "qr-item-" ++ str
+
+
+type UrlId
+    = Attach String
+    | Item String
+
+
+qrCodeView : String -> Html msg
+qrCodeView message =
+    QRCode.encode message
+        |> Result.map QRCode.toSvg
+        |> Result.withDefault
+            (text "Error generating QR code")
diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm
index 0838a985..26dea7a9 100644
--- a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm
+++ b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm
@@ -11,8 +11,19 @@ import Api
 import Api.Model.Attachment exposing (Attachment)
 import Comp.AttachmentMeta
 import Comp.ItemDetail.ConfirmModalView
-import Comp.ItemDetail.Model exposing (Model, Msg(..), NotesField(..), SaveNameState(..), ViewMode(..))
+import Comp.ItemDetail.Model
+    exposing
+        ( Model
+        , Msg(..)
+        , NotesField(..)
+        , SaveNameState(..)
+        , ViewMode(..)
+        , isShowQrAttach
+        )
+import Comp.ItemDetail.ShowQrCode
 import Comp.MenuBar as MB
+import Data.Flags exposing (Flags)
+import Data.Icons as Icons
 import Data.UiSettings exposing (UiSettings)
 import Dict
 import Html exposing (..)
@@ -27,8 +38,8 @@ import Util.Size
 import Util.String
 
 
-view : Texts -> UiSettings -> Model -> Int -> Attachment -> Html Msg
-view texts settings model pos attach =
+view : Texts -> Flags -> UiSettings -> Model -> Int -> Attachment -> Html Msg
+view texts flags settings model pos attach =
     let
         fileUrl =
             Api.fileURL attach.id
@@ -61,6 +72,11 @@ view texts settings model pos attach =
                 Nothing ->
                     span [ class "hidden" ] []
 
+          else if isShowQrAttach model.showQrModel then
+            Comp.ItemDetail.ShowQrCode.view1 flags
+                "border-r border-l border-b dark:border-bluegray-600 h-full"
+                (Comp.ItemDetail.ShowQrCode.Attach attach.id)
+
           else
             div
                 [ class "flex flex-col relative px-2 pt-2 h-full"
@@ -269,6 +285,13 @@ attachHeader texts settings model _ attach =
                                 , href "#"
                                 ]
                           }
+                        , { icon = Icons.showQr
+                          , label = texts.showQrCode
+                          , attrs =
+                                [ onClick (ToggleShowQrAttach attach.id)
+                                , href "#"
+                                ]
+                          }
                         , { icon = "fa fa-trash"
                           , label = texts.deleteThisFile
                           , attrs =
diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm
index d3574835..1289a52f 100644
--- a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm
+++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm
@@ -42,7 +42,10 @@ import Comp.ItemDetail.Model
         , UpdateResult
         , ViewMode(..)
         , initSelectViewModel
+        , initShowQrModel
         , isEditNotes
+        , isShowQrAttach
+        , isShowQrItem
         , resultModel
         , resultModelCmd
         , resultModelCmdSub
@@ -66,6 +69,7 @@ import Dict
 import Html5.DragDrop as DD
 import Http
 import Page exposing (Page(..))
+import Ports
 import Set exposing (Set)
 import Throttle
 import Time
@@ -1607,6 +1611,29 @@ update key flags inav settings msg model =
         RestoreItem ->
             resultModelCmd ( model, Api.restoreItem flags model.item.id SaveResp )
 
+        ToggleShowQrItem id ->
+            let
+                sqm =
+                    model.showQrModel
+
+                next =
+                    { sqm | item = not sqm.item }
+            in
+            resultModel { model | showQrModel = next }
+
+        ToggleShowQrAttach id ->
+            let
+                sqm =
+                    model.showQrModel
+
+                next =
+                    { sqm | attach = not sqm.attach }
+            in
+            resultModel { model | attachmentDropdownOpen = False, showQrModel = next }
+
+        PrintElement id ->
+            resultModelCmd ( model, Ports.printElement id )
+
 
 
 --- Helper
diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm
index 0b4f7559..addfe0a0 100644
--- a/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm
+++ b/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm
@@ -19,12 +19,15 @@ import Comp.ItemDetail.Model
         , Msg(..)
         , NotesField(..)
         , SaveNameState(..)
+        , isShowQrItem
         )
 import Comp.ItemDetail.Notes
+import Comp.ItemDetail.ShowQrCode
 import Comp.ItemDetail.SingleAttachment
 import Comp.ItemMail
 import Comp.MenuBar as MB
 import Comp.SentMails
+import Data.Flags exposing (Flags)
 import Data.Icons as Icons
 import Data.ItemNav exposing (ItemNav)
 import Data.UiSettings exposing (UiSettings)
@@ -36,12 +39,12 @@ import Page exposing (Page(..))
 import Styles as S
 
 
-view : Texts -> ItemNav -> UiSettings -> Model -> Html Msg
-view texts inav settings model =
+view : Texts -> Flags -> ItemNav -> UiSettings -> Model -> Html Msg
+view texts flags inav settings model =
     div [ class "flex flex-col h-full" ]
         [ header texts settings model
         , menuBar texts inav settings model
-        , body texts inav settings model
+        , body texts flags inav settings model
         , itemModal texts model
         ]
 
@@ -146,7 +149,7 @@ menuBar texts inav settings model =
                         [ ( "bg-gray-200 dark:bg-bluegray-600", model.addFilesOpen )
                         ]
                     , if model.addFilesOpen then
-                        title "Close"
+                        title texts.close
 
                       else
                         title texts.addMoreFiles
@@ -156,6 +159,22 @@ menuBar texts inav settings model =
                     ]
                     [ Icons.addFilesIcon2 ""
                     ]
+            , MB.CustomElement <|
+                a
+                    [ classList
+                        [ ( "bg-gray-200 dark:bg-bluegray-600", isShowQrItem model.showQrModel )
+                        ]
+                    , if isShowQrItem model.showQrModel then
+                        title texts.close
+
+                      else
+                        title texts.showQrCode
+                    , onClick (ToggleShowQrItem model.item.id)
+                    , class S.secondaryBasicButton
+                    , href "#"
+                    ]
+                    [ Icons.showQrIcon ""
+                    ]
             , MB.CustomElement <|
                 a
                     [ class S.primaryButton
@@ -214,20 +233,24 @@ menuBar texts inav settings model =
         }
 
 
-body : Texts -> ItemNav -> UiSettings -> Model -> Html Msg
-body texts _ settings model =
+body : Texts -> Flags -> ItemNav -> UiSettings -> Model -> Html Msg
+body texts flags _ settings model =
     div [ class "grid gap-2 grid-cols-1 md:grid-cols-3 h-full" ]
-        [ leftArea texts settings model
-        , rightArea texts settings model
+        [ leftArea texts flags settings model
+        , rightArea texts flags settings model
         ]
 
 
-leftArea : Texts -> UiSettings -> Model -> Html Msg
-leftArea texts settings model =
+leftArea : Texts -> Flags -> UiSettings -> Model -> Html Msg
+leftArea texts flags settings model =
     div [ class "w-full md:order-first md:mr-2 flex flex-col" ]
         [ addDetailForm texts settings model
         , sendMailForm texts settings model
         , Comp.ItemDetail.AddFilesForm.view texts.addFilesForm model
+        , Comp.ItemDetail.ShowQrCode.view flags
+            (S.border ++ " mb-4")
+            model
+            (Comp.ItemDetail.ShowQrCode.Item model.item.id)
         , Comp.ItemDetail.Notes.view texts.notes model
         , div
             [ classList
@@ -245,15 +268,15 @@ leftArea texts settings model =
         ]
 
 
-rightArea : Texts -> UiSettings -> Model -> Html Msg
-rightArea texts settings model =
+rightArea : Texts -> Flags -> UiSettings -> Model -> Html Msg
+rightArea texts flags settings model =
     div [ class "md:col-span-2 h-full" ]
-        (attachmentsBody texts settings model)
+        (attachmentsBody texts flags settings model)
 
 
-attachmentsBody : Texts -> UiSettings -> Model -> List (Html Msg)
-attachmentsBody texts settings model =
-    List.indexedMap (Comp.ItemDetail.SingleAttachment.view texts.singleAttachment settings model)
+attachmentsBody : Texts -> Flags -> UiSettings -> Model -> List (Html Msg)
+attachmentsBody texts flags settings model =
+    List.indexedMap (Comp.ItemDetail.SingleAttachment.view texts.singleAttachment flags settings model)
         model.item.attachments
 
 
diff --git a/modules/webapp/src/main/elm/Comp/SourceManage.elm b/modules/webapp/src/main/elm/Comp/SourceManage.elm
index 1753eb31..cfa7f501 100644
--- a/modules/webapp/src/main/elm/Comp/SourceManage.elm
+++ b/modules/webapp/src/main/elm/Comp/SourceManage.elm
@@ -279,9 +279,6 @@ viewLinks2 texts flags _ source =
 
         styleUrl =
             "truncate px-2 py-2 border-0 border-t border-b border-r font-mono text-sm my-auto rounded-r border-gray-400 dark:border-bluegray-500"
-
-        styleQr =
-            "max-w-min dark:bg-bluegray-400 bg-gray-50 mx-auto md:mx-0"
     in
     div
         []
@@ -350,7 +347,7 @@ viewLinks2 texts flags _ source =
         , div [ class "py-2" ]
             [ div
                 [ class S.border
-                , class styleQr
+                , class S.qrCode
                 ]
                 [ qrCodeView texts appUrl
                 ]
@@ -386,7 +383,7 @@ viewLinks2 texts flags _ source =
         , div [ class "py-2" ]
             [ div
                 [ class S.border
-                , class styleQr
+                , class S.qrCode
                 ]
                 [ qrCodeView texts apiUrl
                 ]
diff --git a/modules/webapp/src/main/elm/Data/Icons.elm b/modules/webapp/src/main/elm/Data/Icons.elm
index f0fd4976..76a0ab7f 100644
--- a/modules/webapp/src/main/elm/Data/Icons.elm
+++ b/modules/webapp/src/main/elm/Data/Icons.elm
@@ -6,9 +6,7 @@
 
 
 module Data.Icons exposing
-    ( addFiles
-    , addFiles2
-    , addFilesIcon
+    ( addFiles2
     , addFilesIcon2
     , concerned
     , concerned2
@@ -60,6 +58,8 @@ module Data.Icons exposing
     , personIcon2
     , search
     , searchIcon
+    , showQr
+    , showQrIcon
     , source
     , source2
     , sourceIcon
@@ -321,26 +321,26 @@ editNotesIcon =
     i [ class editNotes ] []
 
 
-addFiles : String
-addFiles =
-    "file plus icon"
-
-
 addFiles2 : String
 addFiles2 =
     "fa fa-file-upload"
 
 
-addFilesIcon : Html msg
-addFilesIcon =
-    i [ class addFiles ] []
-
-
 addFilesIcon2 : String -> Html msg
 addFilesIcon2 classes =
     i [ class addFiles2, class classes ] []
 
 
+showQr : String
+showQr =
+    "fa fa-qrcode"
+
+
+showQrIcon : String -> Html msg
+showQrIcon classes =
+    i [ class showQr, class classes ] []
+
+
 tag : String
 tag =
     "tag icon"
diff --git a/modules/webapp/src/main/elm/Messages/Comp/ItemDetail.elm b/modules/webapp/src/main/elm/Messages/Comp/ItemDetail.elm
index dda86e66..f64eadb7 100644
--- a/modules/webapp/src/main/elm/Messages/Comp/ItemDetail.elm
+++ b/modules/webapp/src/main/elm/Messages/Comp/ItemDetail.elm
@@ -55,6 +55,8 @@ type alias Texts =
     , sendingMailNow : String
     , formatDateTime : Int -> String
     , mailSendSuccessful : String
+    , showQrCode : String
+    , close : String
     }
 
 
@@ -89,6 +91,8 @@ gb =
     , sendingMailNow = "Sending e-mail…"
     , formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.English
     , mailSendSuccessful = "Mail sent."
+    , showQrCode = "Show the URL to this page as QR code"
+    , close = "Close"
     }
 
 
@@ -123,4 +127,6 @@ de =
     , sendingMailNow = "E-Mail wird gesendet…"
     , formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.German
     , mailSendSuccessful = "E-Mail wurde versendet."
+    , showQrCode = "Den Link zu dieser Seite als QR code anzeigen"
+    , close = "Schließen"
     }
diff --git a/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/SingleAttachment.elm b/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/SingleAttachment.elm
index b26acff6..ef8a57db 100644
--- a/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/SingleAttachment.elm
+++ b/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/SingleAttachment.elm
@@ -31,6 +31,7 @@ type alias Texts =
     , selectModeTitle : String
     , exitSelectMode : String
     , deleteAttachments : String
+    , showQrCode : String
     }
 
 
@@ -51,6 +52,7 @@ gb =
     , selectModeTitle = "Select Mode"
     , exitSelectMode = "Exit Select Mode"
     , deleteAttachments = "Delete attachments"
+    , showQrCode = "Show URL as QR code"
     }
 
 
@@ -71,4 +73,5 @@ de =
     , selectModeTitle = "Auswahlmodus"
     , exitSelectMode = "Auswahlmodus beenden"
     , deleteAttachments = "Anhänge löschen"
+    , showQrCode = "Link als QR Code anzeigen"
     }
diff --git a/modules/webapp/src/main/elm/Page/ItemDetail/View2.elm b/modules/webapp/src/main/elm/Page/ItemDetail/View2.elm
index 1c0d574b..7b5158a0 100644
--- a/modules/webapp/src/main/elm/Page/ItemDetail/View2.elm
+++ b/modules/webapp/src/main/elm/Page/ItemDetail/View2.elm
@@ -61,11 +61,11 @@ viewSidebar texts visible flags settings model =
 
 
 viewContent : Texts -> ItemNav -> Flags -> UiSettings -> Model -> Html Msg
-viewContent texts inav _ settings model =
+viewContent texts inav flags settings model =
     div
         [ id "content"
         , class S.content
         ]
         [ Html.map ItemDetailMsg
-            (Comp.ItemDetail.view2 texts.itemDetail inav settings model.detail)
+            (Comp.ItemDetail.view2 texts.itemDetail flags inav settings model.detail)
         ]
diff --git a/modules/webapp/src/main/elm/Ports.elm b/modules/webapp/src/main/elm/Ports.elm
index 7fd14f39..51438dfa 100644
--- a/modules/webapp/src/main/elm/Ports.elm
+++ b/modules/webapp/src/main/elm/Ports.elm
@@ -8,6 +8,7 @@
 port module Ports exposing
     ( checkSearchQueryString
     , initClipboard
+    , printElement
     , receiveCheckQueryResult
     , receiveUiSettings
     , removeAccount
@@ -48,6 +49,12 @@ port receiveUiSettings : (StoredUiSettings -> msg) -> Sub msg
 port requestUiSettings : AuthResult -> Cmd msg
 
 
+{-| Creates a new window/tab, writes the contents of the given element
+and calls the print dialog.
+-}
+port printElement : String -> Cmd msg
+
+
 setUiTheme : UiTheme -> Cmd msg
 setUiTheme theme =
     internalSetUiTheme (Data.UiTheme.toString theme)
diff --git a/modules/webapp/src/main/elm/Styles.elm b/modules/webapp/src/main/elm/Styles.elm
index e92b29fe..c73865fa 100644
--- a/modules/webapp/src/main/elm/Styles.elm
+++ b/modules/webapp/src/main/elm/Styles.elm
@@ -351,3 +351,8 @@ tableMain =
 tableRow : String
 tableRow =
     "border-t dark:border-bluegray-600"
+
+
+qrCode : String
+qrCode =
+    "max-w-min dark:bg-bluegray-400 bg-gray-50 mx-auto md:mx-0"
diff --git a/modules/webapp/src/main/webjar/docspell.js b/modules/webapp/src/main/webjar/docspell.js
index d13f6889..a5518163 100644
--- a/modules/webapp/src/main/webjar/docspell.js
+++ b/modules/webapp/src/main/webjar/docspell.js
@@ -105,3 +105,31 @@ elmApp.ports.checkSearchQueryString.subscribe(function(args) {
         elmApp.ports.receiveCheckQueryResult.send(answer);
     }
 });
+
+elmApp.ports.printElement.subscribe(function(id) {
+    if (id) {
+        var el = document.getElementById(id);
+        var head = document.getElementsByTagName('head');
+        if (head && head.length > 0) {
+            head = head[0];
+        }
+        if (el) {
+            var w = window.open();
+            w.document.write('<html>');
+            if (head) {
+                w.document.write('<head>');
+                ['title', 'meta'].forEach(function(el) {
+                    var headEls = head.getElementsByTagName(el);
+                    for (var i=0; i<headEls.length; i++) {
+                        w.document.write(headEls.item(i).outerHTML);
+                    }
+                });
+                w.document.write('</head>');
+            }
+            w.document.write('<body>');
+            w.document.write(el.outerHTML);
+            w.document.write('<script type="application/javascript">window.print(); window.close();</script>');
+            w.document.write('</body></html>');
+        }
+    }
+});