├── .gitignore ├── LICENSE ├── README.md ├── docs ├── demos │ ├── demo.html │ ├── html2canvas.min.js │ ├── script.js │ ├── simple │ │ ├── edit.html │ │ └── screenshot.html │ ├── styles.css │ └── welcome_card.jpg ├── favicon.ico ├── images │ ├── FeedbackPlus_Demo.gif │ ├── FeedbackPlus_Editing_Demo.gif │ ├── logo.png │ ├── logo_sm.png │ ├── logo_sm_tp.png │ ├── logo_tp.png │ └── preview.png ├── index.html ├── script.js └── styles.css ├── package.json └── src ├── feedbackplus.css └── feedbackplus.js /.gitignore: -------------------------------------------------------------------------------- 1 | notes.md 2 | .project 3 | docs/images/private -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ColonelParrot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | 4 |
5 |

6 | 7 |

8 | 9 |
10 |
11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 |

19 | 20 |

21 | 22 |

23 | 24 |

25 | FeedbackPlus is an open source Javascript library that allows you to add screenshot taking & screenshot editing functionality to your feedback forms. 26 |

27 | Available for use by cdn or via npm 28 |

29 | The project is inspired by Google's report an issue widget, which allows you to take & edit screenshots. Under the hood, it uses the browser display API and fallbacks to html2canvas if available (see here) 30 |

31 | 32 |

Preview (demo)

33 | 34 | | Taking a Screenshot | Editing screenshot | 35 | | --------------------------------------------- | ----------------------------------------------------- | 36 | | | | 37 | 38 |

(click images to enlarge)

39 | 40 | ## Quickstart 41 | 42 | For more detailed instructions, see the [documentation](https://github.com/puffinsoft/feedbackplus/wiki) 43 | 44 | You can find bare-minimum demo code for screenshotting & screenshot editing in the [demo/simple](/docs/demos/simple/) folder 45 | 46 | ### Import 47 | 48 | npm: 49 | 50 | ```js 51 | $ npm i feedbackplus 52 | import FeedbackPlus from 'feedbackplus' 53 | ``` 54 | 55 | cdn via [jsDelivr](https://www.jsdelivr.com/package/gh/ColonelParrot/feedbackplus) (or with [cdnjs](https://cdnjs.com/libraries/feedbackplus)): 56 | 57 | ```html 58 | 59 | 60 | 61 | 62 | ``` 63 | 64 | ```js 65 | const feedbackPlus = new FeedbackPlus(); 66 | ``` 67 | 68 | ### Capture Screenshot 69 | 70 | ...and draw to canvas 71 | 72 | ```js 73 | const canvas = document.getElementById("canvas"); 74 | feedbackPlus.capture().then(({ bitmap, width, height }) => { 75 | canvas.width = width; 76 | canvas.height = height; 77 | canvas.getContext("2d").drawImage(bitmap, 0, 0); 78 | }); 79 | ``` 80 | 81 | ### Showing Edit Dialog for Screenshot 82 | 83 | ```js 84 | feedbackPlus.showEditDialog(bitmap, function (canvas) { 85 | // user completed edit 86 | FeedbackPlus.canvasToBitmap(canvas).then(({ bitmap }) => { 87 | canvas.getContext("2d").drawImage(bitmap, 0, 0); 88 | feedbackPlus.closeEditDialog(); 89 | }); 90 | }, function () { 91 | // user cancelled edit 92 | feedbackPlus.closeEditDialog(); 93 | }); 94 | ``` 95 | -------------------------------------------------------------------------------- /docs/demos/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | FeedbackPlus Demo: Try out the Javascript screenshot & screenshot editing library 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 |

Submit feedback

