visbug-lib
Version:
<p align="center"> <img src="./assets/visbug.png" width="300" height="300" alt="visbug"> <br> <a href="https://www.npmjs.org/package/visbug"><img src="https://img.shields.io/npm/v/visbug.svg?style=flat" alt="npm latest version number"></a> <a href
1,892 lines (1,555 loc) • 554 kB
JavaScript
import hotkeys from 'hotkeys-js';
import $$ from 'blingblingjs';
import { TinyColor, readability, isReadable } from '@ctrl/tinycolor';
import { querySelectorAllDeep } from 'query-selector-shadow-dom';
const desiredPropMap = {
color: 'rgb(0, 0, 0)',
backgroundColor: 'rgba(0, 0, 0, 0)',
backgroundImage: 'none',
backgroundSize: 'auto',
backgroundPosition: '0% 0%',
borderColor: 'rgb(0, 0, 0)',
borderWidth: '0px',
borderRadius: '0px',
boxShadow: 'none',
padding: '0px',
margin: '0px',
fontFamily: '',
fontSize: '16px',
fontWeight: '400',
textAlign: 'start',
textShadow: 'none',
textTransform: 'none',
lineHeight: 'normal',
letterSpacing: 'normal',
display: 'block',
alignItems: 'normal',
justifyContent: 'normal',
flexDirection: 'row',
flexWrap: 'nowrap',
flexBasis: 'auto',
// flexFlow: 'none',
fill: 'rgb(0, 0, 0)',
stroke: 'none',
gridTemplateColumns: 'none',
gridAutoColumns: 'auto',
gridTemplateRows: 'none',
gridAutoRows: 'auto',
gridTemplateAreas: 'none',
gridArea: 'auto / auto / auto / auto',
gap: 'normal normal',
gridAutoFlow: 'row',
};
const desiredAccessibilityMap = [
'role',
'tabindex',
'aria-*',
'for',
'alt',
'title',
'type',
];
const largeWCAG2TextMap = [
{
fontSize: '24px',
fontWeight: '0'
},
{
fontSize: '18.5px',
fontWeight: '700'
}
];
const getStyle = (el, name) => {
if (document.defaultView && document.defaultView.getComputedStyle) {
name = name.replace(/([A-Z])/g, '-$1');
name = name.toLowerCase();
let s = document.defaultView.getComputedStyle(el, '');
return s && s.getPropertyValue(name)
}
};
const getStyles = el => {
const elStyleObject = el.style;
const computedStyle = window.getComputedStyle(el, null);
const vettedStyles = Object.entries(el.style)
.filter(([prop]) => prop !== 'borderColor')
.filter(([prop]) => desiredPropMap[prop])
.filter(([prop]) => desiredPropMap[prop] != computedStyle[prop])
.map(([prop, value]) => ({
prop,
value: computedStyle[prop].replace(/, rgba/g, '\rrgba'),
}));
// below code sucks, but border-color is only something
// we want if it has border width > 0
// i made it look really hard
const trueBorderColors = Object.entries(el.style)
.filter(([prop]) => prop === 'borderColor' || prop === 'borderWidth' || prop === 'borderStyle')
.map(([prop, value]) => ([
prop,
computedStyle[prop].replace(/, rgba/g, '\rrgba'),
]));
const { borderColor, borderWidth, borderStyle } = Object.fromEntries(trueBorderColors);
const vettedBorders = [];
// todo: push border style!
if (parseInt(borderWidth) > 0) {
vettedBorders.push({
prop: 'borderColor',
value: borderColor,
});
vettedBorders.push({
prop: 'borderStyle',
value: borderStyle,
});
}
return [
...vettedStyles,
...vettedBorders,
].sort(function({prop:propA}, {prop:propB}) {
if (propA < propB) return -1
if (propA > propB) return 1
return 0
})
};
const getComputedBackgroundColor = el => {
let background = undefined
, node = el;
while(true) {
let bg = getStyle(node, 'background-color');
if (bg !== 'rgba(0, 0, 0, 0)') {
background = bg;
break;
}
if (node.nodeName === 'HTML') {
background = 'white';
break;
}
node = findNearestParentElement(node);
}
return background
};
const findNearestParentElement = el =>
el.parentNode && el.parentNode.nodeType === 1
? el.parentNode
: el.parentNode.nodeName === '#document-fragment'
? el.parentNode.host
: el.parentNode.parentNode.host;
const findNearestChildElement = el => {
if (el.shadowRoot && el.shadowRoot.children.length) {
return [...el.shadowRoot.children]
.filter(({nodeName}) =>
!['LINK','STYLE','SCRIPT','HTML','HEAD'].includes(nodeName)
)[0]
}
else if (el.children.length)
return el.children[0]
};
const loadStyles = async stylesheets => {
const fetches = await Promise.all(stylesheets.map(url => fetch(url)));
const texts = await Promise.all(fetches.map(url => url.text()));
const style = document.createElement('style');
style.textContent = texts.reduce((styles, fileContents) =>
styles + fileContents
, '');
document.head.appendChild(style);
};
// returns [full, color, x, y, blur, spread]
const getShadowValues = shadow =>
/([^\)]+\)) ([^\s]+) ([^\s]+) ([^\s]+) ([^\s]+)/.exec(shadow);
// returns [full, color, x, y, blur]
const getTextShadowValues = shadow =>
/([^\)]+\)) ([^\s]+) ([^\s]+) ([^\s]+)/.exec(shadow);
const getA11ys = el => {
const elAttributes = el.getAttributeNames();
return desiredAccessibilityMap.reduce((acc, attribute) => {
if (elAttributes.includes(attribute))
acc.push({
prop: attribute,
value: el.getAttribute(attribute)
});
if (attribute === 'aria-*')
elAttributes.forEach(attr => {
if (attr.includes('aria'))
acc.push({
prop: attr,
value: el.getAttribute(attr)
});
});
return acc
}, [])
};
const getWCAG2TextSize = el => {
const styles = getStyles(el).reduce((styleMap, style) => {
styleMap[style.prop] = style.value;
return styleMap
}, {});
const { fontSize = desiredPropMap.fontSize,
fontWeight = desiredPropMap.fontWeight
} = styles;
const isLarge = largeWCAG2TextMap.some(
(largeProperties) => parseFloat(fontSize) >= parseFloat(largeProperties.fontSize)
&& parseFloat(fontWeight) >= parseFloat(largeProperties.fontWeight)
);
return isLarge ? 'Large' : 'Small'
};
const camelToDash = (camelString = "") =>
camelString.replace(/([A-Z])/g, ($1) =>
"-"+$1.toLowerCase());
const nodeKey = node => {
let tree = [];
let furthest_leaf = node;
while (furthest_leaf) {
tree.push(furthest_leaf);
furthest_leaf = furthest_leaf.parentNode
? furthest_leaf.parentNode
: false;
}
return tree.reduce((path, branch) => `
${path}${branch.tagName}_${branch.className}_${[...node.parentNode.children].indexOf(node)}_${node.children.length}
`, '')
};
const createClassname = (el, ellipse = false) => {
if (!el.className) return ''
const combined = Array.from(el.classList).reduce((classnames, classname) =>
classnames += '.' + classname
, '');
return ellipse && combined.length > 30
? combined.substring(0,30) + '...'
: combined
};
const metaKey = window.navigator.platform.includes('Mac')
? 'cmd'
: 'ctrl';
const altKey = window.navigator.platform.includes('Mac')
? 'opt'
: 'alt';
const notList = ':not(vis-bug):not(script):not(hotkey-map):not(.visbug-metatip):not(visbug-label):not(visbug-handles):not(visbug-corners):not(visbug-grip):not(visbug-gridlines)';
const $ = (query, $context = document) => query && query.nodeType !== undefined ? $$([query], $context) : $$(query, $context);
const deepElementFromPoint = (x, y) => {
const el = document.elementFromPoint(x, y);
const crawlShadows = node => {
if (node.shadowRoot) {
const potential = node.shadowRoot.elementFromPoint(x, y);
if (potential == node) return node
else if (potential.shadowRoot) return crawlShadows(potential)
else return potential
}
else return node
};
const nested_shadow = crawlShadows(el);
return nested_shadow || el
};
const getSide = direction => {
let start = direction.split('+').pop().replace(/^\w/, c => c.toUpperCase());
if (start == 'Up') start = 'Top';
if (start == 'Down') start = 'Bottom';
return start
};
const getNodeIndex = el => {
return [...el.parentElement.parentElement.children]
.indexOf(el.parentElement)
};
function showEdge(el) {
return el.animate([
{ outline: '1px solid transparent' },
{ outline: '1px solid hsla(330, 100%, 71%, 80%)' },
{ outline: '1px solid transparent' },
], 600)
}
let timeoutMap = {};
const showHideSelected = (el, duration = 750) => {
el.setAttribute('data-selected-hide', true);
showHideNodeLabel(el, true);
if (timeoutMap[nodeKey(el)])
clearTimeout(timeoutMap[nodeKey(el)]);
timeoutMap[nodeKey(el)] = setTimeout(_ => {
el.removeAttribute('data-selected-hide');
showHideNodeLabel(el, false);
}, duration);
return el
};
const showHideNodeLabel = (el, show = false) => {
if (!el.hasAttribute('data-label-id'))
return
const label_id = el.getAttribute('data-label-id');
const nodes = $(`
visbug-label[data-label-id="${label_id}"],
visbug-handles[data-label-id="${label_id}"]
`);
nodes.length && show
? nodes.forEach(el =>
el.style.display = 'none')
: nodes.forEach(el =>
el.style.display = null);
};
const htmlStringToDom = (htmlString = "") =>
(new DOMParser().parseFromString(htmlString, 'text/html'))
.body.firstChild;
const isOffBounds = node =>
node.closest && (
node.closest('vis-bug')
|| node.closest('hotkey-map')
|| node.closest('visbug-metatip')
|| node.closest('visbug-ally')
|| node.closest('visbug-label')
|| node.closest('visbug-handles')
|| node.closest('visbug-corners')
|| node.closest('visbug-grip')
|| node.closest('visbug-gridlines')
);
const isSelectorValid = (qs => (
selector => {
try { qs(selector); } catch (e) { return false }
return true
}
))(s => document.createDocumentFragment().querySelector(s));
const swapElements = (src, target) => {
var temp = document.createElement("div");
src.parentNode.insertBefore(temp, src);
target.parentNode.insertBefore(src, target);
temp.parentNode.insertBefore(target, temp);
temp.parentNode.removeChild(temp);
};
const key_events = 'up,down,left,right'
.split(',')
.reduce((events, event) =>
`${events},${event},alt+${event},shift+${event},shift+alt+${event}`
, '')
.substring(1);
const command_events = `${metaKey}+up,${metaKey}+shift+up,${metaKey}+down,${metaKey}+shift+down`;
function Margin(visbug) {
hotkeys(key_events, (e, handler) => {
if (e.cancelBubble) return
e.preventDefault();
pushElement(visbug.selection(), handler.key);
});
hotkeys(command_events, (e, handler) => {
e.preventDefault();
pushAllElementSides(visbug.selection(), handler.key);
});
visbug.onSelectedUpdate(paintBackgrounds);
return () => {
hotkeys.unbind(key_events);
hotkeys.unbind(command_events);
hotkeys.unbind('up,down,left,right'); // bug in lib?
visbug.removeSelectedCallback(paintBackgrounds);
removeBackgrounds(visbug.selection());
}
}
function pushElement(els, direction) {
els
.map(el => showHideSelected(el))
.map(el => ({
el,
style: 'margin' + getSide(direction),
current: parseInt(getStyle(el, 'margin' + getSide(direction)), 10),
amount: direction.split('+').includes('shift') ? 10 : 1,
negative: direction.split('+').includes('alt'),
}))
.map(payload =>
Object.assign(payload, {
margin: payload.negative
? payload.current - payload.amount
: payload.current + payload.amount
}))
.forEach(({el, style, margin}) =>
el.style[style] = `${margin < 0 ? 0 : margin}px`);
}
function pushAllElementSides(els, keycommand) {
const combo = keycommand.split('+');
let spoof = '';
if (combo.includes('shift')) spoof = 'shift+' + spoof;
if (combo.includes('down')) spoof = 'alt+' + spoof;
'up,down,left,right'.split(',')
.forEach(side => pushElement(els, spoof + side));
}
function paintBackgrounds(els) {
els.forEach(el => {
const label_id = el.getAttribute('data-label-id');
document
.querySelector(`visbug-handles[data-label-id="${label_id}"]`)
.backdrop = {
element: createMarginVisual(el),
update: createMarginVisual,
};
});
}
function removeBackgrounds(els) {
els.forEach(el => {
const label_id = el.getAttribute('data-label-id');
const boxmodel = document.querySelector(`visbug-handles[data-label-id="${label_id}"]`)
.$shadow.querySelector('visbug-boxmodel');
if (boxmodel) boxmodel.remove();
});
}
function createMarginVisual(el, hover = false) {
const bounds = el.getBoundingClientRect();
const styleOM = el.computedStyleMap();
const calculatedStyle = getStyle(el, 'margin');
const boxdisplay = document.createElement('visbug-boxmodel');
if (calculatedStyle !== '0px') {
const sides = {
top: styleOM.get('margin-top').value,
right: styleOM.get('margin-right').value,
bottom: styleOM.get('margin-bottom').value,
left: styleOM.get('margin-left').value,
};
Object.entries(sides).forEach(([side, val]) => {
if (typeof val !== 'number')
val = parseInt(getStyle(el, 'padding'+'-'+side).slice(0, -2));
sides[side] = Math.round(val.toFixed(1) * 100) / 100;
});
boxdisplay.position = {
mode: 'margin',
color: hover ? 'purple' : 'pink',
bounds,
sides,
};
}
return boxdisplay
}
const $$1 = (query, $context = document) => query && query.nodeType !== undefined ? $$([query], $context) : $$(query, $context);
let imgs = []
, overlays = []
, dragItem;
const state = {
watching: true,
};
function watchImagesForUpload() {
imgs = $$1([
...document.images,
...$$1('picture'),
...findBackgroundImages(document),
]);
clearWatchers(imgs);
initWatchers(imgs);
}
function toggleWatching({watch}) {
state.watching = watch;
}
const initWatchers = imgs => {
imgs.on('dragover', onDragEnter);
imgs.on('dragleave', onDragLeave);
imgs.on('drop', onDrop);
$$1(document.body).on('dragover', onDragEnter);
$$1(document.body).on('dragleave', onDragLeave);
$$1(document.body).on('drop', onDrop);
$$1(document.body).on('dragstart', onDragStart);
$$1(document.body).on('dragend', onDragEnd);
};
const clearWatchers = imgs => {
imgs.off('dragenter', onDragEnter);
imgs.off('dragleave', onDragLeave);
imgs.off('drop', onDrop);
$$1(document.body).off('dragenter', onDragEnter);
$$1(document.body).off('dragleave', onDragLeave);
$$1(document.body).off('drop', onDrop);
$$1(document.body).on('dragstart', onDragStart);
$$1(document.body).on('dragend', onDragEnd);
imgs = [];
};
const previewFile = file => {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = () => resolve(reader.result);
})
};
// only fired for in-page drag events, track what the user picked up
const onDragStart = ({target}) =>
dragItem = target;
const onDragEnd = e =>
dragItem = undefined;
const onDragEnter = async e => {
e.preventDefault();
e.stopPropagation();
const pre_selected = $$1('img[data-selected=true], [data-selected=true] > img');
if (imgs.some(img => img === e.target)) {
if (!pre_selected.length) {
if (!isFileEvent(e))
previewDrop(e.target);
showOverlay(e.currentTarget, 0);
}
else {
if (pre_selected.some(node => node == e.target) && !isFileEvent(e))
pre_selected.forEach(node =>
previewDrop(node));
pre_selected.forEach((img, i) =>
showOverlay(img, i));
}
}
};
const onDragLeave = e => {
e.stopPropagation();
const pre_selected = $$1('img[data-selected=true], [data-selected=true] > img');
if (!pre_selected.some(node => node === e.target))
resetPreviewed(e.target);
else
pre_selected.forEach(node => resetPreviewed(node));
hideOverlays();
};
const onDrop = async e => {
e.stopPropagation();
e.preventDefault();
const srcs = await getTransferData(dragItem, e);
if (srcs.length) {
const selectedImages = $$1('img[data-selected=true], [data-selected=true] > img');
const targetImages = getTargetContentImages(selectedImages, e);
if (targetImages.length) {
updateContentImages(targetImages, srcs);
}
else {
const bgImages = getTargetBackgroundImages(imgs, e);
updateBackgroundImages(bgImages, srcs[0]);
}
}
hideOverlays();
};
const getTransferData = async (dragItem, e) => {
if (dragItem)
return [dragItem.currentSrc]
return e.dataTransfer.files.length
? await Promise.all([...e.dataTransfer.files]
.filter(file => file.type.includes('image'))
.map(previewFile))
: []
};
const getTargetContentImages = (selected, e) =>
selected.length ? selected
: e.target.nodeName === 'IMG' && !selected.length ? [e.target]
: [];
const updateContentImages = (images, srcs) => {
let i = 0;
images.forEach(img => {
clearDragHistory(img);
updateContentImage(img, srcs[i]);
i = ++i % srcs.length;
});
};
const updateContentImage = (img, src) => {
img.src = src;
if (img.srcset !== '')
img.srcset = src;
const sources = getPictureSourcesToUpdate(img);
if (sources.length)
sources.forEach(source =>
source.srcset = src);
};
const getTargetBackgroundImages = (images, e) =>
images.filter(img =>
img.contains(e.target));
const updateBackgroundImages = (images, src) =>
images.forEach(img => {
clearDragHistory(img);
if (window.getComputedStyle(img).backgroundImage != 'none')
img.style.backgroundImage = `url(${src})`;
});
const getPictureSourcesToUpdate = img =>
Array.from(img.parentElement.children)
.filter(sibling =>
sibling.nodeName === 'SOURCE')
.filter(source =>
!source.media || window.matchMedia(source.media).matches);
const showOverlay = (node, i) => {
if (!state.watching) return
const rect = node.getBoundingClientRect();
const overlay = overlays[i];
if (overlay) {
overlay.update = rect;
}
else {
overlays[i] = document.createElement('visbug-overlay');
overlays[i].position = rect;
document.body.appendChild(overlays[i]);
}
};
const hideOverlays = () => {
overlays.forEach(overlay =>
overlay.remove());
overlays = [];
};
const findBackgroundImages = el => {
const src_regex = /url\(\s*?['"]?\s*?(\S+?)\s*?["']?\s*?\)/i;
return $$1('*').reduce((collection, node) => {
const prop = getStyle(node, 'background-image');
const match = src_regex.exec(prop);
// if (match) collection.push(match[1])
if (match) collection.push(node);
return collection
}, [])
};
const previewDrop = async (node) => {
if (!['lastSrc','lastSrcset','lastSiblings','lastBackgroundImage'].some(prop => node[prop])){
const setSrc = dragItem.currentSrc;
if (window.getComputedStyle(node).backgroundImage !== 'none'){
node.lastBackgroundImage = window.getComputedStyle(node).backgroundImage;
node.style.backgroundImage = `url(${setSrc})`;
}else{
cacheImageState(node);
updateContentImage(node, setSrc);
}
}
};
const cacheImageState = image => {
image.lastSrc = image.src;
image.lastSrcset = image.srcset;
const sibSource = getPictureSourcesToUpdate(image);
if (sibSource.length) {
sibSource.forEach(sib => {
sib.lastSrcset = sib.srcset;
sib.lastSrc = sib.src;
});
}
};
const resetPreviewed = node => {
if (node.lastSrc)
node.src = node.lastSrc;
if (node.lastSrcset)
node.srcset = node.lastSrcset;
const sources = getPictureSourcesToUpdate(node);
if (sources.length)
sources.forEach(source => {
if (source.lastSrcset)
source.srcset = source.lastSrcset;
if (source.lastSrc)
source.src = source.lastSrc;
});
if (node.lastBackgroundImage)
node.style.backgroundImage = node.lastBackgroundImage;
clearDragHistory(node);
};
const clearDragHistory = node => {
['lastSrc','lastSrcset','lastBackgroundImage'].forEach(prop =>
node[prop] = null);
sources = getPictureSourcesToUpdate(node);
if (sources){
sources.forEach(source => {
source.lastSrcset = null;
source.lastSrc = null;
});
}
};
const isFileEvent = e =>
e.dataTransfer.types.some(type => type === 'Files');
const $$2 = (query, $context = document) => query && query.nodeType !== undefined ? $$([query], $context) : $$(query, $context);
const key_events$1 = 'up,down,left,right';
const state$1 = {
drag: {
src: null,
parent: null,
parent_ui: [],
siblings: new Map(),
swapping: new Map(),
},
hover: {
dropzones: [],
observers: [],
},
};
// todo: indicator for when node can descend
// todo: have it work with shadowDOM
function Moveable(visbug) {
hotkeys(key_events$1, (e, {key}) => {
if (e.cancelBubble) return
e.preventDefault();
e.stopPropagation();
visbug.selection().forEach(el => {
moveElement(el, key);
updateFeedback(el);
});
});
visbug.onSelectedUpdate(dragNDrop);
toggleWatching({watch: false});
return () => {
toggleWatching({watch: true});
visbug.removeSelectedCallback(dragNDrop);
clearListeners();
hotkeys.unbind(key_events$1);
}
}
function moveElement(el, direction) {
if (!el) return
switch(direction) {
case 'left':
if (canMoveLeft(el))
el.parentNode.insertBefore(el, el.previousElementSibling);
else
showEdge(el.parentNode);
break
case 'right':
if (canMoveRight(el) && el.nextElementSibling.nextSibling)
el.parentNode.insertBefore(el, el.nextElementSibling.nextSibling);
else if (canMoveRight(el))
el.parentNode.appendChild(el);
else
showEdge(el.parentNode);
break
case 'up':
if (canMoveUp(el))
popOut({el});
break
case 'down':
if (canMoveUnder(el))
popOut({el, under: true});
else if (canMoveDown(el))
el.nextElementSibling.prepend(el);
break
}
}
const canMoveLeft = el => el.previousElementSibling;
const canMoveRight = el => el.nextElementSibling;
const canMoveDown = el => el.nextElementSibling && el.nextElementSibling.children.length;
const canMoveUnder = el => !el.nextElementSibling && el.parentNode && el.parentNode.parentNode;
const canMoveUp = el => el.parentNode && el.parentNode.parentNode;
const popOut = ({el, under = false}) =>
el.parentNode.parentNode.insertBefore(el,
el.parentNode.parentNode.children[
under
? getNodeIndex(el) + 1
: getNodeIndex(el)]);
function dragNDrop(selection) {
if (!selection.length)
return
clearListeners();
const [src] = selection;
const {parentNode} = src;
const validMoveableChildren = [...parentNode.querySelectorAll(':scope > *' + notList)];
const tooManySelected = selection.length !== 1;
const hasNoSiblingsToDrag = validMoveableChildren.length <= 1;
const isAnSVG = src instanceof SVGElement;
if (tooManySelected || hasNoSiblingsToDrag || isAnSVG)
return
validMoveableChildren.forEach(sibling =>
state$1.drag.siblings.set(sibling, createGripUI(sibling)));
state$1.drag.parent = parentNode;
state$1.drag.parent_ui = createParentUI(parentNode);
moveWatch(state$1.drag.parent);
}
const moveWatch = node => {
const $node = $$2(node);
$node.on('mouseleave', dragDrop);
$node.on('dragstart', dragStart);
$node.on('drop', dragDrop);
state$1.drag.siblings.forEach((grip, sibling) => {
sibling.setAttribute('draggable', true);
$$2(sibling).on('dragover', dragOver);
$$2(sibling).on('mouseenter', siblingHoverIn);
$$2(sibling).on('mouseleave', siblingHoverOut);
});
};
const moveUnwatch = node => {
const $node = $$2(node);
$node.off('mouseleave', dragDrop);
$node.off('dragstart', dragStart);
$node.off('drop', dragDrop);
state$1.drag.siblings.forEach((grip, sibling) => {
sibling.removeAttribute('draggable');
$$2(sibling).off('dragover', dragOver);
$$2(sibling).off('mouseenter', siblingHoverIn);
$$2(sibling).off('mouseleave', siblingHoverOut);
});
};
const dragStart = ({target}) => {
if (!state$1.drag.siblings.has(target))
return
state$1.drag.src = target;
state$1.hover.dropzones.push(createDropzoneUI(target));
state$1.drag.siblings.get(target).style.opacity = 0.01;
target.setAttribute('visbug-drag-src', true);
ghostNode(target);
$$2('visbug-hover').forEach(el =>
!el.hasAttribute('visbug-drag-container') && el.remove());
};
const dragOver = e => {
if (
!state$1.drag.src ||
state$1.drag.swapping.get(e.target) ||
e.target.hasAttribute('visbug-drag-src') ||
!state$1.drag.siblings.has(e.currentTarget) ||
e.currentTarget !== e.target
) return
state$1.drag.swapping.set(e.target, true);
swapElements(state$1.drag.src, e.target);
setTimeout(() =>
state$1.drag.swapping.delete(e.target)
, 250);
};
const dragDrop = e => {
if (!state$1.drag.src) return
state$1.drag.src.removeAttribute('visbug-drag-src');
ghostBuster(state$1.drag.src);
if (state$1.drag.siblings.has(state$1.drag.src))
state$1.drag.siblings.get(state$1.drag.src).style.opacity = null;
state$1.hover.dropzones.forEach(zone =>
zone.remove());
state$1.drag.src = null;
};
const siblingHoverIn = ({target}) => {
if (!state$1.drag.siblings.has(target))
return
state$1.drag.siblings.get(target)
.toggleHovering({hovering:true});
};
const siblingHoverOut = ({target}) => {
if (!state$1.drag.siblings.has(target))
return
state$1.drag.siblings.get(target)
.toggleHovering({hovering:false});
};
const ghostNode = ({style}) => {
style.transition = 'opacity .25s ease-out';
style.opacity = 0.01;
};
const ghostBuster = ({style}) => {
style.transition = null;
style.opacity = null;
};
const createDropzoneUI = el => {
const zone = document.createElement('visbug-corners');
zone.position = {el};
document.body.appendChild(zone);
const observer = new MutationObserver(list =>
zone.position = {el});
observer.observe(el.parentNode, {
childList: true,
subtree: true,
});
state$1.hover.observers.push(observer);
return zone
};
const createGripUI = el => {
const grip = document.createElement('visbug-grip');
grip.position = {el};
document.body.appendChild(grip);
const observer = new MutationObserver(list =>
grip.position = {el});
observer.observe(el.parentNode, {
childList: true,
subtree: true,
});
state$1.hover.observers.push(observer);
return grip
};
const createParentUI = parent => {
const hover = document.createElement('visbug-hover');
const label = document.createElement('visbug-label');
hover.position = {el:parent};
hover.setAttribute('visbug-drag-container', true);
label.text = 'Drag Bounds';
label.position = {boundingRect: parent.getBoundingClientRect()};
label.style.setProperty('--label-bg', 'var(--theme-purple)');
document.body.appendChild(hover);
document.body.appendChild(label);
const observer = new MutationObserver(list => {
hover.position = {el:parent};
label.position = {boundingRect: parent.getBoundingClientRect()};
});
observer.observe(parent, {
childList: true,
subtree: true,
});
state$1.hover.observers.push(observer);
return [hover,label]
};
function clearListeners() {
moveUnwatch(state$1.drag.parent);
state$1.hover.observers.forEach(observer =>
observer.disconnect());
state$1.hover.dropzones.forEach(zone =>
zone.remove());
state$1.drag.siblings.forEach((grip, sibling) =>
grip.remove());
state$1.drag.parent_ui.forEach(ui =>
ui.remove());
state$1.hover.observers = [];
state$1.hover.dropzones = [];
state$1.drag.parent_ui = [];
state$1.drag.siblings.clear();
}
const updateFeedback = el => {
let options = '';
// get current elements offset/size
if (canMoveLeft(el)) options += '⇠';
if (canMoveRight(el)) options += '⇢';
if (canMoveDown(el)) options += '⇣';
if (canMoveUp(el)) options += '⇡';
// create/move arrows in absolute/fixed to overlay element
options && console.info('%c'+options, "font-size: 2rem;");
};
const commands = [
'empty page',
'blank page',
'clear canvas',
];
function BlankPagePlugin() {
document
.querySelectorAll('body > *:not(vis-bug):not(script)')
.forEach(node => node.remove());
}
const commands$1 = [
'barrel roll',
'do a barrel roll',
];
async function BarrelRollPlugin() {
document.body.style.transformOrigin = 'center 50vh';
await document.body.animate([
{ transform: 'rotateZ(0)' },
{ transform: 'rotateZ(1turn)' },
], { duration: 1500 }).finished;
document.body.style.transformOrigin = '';
}
const commands$2 = [
'pesticide',
];
async function PesticidePlugin() {
await loadStyles(['https://unpkg.com/pesticide@1.3.1/css/pesticide.min.css']);
}
const commands$3 = [
'trashy',
'construct',
];
async function ConstructPlugin() {
await loadStyles(['https://cdn.jsdelivr.net/gh/t7/construct.css@master/css/construct.boxes.css']);
}
const commands$4 = [
'debug trashy',
'debug construct',
];
async function ConstructDebugPlugin() {
await loadStyles(['https://cdn.jsdelivr.net/gh/t7/construct.css@master/css/construct.debug.css']);
}
const commands$5 = [
'wireframe',
'blueprint',
];
async function WireframePlugin() {
const styles = `
*:not(path):not(g) {
color: hsla(210, 100%, 100%, 0.9) !important;
background: hsla(210, 100%, 50%, 0.5) !important;
outline: solid 0.25rem hsla(210, 100%, 100%, 0.5) !important;
box-shadow: none !important;
}
`;
const style = document.createElement('style');
style.textContent = styles;
document.head.appendChild(style);
}
const commands$6 = [
'skeleton',
'outline',
];
async function SkeletonPlugin() {
const styles = `
*:not(path):not(g) {
color: hsl(0, 0%, 0%) !important;
text-shadow: none !important;
background: hsl(0, 0%, 100%) !important;
outline: 1px solid hsla(0, 0%, 0%, 0.5) !important;
border-color: transparent !important;
box-shadow: none !important;
}
`;
const style = document.createElement('style');
style.textContent = styles;
document.head.appendChild(style);
}
// https://gist.github.com/addyosmani/fd3999ea7fce242756b1
const commands$7 = [
'tag debugger',
'osmani',
];
async function TagDebuggerPlugin() {
for (i = 0; A = document.querySelectorAll('*')[i++];)
A.style.outline = `solid hsl(${(A+A).length*9},99%,50%) 1px`;
}
// http://heydonworks.com/revenge_css_bookmarklet/
const commands$8 = [
'revenge',
'revenge.css',
'heydon',
];
async function RevengePlugin() {
await loadStyles(['https://cdn.jsdelivr.net/gh/Heydon/REVENGE.CSS@master/revenge.css']);
}
const commands$9 = [
'tota11y',
];
async function Tota11yPlugin() {
await import(/* webpackIgnore: true */ 'https://cdnjs.cloudflare.com/ajax/libs/tota11y/0.1.6/tota11y.min.js');
}
const commands$a = [
'shuffle',
];
var ShufflePlugin = async (selectedElement) => {
const getSiblings = (elem) => {
// Setup siblings array and get the first sibling
let siblings = [];
let sibling = elem.firstChild;
// Loop through each sibling and push to the array
while (sibling) {
if (sibling.nodeType === 1 && sibling !== elem) {
siblings.push(sibling);
}
sibling = sibling.nextSibling;
}
return siblings;
};
const shuffle = (array) => {
let currentIndex = array.length, temporaryValue, randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
};
const appendSuffledSiblings = (element, suffledElementsArray) => {
element.innerHTML = '';
for (let i = 0; i < suffledElementsArray.length; i++) {
element.appendChild(suffledElementsArray[i]);
}
};
const { selected } = selectedElement;
selected.map(selectedElem => {
const siblings = getSiblings(selectedElem);
const shuffledSiblings = shuffle(siblings);
appendSuffledSiblings(selectedElem, shuffledSiblings);
});
};
/* source: https://github.com/hail2u/color-blindness-emulation */
const FILTERS = `
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1">
<defs>
<filter id="protanopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.567, 0.433, 0, 0, 0
0.558, 0.442, 0, 0, 0
0, 0.242, 0.758, 0, 0
0, 0, 0, 1, 0"/>
</filter>
<filter id="protanomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.817, 0.183, 0, 0, 0
0.333, 0.667, 0, 0, 0
0, 0.125, 0.875, 0, 0
0, 0, 0, 1, 0"/>
</filter>
<filter id="deuteranopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.625, 0.375, 0, 0, 0
0.7, 0.3, 0, 0, 0
0, 0.3, 0.7, 0, 0
0, 0, 0, 1, 0"/>
</filter>
<filter id="deuteranomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.8, 0.2, 0, 0, 0
0.258, 0.742, 0, 0, 0
0, 0.142, 0.858, 0, 0
0, 0, 0, 1, 0"/>
</filter>
<filter id="tritanopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.95, 0.05, 0, 0, 0
0, 0.433, 0.567, 0, 0
0, 0.475, 0.525, 0, 0
0, 0, 0, 1, 0"/>
</filter>
<filter id="tritanomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.967, 0.033, 0, 0, 0
0, 0.733, 0.267, 0, 0
0, 0.183, 0.817, 0, 0
0, 0, 0, 1, 0"/>
</filter>
<filter id="achromatopsia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.299, 0.587, 0.114, 0, 0
0.299, 0.587, 0.114, 0, 0
0.299, 0.587, 0.114, 0, 0
0, 0, 0, 1, 0"/>
</filter>
<filter id="achromatomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.618, 0.320, 0.062, 0, 0
0.163, 0.775, 0.062, 0, 0
0.163, 0.320, 0.516, 0, 0
0, 0, 0, 1, 0"/>
</filter>
</defs>
</svg>
`;
const types = [
'protanopia',
'protanomaly',
'deuteranopia',
'deuteranomaly',
'tritanopia',
'tritanomaly',
'achromatopsia',
'achromatomaly',
];
const commands$b = [
'colorblind',
'simulate-colorblind',
...types,
];
const state$2 = {
filters_injected: false,
};
const makeFilterSVGNode = () => {
const node = document.createElement('div');
node.innerHTML = FILTERS;
return node.firstElementChild
};
const makeSelectMenu = query => {
const node = document.createElement('select');
node.innerHTML = types
.map(type =>
`<option id="${type}">${type}</option>`)
.join('');
if (!query.includes('colorblind'))
node.querySelector(`#${query}`)
.selected = 'selected';
node.style = `
position: fixed;
top: 10px;
right: 10px;
z-index: 999999999;
`;
node.setAttribute('size', types.length);
node.addEventListener('input', e =>
document.body.style.filter = `url(#${e.target.value})`);
return node
};
async function ColorblindPlugin({selected, query}) {
query = query.slice(1, query.length);
// only inject filters once
if (!state$2.filters_injected) {
const filters = makeFilterSVGNode();
const select = makeSelectMenu(query);
document.body.appendChild(filters);
document.body.appendChild(select);
state$2.filters_injected = true;
}
query.includes('colorblind')
? document.body.style.filter = `url(#${types[0]})`
: document.body.style.filter = `url(#${query})`;
}
const commandsToHash = (plugin_commands, plugin_fn) =>
plugin_commands.reduce((commands, command) =>
Object.assign(commands, {[`/${command}`]:plugin_fn})
, {});
const PluginRegistry = new Map(Object.entries({
...commandsToHash(commands, BlankPagePlugin),
...commandsToHash(commands$1, BarrelRollPlugin),
...commandsToHash(commands$2, PesticidePlugin),
...commandsToHash(commands$3, ConstructPlugin),
...commandsToHash(commands$4, ConstructDebugPlugin),
...commandsToHash(commands$5, WireframePlugin),
...commandsToHash(commands$6, SkeletonPlugin),
...commandsToHash(commands$7, TagDebuggerPlugin),
...commandsToHash(commands$8, RevengePlugin),
...commandsToHash(commands$9, Tota11yPlugin),
...commandsToHash(commands$a, ShufflePlugin),
...commandsToHash(commands$b, ColorblindPlugin),
}));
const PluginHints = [
commands[0],
commands$1[0],
commands$2[0],
commands$3[0],
commands$4[0],
commands$5[0],
commands$6[0],
commands$7[0],
commands$8[0],
commands$9[0],
commands$a[0],
...commands$b,
].map(command => `/${command}`);
const $$3 = (query, $context = document) => query && query.nodeType !== undefined ? $$([query], $context) : $$(query, $context);
let SelectorEngine;
// create input
const search_base = document.createElement('div');
search_base.classList.add('search');
search_base.innerHTML = `
<input list="visbug-plugins" type="search" placeholder="ex: images, .btn, button, text, ..."/>
<datalist id="visbug-plugins">
${PluginHints.reduce((options, command) =>
options += `<option value="${command}">plugin</option>`
, '')}
<option value="h1, h2, h3, .get-multiple">example</option>
<option value="nav > a:first-child">example</option>
<option value="#get-by-id">example</option>
<option value=".get-by.class-names">example</option>
<option value="images">alias</option>
<option value="text">alias</option>
</datalist>
`;
const search = $$3(search_base);
const searchInput = $$3('input', search_base);
const showSearchBar = () => search.attr('style', 'display:block');
const hideSearchBar = () => search.attr('style', 'display:none');
const stopBubbling = e => e.key != 'Escape' && e.stopPropagation();
function Search(node) {
if (node) node[0].appendChild(search[0]);
const onQuery = e => {
e.preventDefault();
e.stopPropagation();
const query = e.target.value;
window.requestIdleCallback(_ =>
queryPage(query));
};
const focus = e =>
searchInput[0].focus();
searchInput.on('click', focus);
searchInput.on('input', onQuery);
searchInput.on('keydown', stopBubbling);
// searchInput.on('blur', hideSearchBar)
showSearchBar();
focus();
// hotkeys('escape,esc', (e, handler) => {
// hideSearchBar()
// hotkeys.unbind('escape,esc')
// })
return () => {
hideSearchBar();
searchInput.off('oninput', onQuery);
searchInput.off('keydown', stopBubbling);
searchInput.off('blur', hideSearchBar);
}
}
function queryPage(query, fn) {
// todo: should stash a cleanup method to be called when query doesnt match
if (PluginRegistry.has(query))
return PluginRegistry.get(query)({
selected: SelectorEngine.selection(),
query
})
if (query == 'links') query = 'a';
if (query == 'buttons') query = 'button';
if (query == 'images') query = 'img';
if (query == 'text') query = 'p,caption,a,h1,h2,h3,h4,h5,h6,small,date,time,li,dt,dd';
if (!query) return SelectorEngine.unselect_all()
if (query == '.' || query == '#' || query.trim().endsWith(',')) return
try {
let matches = querySelectorAllDeep(query + notList);
if (!matches.length) matches = querySelectorAllDeep(query);
if (matches.length) {
matches.forEach(el =>
fn
? fn(el)
: SelectorEngine.select(el));
}
}
catch (err) {}
}
const $$4 = (query, $context = document) => query && query.nodeType !== undefined ? $$([query], $context) : $$(query, $context);
const state$3 = {
distances: [],
target: null,
};
function createMeasurements({$anchor, $target}) {
if (state$3.target == $target && state$3.distances.length) return
else state$3.target = $target;
if (state$3.distances.length) clearMeasurements();
const anchorBounds = $anchor.getBoundingClientRect();
const targetBounds = $target.getBoundingClientRect();
const measurements = [];
// right
if (anchorBounds.right < targetBounds.left) {
measurements.push({
x: anchorBounds.right,
y: anchorBounds.top + (anchorBounds.height / 2) - 3,
d: targetBounds.left - anchorBounds.right,
q: 'right',
});
}
if (anchorBounds.right < targetBounds.right && anchorBounds.right > targetBounds.left) {
measurements.push({
x: anchorBounds.right,
y: anchorBounds.top + (anchorBounds.height / 2) - 3,
d: targetBounds.right - anchorBounds.right,
q: 'right',
});
}
// left
if (anchorBounds.left > targetBounds.right) {
measurements.push({
x: window.innerWidth - anchorBounds.left,
y: anchorBounds.top + (anchorBounds.height / 2) - 3,
d: anchorBounds.left - targetBounds.right,
q: 'left',
});
}
if (anchorBounds.left > targetBounds.left && anchorBounds.left < targetBounds.right) {
measurements.push({
x: window.innerWidth - anchorBounds.left,
y: anchorBounds.top + (anchorBounds.height / 2) - 3,
d: anchorBounds.left - targetBounds.left,
q: 'left',
});
}
// top
if (anchorBounds.top > targetBounds.bottom) {
measurements.push({
x: anchorBounds.left + (anchorBounds.width / 2) - 3,
y: targetBounds.bottom,
d: anchorBounds.top - targetBounds.bottom,
q: 'top',
v: true,
});
}
if (anchorBounds.top > targetBounds.top && anchorBounds.top < targetBounds.bottom) {
measurements.push({
x: anchorBounds.left + (anchorBounds.width / 2) - 3,
y: targetBounds.top,
d: anchorBounds.top - targetBounds.top,
q: 'top',
v: true,
});
}
// bottom
if (anchorBounds.bottom < targetBounds.top) {
measurements.push({
x: anchorBounds.left + (anchorBounds.width / 2) - 3,
y: anchorBounds.bottom,
d: targetBounds.top - anchorBounds.bottom,
q: 'bottom',
v: true,
});
}
if (anchorBounds.bottom < targetBounds.bottom && anchorBounds.bottom > targetBounds.top) {
measurements.push({
x: anchorBounds.left + (anchorBounds.width / 2) - 3,
y: anchorBounds.bottom,
d: targetBounds.bottom - anchorBounds.bottom,
q: 'bottom',
v: true,
});
}
// inside left/right
if (anchorBounds.right > targetBounds.right && anchorBounds.left < targetBounds.left) {
measurements.push({
x: window.innerWidth - anchorBounds.right,
y: anchorBounds.top + (anchorBounds.height / 2) - 3,
d: anchorBounds.right - targetBounds.right,
q: 'left',
});
measurements.push({
x: anchorBounds.left,
y: anchorBounds.top + (anchorBounds.height / 2) - 3,
d: targetBounds.left - anchorBounds.left,
q: 'right',
});
}
// inside top/right
if (anchorBounds.top < targetBounds.top && anchorBounds.bottom > targetBounds.bottom) {
measurements.push({
x: anchorBounds.left + (anchorBounds.width / 2) - 3,
y: anchorBounds.top,
d: targetBounds.top - anchorBounds.top,
q: 'bottom',
v: true,
});
measurements.push({
x: anchorBounds.left + (anchorBounds.width / 2) - 3,
y: targetBounds.bottom,
d: anchorBounds.bottom - targetBounds.bottom,
q: 'top',
v: true,
});
}
// create custom elements for all created measurements
measurements
.map(measurement => Object.assign(measurement, {
d: Math.round(measurement.d.toFixed(1) * 100) / 100
}))
.forEach(measurement => {
const $measurement = document.createElement('visbug-distance');
$measurement.position = {
line_model: measurement,
node_label_id: state$3.distances.length,
};
document.body.appendChild($measurement);
state$3.distances[state$3.distances.length] = $measurement;
});
}
function clearMeasurements() {
if (!state$3.distances) return
$$4('[data-measuring]').forEach(el =>
el.removeAttribute('data-measuring'));
state$3.distances.forEach(node => node.remove());
state$3.distances = [];
}
function takeMeasurementOwnership() {
const distances = [...state$3.distances];
state$3.distances = [];
return distances
}
const key_events$2 = 'up,down,left,right'
.split(',')
.reduce((events, event) =>
`${events},${event},alt+${event},shift+${event},shift+alt+${event}`
, '')
.substring(1);
const command_events$1 = `${metaKey}+up,${metaKey}+shift+up,${metaKey}+down,${metaKey}+shift+down`;
function Padding(visbug) {
hotkeys(key_events$2, (e, handler) => {
if (e.cancelBubble) return
e.preventDefault();
padElement(visbug.selection(), handler.key);
});
hotkeys(command_events$1, (e, handler) => {
e.preventDefault();
padAllElementSides(visbug.selection(), handler.key);
});
visbug.onSelectedUpdate(paintBackgrounds$1);
return () => {
hotkeys.unbind(key_events$2);
hotkeys.unbind(command_events$1);
hotkeys.unbind('up,down,left,right'); // bug in lib?
visbug.removeSelectedCallback(paintBackgrounds$1);
removeBackgrounds$1(visbug.selection());
}
}
function padElement(els, direction) {
els
.map(el => showHideSelected(el))
.map(el => ({
el,
style: 'padding' + getSide(direction),
current: parseInt(getStyle(el, 'padding' + getSide(direction)), 10),
amount: direction.split('+').includes('shift') ? 10 : 1,
negative: direction.split('+').includes('alt'),
}))
.map(payload =>
Object.assign(payload, {
padding: payload.negative
? payload.current - payload.amount
: payload.current + payload.amount
}))
.forEach(({el, style, padding}) =>
el.style[style] = `${padding < 0 ? 0 : padding}px`);
}
function padAllElementSides(els, keycommand) {
const combo = keycommand.split('+');
let spoof = '';
if (combo.includes('shift')) spoof = 'shift+' + spoof;
if (combo.includes('down')) spoof = 'alt+' + spoof;
'up,down,left,right'.split(',')
.forEach(side => padElement(els, spoof + side));
}
function paintBackgrounds$1(els) {
els.forEach(el => {
const label_id = el.getAttribute('data-label-id');
document
.querySelector(`visbug-handles[data-label-id="${label_id}"]`)
.backdrop = {
element: createPaddingVisual(el),
update: createPaddingVisual,
};
});
}
function removeBackgrounds$1(els) {
els.forEach(el => {
const label_id = el.getAttribute('data-label-id');
const boxmodel = document.querySelector(`visbug-handles[data-label-id="${label_id}"]`)
.$shadow.querySelector('visbug-boxmodel');
if (boxmodel) boxmodel.remove();
});
}
function createPaddingVisual(el, hover = false) {
const bounds = el.getBoundingClientRect();
const styleOM = el.computedStyleMap();
const calculatedStyle = getStyle(el, 'padding');
const boxdisplay = document.createElement('visbug-boxmodel');
if (calculatedStyle !== '0px') {
const sides = {
top: styleOM.get('padding-top').value,
right: styleOM.get('padding-right').value,
bottom: styleOM.get('padding-bottom').value,
left: styleOM.get('padding-left').value,
};
Object.entries(sides).forEach(([side, val]) => {
if (typeof val !== 'number')
val = parseInt(getStyle(el, 'padding'+'-'+side).slice(0, -2));
sides[side] = Math.round(val.toFixed(1) * 100) / 100;
});
boxdisplay.position = {
mode: 'padding',
color: hover ? 'purple' : 'pink',
bounds,
sides,
};
}
return boxdisplay
}
const $$5 = (query, $context = document) => query && query.nodeType !== undefined ? $$([query], $context) : $$(query, $context);
const state$4 = {
active: {
tip: null,
target: null,
},
tips: new Map(),
};
const services = {};
function MetaTip({select}) {
services.selectors = {select};
$$5('body').on('mousemove', mouseMove);
$$5('body').on('click', togglePinned);
hotkeys('esc', _ => removeAll());
restorePinnedTips();
return () => {
$$5('body').off('mousemove', mouseMove);
$$5('body').off('click', togglePinned);
hotkeys.unbind('esc');
hideAll();
}
}
const mouseMove = e => {
const target = deepElementFromPoint(e.clientX, e.clientY);
if (isOffBounds(target) || target.nodeName === 'VISBUG-METATIP' || target.hasAttribute('data-metatip')) { // aka: mouse out
if (state$4.active.tip) {
wipe({
tip: state$4.active.tip,
e: {target: state$4.active.target},
});
clearActive();
}
return
}
toggleTargetCursor(e.altKey, target);
showTip(target, e);
};
function showTip(target, e) {
if (!state$4.active.tip) { // create
const tip = render(target);
document.body.appendChild(tip);
positionTip(tip, e);
observe({tip, target});
state$4.active.tip =