// ==UserScript==
// @name OnShape helper
// @namespace V@no
// @version 25.8.1
// @description Various tweaks for OnShape, such as remap F2 for rename (SHIFT + N)
// @author V@no
// @license MIT
// @match https://cad.onshape.com/documents
// @match https://cad.onshape.com/documents?*
// @match https://cad.onshape.com/documents/*
// @icon https://onshape.com/favicon.png
// @grant none
// ==/UserScript==
(CSS =>
{
// eslint-disable-next-line no-unused-expressions
"use strict";
/*
^ = CTRL
! = ALT
+ = SHIFT
*/
const VERSION = "25.8.1";
const CHANGES = `+ Add configuration btn remembers last used item
! help popup in FS editor with dark mode
! dimension input style affected configurations input`;
const map = {
"F2": {key: "N", code: "KeyN", keyCode: 78, shiftKey: true}
};
const elStyle = document.createElement("style");
elStyle.id = "onShapeHelper";
elStyle.textContent = CSS;
document.head.append(elStyle);
let mouseEvent = {};
document.addEventListener("mousemove", evt =>
{
mouseEvent = evt;
}, false);
document.body.addEventListener("keydown", evt =>
{
let modifier = "";
modifier = evt.altKey ? "!" : "";
modifier = evt.shiftKey ? "+" : "";
modifier = evt.ctrlKey || evt.metaKey ? "^" : "";
const key = modifier + evt.code;
if (!evt.isTrusted || !(key in map) || evt.altKey || evt.shiftKey || evt.ctrlKey || evt.metaKey)
return;// console.log(evt, mouseEvent);
if (mouseEvent.target)
{
evt.target.dispatchEvent(new KeyboardEvent(evt.type, Object.assign({}, evt, {key: " ", code: "space", keyCode: 32}, {bubbles: true})));
mouseEvent.target.dispatchEvent(new PointerEvent("click", mouseEvent));
}
evt.target.dispatchEvent(new KeyboardEvent(evt.type, Object.assign({}, evt, map[key], {bubbles: true})));
}, true);
const dataValue = (el, value) =>
{
el.dataset.value = value;
};
const changeByRegex = /Change by (.+) at (.+)$/;
const getChildIndex = child =>
{
let i = 0;
while ((child = child.previousElementSibling) !== null)
i++;
return i;
};
// eslint-disable-next-line no-unused-vars
const observer = new MutationObserver((mutationList, _observer) =>
{
const types = {};
for (const mutation of mutationList)
{
for(const node of mutation.addedNodes)
{
if (node.nodeType !== 1)
continue;
/* ----------------------------- input boxes ----------------------------- */
if (node.matches("input:not(.OSH)"))
{
node.classList.add("OSH");
node.parentElement.classList.add("OSH", "input_box");
const eventHandler = () => dataValue(node.parentElement, node.value);
node.addEventListener("input", eventHandler);
// inserted variables don't trigger input event, so we need to check for changes
let previousValue = null;
const loop = timestamp =>
{
if (previousValue !== node.value)
{
previousValue = node.value;
eventHandler();
}
if (node.isConnected)
return requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
}
/* ------------------------- version and history ------------------------- */
if (node.matches(".os-flex-table-row:not(.change, .OSH, .separator)"))
{
node.classList.add("OSH");
const elDescription = document.createElement("div");
elDescription.classList.add("os-flex-col", "os-item-description", "inside-document", "OSH_description");
elDescription.textContent = node.dataset.bsExpandedContent || "";
node.append(elDescription);
if (node.dataset.bsExpandedContent)
types.historyDescription = true;
}
/* --------------------- version and history changes --------------------- */
if (node.matches(".os-flex-table-row.change:not(.OSH)"))
{
node.classList.add("OSH");
const changeBy = node.dataset.bsOriginalTitle.match(changeByRegex);
let parentChangeBy = "";
for(let i = getChildIndex(node); i >= 0; --i)
{
const elSibling = node.parentElement.children[i].querySelector(".os-item-modified-by");
if (elSibling)
{
parentChangeBy = elSibling.textContent.trim();
break;
}
}
const elModified = node.querySelector(".os-flex-col.os-item-modified-date.inside-document");
elModified.innerHTML = (parentChangeBy === changeBy[1] ? `` : `${changeBy[1]}\n`) + changeBy[2];
node.classList.toggle("OSH_single_line", parentChangeBy === changeBy[1]);
}
/* ---------------------------- version graph ---------------------------- */
if (node.matches("line") && !types.versionGraph && node.closest(".os-version-graph"))
{
types.versionGraph = node.parentElement;
}
/* ---------------------------- configuration ---------------------------- */
if (!node.classList.contains(".single-table-container.os-virtual-scroll-section:not(.OSH_conf)"))
{
const nlNodes = node.querySelectorAll(`a:not(.OSH_conf)[ng-click="configurationTable.moveParameterUp()"], a:not(.OSH_conf)[ng-click="configurationTable.moveParameterDown()"`);
if (nlNodes.length > 0)
{
types.configuration = nlNodes.length;
node.classList.add("OSH_conf");
}
for(let i = 0; i < nlNodes.length; i++)
{
const elA = nlNodes[i];
elA.classList.add("OSH_conf");
const elParent = elA.closest("div.os-table-header-responsive-last-row>div.d-flex");
elParent.classList.add("OSH_conf_row");
if (elParent.upDown === undefined)
elParent.upDown = {};
const type = elA.matches(`[ng-click="configurationTable.moveParameterUp()"]`);
if (elParent.upDown[type])
elParent.upDown[type].replaceWith(elA);
else
elParent.prepend(elA);
elParent.upDown[type] = elA;
elA.classList.add(type ? "UP" : "DOWN");
elA.title = elA.textContent;
elA.textContent = type ? "▲" : "▼";
elA.addEventListener("click", () => moved(elA.parentElement.parentElement.parentElement));
}
}
/* ----------------------- add configuration button ---------------------- */
if (node.matches("#right-content-pane > div > div > div.content-footer.os-row > div.button-container > div:not(.OSH)"))
{
node.classList.add("OSH");
const elButton_orig = node.querySelector("button"); //add configuration button
const elButton = elButton_orig.cloneNode(true);
elButton_orig.parentElement.replaceChild(elButton, elButton_orig);
const nlSelectItems = node.querySelectorAll("a.dropdown-item");
const label = elButton_orig.lastChild.textContent.match(/^(.+\s)\S+/)[1];
const elSelectItems = [];
for(let i = 0; i < nlSelectItems.length; i++)
{
const el = nlSelectItems[i].cloneNode(true);
elSelectItems.push(el);
nlSelectItems[i].parentElement.replaceChild(el, nlSelectItems[i]);
const text = el.textContent.match(/\s(\S+?)$/)[1];
el.dataset.text = String(text).charAt(0).toUpperCase() + String(text).slice(1);
}
const setLabel = index =>
{
if (!elSelectItems[index].dataset.text)
return;
elButton.dataset.value = index;
elButton.replaceChild(elSelectItems[index].firstElementChild.cloneNode(true), elButton.firstElementChild);
elButton.lastChild.textContent = label + elSelectItems[index].dataset.text;
};
setLabel(~~localStorage.getItem("OSH_confAddButton"));
elButton.addEventListener("click", evt =>
{
evt.preventDefault();
evt.stopPropagation();
nlSelectItems[evt.target.dataset.value].click();
});
node.addEventListener("click", evt =>
{
if (!evt.isTrusted)
return; // ignore synthetic events
/* --------------------------- dropdown item --------------------------- */
if (evt.target.matches("a"))
{
const index = elSelectItems.indexOf(evt.target);
localStorage.setItem("OSH_confAddButton", index);
setLabel(index);
elButton.click();
}
});
}
/* ---------------------------- message bubble --------------------------- */
if (node.matches(`div[ng-include="'/project/web/woolsthorpe/app/partials/toolbarMessageBubble.html'"]`) && node.parentElement !== document.body)
{
document.body.append(node);
}
if (node.matches(".d-flex.flex-column.ng-star-inserted:not(.OSH)"))
{
node.classList.add("OSH");
types.documentList = node;
}
} // for added nodes
} // for mutation list
if (types.configuration)
{
if (!document.querySelector("div.single-table-container.os-virtual-scroll-section:first-child .UP"))
{
const elRow = document.querySelector("div.single-table-container.os-virtual-scroll-section:first-child div.OSH_conf_row");
const elA = elRow.firstChild.cloneNode(true);
elA.setAttribute("ng-click", "configurationTable.moveParameterUp()");
elA.classList.remove("DOWN");
elA.title = "Move UP";
elA.classList.add("OSH_conf", "UP");
elA.textContent = "▲";
elRow.prepend(elA);
const elParent = elA.closest("div.os-table-header-responsive-last-row>div.d-flex");
elParent.upDown[true] = elA;
}
if (!document.querySelector("div.single-table-container.os-virtual-scroll-section:last-child .DOWN"))
{
const elRow = document.querySelector("div.single-table-container.os-virtual-scroll-section:last-child div.OSH_conf_row");
if (elRow)
{
const elA = elRow.firstChild.cloneNode(true);
elA.setAttribute("ng-click", "configurationTable.moveParameterDown()");
elA.classList.remove("UP");
elA.title = "Move DOWN";
elA.classList.add("OSH_conf", "DOWN");
elA.textContent = "▼";
elRow.append(elA);
const elParent = elA.closest("div.os-table-header-responsive-last-row>div.d-flex");
elParent.upDown[false] = elA;
}
}
}
if (types.versionGraph)
{
const nlLines = types.versionGraph.querySelectorAll("line");
let max = 0;
let min = 1e10;
for(let i = 0; i < nlLines.length; i++)
{
const elLine = nlLines[i];
max = Math.max(max, Number.parseFloat(elLine.getAttribute("x1")));
min = Math.min(min, Number.parseFloat(elLine.getAttribute("x1")));
}
const elGraph = types.versionGraph.closest(".document-panel-main-content");
elGraph.style.setProperty("--os-version-graph-width", `${max - min + 28}px`);
elGraph.style.setProperty("--os-version-graph-left", `-${min - 14}px`);
}
if (types.historyDescription)
{
document.querySelector(".versions-history-table-container").classList.add("OSH_description");
}
/* --------------- prevent document folder open in a new tab --------------- */
const elFolder = document.querySelector("a.folder[target='_blank']");
if (elFolder)
elFolder.removeAttribute("target");
// if (types.documentList)
// {
// const node = types.documentList;
// const elSplitter = node.querySelector("osx-splitter");
// const saveStyle = () => localStorage.setItem("OSH_splitterStyle", elSplitter.getAttribute("style"));
// let timer = null;
// const mutationObserver2 = new MutationObserver(mutationList2 =>
// {
// if (!elSplitter.classList.contains("OSH"))
// {
// elSplitter.classList.add("OSH");
// const savedStyle = localStorage.getItem("OSH_splitterStyle");
// if (savedStyle)
// {
// elSplitter.setAttribute("style", savedStyle);
// elSplitter.querySelector(".cdk-drag.gutter-handle").dispatchEvent(new Event("dragstart", {bubbles: true}));
// return;
// }
// }
// clearTimeout(timer);
// timer = setTimeout(saveStyle, 500);
// console.log("OSH: MutationObserver2", mutationList2);
// });
// mutationObserver2.observe(elSplitter, { attributeFilter: ["style"], attributeOldValue: true });
// }
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
const moved = el =>
{
moved.clear();
el.classList.add("moved");
moved.el = el;
moved.timer = setTimeout(moved.clear, 2000);
};
moved.clear = () =>
{
clearTimeout(moved.timer);
if (moved.el)
{
moved.el.classList.remove("moved");
moved.el = null;
}
};
console.log(`OnShape helper v${VERSION} loaded`, "https://greasyfork.org/en/scripts/522636");
})(`
.OSH_hidden {
display: none !important;
}
/* ------------------------ dimension edit input box ------------------------ */
.dimension-edit-container .ns-feature-parameter .bti-numeric-text,
.dimension-edit-container os-quantity-parameter input,
.dimension-edit
{
max-width: unset;
z-index: 9999;
text-align: center;
}
.dimension-edit-container .input_box.OSH::before,
.dimension-edit-container .input_box.OSH::after {
box-sizing: border-box;
}
.dimension-edit-container .input_box.OSH {
display: inline-grid;
vertical-align: top;
align-items: center;
position: relative;
}
.dimension-edit-container .input_box.OSH::after,
.dimension-edit-container .input_box.OSH input
{
width: auto;
min-width: 1em;
grid-area: 1/2;
font: inherit;
padding: 0 0.25em 0 0;
margin: 0;
resize: none;
background: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: none;
}
/* --- this will force to extend the width of the input to fit the content -- */
.dimension-edit-container .input_box.OSH::after {
content: attr(data-value) " ";
visibility: hidden;
white-space: pre-wrap;
}
/* ----------------------- configuration input fields ----------------------- */
.os-select-bootstrap .os-select-match-text span,
.os-param-wrapper > .os-param-text {
text-align: right;
}
.open > .dropdown-menu {
right: 0;
}
/* --------------------------- configuration panel -------------------------- */
div.OSH_conf_row > .OSH_conf {
font-size: x-large;
padding: 0 0.2em;
line-height: 1em;
}
div.OSH_conf_row > .OSH_conf:hover {
background-color: var(--os-table-cell-fill--hover);
}
div.OSH_conf_row > .OSH_conf.UP {
order: 1;
}
div.OSH_conf_row > .OSH_conf.DOWN {
order: 2;
}
div.OSH_conf_row > :not(.OSH_conf) {
order: 3;
}
div.moved {
background-color: var(--os-alert-background-success);
}
div.single-table-container.os-virtual-scroll-section:first-child .UP,
div.single-table-container.os-virtual-scroll-section:last-child .DOWN {
opacity: 0.5;
pointer-events: none;
}
/* --------------------- Message bubble move to the top --------------------- */
os-message-bubble .os-message-bubble-container.document-message-bubble {
top: 5px;
}
.os-speech-bubble-container
{
top: 0;
}
/* ----------------------------- version history ---------------------------- */
.versions-history-table-container .os-flex-col.os-item-workspace-or-version-graph.inside-document {
flex: initial !important;
}
/* -------------------------- version history graph ------------------------- */
.versions-history-table-container .os-flex-col.os-item-workspace-or-version-graph.inside-document {
min-width: var(--os-version-graph-width, 140px);
}
.os-version-graph > svg {
margin-left: var(--os-version-graph-left, 0);
}
/* ------------ version history search result header modified by ------------ */
.versions-history-table-container .os-flex-col.history-search-results-header:last-child,
.versions-history-table-container .os-flex-table-row.history-search-result .os-flex-col:not(.os-item-workspace-or-version-actions).os-item-modified-date,
/* -------------------------- version history user -------------------------- */
.versions-history-table-container .os-flex-col.os-item-modified-by-and-date.inside-document,
/* ---------------------- version history modified date --------------------- */
.os-flex-col.os-item-modified-date.inside-document,
/* ----------------------- version history description ---------------------- */
.versions-history-table-container.OSH_description .os-flex-col.history-search-results-header,
.versions-history-table-container.OSH_description .os-flex-col.os-item-description{
flex: none;
}
/* ----------------------- version history description ---------------------- */
.versions-history-table-container .os-flex-col.os-item-modified-date.inside-document,
.versions-history-table-container .os-flex-col.os-item-workspace-or-version-name.inside-document,
.versions-history-table-container .os-flex-col.os-item-workspace-or-version-description.inside-document {
max-width: unset;
}
/* ----------------------- version history change time ---------------------- */
.os-flex-col.os-item-modified-date.inside-document {
font-size: 0.8em;
white-space: pre;
line-height: 1em;
text-align: end;
max-width: 10em !important;
text-overflow: ellipsis;
overflow: hidden;
padding-top: 0.3em;
}
.OSH_single_line > .os-flex-col.os-item-modified-date.inside-document {
padding-top: 0.9em;
}
/* ------------------- version history description column ------------------- */
.versions-history-table-container:not(.OSH_description) .OSH_description {
display: none !important;
}
.versions-history-table-container.OSH_description .os-flex-col.os-item-modified-by-and-date.inside-document + .ng-hide,
.versions-history-table-container.OSH_description .os-flex-col.os-item-description {
display: block !important;
order: 3;
}
.versions-history-table-container.OSH_description .os-item-modified-by-and-date {
order: 4;
}
.versions-history-table-container.OSH_description .os-item-workspace-or-version-name {
order: 2;
}
.versions-history-table-container.OSH_description .os-item-workspace-or-version-graph:not(.change-item) {
order: 1;
}
/* just a visual indicator that script is running - a green dot on the logo */
osx-navbar-logo-component > a {
position: relative;
}
osx-navbar-logo-component > a::before {
content: "";
position: absolute;
background-color: green;
left: 12px;
top: 18px;
font-size: 2em;
width: 5px;
height: 5px;
border-radius: 100%;
}
/* ---------------------------- dark mode tweaks ---------------------------- */
[data-os-theme=dark] .fs-doc-body a,
[data-os-theme=dark] .fs-doc-body a code
{
color: var(--bs-link-color);
}
[data-os-theme=dark] .fs-doc-body .fs-parameter-name
{
color: var(--os-text-tertiary--static);
}
`);