33 |
34 |
35 |
What's your name?
36 |
37 | 38 | 39 |
40 |
Describe the issue
41 |
42 | 43 | 44 |
45 |
A screenshot will help us better understand the issue.
46 |
47 | 56 | 57 | 72 |
73 | 78 |
79 |
80 |
81 | View source on 82 | Github 83 |
84 | 85 |
86 |
87 | 88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /docs/demos/script.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const screenshotButton = document.getElementById("screenshot-button") 3 | 4 | const screenshotResult = document.getElementById("screenshot-result") 5 | const screenshotCanvas = document.getElementById("screenshot-canvas") 6 | 7 | const screenshotEdit = document.getElementById("screenshot-edit") 8 | const screenshotDelete = document.getElementById("screenshot-delete") 9 | const feedbackPlus = new FeedbackPlus() 10 | 11 | const snackbar = document.getElementById("feedbackplus-snackbar") 12 | 13 | let screenshot; 14 | screenshotButton.addEventListener('click', function () { 15 | if (FeedbackPlus.isSupported()) { 16 | showScreenshotLoading() 17 | feedbackPlus.capture().then(bitmap => { 18 | tooltip.hide() 19 | hideScreenshotLoading() 20 | screenshot = bitmap; 21 | updateResultCanvas() 22 | }).catch(e => hideScreenshotLoading()) 23 | } else { 24 | snackbar.MaterialSnackbar.showSnackbar({ 25 | message: 'Your device does not support this feature', 26 | timeout: 5000, 27 | }) 28 | } 29 | }) 30 | 31 | screenshotEdit.addEventListener('click', function () { 32 | feedbackPlus.showEditDialog(screenshot.bitmap, function (canvas) { 33 | FeedbackPlus.canvasToBitmap(canvas).then(bitmap => { 34 | screenshot = bitmap; 35 | updateResultCanvas() 36 | feedbackPlus.closeEditDialog() 37 | }) 38 | }, function () { 39 | feedbackPlus.closeEditDialog() 40 | }) 41 | }) 42 | 43 | screenshotDelete.addEventListener('click', function () { 44 | screenshot = null; 45 | screenshotButton.style.display = "block" 46 | screenshotResult.style.display = "none" 47 | tooltip.show() 48 | }) 49 | 50 | function updateResultCanvas() { 51 | screenshotButton.style.display = "none" 52 | screenshotResult.style.display = "block" 53 | const newHeight = (screenshot.height / screenshot.width) * 330 54 | screenshotCanvas.width = 330; 55 | screenshotCanvas.height = newHeight; 56 | screenshotCanvas.getContext('2d').drawImage(screenshot.bitmap, 0, 0, 330, newHeight) 57 | } 58 | 59 | function showScreenshotLoading(){ 60 | document.getElementById("screenshot-button-notloading").style.display = "none"; 61 | document.getElementById("screenshot-button-loading").style.display = "block"; 62 | } 63 | 64 | function hideScreenshotLoading(){ 65 | document.getElementById("screenshot-button-notloading").style.display = "block"; 66 | document.getElementById("screenshot-button-loading").style.display = "none"; 67 | } 68 | 69 | const tooltip = tippy(screenshotButton, { 70 | content: 'Try it out!', 71 | placement: 'right' 72 | }) 73 | tooltip.show() 74 | })(); -------------------------------------------------------------------------------- /docs/demos/simple/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | FeedbackPlus - Edit Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 44 | 45 | 46 | 47 |
48 | 50 | 51 |
52 |

Screenshot will appear here

53 | 54 |
55 | 56 | 58 |
59 | 60 | 61 | 62 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /docs/demos/simple/screenshot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | FeedbackPlus - Screenshot Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 44 | 45 | 46 | 47 |
48 | 49 | 50 |
51 |

Screenshot will appear here

