Skip to content

Instantly share code, notes, and snippets.

@aldrinmartoq
Created April 11, 2024 04:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aldrinmartoq/f5ee991a9aa41a6fa6a08e13d5cb57e4 to your computer and use it in GitHub Desktop.
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.
<!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/bootstrap@5.3.3/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/html2canvas@1.4.1/dist/html2canvas.esm.js"
}
}
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/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