Skip to content

Instantly share code, notes, and snippets.

@Stvad
Last active April 26, 2022 23:54
Show Gist options
  • Save Stvad/360233135351d41f0411ce275b16214b to your computer and use it in GitHub Desktop.
Save Stvad/360233135351d41f0411ce275b16214b to your computer and use it in GitHub Desktop.
iPad better web annotation bookmarklet

Bookmarklet that improves web annotation experience on iPad

It combines a https://hypothes.is setup with UX improvements for iPad.

Watch the video

Usage

  1. Run the bookmarklet.
  2. Double tap screen with a finger to enter annotation mode.
  3. Now you can use Scribble UX anywhere on the page.
  4. Edit/highlight things adding the highlights to Hypothesis.
  5. Double tap again to exit annotation mode.

Installation

  1. How to create bookmarklets in general: https://www.cultofmac.com/500532/how-to-add-bookmarklet-mobile-iphone-safari/
  2. Use the instructions above and content of this file as the link in the bookmarklet.

How does it work

The key idea is using doucument.designMode = "on" to make anything on the page editable.
Which makes Scribble UX available automatically.

javascript:(function%20()%20%7B%0A%0A%20%20%20%20setUpHypothesIs()%0A%0A%20%20%20%20let%20lastTapTime%20=%200%0A%0A%20%20%20%20function%20isDoubleTap(maxDelta%20=%2050)%20%7B%0A%20%20%20%20%20%20%20%20const%20delta%20=%20Date.now()%20-%20lastTapTime%0A%20%20%20%20%20%20%20%20return%20delta%20%3C%20maxDelta%20&&%20delta%20%3E%200%0A%20%20%20%20%7D%0A%0A%20%20%20%20const%20body%20=%20document.querySelector('body')%0A%20%20%20%20function%20toggleDesignMode()%20%7B%0A%20%20%20%20%20%20%20%20console.log('toggle')%0A%20%20%20%20%20%20%20%20document.designMode%20=%20document.designMode%20===%20'on'%20?%20'off'%20:%20'on'%0A%20%20%20%20%7D%0A%0A%20%20%20%20body.addEventListener('touchstart',%20async%20evt%20=%3E%20%7B%0A%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20//%20should%20be%20either%20%22stylus%22%20or%20%22direct%22%0A%20%20%20%20%20%20%20%20%20%20%20%20//%20from%20https://stackoverflow.com/questions/34986373/javascript-touch-event-distinguishing-finger-vs-apple-pencil%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(evt.touches%5B0%5D.touchType%20===%20'direct')%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20(isDoubleTap(300))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20toggleDesignMode()%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20lastTapTime%20=%20Date.now()%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20console.error(e)%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D)%0A%0A%20%20%20%20function%20setUpHypothesIs()%20%7B%0A%20%20%20%20%20%20%20%20window.hypothesisConfig%20=%20function%20()%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%7BshowHighlights:%20true,%20appType:%20'bookmarklet'%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20var%20d%20=%20document,%20s%20=%20d.createElement('script')%0A%20%20%20%20%20%20%20%20s.setAttribute('src',%20'https://hypothes.is/embed.js')%0A%20%20%20%20%20%20%20%20d.body.appendChild(s)%0A%0A%20%20%20%20%7D%0A%7D)()%0A
import {readFileSync, writeFileSync} from 'fs';
const url = encodeURI(readFileSync('./highlight-mode.js', 'utf8'));
writeFileSync('bookmarklet', url)
javascript:(function () {
setUpHypothesIs()
let lastTapTime = 0
function isDoubleTap(maxDelta = 50) {
const delta = Date.now() - lastTapTime
return delta < maxDelta && delta > 0
}
const body = document.querySelector('body')
function toggleDesignMode() {
console.log('toggle')
document.designMode = document.designMode === 'on' ? 'off' : 'on'
}
body.addEventListener('touchstart', async evt => {
try {
// should be either "stylus" or "direct"
// from https://stackoverflow.com/questions/34986373/javascript-touch-event-distinguishing-finger-vs-apple-pencil
if (evt.touches[0].touchType === 'direct') {
if (isDoubleTap(300)) {
toggleDesignMode()
}
lastTapTime = Date.now()
}
} catch (e) {
console.error(e)
}
})
function setUpHypothesIs() {
window.hypothesisConfig = function () {
return {showHighlights: true, appType: 'bookmarklet'}
}
var d = document, s = d.createElement('script')
s.setAttribute('src', 'https://hypothes.is/embed.js')
d.body.appendChild(s)
}
})()
//todo double tap to select element
// https://gist.github.com/Stvad/360233135351d41f0411ce275b16214b
(function () {
let lastPencilTapTime = 0
function isDoubleTap(maxDelta = 50) {
const delta = Date.now() - lastPencilTapTime
return delta < maxDelta && delta > 0
}
const body = document.querySelector('body')
function touchHandler(event) {
console.log('touchHandler', event)
var touches = event.changedTouches,
first = touches[0],
type = "";
switch(event.type)
{
case "touchstart": type = "dblclick"; break;
case "touchmove": type = "mousemove"; break;
case "touchend": type = "mouseup"; break;
default: return;
}
// initMouseEvent(type, canBubble, cancelable, view, clickCount,
// screenX, screenY, clientX, clientY, ctrlKey,
// altKey, shiftKey, metaKey, button, relatedTarget);
var simulatedEvent = document.createEvent("MouseEvent");
simulatedEvent.initMouseEvent(type, true, true, window, 1,
first.screenX, first.screenY,
first.clientX, first.clientY, false,
false, false, false, 0/*left*/, null);
first.target.dispatchEvent(simulatedEvent);
event.preventDefault();
}
function init()
{
document.addEventListener("touchstart", touchHandler, true);
document.addEventListener("touchmove", touchHandler, true);
document.addEventListener("touchend", touchHandler, true);
document.addEventListener("touchcancel", touchHandler, true);
}
// init()
body.addEventListener('touchstart', async evt => {
try {
// should be either "stylus" or "direct"
// from https://stackoverflow.com/questions/34986373/javascript-touch-event-distinguishing-finger-vs-apple-pencil
if (evt.touches[0].touchType === 'stylus') {
selectTappedWord(evt)
}
} catch (e) {
console.error(e)
}
})
function selectTappedWord(evt) {
const touchRange = getWord(evt.touches[0])
// replaceSelectionWith(touchRange)
}
function replaceSelectionWith(range) {
const sel = window.getSelection()
console.log({sel, range})
sel.removeAllRanges()
sel.addRange(range)
}
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))
function selectElementContents(el) {
const range = document.createRange()
range.selectNodeContents(el)
replaceSelectionWith(range)
}
function getWord(touch) {
const sel = window.getSelection()
const range = document.caretRangeFromPoint(touch.clientX, touch.clientY)
sel.removeAllRanges()
sel.addRange(range)
if (sel.isCollapsed) {
sel.modify('move', 'forward', 'character')
sel.modify('move', 'backward', 'word')
sel.modify('extend', 'forward', 'word')
// t = selection.toString();
//s.modify('move', 'forward', 'character'); //clear selection
} else {return }
// else {
// t = selection.toString();
// }
console.log(sel)
// return selection.getRangeAt(0)
return range
// const range = document.caretRangeFromPoint(touch.clientX, touch.clientY)
// return range
}
// from https://stackoverflow.com/questions/47282265/html5-touch-event-getting-word-touched
function getWordO(e) {
// FF gives us a shortcut
let target = e.target,
// We will use this to get the positions of our textNodes
range = document.createRange(),
rect, i
// so first let's get the textNode that was clicked
if (target.nodeType !== 3) {
const children = target.childNodes
i = 0
while (i < children.length) {
range.selectNode(children[i])
rect = range.getBoundingClientRect()
if (rect.left <= e.clientX && rect.right >= e.clientX &&
rect.top <= e.clientY && rect.bottom >= e.clientY) {
target = children[i]
break
}
i++
}
}
if (target.nodeType !== 3) {
return '[not a textNode]'
}
// Now, let's split its content to words
let words = target.nodeValue.split(' '),
textNode, newText
i = 0
while (i < words.length) {
// create a new textNode with only this word
textNode = document.createTextNode((i ? ' ' : '') + words[i])
newText = words.slice(i + 1)
// update the original node's text
target.nodeValue = newText.length ? (' ' + newText.join(' ')) : ''
// insert our new textNode
target.parentNode.insertBefore(textNode, target)
// get its position
range.selectNode(textNode)
rect = range.getBoundingClientRect()
// if it is the one
if (rect.left <= e.clientX && rect.right >= e.clientX &&
rect.top <= e.clientY && rect.bottom >= e.clientY) {
// return words[i];
return range
}
i++
}
}
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment