Source: shadow-dom.js

/**
 * Contains all of the Shadow DOMs we want to make
 * the right column work without bloating to code too much
 */

const consistentStyle =
`form {
  height: 100%;
  position: relative;
  color: var(--text-color);
  font-size: 0.8em;
}

fieldset {
  width: 99%;
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  border-radius: 10px;
  box-sizing: border-box;
  padding: 0;
  margin: 0;
  background-color: var(--fieldset-bg);
  border: 1px solid var(--fieldset-border);
}

#attr-delete {
  display: block;
  position: absolute;
  bottom: 2.5em;
  left: 50%;
  transform: translateX(-50%);
  right: auto;

  width: 80%;
  font-size: 1.5em;
  background-color: red;
  border: 0;
  padding: 0.4rem;
  color: white;
  border-radius: 3px;
}

#attr-delete:hover {
  background-color: rgb(227, 0, 0);
}

.attr-button {
  width: 10px;
  height: 10px;
}

#attr-text {
  width: 90%;
  height: 7em;
  border-radius: 6px;
  box-sizing: border-box;
  font-size: 1em;
  border: none;
  background-color: var(--input-bg);
  padding: 0.4em;
  resize: none;
}

#attr-font-size, #attr-font-style {
  background-color: var(--input-bg);
  border: none;
  padding-left: 0.3em;
  border-radius: 6px;
  margin: 0.2em 0.2em 0.5em 0.2em;
  height: 1.5em;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}

#attr-font-style {
  width: 65%;
}

#attr-font-size {
  width: 20%
}

#attr-text-modifiers {
  position: relative;
  margin: auto auto;
  width: 80%;
  padding-left: 5px;
  padding-right: 5px;
  border-radius: 3px;
  background-color: var(--input-bg);
}

#attr-bold, #attr-italics, #attr-underline {
  accent-color: var(--fieldset-bg);
}

#attr-height {
  margin-right: 0.5em;
}

#attr-width {
  margin-right: 0.5em;
}

.thin-number {
  width: 22%;
  border-radius: 6px;
  border: none;
  background-color: var(--input-bg);
  margin-bottom: 1em;
  height: 1.5em;
}

#attr-color-div {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  margin-bottom: 1em;
  margin-top: -0.2em
}
  
#attr-color-picker {
  border-radius: 50%;
  inline-size: 30px;
  block-size: 30px;
  border-width: 1px;
  border-style: solid;
  border: white;
  background-color: var(--input-bg);
}

#attr-hex-color {
  width: 65%;
  height: 1.7em;
  border-radius: 6px;
  border: none;
  background-color: var(--input-bg);
}

#attr-up-one, #attr-up-all, #attr-down-one, #attr-down-all {
  border-radius: 6px;
  background-color: var(--input-bg);
  border: none;
  width: 15%;
  height: 1.5em;
  margin: 0 0.1em 1em;
  padding: 0;
}

#attr-up-one img {
  transform: rotate(90deg);
  width: 16px;
  height: 16px;
  vertical-align: middle;
}

#attr-down-one img {
  transform: rotate(-90deg);
  width: 16px;
  height: 16px;
  vertical-align: middle;
}

#attr-up-all img {
  width: 16px;
  height: 16px;
  vertical-align: middle;
}

#attr-down-all img {
  transform: rotate(-180deg);
  width: 16px;
  height: 16px;
  vertical-align: middle;
}

#attr-x, #attr-y {
  margin-bottom: 20px;
  border-radius: 6px;
  background-color: var(--input-bg);
  border: none;
  height 1.7em;
  width: 25%;
  padding-left: 0.5em;
}

`;

class TextboxElement extends HTMLElement {
  constructor() {
    super();

    const shadowDOM = this.attachShadow({ mode: 'open' });
    const elementRoot = document.createElement('form');
    elementRoot.innerHTML =
        `<h2>Attribute Editor</h2>
        
        <fieldset>
        <h3>Content</h3>
        <textarea id="attr-text" name="attr-text"></textarea>
        
        <hr>

        <h3>Typeface</h3>
        <select id="attr-font-style" name="attr-font-style">
          <option value="Arial">Arial</option>
          <option value="Times New Roman">Times New Roman</option>
          <option value="Helvetica">Helvetica</option>
          <option value="Futura">Futura</option>
          <option value="Garamond">Garamond</option>
          <option value="Verdana">Verdana</option>
          <option value="Trebuchet">Trebuchet</option>
          <option value="Georgia">Georgia</option>
        </select>
        <input type="number" id="attr-font-size" name="attr-font-size" class="thin-number" />
        
        <div id="attr-text-modifiers">
        <label for="attr-bold"><img src="./icons/bold.png" class="attr-button"/></label>
        <input type="checkbox" id="attr-bold" name="attr-bold" />
        
        <label for="attr-italics"><img src="./icons/italics.png" class="attr-button"/></label>
        <input type="checkbox" id="attr-italics" name="attr-italics" />

        <label for="attr-underline"><img src="./icons/underline.png" class="attr-button"/></label>
        <input type="checkbox" id="attr-underline" name="attr-underline" />
        </div>

        <hr>
        
        <h3>Color</h3>
        <div id="attr-color-div">
        <input type="color" id="attr-color-picker" name="attr-color-picker">
        <input type="text" id="attr-hex-color" name="attr-hex-color" autocomplete="off" />
        </div>

        <hr>

        <h3>Layer & Position</h3>

        <div id="attr-layer">
          Layer
          <button type="button" id="attr-up-one">
             <img src="./icons/arrow-left.svg" />
          </button>
          <button type="button" id="attr-down-one">
            <img src="./icons/arrow-left.svg" />
          </button>
          <button type="button" id="attr-up-all">
            <img src="./icons/arrow_double.png" />
          </button>
          <button type="button" id="attr-down-all">
            <img src="./icons/arrow_double.png" />
          </button>
        </div>

        <label for="attr-x">x: </label>
        <input type="number" id="attr-x" name="attr-x" class="thin-number" />
        <label for="attr-y">y: </label>
        <input type="number" id="attr-y" name="attr-y" class="thin-number" />

        </fieldset>

        <button type="button" id="attr-delete" name="attr-delete">Delete</button>`;

    const style = document.createElement('style');
    style.innerHTML = consistentStyle;

    shadowDOM.append(elementRoot);
    shadowDOM.append(style);

    this.initListeners();
  }

