Commit ccc1b68b authored by Andrea Giammarchi's avatar Andrea Giammarchi

Issue reporter screenshot

parent 93e8b26e
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
* Copyright (C) 2006-present eyeo GmbH
*
* Adblock Plus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* Adblock Plus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
io-highlighter,
io-highlighter *::before,
io-highlighter *::after
{
box-sizing: border-box;
}
io-highlighter
{
position: relative;
display: block;
border: 1px solid #979797;
}
io-highlighter .split
{
display: flex;
height: 100%;
}
io-highlighter .options
{
padding: 12px;
color: #4A4A4A;
background-color: #f1f1f1;
border-right: 1px solid #979797;
}
io-highlighter canvas
{
flex-grow: 1;
user-select: none;
touch-action: none;
pointer-events: none;
width: 100%;
height: 100%;
}
io-highlighter[drawing] canvas
{
pointer-events: all;
}
io-highlighter .options .highlight
{
background-image: url(icons/highlight.svg?off#off);
}
io-highlighter[drawing="highlight"] .options .highlight
{
background-image: url(icons/highlight.svg?on#on);
}
io-highlighter .options .hide
{
background-image: url(icons/hide.svg?off#off);
}
io-highlighter[drawing="hide"] .options .hide
{
background-image: url(icons/hide.svg?on#on);
}
io-highlighter .options .highlight,
io-highlighter .options .hide
{
width: 70px;
min-height: 70px;
padding: 0;
padding-top: 40px;
margin-bottom: 12px;
font-size: 0.7rem;
border-width: 0;
border-radius: 12px;
outline: none;
color: inherit;
background-position: center 12px;
background-repeat: no-repeat;
word-break: break-all;
}
io-highlighter[drawing="highlight"] .options .highlight,
io-highlighter[drawing="hide"] .options .hide
{
color: #FFF;
background-color: #9b9b9b;
}
io-highlighter .closer
{
position: absolute;
display: block;
width: 24px;
height: 24px;
cursor: pointer;
border-radius: 24px;
transform: translateX(-12px) translateY(-12px);
background-color: #4a4a4a;
}
io-highlighter .closer img
{
margin: 6px;
width: 12px;
}
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
* Copyright (C) 2006-present eyeo GmbH
*
* Adblock Plus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* Adblock Plus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const {wire, utils} = require("./io-element");
// use native rIC where available, fallback to setTimeout otherwise
const requestIdleCallback = window.requestIdleCallback || setTimeout;
// at this point this is just a helper class
// for op-highlighter component but it could
// become a generic draw-on-canvas helper too
module.exports = class DrawingHandler
{
constructor(canvas, maxSize)
{
this.paths = new Set();
this.canvas = canvas;
this.maxSize = maxSize;
// the canvas needs proper width and height
const canvasRect = canvas.getBoundingClientRect();
canvas.width = canvasRect.width;
canvas.height = canvasRect.height;
// it also needs to intercept all events
if ("onpointerup" in canvas)
{
// the instance is the handler itself, no need to bind anything
canvas.addEventListener("pointerdown", this, {passive: false});
canvas.addEventListener("pointermove", this, {passive: false});
canvas.addEventListener("pointerup", this, {passive: false});
document.addEventListener("pointerup", this, {passive: false});
}
else
{
// some browser might not have pointer events.
// the fallback should be regular mouse events
this.onmousedown = this.onpointerdown;
this.onmousemove = this.onpointermove;
this.onmouseup = this.onpointerup;
canvas.addEventListener("mousedown", this, {passive: false});
canvas.addEventListener("mousemove", this, {passive: false});
canvas.addEventListener("mouseup", this, {passive: false});
document.addEventListener("mouseup", this, {passive: false});
}
}
// draws an image and it starts processing its data
// in an asynchronous, not CPU greedy, way.
// It returns a promise that will resolve only
// once the image has been fully processed.
// Meanwhile, it is possible to draw rectangles on top.
changeColorDepth(image)
{
this.clear();
const startW = image.naturalWidth;
const startH = image.naturalHeight;
const ratioW = Math.min(this.canvas.width, this.maxSize) / startW;
const ratioH = Math.min(this.canvas.height, this.maxSize) / startH;
const ratio = Math.min(ratioW, ratioH);
const endW = startW * ratio;
const endH = startH * ratio;
this.ctx.drawImage(image,
0, 0, startW, startH,
0, 0, endW, endH);
this.imageData = this.ctx.getImageData(
0, 0, this.canvas.width, this.canvas.height);
const data = this.imageData.data;
const mapping = [0x00, 0x55, 0xAA, 0xFF];
return new Promise(resolve =>
{
const remap = i =>
{
for (; i < data.length; i++)
{
data[i] = mapping[data[i] >> 6];
if (i > 0 && i % 5000 == 0)
{
// faster when possible, otherwise less intrusive
// than a promise based on setTimeout as in legacy code
return requestIdleCallback(() =>
{
this.draw();
requestIdleCallback(() => remap(i + 1));
});
}
}
resolve();
};
remap(0);
});
}
// setup the context the first time, and clean the area
clear()
{
if (!this.ctx)
{
this.ctx = this.canvas.getContext("2d");
this.ctx.lineJoin = "round";
this.ctx.lineWidth = 4;
this.ctx.strokeStyle = "rgb(208, 1, 27)";
this.ctx.fillStyle = "rgb(0, 0, 0)";
}
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
// draw the image during or after it's being processed
// and draw on top all rectangles
draw()
{
this.clear();
if (this.imageData)
{
this.ctx.putImageData(this.imageData, 0, 0);
}
for (const rect of this.paths)
{
const method = `${rect.type}Rect`;
this.ctx[method](
rect.x,
rect.y,
rect.width,
rect.height
);
}
}
// central event dispatcher
// https://dom.spec.whatwg.org/#interface-eventtarget
handleEvent(event)
{
this[`on${event.type}`](event);
}
// pointer events to draw on canvas
onpointerdown(event)
{
// avoid multiple pointers/fingers
if (this.drawing || !utils.event.isLeftClick(event))
return;
// react only if not drawing already
stop(event);
this.drawing = true;
const start = getCoordinates(event);
// set current rect to speed up coordinates updates
this.rect = {
type: this.mode,
x: start.x,
y: start.y,
width: 0,
height: 0
};
this.paths.add(this.rect);
}
onpointermove(event)
{
// only if drawing
if (!this.drawing)
return;
// update the current rect coordinates
stop(event);
this.updateRect(event);
// update the canvas view
this.draw();
}
onpointerup(event)
{
// drop only if drawing
// avoid issues when this event happens
// outside the expected DOM node (or outside the browser)
if (!this.drawing)
return;
stop(event);
if (event.currentTarget === this.canvas)
{
this.updateRect(event);
}
this.draw();
this.drawing = false;
// get out of here if the mouse didn't move at all
if (!this.rect.width && !this.rect.height)
{
// also drop current rect from the list: it's useless.
this.paths.delete(this.rect);
return;
}
const rect = this.rect;
const parent = this.canvas.parentNode;
const closeCoords = getRelativeCoordinates(
this.canvas,
rect,
{
x: rect.x + rect.width,
y: rect.y + rect.height
}
);
// use the DOM to show the close event
// - always visible, even outside the canvas
// - no need to re-invent hit-test coordinates
// - no need to redraw without closers later on
parent.appendChild(wire()`
<span
class="closer"
onclick="${evt =>
{
if (!utils.event.isLeftClick(evt))
return;
// when clicked, remove the related rectangle
// and draw the canvas again
stop(evt);
parent.removeChild(evt.currentTarget);
this.paths.delete(rect);
this.draw();
}}"
style="${{
// always top right corner
top: closeCoords.y + "px",
left: closeCoords.x + "px"
}}"
>
<img src="/skin/icons/delete.svg" />
</span>`);
}
// update current rectangle size
updateRect(event)
{
const coords = getCoordinates(event);
this.rect.width = coords.x - this.rect.x;
this.rect.height = coords.y - this.rect.y;
}
};
// helper to retrieve absolute coordinates
function getCoordinates(event)
{
let el = event.currentTarget;
let x = 0;
let y = 0;
do
{
x += el.offsetLeft - el.scrollLeft;
y += el.offsetTop - el.scrollTop;
} while (
(el = el.offsetParent) &&
!isNaN(el.offsetLeft) &&
!isNaN(el.offsetTop)
);
return {x: event.clientX - x, y: event.clientY - y};
}
// helper to retrieve absolute page coordinates
// of a generic target node
function getRelativeCoordinates(canvas, start, end)
{
const x = Math.max(start.x, end.x) + canvas.offsetLeft;
const y = Math.min(start.y, end.y) + canvas.offsetTop;
return {x: Math.round(x), y: Math.round(y)};
}
// prevent events from doing anything
// in the current node, and every parent too
function stop(event)
{
event.preventDefault();
event.stopPropagation();
}
......@@ -69,6 +69,18 @@ const DOMUtils = {
}
return !!value;
}
},
event: {
// returns true if it's a left click or a touch event.
// The left mouse button value is 0 and this
// is compatible with pointers/touch events
// where `button` might not be there.
isLeftClick(event)
{
const re = /^(?:click|mouse|touch|pointer)/;
return re.test(event.type) && !event.button;
}
}
};
......
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
* Copyright (C) 2006-present eyeo GmbH
*
* Adblock Plus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* Adblock Plus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const IOElement = require("./io-element");
const DrawingHandler = require("./drawing-handler");
const {utils} = IOElement;
// <io-highlighter data-max-size=800 />
class IOHighlighter extends IOElement
{
// define an initial state per each new instance
// https://viperhtml.js.org/hyperhtml/documentation/#components-2
get defaultState()
{
return {drawing: "", changeDepth: null};
}
// resolves once the image depth has been fully changed
// comp.changeDepth.then(...)
get changeDepth()
{
return this.state.changeDepth;
}
// returns true if there were hidden/highlighted areas
get edited()
{
return this.drawingHandler ? this.drawingHandler.paths.size > 0 : false;
}
// process an image and setup changeDepth promise
// returns the component for chainability sake
// comp.edit(imageOrString).changeDepth.then(...);
edit(source)
{
return this.setState({
changeDepth: new Promise((res, rej) =>
{
const changeDepth = image =>
{
this.drawingHandler.changeColorDepth(image).then(res, rej);
};
if (typeof source === "string")
{
// create an image and use the source as data
const img = this.ownerDocument.createElement("img");
img.onload = () => changeDepth(img);
img.onerror = rej;
img.src = source;
}
else
{
// assume the source is an Image already
// (or anything that can be drawn on a canvas)
changeDepth(source);
}
})
});
}
// the component content (invoked automatically on state change too)
render()
{
if (this.state.drawing)
this.setAttribute("drawing", this.state.drawing);
else
this.removeAttribute("drawing");
this.html`
<div class="split">
<div class="options">
<button
tabindex="-1"
class="highlight"
onclick="${
event =>
{
if (utils.event.isLeftClick(event))
changeMode(this, "highlight");
}
}"
>
${{i18n: "issueReporter_screenshot_highlight"}}
</button>
<button
tabindex="-1"
class="hide"
onclick="${
event =>
{
if (utils.event.isLeftClick(event))
changeMode(this, "hide");
}
}"
>
${{i18n: "issueReporter_screenshot_hide"}}
</button>
</div>
<canvas />
</div>`;
// first time only, initialize the DrawingHandler
// through the newly created canvas
if (!this.drawingHandler)
this.drawingHandler = new DrawingHandler(
this.querySelector("canvas"),
parseInt(this.dataset.maxSize, 10) || 800
);
}
// shortcut for internal canvas.toDataURL()
toDataURL()
{
return this.querySelector("canvas").toDataURL();
}
}
IOHighlighter.define("io-highlighter");
const changeMode = (self, mode) =>
{
const drawing = self.state.drawing === mode ? "" : mode;
self.drawingHandler.mode = mode === "hide" ? "fill" : "stroke";
self.setState({drawing});
};
......@@ -59,6 +59,12 @@
"issueReporter_sendPage_heading": {
"message": "Send report"
},
"issueReporter_screenshot_highlight": {
"message": "Highlight"
},
"issueReporter_screenshot_hide": {
"message": "Hide"
},
"issueReporter_sending": {
"message": "Please wait while Adblock Plus is submitting your report."
},
......
......@@ -18,8 +18,9 @@
"watch": "npm run watch:desktop-options.js & npm run watch:desktop-options.css",
"watch:desktop-options.css": "watch 'npm run bundle:desktop-options.css' ./css",
"watch:desktop-options.js": "watch 'npm run bundle:desktop-options.js' ./js",
"test": "npm run test:io-element && npm run test:io-steps && npm run test:io-toggle && http-server",
"test": "npm run test:io-element && npm run test:io-highlighter && npm run test:io-steps && npm run test:io-toggle && http-server",
"test:io-element": "npm run bashForTest:js io-element && npm run bundleTest:js io-element",
"test:io-highlighter": "npm run bashForTest:js io-highlighter && npm run bundleTest:js io-highlighter && npm run bashForTest:css io-highlighter && cp -R ./skin/icons smoke/css && cp ./tests/image.base64.txt ./smoke",
"test:io-steps": "npm run bashForTest:js io-steps && npm run bundleTest:js io-steps && npm run bashForTest:css io-steps",
"test:io-toggle": "npm run bashForTest:js io-toggle && npm run bundleTest:js io-toggle && npm run bashForTest:css io-toggle",
"postinstall": "npm run bundle"
......
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="22">
<view id="off" viewBox="0 0 36 22"/>
<g fill="#4A4A4A" fill-rule="evenodd" transform="translate(0 -2)">
<rect width="14" height="6" y="17.544"/>
<g transform="rotate(-45 29.864 -3.778)">
<rect width="16.196" height="8" x="4.723" y="1.002"/>
<polygon points="1.169 2.175 6.819 7.822 1.169 7.822" transform="rotate(45 3.994 4.999)"/>
<path d="M21.652442,0.995292341 L24.7524419,0.995292341 L24.7524419,0.995292341 C25.8570114,0.995292341 26.7524419,1.89072284 26.7524419,2.99529234 L26.7524419,6.99529234 L26.7524419,6.99529234 C26.7524419,8.09986184 25.8570114,8.99529234 24.7524419,8.99529234 L21.652442,8.99529234 L21.652442,0.995292341 Z"/>
</g>
</g>
<view id="on" viewBox="36 0 36 22"/>
<g fill="#FFF" fill-rule="evenodd" transform="translate(36 -2)">
<rect width="14" height="6" y="17.544"/>
<g transform="rotate(-45 29.864 -3.778)">
<rect width="16.196" height="8" x="4.723" y="1.002"/>
<polygon points="1.169 2.175 6.819 7.822 1.169 7.822" transform="rotate(45 3.994 4.999)"/>
<path d="M21.652442,0.995292341 L24.7524419,0.995292341 L24.7524419,0.995292341 C25.8570114,0.995292341 26.7524419,1.89072284 26.7524419,2.99529234 L26.7524419,6.99529234 L26.7524419,6.99529234 C26.7524419,8.09986184 25.8570114,8.99529234 24.7524419,8.99529234 L21.652442,8.99529234 L21.652442,0.995292341 Z"/>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="29" height="28">
<view id="off" viewBox="0 0 29 28"/>
<defs>
<rect id="highlight-a-off" width="29" height="28"/>
<mask id="highlight-b-off" width="29" height="28" x="0" y="0" fill="#FFF">
<use xlink:href="#highlight-a-off"/>
</mask>
</defs>
<use fill="none" fill-rule="evenodd" stroke="#4A4A4A" stroke-dasharray="5" stroke-width="4" mask="url(#highlight-b-off)" xlink:href="#highlight-a-off"/>
<view id="on" viewBox="29 0 29 28"/>
<defs>
<rect id="highlight-a-on" width="29" height="28" transform="translate(29 0)"/>
<mask id="highlight-b-on" width="29" height="28" x="0" y="0" fill="#FFF">
<use xlink:href="#highlight-a-on"/>
</mask>
</defs>
<use fill="none" fill-rule="evenodd" stroke="#FFF" stroke-dasharray="5" stroke-width="4" mask="url(#highlight-b-on)" xlink:href="#highlight-a-on"/>
</svg>
This diff is collapsed.
<!DOCTYPE html>
<!--
- This file is part of Adblock Plus <https://adblockplus.org/>,
- Copyright (C) 2006-present eyeo GmbH
-
- Adblock Plus is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License version 3 as
- published by the Free Software Foundation.
-
- Adblock Plus is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
-->
<html>
<head>
<meta charset="utf-8">
<title>Test io-toggle.js</title>
<link rel="stylesheet" href="../skin/common.css">
<link rel="stylesheet" href="../skin/fonts.css">
<link rel="stylesheet" href="../skin/desktop-options.css">
<link rel="stylesheet" href="../smoke/css/io-highlighter.css">
<script>
function testSnapshot()
{
const comp = document.querySelector("io-highlighter");
const img = document.querySelector("img.snapshot") ||
document.body.appendChild(new Image);
img.className = 'snapshot';
img.src = comp.toDataURL();
}
</script>
<script defer src="../polyfill.js"></script>
<script defer src="../ext/common.js"></script>
<script defer src="../ext/content.js"></script>
<script defer src="../common.js"></script>
<script defer src="../i18n.js"></script>
<script defer src="./io-highlighter.js"></script>
<style>
body
{
display: block;
}
io-highlighter
{
width: 640px;
height: 400px;
}
</style>
</head>
<body style="background:white;">
<io-highlighter data-max-size="800"></io-highlighter>
<div style="text-align:center;padding:8px;width:640px;">
<button disabled
id="snapshot"
style="margin:auto"
onclick="testSnapshot()"
>
test snapshot
</button>
</div>
</body>
</html>
/* globals require */
"use strict";
require("../js/io-highlighter");
document.addEventListener(
"DOMContentLoaded",
() =>
{
fetch("./image.base64.txt").then(body => body.text()).then(data =>
{
const ioHighlighter = document.querySelector("io-highlighter");
ioHighlighter.edit(data);
ioHighlighter.changeDepth.then(() =>
{
document.querySelector("#snapshot").disabled = false;
});
});
}