Skip to content

Commit

Permalink
replace bug-assist.js with issue.js
Browse files Browse the repository at this point in the history
This allows issues to contain quotes from the document by selecting text.
It also provides a polyfill for selectionchange events

Signed-off-by: AutomatedTester <[email protected]>
  • Loading branch information
andreastt authored and AutomatedTester committed Oct 5, 2016
1 parent adee9ff commit cd88453
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 75 deletions.
71 changes: 0 additions & 71 deletions bug-assist.js

This file was deleted.

169 changes: 169 additions & 0 deletions issue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// issue.js provides a widget that will appear in HTML documents when
// text is selected that allows extraction of prose to form quotations of
// new GitHub issues.
//
// The widget will appear when the user selects text in the document.
// When clicking the button that appears, the selected text is quoted,
// along with references to the chapter and section from whence the quote
// came, in the new GitHub issue that is created.
//
// Configure by setting the GitHub project URL and any optional parameter
// fields in the new issue form you want populated:
//
// <html
// data-issue-url="https://github.com/w3c/webdriver"
// data-issue-param-milestone="Level 1">
//
// Inspired by the “Simple Bug File Assistant” for the W3C Bugzilla
// bug tracker.
//
// © 2016 Andreas Tolfsen <[email protected]>
// Licensed under the MIT license.

document.addEventListener("DOMContentLoaded", function() {
"use strict";

const ISSUE_PARAM_PREFIX = "data-issue-param-";
const BASE_BUTTON_STYLE = `
color: #fff;
cursor: pointer;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.15);
background-color: #60b044;
background-image: linear-gradient(#8add6d, #60b044);
border: 1px solid #d5d5d5;
whitespace: nowrap;
border-radius: 3px;
line-height: 20px;
font-weight: 600;
font-size: 12px;
padding: 6px 12px;
border-color: #5ca941;
`;

let baseUrl = document.documentElement.attributes["data-issue-url"].value;
let newIssueUrl = baseUrl + (baseUrl.endsWith("/") ? "issues/new" : "/issues/new");

let inputs = {body: ""};
[].forEach.call(document.documentElement.attributes, attr => {
if (attr.name.startsWith(ISSUE_PARAM_PREFIX))
inputs[attr.name.substr(ISSUE_PARAM_PREFIX.length)] = attr.value;
});

function formatSelection(sel) {
let quoteText = text => text.split("\n").map(el => "> " + el).join("\n");

// TODO(ato): Make this just construct a tree of h2/h3
let findSection = (el, localName) => {
let sectionEl = el.closest("section");
if (!sectionEl)
return null;
let heading = sectionEl.querySelector(":scope > " + localName);
if (!heading)
return findSection(sectionEl.parentNode, localName);

let relUrl = location.href + "#" + encodeURIComponent(heading.id);

return {
url: relUrl,
text: heading.textContent,
};
};

let parent = sel.anchorNode.parentElement;

let chapter = findSection(parent, "h2");
let subchapter = findSection(parent, "h3");
let desc = quoteText(sel.toString());

let rv = "";
if (chapter)
rv = `In chapter [${chapter.text}](${chapter.url})`;
if (subchapter)
rv += `, section [${subchapter.text}](${subchapter.url}):`;
if (chapter || subchapter)
rv += "\n";
return rv + desc;
}

function getAbsolutePosition(node) {
let bodyRect = document.body.getBoundingClientRect();
let nodeRect = node.getBoundingClientRect();
return {
top: nodeRect.top - bodyRect.top,
left: nodeRect.left - bodyRect.left,
};
}

let widget = new class {
constructor(rootEl) {
this.parent = rootEl;
this.el = null;
this.shown = false;
}

show() {
if (!this.el) {
let form = this.parent.appendChild(document.createElement("form"));
form.action = newIssueUrl;
form.target = "_blank";

let submit = form.appendChild(document.createElement("input"));
submit.type = "submit";
submit.accessKey = "f";
submit.value = "File an issue";
submit.style.cssText = BASE_BUTTON_STYLE;

Object.keys(inputs).forEach(name => {
let input = form.appendChild(document.createElement("input"));
input.type = "hidden";
input.name = name;
input.value = inputs[name];
inputs[name] = input;
});

form.addEventListener("submit", this.click);

this.submitEl = submit;
this.el = form;
this.el.style.visibility = "visible";
}

this.el.style.visibility = "visible";
this.shown = true;
this.paint();
}

paint() {
if (!this.shown)
return;

let pos = getAbsolutePosition(window.getSelection().getRangeAt(0));

this.el.style.position = "absolute";
this.el.style.top = (pos.top - 40) + "px";
this.el.style.left = (pos.left + 45) + "px";
}

hide() {
if (!this.shown)
return;
this.el.style.visibility = "hidden";
this.shown = false;
}

click() {
let sel = window.getSelection();
if (sel.toString().length > 0)
inputs.body.value = formatSelection(sel);
}
}(document.documentElement);

document.addEventListener("selectionchange", () => {
if (window.getSelection().toString().length == 0)
widget.hide();
else
widget.show();
}, false);

document.addEventListener("scroll", widget.paint, false);
}, false);
140 changes: 140 additions & 0 deletions selectionchange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// github.com/2is10/selectionchange-polyfill

