COOLcsv (I don't know what else to add)

COOLcsv

- probably the most (un)cool CSV viewer Glitch extension

What the heck is “CoOlCsV”??/? It’s a Glitch “extension” designed for the purpose of… Viewing .CSVs.

There’s not really much to showcase about a simple .CSV viewer and all, but I mean, who doesn’t love extensions? I got like approximately 15 browser extensions excluding my own browser extensions. And we got, approximately, 0 Glitch extensions at all. Why not change that? Why not let ourselves create tens of thousands of Glitch extensions? Like imagine a Glitch site for installing extensions, imagine having a separate bookmark bar dedicated to Glitch extensions.

Anyway, I am planning to create a Glitch extension library. Not much to add to the ecosystem, but nothing except you is watching me, so I can add on to the application object a section for extension-making.

If you’d like to help with my totally (un)COOL .CSV viewer (get it?), hit me up! I’m planning to create a private GitHub repo for an “organization” of COOLcsv anyway, but it’s not like I am asking for a team for COOLcsv, I thought I’d make the project myself, as always, but I mean, if it isn’t supposed like that, then I suppose I’ll give sharing my services a shot.

I used to be in Replit instead of here, but because of their crappy downgrades, I decided here would be an… “Meh” option to switch. Also I was “community banned” so that’s another plus. Just hope they don’t find I’m calling it “Lameplit” for now. I wasn’t able to share my wicked repls, so I got used to making my projects alone, and got bored of this Repl-idiocy, so again, I switched here, but I didn’t think Glitch would have community this committed to help each other in the making of projects.

Anyways, COOLcsv, CSV viewer, Replit < Glitch (including communities).

:stuck_out_tongue_closed_eyes:

1 Like

Looks really cool, could I join the github org?

When I create it, yeah! Just tell me your Github username and I definitely will add you (sources: trust me bro)

OK, I think I got @Haizlbliek’s Github too, will invite you

2 Likes

Fancy Glitch Extension Library apart, here’s a userscript I made that will give you a fully functional CSV editor when clicking “COOLcsv” on CSV files.

It autosaves, has plenty of keyboard shortcuts and all of the options you would expect. Feel free to give feedback! I would commit it to the github but forking is disabled and I need to fork the repo to edit it.


image

// ==UserScript==
// @name          	COOLcsv
// @description     CSV editor for Glitch
// @author			Tiago Rangel (tiagorangel.com)
// @include         http://glitch.com/edit
// @version         0.1
// ==/UserScript==

/* SimpleCSVEditor by https://github.com/dag0310/simple-csv-editor */

class SimpleCsvEditor {
  constructor({
    id = "simpleCsvEditor",
    data = "",
    onChange = null,
    warnOnDelete = true,
    showControls = true,
    controlLabels = {
      addRowBefore: "+ ↑",
      addRowAfter: "+ ↓",
      addColumnBefore: "+ ←",
      addColumnAfter: "+ →",
      deleteRow: "✖",
      deleteColumn: "✖",
      deleteAll: "✖",
      deleteRowWarning: "Delete this row?",
      deleteColumnWarning: "Delete this column?",
      deleteAllWarning: "Delete all cells?",
      deleteButton: "Delete",
      cancelButton: "Cancel",
    },
    delimiter = null,
    quoteChar = '"',
  } = {}) {
    if (window.Papa == null) {
      throw new ReferenceError("Papa is not defined");
    }
    this.editor = document.getElementById(id);
    if (this.editor == null) {
      throw new ReferenceError(
        `No editor element found like <div id="${id}"></div>`
      );
    }

    this.table = this.editor.appendChild(document.createElement("table"));

    this.onChange = onChange;
    this.warnOnDelete = warnOnDelete;
    this.showControls = showControls;
    this.controlLabels = controlLabels;

    this.papaParseConfig = {
      quoteChar,
      header: false,
      dynamicTyping: false,
      skipEmptyLines: true,
    };
    if (delimiter != null) {
      this.papaParseConfig.delimiter = delimiter;
    }

    this.setCsv(data);
  }

  #setDeleteButtonDisabledStates() {
    for (const button of this.table.getElementsByClassName("deleteRow")) {
      button.disabled = this.table.rows.length === (this.showControls ? 2 : 1);
    }
    for (const button of this.table.getElementsByClassName("deleteColumn")) {
      button.disabled =
        this.table.rows[0].cells.length === (this.showControls ? 2 : 1);
    }
  }

  #triggerOnChange() {
    this.#setDeleteButtonDisabledStates();
    if (this.onChange == null) {
      return;
    }
    this.onChange(this.getCsv());
  }

  #buildBasicButton(labelKey) {
    const button = document.createElement("button");
    button.type = "button";
    button.tabIndex = -1;
    button.className = labelKey;
    button.innerText = this.controlLabels[labelKey];
    return button;
  }

  #buildAddRowButton(offsetIndex, labelKey) {
    const button = this.#buildBasicButton(labelKey);
    button.addEventListener("click", (event) => {
      this.addRow(
        event.target.parentElement.parentElement.rowIndex + offsetIndex
      );
    });
    return button;
  }

  #buildAddColumnButton(offsetIndex, labelKey) {
    const button = this.#buildBasicButton(labelKey);
    button.addEventListener("click", (event) => {
      this.addColumn(event.target.parentElement.cellIndex + offsetIndex);
    });
    return button;
  }

  #buildDeleteDialog(text, deleteFunction) {
    const dialog = document.createElement("dialog");
    dialog.innerText = text;

    const form = document.createElement("form");
    dialog.appendChild(form);

    const cancelBtn = document.createElement("button");
    cancelBtn.value = "cancel";
    cancelBtn.formMethod = "dialog";
    cancelBtn.innerText = this.controlLabels.cancelButton;
    form.appendChild(cancelBtn);

    const deleteBtn = document.createElement("button");
    deleteBtn.value = "default";
    deleteBtn.innerText = this.controlLabels.deleteButton;
    form.appendChild(deleteBtn);

    dialog.addEventListener("close", (event) => {
      if (event.target.returnValue === "delete") {
        deleteFunction();
      }
    });

    deleteBtn.addEventListener("click", (event) => {
      event.preventDefault();
      dialog.close("delete");
    });

    return dialog;
  }

  #buildDeleteButton(button, deleteWarning, deleteFunction) {
    const dialog = this.#buildDeleteDialog(deleteWarning, deleteFunction);
    button.addEventListener("click", () => {
      if (this.warnOnDelete) {
        dialog.showModal();
      } else {
        deleteFunction();
      }
    });
    button.appendChild(dialog);
    return button;
  }

  #buildDeleteRowButton(labelKey) {
    const button = this.#buildBasicButton(labelKey);
    return this.#buildDeleteButton(
      button,
      this.controlLabels.deleteRowWarning,
      () => {
        this.deleteRow(button.parentElement.parentElement.rowIndex);
      }
    );
  }

  #buildDeleteColumnButton(labelKey) {
    const button = this.#buildBasicButton(labelKey);
    return this.#buildDeleteButton(
      button,
      this.controlLabels.deleteColumnWarning,
      () => {
        this.deleteColumn(button.parentElement.cellIndex);
      }
    );
  }

  #buildDeleteAllButton(labelKey) {
    const button = this.#buildBasicButton(labelKey);
    return this.#buildDeleteButton(
      button,
      this.controlLabels.deleteAllWarning,
      () => {
        this.deleteAll();
      }
    );
  }

  #addColumnControlCell(row, cellIndex) {
    const cell = document.createElement("th");
    cell.appendChild(this.#buildAddColumnButton(0, "addColumnBefore"));
    cell.appendChild(this.#buildDeleteColumnButton("deleteColumn"));
    cell.appendChild(this.#buildAddColumnButton(1, "addColumnAfter"));
    row.insertBefore(cell, row.cells[cellIndex]);
  }

  #addRowControlCell(row, cellIndex) {
    const cell = document.createElement("th");
    cell.appendChild(this.#buildAddRowButton(0, "addRowBefore"));
    cell.appendChild(this.#buildDeleteRowButton("deleteRow"));
    cell.appendChild(this.#buildAddRowButton(1, "addRowAfter"));
    row.insertBefore(cell, row.cells[cellIndex]);
  }

  static #jumpToEndOfCell(cell) {
    if (cell == null) {
      return;
    }
    if (cell.firstChild == null) {
      cell.appendChild(document.createTextNode(""));
    }
    const textNode = cell.firstChild;
    const range = document.createRange();
    range.setStart(textNode, cell?.firstChild?.textContent.length);
    range.collapse(true);
    const selection = window.getSelection();
    selection.removeAllRanges();
    selection.addRange(range);
  }

  #addDataCellToRow(row, cellIndex) {
    const newCell = row.insertCell(cellIndex);
    newCell.contentEditable = true;
    if (document.execCommand != null) {
      newCell.addEventListener("paste", (event) => {
        event.preventDefault();
        document.execCommand(
          "insertHTML",
          false,
          event.clipboardData.getData("text/plain")
        );
      });
    }
    newCell.addEventListener("input", () => {
      this.#triggerOnChange();
    });
    newCell.addEventListener("keydown", (event) => {
      const { rows } = row.parentElement;
      switch (event.key) {
        case "Enter": {
          event.preventDefault();
          const newRowIndex = event.shiftKey ? row.rowIndex : row.rowIndex + 1;
          this.addRow(newRowIndex);
          rows[newRowIndex].cells[newCell.cellIndex].focus();
          break;
        }
        case "ArrowUp":
          event.preventDefault();
          SimpleCsvEditor.#jumpToEndOfCell(
            rows[row.rowIndex - 1]?.cells[newCell.cellIndex]
          );
          break;
        case "ArrowDown":
          event.preventDefault();
          SimpleCsvEditor.#jumpToEndOfCell(
            rows[row.rowIndex + 1]?.cells[newCell.cellIndex]
          );
          break;
        default: // Do nothing
      }
    });
  }

  getCsv() {
    const stringsInArraysOfArrays = Array.from(this.table.rows)
      .slice(this.showControls ? 1 : 0)
      .map((row) =>
        Array.from(row.cells)
          .slice(0, this.showControls ? -1 : undefined)
          .map((cell) => cell.innerText)
      );

    const config = {
      delimiter: this.delimiterUsed,
      header: false,
      newline: this.lineBreakUsed,
      skipEmptyLines: "greedy",
    };

    return (
      window.Papa.unparse(stringsInArraysOfArrays, config) +
      (this.lastLineEmpty ? this.lineBreakUsed : "")
    );
  }

  setCsv(data) {
    const result = window.Papa.parse(data, this.papaParseConfig);

    this.lineBreakUsed = result.meta.linebreak;
    this.delimiterUsed = result.meta.delimiter;
    this.lastLineEmpty = data.slice(-1) === this.lineBreakUsed;

    this.table.innerHTML = "";

    for (const [lineIndex, lineTokens] of result.data.entries()) {
      for (const [tokenIndex, token] of lineTokens.entries()) {
        if (this.table.rows[lineIndex] == null) {
          const numCells =
            lineIndex <= 0
              ? lineTokens.length
              : this.table.rows[lineIndex - 1].cells.length;
          const newRow = this.table.insertRow(-1);
          for (let cellIndex = 0; cellIndex < numCells; cellIndex += 1) {
            this.#addDataCellToRow(newRow, -1);
          }
        }
        if (this.table.rows[lineIndex].cells[tokenIndex] == null) {
          for (const row of this.table.rows) {
            this.#addDataCellToRow(row, -1);
          }
        }
        this.table.rows[lineIndex].cells[tokenIndex].innerText = token;
      }
    }
    if (this.table.rows.length <= 0) {
      this.#addDataCellToRow(this.table.insertRow(0), 0);
    }
    if (this.showControls) {
      const columnControlsRow = this.table.insertRow(0);
      for (
        let cellIndex = 0;
        cellIndex < this.table.rows[1].cells.length;
        cellIndex += 1
      ) {
        this.#addColumnControlCell(columnControlsRow, -1);
      }
      for (const row of this.table.rows) {
        if (row.rowIndex === 0) {
          const cell = document.createElement("th");
          cell.appendChild(this.#buildDeleteAllButton("deleteAll"));
          row.appendChild(cell);
        } else {
          this.#addRowControlCell(row, -1);
        }
      }
    }
    this.#setDeleteButtonDisabledStates();

    return result.errors;
  }

  addRow(rowIndex) {
    const firstDataRowIndex = this.showControls ? 1 : 0;
    const firstDataRow =
      this.table.rows.length > firstDataRowIndex
        ? this.table.rows[firstDataRowIndex]
        : null;
    const newRow = this.table.insertRow(rowIndex);
    const numCells = (firstDataRow ?? newRow).cells.length;
    for (let cellIndex = 0; cellIndex < numCells; cellIndex += 1) {
      if (this.showControls && cellIndex === numCells - 1) {
        this.#addRowControlCell(newRow, -1);
      } else {
        this.#addDataCellToRow(newRow, -1);
      }
    }
    this.#triggerOnChange();
  }

  addColumn(cellIndex) {
    for (const row of this.table.rows) {
      if (this.showControls && row.rowIndex === 0) {
        this.#addColumnControlCell(row, cellIndex);
      } else {
        this.#addDataCellToRow(row, cellIndex);
      }
    }
    this.#triggerOnChange();
  }

  deleteRow(rowIndex) {
    if (this.table.rows.length <= (this.showControls ? 2 : 1)) {
      return;
    }
    this.table.deleteRow(rowIndex);
    this.#triggerOnChange();
  }

  deleteColumn(columnIndex) {
    if (this.table.rows[0].cells.length <= (this.showControls ? 2 : 1)) {
      return;
    }
    for (const row of this.table.rows) {
      row.deleteCell(columnIndex);
    }
    this.#triggerOnChange();
  }

  deleteAll() {
    this.setCsv("");
    this.#triggerOnChange();
  }
}