  initListeners() {
    const text = this.shadowRoot.querySelector('#attr-text');
    text.addEventListener('input', () => {
      this.obj.text = text.value;
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    const fontSize = this.shadowRoot.querySelector('#attr-font-size');
    fontSize.addEventListener('input', () => {
      this.obj.fontSize = parseInt(fontSize.value, 10);
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    const bold = this.shadowRoot.querySelector('#attr-bold');
    bold.addEventListener('change', () => {
      this.obj.bold = bold.checked;
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    const italics = this.shadowRoot.querySelector('#attr-italics');
    italics.addEventListener('change', () => {
      this.obj.italics = italics.checked;
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    const underline = this.shadowRoot.querySelector('#attr-underline');
    underline.addEventListener('change', () => {
      this.obj.underline = underline.checked;
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    const fontStyle = this.shadowRoot.querySelector('#attr-font-style');
    fontStyle.addEventListener('input', () => {
      this.obj.fontStyle = fontStyle.value;
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    const x = this.shadowRoot.querySelector('#attr-x');
    x.addEventListener('input', () => {
      this.obj.x = parseInt(x.value, 10);
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    const y = this.shadowRoot.querySelector('#attr-y');
    y.addEventListener('input', () => {
      this.obj.y = parseInt(y.value, 10);
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    const color = this.shadowRoot.querySelector('#attr-color-picker');
    const rgb = this.shadowRoot.querySelector('#attr-hex-color');
    color.addEventListener('input', (e) => {
      rgb.value = color.value;
      const rgbVal = hexToRgb(e.target.value);
      this.obj.r = rgbVal[0];
      this.obj.g = rgbVal[1];
      this.obj.b = rgbVal[2];
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    rgb.addEventListener('input', () => {
      color.value = rgb.value;
      const rgbVal = hexToRgb(rgb.value);
      this.obj.r = rgbVal[0];
      this.obj.g = rgbVal[1];
      this.obj.b = rgbVal[2];
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    const up = this.shadowRoot.querySelector('#attr-up-one');
    up.addEventListener('click', () => { this.obj.parent.triggerUpKey(true, false); });

    const down = this.shadowRoot.querySelector('#attr-down-one');
    down.addEventListener('click', () => { this.obj.parent.triggerDownKey(true, false); });

    const upAll = this.shadowRoot.querySelector('#attr-up-all');
    upAll.addEventListener('click', () => { this.obj.parent.triggerUpKey(true, true); });

    const downAll = this.shadowRoot.querySelector('#attr-down-all');
    downAll.addEventListener('click', () => { this.obj.parent.triggerDownKey(true, true); });

    const del = this.shadowRoot.querySelector('#attr-delete');
    del.addEventListener('click', () => {
      this.obj.parent.triggerDeleteKey();
    });
  }

  load(obj) {
    this.obj = obj;
    this.update();
  }

  update() {
    const text = this.shadowRoot.querySelector('#attr-text');
    text.value = this.obj.text;

    const fontSize = this.shadowRoot.querySelector('#attr-font-size');
    fontSize.value = this.obj.fontSize;

    const bold = this.shadowRoot.querySelector('#attr-bold');
    const italics = this.shadowRoot.querySelector('#attr-italics');
    const underline = this.shadowRoot.querySelector('#attr-underline');
    bold.checked = this.obj.bold;
    italics.checked = this.obj.italics;
    underline.checked = this.obj.underline;

    const fontStyle = this.shadowRoot.querySelector('#attr-font-style');
    fontStyle.value = this.obj.fontStyle;

    const x = this.shadowRoot.querySelector('#attr-x');
    const y = this.shadowRoot.querySelector('#attr-y');
    x.value = Math.round(this.obj.x);
    y.value = Math.round(this.obj.y);

    const rgb = this.shadowRoot.querySelector('#attr-hex-color');
    rgb.value = rgbToHex(this.obj.r, this.obj.g, this.obj.b);

    const color = this.shadowRoot.querySelector('#attr-color-picker');
    color.value = rgb.value;
  }
}

class BoxElement extends HTMLElement {
  constructor() {
    super();

    const shadowDOM = this.attachShadow({ mode: 'open' });
    const elementRoot = document.createElement('form');
    elementRoot.innerHTML =
        `<h2>Attribute Editor</h2>
        
        <fieldset>
        
        <h3>Dimensions</h3>

        <label for="attr-width">w: </label>
        <input type="number" id="attr-width" name="attr-width" class="thin-number" />
        <label for="attr-height">h: </label>
        <input type="number" id="attr-height" name="attr-height" class="thin-number" />

        <hr>

        <h3>Color</h3>
        <div id="attr-color-div">
        <input type="color" id="attr-color-picker" name="attr-color-picker">
        <input type="text" id="attr-hex-color" name="attr-hex-color autocomplete="off"" />
        </div>

        <hr>

        <h3>Layer & Position</h3>

        <div id="attr-layer">
          Layer
          <button type="button" id="attr-up-one">
             <img src="./icons/arrow-left.svg" />
          </button>
          <button type="button" id="attr-down-one">
            <img src="./icons/arrow-left.svg" />
          </button>
          <button type="button" id="attr-up-all">
            <img src="./icons/arrow_double.png" />
          </button>
          <button type="button" id="attr-down-all">
            <img src="./icons/arrow_double.png" />
          </button>
        </div>

        <label for="attr-x">x: </label>
        <input type="number" id="attr-x" name="attr-x" class="thin-number" />
        <label for="attr-y">y: </label>
        <input type="number" id="attr-y" name="attr-y" class="thin-number" />

        </fieldset>
        
        <button type="button" id="attr-delete" name="attr-delete">Delete</button>`;

    const style = document.createElement('style');
    style.innerHTML = consistentStyle;

    shadowDOM.append(elementRoot);
    shadowDOM.append(style);

    this.initListeners();
  }

  initListeners() {
    const x = this.shadowRoot.querySelector('#attr-x');
    const width = this.shadowRoot.querySelector('#attr-width');
    x.addEventListener('input', () => {
      this.obj.x1 = parseInt(x.value, 10);
      this.obj.x2 = parseInt(x.value, 10) + parseInt(width.value, 10);
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    width.addEventListener('input', () => {
      this.obj.x1 = parseInt(x.value, 10);
      this.obj.x2 = parseInt(x.value, 10) + parseInt(width.value, 10);
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    const y = this.shadowRoot.querySelector('#attr-y');
    const height = this.shadowRoot.querySelector('#attr-height');
    y.addEventListener('input', () => {
      this.obj.y1 = parseInt(y.value, 10);
      this.obj.y2 = parseInt(y.value, 10) + parseInt(height.value, 10);
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    height.addEventListener('input', () => {
      this.obj.y1 = parseInt(y.value, 10);
      this.obj.y2 = parseInt(y.value, 10) + parseInt(height.value, 10);
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    const color = this.shadowRoot.querySelector('#attr-color-picker');
    const rgb = this.shadowRoot.querySelector('#attr-hex-color');
    color.addEventListener('input', (e) => {
      rgb.value = color.value;
      const rgbVal = hexToRgb(e.target.value);
      this.obj.r = rgbVal[0];
      this.obj.g = rgbVal[1];
      this.obj.b = rgbVal[2];
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    rgb.addEventListener('input', () => {
      color.value = rgb.value;
      const rgbVal = hexToRgb(rgb.value);
      this.obj.r = rgbVal[0];
      this.obj.g = rgbVal[1];
      this.obj.b = rgbVal[2];
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    const up = this.shadowRoot.querySelector('#attr-up-one');
    up.addEventListener('click', () => { this.obj.parent.triggerUpKey(true, false); });

    const down = this.shadowRoot.querySelector('#attr-down-one');
    down.addEventListener('click', () => { this.obj.parent.triggerDownKey(true, false); });

    const upAll = this.shadowRoot.querySelector('#attr-up-all');
    upAll.addEventListener('click', () => { this.obj.parent.triggerUpKey(true, true); });

    const downAll = this.shadowRoot.querySelector('#attr-down-all');
    downAll.addEventListener('click', () => { this.obj.parent.triggerDownKey(true, true); });

    const del = this.shadowRoot.querySelector('#attr-delete');
    del.addEventListener('click', () => {
      this.obj.parent.triggerDeleteKey();
    });
  }

  load(obj) {
    this.obj = obj;
    this.update();
  }

  update() {
    const x = this.shadowRoot.querySelector('#attr-x');
    const y = this.shadowRoot.querySelector('#attr-y');
    x.value = Math.round(Math.min(this.obj.x1, this.obj.x2));
    y.value = Math.round(Math.min(this.obj.y1, this.obj.y2));

    const width = this.shadowRoot.querySelector('#attr-width');
    const height = this.shadowRoot.querySelector('#attr-height');
    width.value = Math.round(Math.abs(this.obj.x2 - this.obj.x1));
    height.value = Math.round(Math.abs(this.obj.y2 - this.obj.y1));

    const rgb = this.shadowRoot.querySelector('#attr-hex-color');
    rgb.value = rgbToHex(this.obj.r, this.obj.g, this.obj.b);

    const color = this.shadowRoot.querySelector('#attr-color-picker');
    color.value = rgb.value;
  }
}

class ImageElement extends HTMLElement {
  constructor() {
    super();

    const shadowDOM = this.attachShadow({ mode: 'open' });
    const elementRoot = document.createElement('form');

    elementRoot.innerHTML =
        `<h2>Attribute Editor</h2>
        
        <fieldset>
        
        <h3>Dimensions</h3>

        <label for="attr-width">w: </label>
        <input type="number" id="attr-width" name="attr-width" class="thin-number" />
        <label for="attr-height">h: </label>
        <input type="number" id="attr-height" name="attr-height" class="thin-number" />

        <hr>

        <h3>Layer & Position</h3>

        <div id="attr-layer">
          Layer
          <button type="button" id="attr-up-one">
             <img src="./icons/arrow-left.svg" />
          </button>
          <button type="button" id="attr-down-one">
            <img src="./icons/arrow-left.svg" />
          </button>
          <button type="button" id="attr-up-all">
            <img src="./icons/arrow_double.png" />
          </button>
          <button type="button" id="attr-down-all">
            <img src="./icons/arrow_double.png" />
          </button>
        </div>

        <label for="attr-x">x: </label>
        <input type="number" id="attr-x" name="attr-x" class="thin-number" />
        <label for="attr-y">y: </label>
        <input type="number" id="attr-y" name="attr-y" class="thin-number" />

        </fieldset>

        <button type="button" id="attr-delete" name="attr-delete">Delete</button>`;

    const style = document.createElement('style');
    style.innerHTML = consistentStyle;

    shadowDOM.append(elementRoot);
    shadowDOM.append(style);

    this.initListeners();
  }

  initListeners() {
    const x = this.shadowRoot.querySelector('#attr-x');
    const width = this.shadowRoot.querySelector('#attr-width');
    x.addEventListener('input', () => {
      this.obj.x1 = parseInt(x.value, 10);
      this.obj.x2 = parseInt(x.value, 10) + parseInt(width.value, 10);
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    width.addEventListener('input', () => {
      this.obj.x1 = parseInt(x.value, 10);
      this.obj.x2 = parseInt(x.value, 10) + parseInt(width.value, 10);
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    const y = this.shadowRoot.querySelector('#attr-y');
    const height = this.shadowRoot.querySelector('#attr-height');
    y.addEventListener('input', () => {
      this.obj.y1 = parseInt(y.value, 10);
      this.obj.y2 = parseInt(y.value, 10) + parseInt(height.value, 10);
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    height.addEventListener('input', () => {
      this.obj.y1 = parseInt(y.value, 10);
      this.obj.y2 = parseInt(y.value, 10) + parseInt(height.value, 10);
      this.obj.parent.onAnyChange();
      this.obj.parent.renderCanvas();
    });

    const up = this.shadowRoot.querySelector('#attr-up-one');
    up.addEventListener('click', () => { this.obj.parent.triggerUpKey(true, false); });

    const down = this.shadowRoot.querySelector('#attr-down-one');
    down.addEventListener('click', () => { this.obj.parent.triggerDownKey(true, false); });

    const upAll = this.shadowRoot.querySelector('#attr-up-all');
    upAll.addEventListener('click', () => { this.obj.parent.triggerUpKey(true, true); });

    const downAll = this.shadowRoot.querySelector('#attr-down-all');
    downAll.addEventListener('click', () => { this.obj.parent.triggerDownKey(true, true); });

    const del = this.shadowRoot.querySelector('#attr-delete');
    del.addEventListener('click', () => {
      this.obj.parent.triggerDeleteKey();
    });
  }

  load(obj) {
    this.obj = obj;
    this.update();
  }

  update() {
    const x = this.shadowRoot.querySelector('#attr-x');
    const y = this.shadowRoot.querySelector('#attr-y');
    x.value = Math.round(Math.min(this.obj.x1, this.obj.x2));
    y.value = Math.round(Math.min(this.obj.y1, this.obj.y2));

    const width = this.shadowRoot.querySelector('#attr-width');
    const height = this.shadowRoot.querySelector('#attr-height');
    width.value = Math.round(Math.abs(this.obj.x2 - this.obj.x1));
    height.value = Math.round(Math.abs(this.obj.y2 - this.obj.y1));
  }
}

function rgbToHex(r, g, b) {
  const rgb = (r << 16) | (g << 8) | b;
  const str = rgb.toString(16);
  const fill = '0';
  return `#${fill.repeat(6 - str.length)}${str}`;
}

function hexToRgb(hex) {
  try {
    const rgb = parseInt(hex.substring(1), 16);
    const r = rgb >> 16;
    const g = (rgb >> 8) % 256;
    const b = rgb % 256;
    return [r, g, b];
  } catch (e) {
    return [0, 0, 0];
  }
}

customElements.define('textbox-attributes', TextboxElement);
customElements.define('box-attributes', BoxElement);
customElements.define('icon-attributes', ImageElement);