var selectionchange = (function (undefined) {

var MAC = /^Mac/.test(navigator.platform);
var MAC_MOVE_KEYS = [65, 66, 69, 70, 78, 80]; // A, B, E, F, P, N from support.apple.com/en-ie/HT201236
var SELECT_ALL_MODIFIER = MAC ? 'metaKey' : 'ctrlKey';
var RANGE_PROPS = ['startContainer', 'startOffset', 'endContainer', 'endOffset'];
var HAS_OWN_SELECTION = {INPUT: 1, TEXTAREA: 1};

var ranges;

return {
start: function (doc) {
var d = doc || document;
if (ranges || !hasNativeSupport(d) && (ranges = newWeakMap())) {
if (!ranges.has(d)) {
ranges.set(d, getSelectionRange(d));
on(d, 'input', onInput);
on(d, 'keydown', onKeyDown);
on(d, 'mousedown', onMouseDown);
on(d, 'mousemove', onMouseMove);
on(d, 'mouseup', onMouseUp);
on(d.defaultView, 'focus', onFocus);
}
}
},
stop: function (doc) {
var d = doc || document;
if (ranges && ranges.has(d)) {
ranges['delete'](d);
off(d, 'input', onInput);
off(d, 'keydown', onKeyDown);
off(d, 'mousedown', onMouseDown);
off(d, 'mousemove', onMouseMove);
off(d, 'mouseup', onMouseUp);
off(d.defaultView, 'focus', onFocus);
}
}
};

function hasNativeSupport(doc) {
var osc = doc.onselectionchange;
if (osc !== undefined) {
try {
doc.onselectionchange = 0;
return doc.onselectionchange === null;
} catch (e) {
} finally {
doc.onselectionchange = osc;
}
}
return false;
}

function newWeakMap() {
if (typeof WeakMap !== 'undefined') {
return new WeakMap();
} else {
console.error('selectionchange: WeakMap not supported');
return null;
}
}

function getSelectionRange(doc) {
var s = doc.getSelection();
return s.rangeCount ? s.getRangeAt(0) : null;
}

function on(el, eventType, handler) {
el.addEventListener(eventType, handler, true);
}

function off(el, eventType, handler) {
el.removeEventListener(eventType, handler, true);
}

function onInput(e) {
if (!HAS_OWN_SELECTION[e.target.tagName]) {
dispatchIfChanged(this, true);
}
}

function onKeyDown(e) {
var code = e.keyCode;
if (code === 65 && e[SELECT_ALL_MODIFIER] && !e.shiftKey && !e.altKey || // Ctrl-A or Cmd-A
code >= 35 && code <= 40 || // home, end and arrow key
e.ctrlKey && MAC && MAC_MOVE_KEYS.indexOf(code) >= 0) {
if (!HAS_OWN_SELECTION[e.target.tagName]) {
setTimeout(dispatchIfChanged.bind(null, this), 0);
}
}
}

function onMouseDown(e) {
if (e.button === 0) {
on(this, 'mousemove', onMouseMove);
setTimeout(dispatchIfChanged.bind(null, this), 0);
}
}

function onMouseMove(e) { // only needed while primary button is down
if (e.buttons & 1) {
dispatchIfChanged(this);
} else {
off(this, 'mousemove', onMouseMove);
}
}

function onMouseUp(e) {
if (e.button === 0) {
setTimeout(dispatchIfChanged.bind(null, this), 0);
} else {
off(this, 'mousemove', onMouseMove);
}
}

function onFocus() {
setTimeout(dispatchIfChanged.bind(null, this.document), 0);
}

function dispatchIfChanged(doc, force) {
var r = getSelectionRange(doc);
if (force || !sameRange(r, ranges.get(doc))) {
ranges.set(doc, r);
setTimeout(doc.dispatchEvent.bind(doc, new Event('selectionchange')), 0);
}
}

function sameRange(r1, r2) {
return r1 === r2 || r1 && r2 && RANGE_PROPS.every(function (prop) {
return r1[prop] === r2[prop];
});
}
})();

if (typeof module !== 'undefined') {
// CommonJS/Node compatibility.
module.exports = selectionchange;
}
Loading

0 comments on commit cd88453

Please sign in to comment.