forked from w3c/webdriver
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
adee9ff
commit cd88453
Showing
4 changed files
with
314 additions
and
75 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains 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
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); |
This file contains 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
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; | ||
} |
Oops, something went wrong.