// ==UserScript==
// @name Add button to move widget bar to left side - tradingview.com
// @namespace Itsnotlupus Industries
// @match https://www.tradingview.com/*
// @grant none
// @version 1.1
// @author itsnotlupus
// @license MIT
// @description Allows user to move widgets like the chatbox to the left side of the app.
// ==/UserScript==
/* jshint esversion:11 */
// GENERIC UTILITIES
/** calls a function whenever the DOM changes */
const observeDOM = (fn, e = document.documentElement, config = { attributes: 1, childList: 1, subtree: 1 }) => {
const observer = new MutationObserver(fn);
observer.observe(e, config);
return () => observer.disconnect();
};
/** check a condition on every DOM change until truthy. async returns truthy value */
const untilDOM = f => new Promise((r,_,d = observeDOM(() => (_=f()) && d() | r(_) )) => 0);
const crel = (name, attrs, ...children) => ((e = Object.assign(document.createElement(name), attrs)) => (e.append(...children), e))();
const svg = (name, attrs, ...children) => {
const e = document.createElementNS('http://www.w3.org/2000/svg', name);
Object.entries(attrs).forEach(([key,val]) => e.setAttribute(key, val));
e.append(...children);
return e;
}
const $ = s => document.querySelector(s);
// SCRIPT LOGIC BEGINS HERE.
const LEFT_TOOLBAR_WIDTH = 52;
const RIGHT_WIDGETBAR_WIDTH = 46;
run();
async function run() {
// If we can't initialize within 60 seconds, let users know the script isn't able to work as intended.
const failTimer = setTimeout(() => {
const msg = 'UserScript "Add button to move widget bar to left side" has become incompatible with TradingView and can no longer work.\nPlease disable or update this script.';
console.log(`%c${msg}`, 'font-weight:600;font-size:2em;color:red');
throw new Error(msg);
}, 60 * 1000);
// wait for app to load and store refs to relevant elements
const [
layoutAreaCenter,
layoutAreaTradingPanel,
layoutAreaLeft,
layoutAreaRight,
layoutAreaBottom,
chartContainer,
widgetbarPages,
widgetbarWrap,
widgetbarTabs,
drawingToolbar,
widgetToolbarFiller
] = await untilDOM(()=>{
const elts = [
$`.layout__area--center`,
$`.layout__area--tradingpanel`,
$`.layout__area--left`,
$`.layout__area--right`,
$`.layout__area--bottom`,
$`.chart-container`,
$`.widgetbar-pages`,
$`.widgetbar-wrap`,
$`.widgetbar-tabs`,
$`#drawing-toolbar`,
$`.widgetbar-tabs div[class^='filler-']`
];
return elts.some(e=>!e) ? null : elts;
});
// If we're here, we're probably fine.
clearTimeout(failTimer);
// persistent state
let widgetsMovedLeft = localStorage.widgetsMovedLeft === 'true';
let lastAd = 0;
// augment UI
// clone a button and customize it to make our "move widgets" button.
const button = widgetToolbarFiller.nextElementSibling.cloneNode(true);
button.dataset.name = "move_widgets";
button.dataset.tooltip = button.ariaLabel = "Move Widgets to Other Side of Chart"; // l10n schm10n.
button.querySelector('svg').remove();
button.querySelector('span').append(svg('svg', { width: 44, height: 44, viewBox: "0 0 21 21" }, // random SVG icon goes here. a UX person I am not. https://www.svgrepo.com/svg/343314/toggles
svg('g', { fill:"none", "fill-rule":"evenodd", stroke:"currentColor", "stroke-width":"0.4", "stroke-linecap":"round", "stroke-linejoin":"round", transform:"translate(3 4)" },
svg('circle', { cx:"3.5", cy:"3.5", r:"3"}),
svg('path', {d:"M6 1.5h6.5c.8 0 2 .3 2 2s-1.2 2-2 2H6m5.5 8a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"}),
svg('path', {d:"M9 8.5H2.5c-.8 0-2 .3-2 2s1.2 2 2 2H9"})
)
));
button.addEventListener('click', () => toggleWidgets());
widgetToolbarFiller.after(button);
// apply state to app.
toggleWidgets(widgetsMovedLeft);
// start observing DOM to adjust layout continuously.
observeDOM(adjustLayout);
function toggleWidgets(left = !widgetsMovedLeft) {
const parent = left ? layoutAreaLeft : layoutAreaRight;
parent.prepend(widgetbarWrap);
widgetbarTabs.style.right = left ? '' : '0';
widgetbarTabs.style.left = left ? '0' : '';
widgetbarPages.style.right = left ? '51px' : '';
widgetsMovedLeft = left;
localStorage.widgetsMovedLeft = left;
adjustLayout();
}
function adjustLayout() {
const widgetWidth = RIGHT_WIDGETBAR_WIDTH + widgetbarPages.clientWidth;
const rightWidth = widgetsMovedLeft ? 0 : widgetWidth;
const leftWidth = LEFT_TOOLBAR_WIDTH + (widgetsMovedLeft ? widgetWidth : 0);
const centerWidth = innerWidth - leftWidth - rightWidth - 8;
const centerLeft = leftWidth + 4;
set(drawingToolbar, 'width', LEFT_TOOLBAR_WIDTH);
set(drawingToolbar, 'marginLeft', leftWidth - LEFT_TOOLBAR_WIDTH + 2);
set(layoutAreaRight, 'width', rightWidth);
set(layoutAreaLeft, 'width', leftWidth);
set(layoutAreaCenter, 'width', centerWidth);
set(layoutAreaCenter, 'left', centerLeft);
set(chartContainer, 'width', centerWidth);
set(layoutAreaBottom, 'width', centerWidth);
set(layoutAreaBottom, 'left', centerLeft);
set(layoutAreaTradingPanel, 'right', rightWidth);
// remove some nags since we're already here.
document.querySelector("div[data-role='toast-container']").querySelector('button')?.click();
const gopro = document.querySelector("[data-dialog-name='gopro']");
if (gopro) {
// does it have a close button?
const closeButton = gopro.querySelector('button[aria-label="close"],button[class^="close"]');
if (closeButton) {
closeButton.click();
} else {
// unclosable nag? Cool.
console.error('FOUND an UNCLOSABLE GOPRO nag. My favorite!');
gopro.parentElement.remove();
}
}
function set(elt, prop, val) {
elt.style[prop] = val + 'px';
}
}
}