Add button to move widget bar to left side - tradingview.com

Allows user to move widgets like the chatbox to the left side of the app.

Fra 11.04.2023. Se den seneste versjonen.

// ==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.0
// @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() {
  // wait for stuff to load
  await untilDOM(()=>$`.widgetbar-wrap`);

  // store refs to relevant elements
  const layoutAreaCenter = $`.layout__area--center`;
  const layoutAreaTradingPanel = $`.layout__area--tradingpanel`;
  const layoutAreaLeft = $`.layout__area--left`;
  const layoutAreaRight = $`.layout__area--right`;
  const layoutAreaBottom = $`.layout__area--bottom`;
  const chartContainer = $`.chart-container`;
  const widgetbarPages = $`.widgetbar-pages`;
  const widgetbarWrap = $`.widgetbar-wrap`;
  const widgetbarTabs = $`.widgetbar-tabs`;
  const drawingToolbar = $`#drawing-toolbar`;
  const widgetToolbarFiller = $`.widgetbar-tabs div[class^='filler-']`;

  // This script is inherently tightly coupled to a number of small implementation details on tradingview.com
  // Whenever the TradingView app is updated, this script may stop working properly.
  // Here, we attempt to detect fundamental changes and bail out rather than breaking the app.
  // This is a "best effort" check, and this script might still break future versions of the app.
  // If that happens, disabling the script and reloading the app should fix things.
  if (!layoutAreaCenter || !layoutAreaTradingPanel || !layoutAreaLeft || !layoutAreaRight || !layoutAreaBottom || !chartContainer || !widgetbarPages || !widgetbarWrap || !widgetbarTabs || !drawingToolbar || !widgetToolbarFiller) {
    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);
  }

  // persistent state
  let widgetsMovedLeft = localStorage.widgetsMovedLeft === 'true';

  // 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.title = "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 rightWidth = widgetsMovedLeft ? 0 : RIGHT_WIDGETBAR_WIDTH;
    const leftWidth = LEFT_TOOLBAR_WIDTH + (widgetsMovedLeft ? RIGHT_WIDGETBAR_WIDTH + parseInt(widgetbarPages.style.width) : 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();
    document.querySelector("[data-dialog-name='gopro']")?.querySelector('button')?.click();

    function set(elt, prop, val) {
      // a gentle style setter that doesn't trigger unnecessary DOM mutations.
      if (elt.style[prop] !== val +'px') {
        elt.style[prop] = val + 'px';
      }
    }
  }
}