/* END SimpleCSVEditor */

(function (application) {
  if (!application) {
    return;
  }
  
  if (false) {
    var Papa;
  }

  let currentFilePath = application.currentFileInfo().path;

  function loop() {
    requestAnimationFrame(loop);

    if (!application) {
      return;
    }

    let currentFile = application.currentFileInfo();
    if (currentFile.path == currentFilePath) {
      return;
    }

    currentFilePath = currentFile.path;

    document.querySelectorAll("[data-coolcsv-topbar]").forEach((e) => {
      e.remove();
    });

    if (!(currentFile.extension === "csv")) {
      return;
    }

    const topbar = document.createElement("div");
    topbar.classList.add("editor-helper");
    topbar.classList.add("css-151m034");
    topbar.setAttribute("data-coolcsv-topbar", "");
    topbar.innerHTML = `<div class="css-br04fe">${currentFile.path
      .split("/")
      .pop()}</div><div class="css-1xeoop3"><button class="css-1a7kqr" data-coolcsv-button><div class="css-1licayo"><div class="css-vurnku"><img data-module="Icon" src="https://cdn.jsdelivr.net/gh/twitter/[email protected]/assets/svg/1f5a8.svg" class="css-f28wfp"></div><div class="css-vurnku">COOLcsv</div></div></button></div>`; // 1f5a8 is logo

    document.querySelector("article.text-editor#text-editor").prepend(topbar);

    topbar
      .querySelector("[data-coolcsv-button]")
      .addEventListener("click", function () {
        const el = document.createElement("div");
        el.classList.add("overlay-background");
        el.innerHTML = `<dialog class="overlay"><section style="display:flex"><h1>${currentFile.path
          .split("/")
          .pop()}</h1></section><section class="info">
<div id="CSVEditor"></div>
</section></dialog>`;

        document.body.addEventListener("click", function (e) {
          if (!el) {
            return;
          }

          if (e.target == el) {
            el.remove();
          }
        });
        document.querySelector("#application").appendChild(el);
        const fileContent = application.fileByPath(currentFile.path).I.content;
        const simpleCsvEditor = new SimpleCsvEditor({
          id: "CSVEditor",
          data: fileContent,
          onChange: (csvData) => {
            application.fileByPath(currentFile.path).content(csvData);
          },
          delimiter: ",",
        });
      });
  }

  const _ppscript = document.createElement("script"); // THIS STANDS FOR **P**APA**P**ARSE OK?????
  _ppscript.setAttribute(
    "src",
    "https://unpkg.com/[email protected]/papaparse.min.js"
  );
  document.body.appendChild(_ppscript);

  _ppscript.addEventListener("load", loop);
  
  const css = document.createElement("style");
  css.innerHTML = `#CSVEditor button {
    background-color: rgba(0, 0, 0, 0.1);
    border: none;
    border-radius: 3px;
}
#CSVEditor button:hover {
    opacity: .5;
}
`
  document.body.appendChild(css)
})(application);

For now on, I will be the only maintainer of COOLcsv. No organizations now.

1 Like

Also, the userscript is unofficial. (mostly)

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.