Created
April 11, 2024 04:33
-
-
Save aldrinmartoq/f5ee991a9aa41a6fa6a08e13d5cb57e4 to your computer and use it in GitHub Desktop.
Using htmlcanvas as a bug report tool, this sample is a self-contained app using Bootstrap 5 and Vue 3.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Poll: htmlcanvas</title> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> | |
<script type='importmap'> | |
{ | |
"imports": { | |
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js", | |
"html2canvas": "https://unpkg.com/[email protected]/dist/html2canvas.esm.js" | |
} | |
} | |
</script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script> | |
<style> | |
.bug_report_form { | |
z-index: 2147483647; | |
position: fixed; | |
bottom: 70px; | |
right: 20px; | |
padding: 20px; | |
width: 500px; | |
max-height: 60%; | |
overflow: scroll; | |
box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.5); | |
border-radius: 8px; | |
background: #fff; | |
} | |
.slide-enter-active, .slide-leave-active { | |
transition: all 0.4s ease-in-out; | |
} | |
.slide-enter-from, .slide-leave-to { | |
right: 0%; | |
opacity: 0; | |
} | |
.screenshot-enter-active, .screenshot-leave-active { | |
transition: all 0.4s ease-in-out; | |
} | |
.screenshot-enter-from, .screenshot-leave-to { | |
opacity: 0; | |
} | |
.screenshot { | |
border: 1px solid black; | |
} | |
.screenshot img { | |
width: 100%; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="app"> | |
<nav class="navbar navbar-expand-lg bg-body-tertiary"> | |
<div class="container-fluid d-flex"> | |
<a class="navbar-brand" href="#">My Cool App</a> | |
<button class="btn btn-danger" @click="clear_and_show">Bug Report Screenshooter</button> | |
</div> | |
</nav> | |
<main class="container-fluid"> | |
<h1>Hey, Jessie!</h1> | |
<h2>How do you feel about html to cavnas?</h2> | |
<div class="form-check"> | |
<input class="form-check-input" type="checkbox" value="" id="check-a"> | |
<label class="form-check-label" for="check-a"> | |
It Sucks! | |
</label> | |
</div> | |
<div class="form-check"> | |
<input class="form-check-input" type="checkbox" value="" id="check-b"> | |
<label class="form-check-label" for="check-b"> | |
I don't care while it works | |
</label> | |
</div> | |
<div class="form-check"> | |
<input class="form-check-input" type="checkbox" value="" id="check-c" checked> | |
<label class="form-check-label" for="check-c"> | |
Why canvas is mispelled?????? | |
</label> | |
</div> | |
</main> | |
<transition name="slide"> | |
<div class="bug_report_form" v-show="show_bug_report" data-html2canvas-ignore style="display: none;"> | |
<h2 class="d-flex justify-content-between"> | |
Bug report Screenshooter | |
<button class="btn btn-outline-secondary" @click="show_bug_report = false">Close</button> | |
</h2> | |
<p v-if="state.sent">Thanks! Your report help us a lot.</p> | |
<fieldset :disabled="state.sending" v-else> | |
<div class="mb-3"> | |
<label for="description" class="form-label">What went wrong?</label> | |
<textarea ref="description_textarea" class="form-control" id="description" rows="3" placeholder="Describe your issue…" v-model="description"></textarea> | |
</div> | |
<div class="mb-3 d-flex flex-column gap-1"> | |
<transition-group name="slide" tag="div"> | |
<div class="screenshot" v-for="(screenshot, index) in screenshots" :key="screenshot.stamp"> | |
<button class="btn btn-sm btn-danger" @click="screenshots.splice(index, 1)"> X remove</button> | |
<img :src="screenshot.src"> | |
</div> | |
</transition-group> | |
<button class="btn btn-success form-control" @click="take_screenshot" :disabled="state.taking" v-if="take_message"> | |
<div class="spinner-border spinner-border-sm" role="status" v-if="state.taking"></div> | |
{{ take_message }} | |
</button> | |
<p ref="attachment_count" class="form-label">{{ screenshots.length }} out of {{ max_screenshots }} attachments</label> | |
</div> | |
<div class="d-flex justify-content-between"> | |
<button class="btn btn-primary" @click="send_bug_report" v-if="!state.sending">Send Report</button> | |
<button class="btn btn-primary" disabled v-else> | |
<div class="spinner-border spinner-border-sm" role="status"></div> | |
Sending… | |
</button> | |
<button class="btn btn-danger" @click="clear_and_show">Clear form</button> | |
</div> | |
<hr/> | |
<p class="text-body-secondary">Pssst… Data hidden to the user</p> | |
<code>post endpoint url</code> | |
<input class="form-control" type="text" v-model="url"> | |
<code>current page location</code> | |
<textarea class="form-control" v-model="location"></textarea> | |
<code>current page HTML</code> | |
<textarea class="form-control" v-model="body_html"></textarea> | |
</fieldset> | |
</div> | |
</transition> | |
</div> | |
<script type="module"> | |
import { createApp } from 'vue' | |
import html2canvas from 'html2canvas' | |
window.aaa = createApp({ | |
data() { | |
return { | |
url: 'https://example.com/api/bug_report_endpoint', | |
show_bug_report: false, | |
state: { | |
taking: false, | |
sending: false, | |
sent: false | |
}, | |
sending: false, | |
screenshots: [], | |
max_screenshots: 3, | |
body_html: null, | |
location: null | |
} | |
}, | |
computed: { | |
screenshot_count() { return this.screenshots.length }, | |
take_message() { | |
if (this.screenshot_count == 0) { | |
return 'Take a screenshot' | |
} else if (this.screenshot_count == this.max_screenshots - 1) { | |
return 'Take last screenshot' | |
} else if (this.screenshot_count < this.max_screenshots - 1) { | |
return 'Take another screenshot' | |
} | |
return null; | |
} | |
}, | |
methods: { | |
take_screenshot() { | |
this.state.taking = true | |
this.$nextTick(() => { | |
html2canvas(document.body).then(canvas => { | |
this.screenshots.push({ stamp: new Date().getTime(), src: canvas.toDataURL('image/jpg')}) | |
this.state.taking = false | |
this.$nextTick(() => this.$refs.attachment_count.scrollIntoView({ behavior: 'smooth', block: 'nearest' })) | |
}) | |
}) | |
}, | |
send_bug_report() { | |
this.state.sending = true | |
let form_data = new FormData() | |
form_data.append('description', this.description) | |
form_data.append('location', this.location) | |
form_data.append('body_html', this.body_html) | |
this.screenshots.forEach((screenshot, index) => { | |
form_data.append('screenshots[]', this.dataURLtoFile(screenshot.src, `screenshot_${index + 1}.jpg`)) | |
}) | |
fetch(this.url, { | |
method: 'post', | |
body: form_data, | |
headers: { | |
'Authorization': 'Basic Foo' | |
} | |
}) | |
.then(response => console.log('response', response)) | |
.finally(() => { | |
this.state.sending = false | |
this.state.sent = true | |
}) | |
}, | |
clear_and_show() { | |
this.description = null | |
this.screenshots = [] | |
this.state.sent = this.state.taking = this.state.sending = false | |
this.show_bug_report = true | |
}, | |
dataURLtoFile(dataurl, filename) { | |
let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], | |
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n) | |
while(n--) { | |
u8arr[n] = bstr.charCodeAt(n) | |
} | |
return new File([u8arr], filename, { type: mime }) | |
} | |
}, | |
watch: { | |
show_bug_report(value) { | |
if (value) { | |
this.location = window.location | |
this.body_html = document.body.innerHTML | |
this.$nextTick(() => this.$refs.description_textarea.focus()) | |
} | |
} | |
} | |
}).mount('#app') | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment