UNPKG

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
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 =