├── .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 | Name...
39 |
40 |
Describe the issue
41 |
42 |
43 | Describe the issue...
44 |
45 |
A screenshot will help us better understand the issue.
46 |
47 |
49 |
50 | Take Screenshot
51 |
52 |
55 |
56 |
57 |
58 |
60 | delete
61 |
62 |
63 |
66 | edit
67 | Highlight or Hide Info
68 |
69 |
70 |
71 |
72 |
73 |
78 |
79 |
80 |
84 |
85 |
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 |
Take
49 | Screenshot
50 |
51 |
52 |
Screenshot will appear here
53 |
54 |
55 |
56 |
Edit
57 | Screenshot
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 |
Take Screenshot
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 |
74 |
75 |
79 |
80 |
81 |
$ npm i feedbackplus
82 | $ import FeedbackPlus from 'feedbackplus'
83 |
85 |
88 |
89 |
91 |
93 |
94 |
95 |
96 |
97 |
98 |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/ColonelParrot/feedbackplus@master/src/feedbackplus.min.css" />
99 | <script src="https://cdn.jsdelivr.net/gh/ColonelParrot/feedbackplus@master/src/feedbackplus.min.js" defer></script>
100 |
102 |
105 |
106 |
108 |
110 |
111 |
112 |
113 |
114 |
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 = `
`
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 | })));
--------------------------------------------------------------------------------