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.

// ==UserScript==
// @name           Add button to move widget bar to left side - tradingview.com
// @namespace      Itsnotlupus Industries
// @match          https://*.tradingview.com/*
// @grant          none
// @version        1.2
// @author         itsnotlupus
// @license        MIT
// @description    Allows user to move widgets like the chatbox to the left side of the app.
// @require        https://greatest.deepsurf.us/scripts/468394-itsnotlupus-tiny-utilities/code/utils.js
// @require        https://greatest.deepsurf.us/scripts/471000-itsnotlupus-i18n-support/code/i18n.js
// @grant          GM_xmlhttpRequest
// @grant          GM_setValue
// @grant          GM_getValue
// ==/UserScript==

/* jshint esversion:11 */

const LEFT_TOOLBAR_WIDTH = 52;
const RIGHT_WIDGETBAR_WIDTH = 46;

(async function main() {

  const strings = {
    scriptFailure: '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.',
    buttonLabel: "Move Widgets to Other Side of Chart",
    nagLog: "We found an UNCLOSABLE modal dialog. My favorite!"
  };

  i18n.setEnStrings([], strings);
  await i18n.initLanguage();

  // 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 = t`scriptFailure`;
    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 = t`buttonLabel`;
  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;

    const set = (elt, props) => Object.keys(props).forEach(p => elt.style[p] = props[p] + 'px');

    set(drawingToolbar, {
      width: LEFT_TOOLBAR_WIDTH,
      marginLeft: leftWidth - LEFT_TOOLBAR_WIDTH + 2
    });

    set(layoutAreaRight, { width: rightWidth });
    set(layoutAreaLeft, { width: leftWidth });

    set(layoutAreaCenter, {
      width: centerWidth,
      left: centerLeft
    });
    set(chartContainer, { width: centerWidth });
    set(layoutAreaBottom, {
      width: centerWidth,
      left: centerLeft
    });
    set(layoutAreaTradingPanel, { right: rightWidth });

    // remove some nags since we're already here.
    $`div[data-role='toast-container']`.querySelector('button')?.click();
    const gopro = $`[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(t`nagLog`);
        gopro.parentElement.remove();
      }
    }
  }
})();