52 | 53 |
54 |
55 | 56 | 57 | 58 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /docs/demos/styles.css: -------------------------------------------------------------------------------- 1 | .demo-card-wide > .mdl-card__title { 2 | color: #fff; 3 | height: 100px; 4 | background: url("welcome_card.jpg") center / cover; 5 | } 6 | .demo-card-wide > .mdl-card__menu { 7 | color: #fff; 8 | } 9 | 10 | #feedback-modal { 11 | position: fixed; 12 | top: 50%; 13 | left: 50%; 14 | transform: translate(-50%, -50%); 15 | } 16 | 17 | input, 18 | label { 19 | font-family: "Roboto" !important; 20 | } 21 | 22 | #feedback-modal h6 { 23 | margin-top: 0 !important; 24 | margin-bottom: 0 !important; 25 | } 26 | 27 | .feedbackplus-modal h2 { 28 | font-size: initial; 29 | } 30 | 31 | #screenshot-result #screenshot-delete { 32 | position: absolute; 33 | top: -10px; 34 | right: -10px; 35 | z-index: 1; 36 | color: red; 37 | background-color: white; 38 | } 39 | 40 | textarea { 41 | resize: none !important; 42 | } 43 | 44 | #screenshot-button { 45 | width: 100%; 46 | } 47 | 48 | #screenshot-result { 49 | display: block; 50 | position: relative; 51 | } 52 | 53 | #screenshot-result #screenshot-edit-layer { 54 | position: absolute; 55 | top: 0; 56 | left: 0; 57 | height: 100%; 58 | width: 100%; 59 | background-color: rgb(0, 0, 0, 0.1); 60 | display: flex; 61 | align-items: center; 62 | justify-content: center; 63 | border-radius: 10px; 64 | border: 2px solid grey; 65 | overflow: hidden; 66 | } 67 | 68 | #github { 69 | position: fixed; 70 | bottom: 10px; 71 | left: 50%; 72 | transform: translateX(-50%); 73 | } 74 | 75 | .mdl-spinner { 76 | height: 20px !important; 77 | width: 20px !important; 78 | display: flex !important; 79 | margin: auto; 80 | } 81 | -------------------------------------------------------------------------------- /docs/demos/welcome_card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puffinsoft/feedbackplus/085a2d3c02003a241f9db6ab788fcef424ec8ee0/docs/demos/welcome_card.jpg -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puffinsoft/feedbackplus/085a2d3c02003a241f9db6ab788fcef424ec8ee0/docs/favicon.ico -------------------------------------------------------------------------------- /docs/images/FeedbackPlus_Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puffinsoft/feedbackplus/085a2d3c02003a241f9db6ab788fcef424ec8ee0/docs/images/FeedbackPlus_Demo.gif -------------------------------------------------------------------------------- /docs/images/FeedbackPlus_Editing_Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puffinsoft/feedbackplus/085a2d3c02003a241f9db6ab788fcef424ec8ee0/docs/images/FeedbackPlus_Editing_Demo.gif -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puffinsoft/feedbackplus/085a2d3c02003a241f9db6ab788fcef424ec8ee0/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/logo_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puffinsoft/feedbackplus/085a2d3c02003a241f9db6ab788fcef424ec8ee0/docs/images/logo_sm.png -------------------------------------------------------------------------------- /docs/images/logo_sm_tp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puffinsoft/feedbackplus/085a2d3c02003a241f9db6ab788fcef424ec8ee0/docs/images/logo_sm_tp.png -------------------------------------------------------------------------------- /docs/images/logo_tp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puffinsoft/feedbackplus/085a2d3c02003a241f9db6ab788fcef424ec8ee0/docs/images/logo_tp.png -------------------------------------------------------------------------------- /docs/images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puffinsoft/feedbackplus/085a2d3c02003a241f9db6ab788fcef424ec8ee0/docs/images/preview.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | FeedbackPlus: Javascript screenshot & screenshot editing library 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 |
35 | 36 | 46 | 47 | 51 |

Screenshotting & screenshot editing for your feedback forms

52 |
53 | 55 | 60 | View on Github 61 | 62 | 63 | 65 | 66 | 69 | 70 | View on npm 71 | 72 |
73 |
74 |
75 |
76 |
npm
77 |
html
78 |
79 |
80 |
81 |
$ npm i feedbackplus
 82 | $ import FeedbackPlus from 'feedbackplus'
83 | 85 | 88 | 89 | 91 | 93 | 94 |
95 |
96 | 113 |
114 |
115 | 116 | Try the demo! 117 | 118 |
119 | documentationcdnjsjsDelivrlicense 123 |
124 | 125 | 126 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /docs/script.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const clipboardData = { 3 | npm: "npm i feedbackplus\nimport FeedbackPlus from 'feedbackplus'", 4 | html: `\n` 5 | } 6 | 7 | $('.terminal-copy').click(function () { 8 | const clipboardKey = $(this).data('clipboard-key') 9 | navigator.clipboard.writeText(clipboardData[clipboardKey]).then(() => { 10 | $(this).hide().siblings('.terminal-copied').show() 11 | setTimeout(() => { 12 | $(this).show().siblings('.terminal-copied').hide() 13 | }, 1250) 14 | }) 15 | }) 16 | 17 | $('.installation-option').click(function(){ 18 | $('.installation-option').removeClass('active-option') 19 | $(this).addClass('active-option') 20 | const id = $(this).data('id') 21 | $('.installation .option').hide() 22 | $(`.installation .option[data-id="${id}"]`).show() 23 | }) 24 | 25 | hljs.highlightAll(); 26 | })() -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Inter&display=swap"); 3 | 4 | :root { 5 | --theme-blue-light: #e7f5ff; 6 | --theme-blue-dark: #0e68b5; 7 | --theme-blue-bright: #00c7fd; 8 | 9 | --font-mono: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", 10 | monospace; 11 | } 12 | 13 | html, 14 | body { 15 | height: 100%; 16 | margin: 0; 17 | background-color: var(--theme-blue-light); 18 | } 19 | 20 | .divider { 21 | height: 1px; 22 | width: 50%; 23 | background-color: #8a8a8a; 24 | margin: auto; 25 | } 26 | 27 | #hero { 28 | padding: 80px 20px; 29 | padding-top: 150px; 30 | text-align: center; 31 | } 32 | 33 | #hero #logo { 34 | font-family: "Source Sans Pro"; 35 | display: flex; 36 | justify-content: center; 37 | } 38 | 39 | #hero #logo p { 40 | font-size: 75px; 41 | font-weight: 800; 42 | } 43 | 44 | #hero #subtitle { 45 | text-align: center; 46 | font-size: 30px; 47 | margin-top: 50px; 48 | font-family: "Source Sans Pro"; 49 | line-height: 33px; 50 | } 51 | 52 | .view-on .view-on-option { 53 | padding: 11px 16px; 54 | border-radius: 8px; 55 | background: white; 56 | color: black; 57 | text-decoration: none; 58 | font-family: "Source Sans Pro"; 59 | font-size: 16px; 60 | display: inline-flex; 61 | align-items: center; 62 | } 63 | 64 | .view-on .view-on-option svg { 65 | margin-right: 8px; 66 | } 67 | 68 | .installation { 69 | font-family: "Source Sans Pro"; 70 | background-color: var(--theme-blue-dark); 71 | color: white; 72 | padding: 50px 20px; 73 | } 74 | 75 | .installation .installation-options { 76 | display: flex; 77 | font-size: 20px; 78 | width: 80%; 79 | max-width: 1000px; 80 | margin: auto; 81 | } 82 | 83 | .installation .installation-option { 84 | user-select: none; 85 | cursor: pointer; 86 | } 87 | 88 | .installation .installation-option.active-option { 89 | text-decoration: underline; 90 | } 91 | 92 | .installation .option { 93 | width: 80%; 94 | max-width: 1000px; 95 | margin: auto; 96 | margin-top: 20px; 97 | } 98 | 99 | .installation .terminal { 100 | font-size: 14px; 101 | line-height: 34px; 102 | background-color: rgb(0, 0, 0, 0.5); 103 | border: 1px solid rgba(166, 175, 194, 0.25); 104 | border-radius: 8px; 105 | padding: 11px 16px; 106 | font-family: var(--font-mono); 107 | position: relative; 108 | overflow: auto; 109 | white-space: nowrap; 110 | transition: 0.3s; 111 | } 112 | 113 | .terminal pre { 114 | overflow: auto; 115 | margin: 0; 116 | } 117 | 118 | .terminal code { 119 | background-color: transparent; 120 | padding: 0 !important; 121 | overflow: hidden; 122 | } 123 | 124 | .terminal code::-webkit-scrollbar { 125 | width: 10px; 126 | } 127 | 128 | .terminal code::-webkit-scrollbar-thumb { 129 | border: 4px solid rgba(0, 0, 0, 0); 130 | background-clip: padding-box; 131 | border-radius: 9999px; 132 | background-color: rgb(255, 255, 255, 0.5); 133 | } 134 | 135 | .installation .terminal:hover { 136 | border-color: rgba(166, 175, 194, 0.8); 137 | } 138 | 139 | .installation .terminal .terminal-icon { 140 | position: absolute; 141 | right: 10px; 142 | top: 10px; 143 | cursor: pointer; 144 | opacity: 0; 145 | transition: 0.2s; 146 | } 147 | 148 | .installation .terminal:hover .terminal-icon{ 149 | opacity: 1; 150 | } 151 | 152 | .installation .terminal .terminal-copied { 153 | display: none; 154 | } 155 | 156 | .further-steps { 157 | text-align: center; 158 | font-size: 20px; 159 | font-family: "Source Sans Pro"; 160 | margin-top: 40px; 161 | padding-bottom: 40px; 162 | } 163 | 164 | .further-steps a { 165 | text-decoration: none; 166 | } 167 | 168 | #view-demo { 169 | padding: 14px 19px; 170 | border-radius: 8px; 171 | background: white; 172 | color: black; 173 | text-decoration: none; 174 | font-family: "Source Sans Pro"; 175 | font-size: 25px; 176 | display: inline-flex; 177 | align-items: center; 178 | margin-bottom: 40px; 179 | } 180 | 181 | @media only screen and (max-width: 550px) { 182 | #hero #logo p { 183 | font-size: 60px; 184 | } 185 | 186 | #hero #subtitle { 187 | font-size: 25px; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feedbackplus", 3 | "version": "1.6.0", 4 | "homepage": "https://colonelparrot.github.io/feedbackplus/", 5 | "description": "Screenshotting and screenshot editing for your feedback forms with JavaScript.", 6 | "main": "src/feedbackplus.js", 7 | "directories": { 8 | "doc": "docs" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/puffinsoft/feedbackplus" 16 | }, 17 | "keywords": [ 18 | "js", 19 | "feedback-form" 20 | ], 21 | "author": "ColonelParrot", 22 | "license": "MIT" 23 | } 24 | -------------------------------------------------------------------------------- /src/feedbackplus.css: -------------------------------------------------------------------------------- 1 | .feedbackplus.feedbackplus-modal { 2 | position: fixed; 3 | width: 90%; 4 | top: 50%; 5 | left: 50%; 6 | transform: translate(-50%, -50%); 7 | z-index: 2147483647; 8 | border-radius: 10px; 9 | background-color: white; 10 | box-sizing: border-box; 11 | } 12 | 13 | .feedbackplus.feedbackplus-canvas-container { 14 | overflow: auto; 15 | width: 100%; 16 | height: calc(90vh - 102px); 17 | position: relative; 18 | scrollbar-width: thin; 19 | } 20 | 21 | .feedbackplus.feedbackplus-canvas-container::-webkit-scrollbar { 22 | width: 10px; 23 | height: 10px; 24 | } 25 | 26 | .feedbackplus.feedbackplus-canvas-container::-webkit-scrollbar-thumb { 27 | background-color: rgb(0, 0, 0, 0.2); 28 | } 29 | 30 | .feedbackplus.feedbackplus-canvas-container::-webkit-scrollbar-thumb:hover { 31 | background-color: rgb(0, 0, 0, 0.3); 32 | } 33 | 34 | .feedbackplus.feedbackplus-canvas-container::-webkit-scrollbar-thumb:active { 35 | background-color: rgb(0, 0, 0, 0.4); 36 | } 37 | 38 | .feedbackplus.feedbackplus-highlight { 39 | position: absolute; 40 | border: 5px solid #fcc934; 41 | } 42 | 43 | .feedbackplus.feedbackplus-highlight:hover { 44 | background-color: rgb(122, 167, 255, 0.1); 45 | } 46 | 47 | .feedbackplus.feedbackplus-hide { 48 | position: absolute; 49 | background-color: black; 50 | } 51 | 52 | .feedbackplus.feedbackplus-hide:hover { 53 | background-color: rgb(0, 0, 0, 0.8); 54 | } 55 | 56 | .feedbackplus.feedbackplus-highlight .feedbackplus.feedbackplus-tool-close { 57 | top: -17px; 58 | right: -28px; 59 | } 60 | 61 | .feedbackplus.feedbackplus-hide .feedbackplus.feedbackplus-tool-close { 62 | top: -10px; 63 | right: -22px; 64 | } 65 | 66 | .feedbackplus.feedbackplus-tool-close { 67 | display: none; 68 | margin-right: 10px; 69 | position: absolute; 70 | background-color: white; 71 | border-radius: 50%; 72 | border: 2px solid black; 73 | padding: 5px; 74 | cursor: pointer; 75 | height: 15px; 76 | width: 15px; 77 | } 78 | 79 | .feedbackplus.feedbackplus-hide:hover .feedbackplus.feedbackplus-tool-close, 80 | .feedbackplus.feedbackplus-highlight:hover 81 | .feedbackplus.feedbackplus-tool-close { 82 | display: block; 83 | } 84 | 85 | .feedbackplus.feedbackplus-header, 86 | .feedbackplus.feedbackplus-footer { 87 | padding: 10px; 88 | box-sizing: border-box; 89 | display: flex; 90 | align-items: center; 91 | justify-content: space-between; 92 | } 93 | 94 | .feedbackplus.feedbackplus-header { 95 | height: 50px; 96 | max-height: 50px; 97 | } 98 | 99 | .feedbackplus.feedbackplus-footer { 100 | max-height: 52px; 101 | } 102 | 103 | .feedbackplus.feedbackplus-header h2 { 104 | margin: 5px 20px; 105 | font-size: 16px; 106 | font-weight: normal; 107 | } 108 | 109 | .feedbackplus.feedbackplus-footer { 110 | padding: 10px 20px; 111 | padding-top: 0px; 112 | } 113 | 114 | .feedbackplus.feedbackplus-finish-actions, 115 | .feedbackplus.feedbackplus-tools { 116 | margin-top: 10px; 117 | } 118 | 119 | .feedbackplus.feedbackplus-backdrop { 120 | position: fixed; 121 | width: 100%; 122 | height: 100%; 123 | top: 0; 124 | left: 0; 125 | z-index: 2147483646; 126 | background-color: rgb(0, 0, 0, 0.5); 127 | } 128 | 129 | .feedbackplus.feedbackplus-tools { 130 | display: flex; 131 | } 132 | 133 | .feedbackplus.feedbackplus-button { 134 | padding: 5px 20px; 135 | border-radius: 25px; 136 | cursor: pointer; 137 | user-select: none; 138 | display: flex; 139 | align-items: center; 140 | border: 1px solid #808080; 141 | color: #0b57d0; 142 | font-size: 14px; 143 | } 144 | 145 | .feedbackplus.feedbackplus-button:hover { 146 | border-color: #0b57d0; 147 | background-color: #f5f5f5; 148 | } 149 | 150 | .feedbackplus.feedbackplus-button:active { 151 | background-color: #ededed; 152 | } 153 | 154 | .feedbackplus.feedbackplus-button.feedbackplus-active { 155 | border-color: #0b57d0; 156 | background-color: #e0e0e0; 157 | } 158 | 159 | .feedbackplus.feedbackplus-tool-icon { 160 | margin-right: 7.6px; 161 | } 162 | 163 | .feedbackplus.feedbackplus-finish-actions { 164 | display: flex; 165 | } 166 | 167 | .feedbackplus.feedbackplus-close { 168 | cursor: pointer; 169 | } 170 | 171 | @media only screen and (max-width: 600px) { 172 | .feedbackplus.feedbackplus-modal { 173 | top: 0; 174 | left: 0; 175 | transform: none; 176 | height: 100%; 177 | width: 100%; 178 | border-radius: 0; 179 | } 180 | 181 | .feedbackplus.feedbackplus-canvas-container { 182 | height: calc(100% - 102px); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/feedbackplus.js: -------------------------------------------------------------------------------- 1 | /*! FeedbackPlus v1.6.0 | (c) ColonelParrot and other contributors | MIT License */ 2 | 3 | ; (function (global, factory) { 4 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 5 | typeof define === 'function' && define.amd ? define(factory) : 6 | global.FeedbackPlus = factory() 7 | }(this, (function () { 8 | 'use strict'; 9 | class FeedbackPlus { 10 | 11 | // create necessary elements for reuse 12 | // also allows for customization by accessing the modal & backdrop properties 13 | constructor() { 14 | const modal = document.createElement('div') 15 | modal.classList.add('feedbackplus', 'feedbackplus-modal') 16 | modal.innerHTML = `

Edit Screenshot

` 17 | 18 | const backdrop = document.createElement('div') 19 | backdrop.classList.add('feedbackplus', 'feedbackplus-backdrop') 20 | 21 | this.modal = modal; 22 | this.backdrop = backdrop; 23 | } 24 | 25 | /** 26 | * Checks whether the base APIs needed are supported. Does not account for the presence of html2canvas 27 | */ 28 | static isRootSupported() { 29 | return !!navigator?.mediaDevices?.getDisplayMedia && !!window.HTMLCanvasElement; 30 | } 31 | 32 | /** 33 | * Checks whether the library can be used. Accounts for presence of html2canvas 34 | */ 35 | static isSupported() { 36 | const isRootSupported = FeedbackPlus.isRootSupported() 37 | if (!isRootSupported) { 38 | return !!window.html2canvas 39 | } 40 | return isRootSupported 41 | } 42 | 43 | /** 44 | * Captures a screenshot of the page. Fallbacks to `html2canvas` if available 45 | * @param {Number} timeout - Default `500`ms. Timeout between call & capture. Can be used to wait for animations to finish before capture 46 | * @returns {Promise} promise - A promise that, when resolved, provides an ImageBitmap 47 | */ 48 | async capture(timeout = 500) { 49 | if (FeedbackPlus.isSupported()) { 50 | if (FeedbackPlus.isRootSupported()) { 51 | return new Promise((resolve, reject) => { 52 | navigator.mediaDevices.getDisplayMedia({ video: true, preferCurrentTab: true }).then(stream => { 53 | const video = document.createElement('video') 54 | setTimeout(() => { 55 | video.srcObject = stream 56 | video.onloadedmetadata = () => { 57 | video.play() 58 | video.pause() 59 | 60 | const canvas = document.createElement('canvas') 61 | canvas.width = video.videoWidth 62 | canvas.height = video.videoHeight 63 | canvas.getContext('2d').drawImage(video, 0, 0) 64 | stream.getVideoTracks().forEach(track => track.stop()) 65 | FeedbackPlus.canvasToBitmap(canvas).then(bitmap => resolve(bitmap)) 66 | } 67 | }, timeout) 68 | }).catch(e => reject(e)) 69 | }) 70 | } else { 71 | return new Promise((resolve, reject) => { 72 | setTimeout(function () { 73 | html2canvas(document.body, { logging: false, windowWidth: window.screen.width + 'px' }).then(function (canvas) { 74 | FeedbackPlus.canvasToBitmap(canvas).then(bitmap => { 75 | resolve(bitmap) 76 | }) 77 | }).catch(e => reject(e)) 78 | }, timeout) 79 | }) 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * Shows an edit dialog for an ImageBitmap. Should be used with the ImageBitmap 86 | * returned from {@link capture()} 87 | * @param {ImageBitmap} bitmap - The ImageBitmap to edit 88 | * @param {Function} onComplete - callback function called when user finishes editing 89 | * @param {Function} onCancel - callback function called when user cancels edit 90 | * @param {Object} options - options for edit dialog. See the docs for more details 91 | * @returns {HTMLCanvasElement} canvas - Canvas passed to `onComplete` function, nothing passed to `onCancel` 92 | */ 93 | showEditDialog(bitmap, onComplete, onCancel, options) { 94 | options = options || {}; 95 | options.allowHighlight = options.allowHighlight ?? true; 96 | options.allowHide = options.allowHide ?? true; 97 | const { allowHighlight, allowHide } = options; 98 | 99 | function clearListeners() { 100 | canvasContainer.removeEventListener('mousedown', mousedownListener) 101 | canvasContainer.removeEventListener('mousemove', mouseMoveListener) 102 | document.body.removeEventListener('mouseup', mouseUpListener) 103 | canvasContainer.removeEventListener('mousedown', editDeleteMousedownListener, true) 104 | doneButton.removeEventListener('click', doneListener) 105 | cancelButton.removeEventListener('click', cancelListener) 106 | closeButton.removeEventListener('click', cancelListener) 107 | modalToolsOptions.forEach(option => { 108 | option.removeEventListener('click', modalOptionListener) 109 | }) 110 | document.body.removeEventListener('keypress', undoListener) 111 | } 112 | const { modal, backdrop } = this; 113 | const modalToolsOptions = modal.querySelectorAll('.feedbackplus.feedbackplus-tool') 114 | const modalOptionListener = function (e) { 115 | modalToolsOptions.forEach(option => option.classList.remove('feedbackplus-active')) 116 | e.target.closest('.feedbackplus.feedbackplus-tool').classList.add('feedbackplus-active') 117 | } 118 | modalToolsOptions.forEach(option => { 119 | option.addEventListener('click', modalOptionListener) 120 | }) 121 | const canvas = modal.querySelector('.feedbackplus.feedbackplus-canvas') 122 | canvas.width = bitmap.width; 123 | canvas.height = bitmap.height; 124 | canvas.getContext('2d').drawImage(bitmap, 0, 0) 125 | 126 | const cloneCanvas = document.createElement('canvas') 127 | cloneCanvas.width = bitmap.width; 128 | cloneCanvas.height = bitmap.height; 129 | cloneCanvas.getContext('2d').drawImage(bitmap, 0, 0) 130 | 131 | const feedbackHighlightTool = modal.querySelector('.feedbackplus.feedbackplus-highlight-tool') 132 | const feedbackHideTool = modal.querySelector('.feedbackplus.feedbackplus-hide-tool') 133 | 134 | const feedbackHighlight = document.createElement('div') 135 | feedbackHighlight.classList.add('feedbackplus', 'feedbackplus-highlight', 'feedbackplus-edit') 136 | feedbackHighlight.innerHTML = `` 137 | 138 | const feedbackHide = document.createElement('div') 139 | feedbackHide.classList.add('feedbackplus', 'feedbackplus-hide', 'feedbackplus-edit') 140 | feedbackHide.innerHTML = `` 141 | let highlightElem = null; 142 | const canvasContainer = modal.querySelector('.feedbackplus.feedbackplus-canvas-container') 143 | let startX; 144 | let startY; 145 | 146 | const mousedownListener = function (e) { 147 | if (highlightElem) { 148 | mouseUpListener(e) 149 | } 150 | 151 | let isHighlight = feedbackHighlightTool.classList.contains('feedbackplus-active') 152 | if (allowHighlight && isHighlight) { 153 | highlightElem = feedbackHighlight.cloneNode(true) 154 | } else if (allowHide && feedbackHideTool.classList.contains('feedbackplus-active')) { 155 | highlightElem = feedbackHide.cloneNode(true) 156 | } else { 157 | return; 158 | } 159 | const x = e.pageX; 160 | const y = e.pageY; 161 | const offset = canvasContainer.getBoundingClientRect() 162 | const [realX, realY] = [x - offset.left + canvasContainer.scrollLeft - (isHighlight ? 5 : 0), y - offset.top + canvasContainer.scrollTop - (isHighlight ? 5 : 0)] 163 | startX = realX; 164 | startY = realY; 165 | highlightElem.style.top = startY + "px"; 166 | highlightElem.style.left = startX + "px"; 167 | canvasContainer.appendChild(highlightElem) 168 | } 169 | canvasContainer.addEventListener('mousedown', mousedownListener) 170 | 171 | const mouseMoveListener = function (e) { 172 | if (highlightElem) { 173 | e.preventDefault(); 174 | let isHighlight = feedbackHighlightTool.classList.contains('feedbackplus-active') 175 | 176 | const offset = canvasContainer.getBoundingClientRect() 177 | const x = e.pageX - offset.left + canvasContainer.scrollLeft - (isHighlight ? 5 : 0); 178 | const y = e.pageY - offset.top + canvasContainer.scrollTop - (isHighlight ? 5 : 0); 179 | const Xdiff = (x - startX); 180 | const Ydiff = (y - startY); 181 | if (Xdiff < 0) { 182 | highlightElem.style.left = startX + Xdiff + "px"; 183 | highlightElem.style.width = startX - x + "px"; 184 | } else { 185 | highlightElem.style.left = startX + "px"; 186 | highlightElem.style.width = Xdiff + "px"; 187 | } 188 | if (Ydiff < 0) { 189 | highlightElem.style.top = startY + Ydiff + "px"; 190 | highlightElem.style.height = startY - y + "px"; 191 | } else { 192 | highlightElem.style.top = startY + "px"; 193 | highlightElem.style.height = Ydiff + "px"; 194 | } 195 | 196 | } 197 | } 198 | canvasContainer.addEventListener('mousemove', mouseMoveListener) 199 | 200 | const mouseUpListener = function (e) { 201 | if (highlightElem) { 202 | highlightElem.querySelector('.feedbackplus.feedbackplus-tool-close').style.removeProperty('display') 203 | if (highlightElem.offsetHeight < 30 && highlightElem.offsetWidth < 30) { 204 | highlightElem.remove(); 205 | } 206 | highlightElem = null; 207 | } 208 | } 209 | document.body.addEventListener('mouseup', mouseUpListener) 210 | 211 | const editDeleteMousedownListener = function (e) { 212 | if (e.target.closest('.feedbackplus-tool-close')) { 213 | e.stopPropagation() 214 | e.target.closest('.feedbackplus-highlight')?.remove() 215 | e.target.closest('.feedbackplus-hide')?.remove() 216 | } 217 | } 218 | canvasContainer.addEventListener('mousedown', editDeleteMousedownListener, true) 219 | 220 | const doneListener = () => { 221 | const edits = canvasContainer.querySelectorAll('.feedbackplus.feedbackplus-edit') 222 | const cloneCanvasContext = cloneCanvas.getContext('2d') 223 | cloneCanvasContext.lineWidth = 5 224 | cloneCanvasContext.strokeStyle = "#FCC934" 225 | cloneCanvasContext.fillStyle = "black" 226 | edits.forEach(edit => { 227 | let { top, left, width, height } = edit.style; 228 | top = +top.slice(0, -2) 229 | left = +left.slice(0, -2) 230 | width = +width.slice(0, -2) 231 | height = +height.slice(0, -2) 232 | if (edit.classList.contains('feedbackplus-highlight')) { 233 | cloneCanvasContext.strokeRect(left + 2.5, top + 2.5, width + 5, height + 5) 234 | } else if (edit.classList.contains('feedbackplus-hide')) { 235 | cloneCanvasContext.fillRect(left, top, width, height) 236 | } 237 | }) 238 | 239 | clearListeners() 240 | this.clearEdits() 241 | onComplete(cloneCanvas) 242 | } 243 | const doneButton = modal.querySelector('.feedbackplus.feedbackplus-complete') 244 | doneButton.addEventListener('click', doneListener) 245 | 246 | const cancelButton = modal.querySelector('.feedbackplus.feedbackplus-cancel') 247 | const closeButton = modal.querySelector('.feedbackplus.feedbackplus-close') 248 | 249 | const cancelListener = () => { 250 | clearListeners() 251 | this.clearEdits() 252 | onCancel() 253 | } 254 | cancelButton.addEventListener('click', cancelListener) 255 | closeButton.addEventListener('click', cancelListener) 256 | 257 | const undoListener = (e) => { 258 | if (e.ctrlKey && e.keyCode == 26) { 259 | const edits = canvasContainer.querySelectorAll('.feedbackplus.feedbackplus-edit') 260 | const lastEdit = edits[edits.length - 1] 261 | if (lastEdit) { 262 | lastEdit.remove() 263 | } 264 | } 265 | } 266 | 267 | document.body.addEventListener('keypress', undoListener) 268 | 269 | document.body.appendChild(modal) 270 | document.body.appendChild(backdrop) 271 | 272 | feedbackHideTool.style.display = allowHide ? "inherit" : "none"; 273 | feedbackHighlightTool.style.display = allowHighlight ? "inherit" : "none"; 274 | 275 | const visibleTools = modal.querySelectorAll('.feedbackplus.feedbackplus-tool') 276 | visibleTools.forEach(e => e.classList.remove('feedbackplus-active')) 277 | for (const tool of visibleTools) { 278 | if (window.getComputedStyle(tool).display != "none") { 279 | tool.classList.add('feedbackplus-active') 280 | break; 281 | } 282 | } 283 | } 284 | 285 | /** 286 | * Closes the edit dialog. Should be called in the `onComplete` and `onCancel` callbacks for {@link showEditDialog()} 287 | */ 288 | closeEditDialog() { 289 | const { modal, backdrop } = this; 290 | modal.remove() 291 | backdrop.remove() 292 | } 293 | 294 | /** 295 | * Removes all edits made by the user thus far in the edit dialog 296 | */ 297 | clearEdits() { 298 | const { modal, backdrop } = this; 299 | const canvasContainer = modal.querySelector('.feedbackplus.feedbackplus-canvas-container') 300 | const edits = canvasContainer.querySelectorAll('.feedbackplus.feedbackplus-edit') 301 | edits.forEach(edit => edit.remove()) 302 | } 303 | 304 | /** 305 | * Converts a canvas to an ImageBitmap. Can be called on the canvas returned by {@link showEditDialog()} to redraw on canvas 306 | * @param {HTMLCanvasElement} canvas - The canvas 307 | * @returns {Promise} promise - A promise which, when resolved, returns the ImageBitmap 308 | */ 309 | static canvasToBitmap(canvas) { 310 | return new Promise((resolve, reject) => { 311 | const canvasContext = canvas.getContext('2d') 312 | const imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height) 313 | createImageBitmap(imageData).then(bitmap => { 314 | resolve({ 315 | bitmap, 316 | width: bitmap.width, 317 | height: bitmap.height 318 | }) 319 | }) 320 | }) 321 | } 322 | } 323 | 324 | if(typeof module !== 'undefined'){ 325 | module.exports = { FeedbackPlus } 326 | } 327 | return FeedbackPlus 328 | }))); --------------------------------------------------------------------------------