WIP: New approach (see description)
- Add edit table button (beside expand table button) - Parse the table contents - Display the table as a spreadsheet using the Importabular library
This commit is contained in:
parent
b62f595eeb
commit
9bfc23c599
17
README.md
17
README.md
|
@ -4,8 +4,21 @@
|
|||
|
||||
A theme component that adds a button to the composer tools to easily build tables in markdown.
|
||||
|
||||
## To Do List:
|
||||
## To Do List
|
||||
|
||||
- [X] Add table builder functionality
|
||||
- [ ] Possibly add the ability to edit tables
|
||||
- [X] Add front-end tests
|
||||
- [X] Add front-end tests
|
||||
|
||||
## Edit Table Functionality (WIP)
|
||||
|
||||
Known Issues:
|
||||
|
||||
- Clicking expand table button showing empty modal
|
||||
- After viewing an edit table modal once, a reload is required for it to work again or with another table, otherwise the following error appears:
|
||||
|
||||
```js
|
||||
You need to pass a node argument to Importabular, like this : new Importabular({node: document.body})
|
||||
```
|
||||
|
||||
This is possibly due to not having a unique id? Perhaps moving functionality to a component will resolve this.
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
"theme_version": "0.0.1",
|
||||
"minimum_discourse_version": null,
|
||||
"maximum_discourse_version": null,
|
||||
"assets": {},
|
||||
"assets": {
|
||||
"importabular": "assets/importabular.js"
|
||||
},
|
||||
"modifiers": {
|
||||
"svg_icons": [
|
||||
"align-left",
|
||||
|
|
|
@ -0,0 +1,726 @@
|
|||
!(function (t, e) {
|
||||
"object" == typeof exports && "object" == typeof module
|
||||
? (module.exports = e())
|
||||
: "function" == typeof define && define.amd
|
||||
? define([], e)
|
||||
: "object" == typeof exports
|
||||
? (exports.Importabular = e())
|
||||
: (t.Importabular = e());
|
||||
})(self, function () {
|
||||
return (() => {
|
||||
"use strict";
|
||||
var t = {
|
||||
103: (t, e, i) => {
|
||||
i.d(e, { default: () => l });
|
||||
class s {
|
||||
constructor() {
|
||||
var t, e;
|
||||
(e = {}),
|
||||
(t = "_data") in this
|
||||
? Object.defineProperty(this, t, {
|
||||
value: e,
|
||||
enumerable: !0,
|
||||
configurable: !0,
|
||||
writable: !0,
|
||||
})
|
||||
: (this[t] = e);
|
||||
}
|
||||
_setVal(t, e, i) {
|
||||
const s = this._data,
|
||||
n = (function (t) {
|
||||
return 0 === t ? "0" : t ? t.toString() : "";
|
||||
})(i);
|
||||
var o;
|
||||
n
|
||||
? (s[t] || (s[t] = {}), (s[t][e] = n))
|
||||
: s[t] &&
|
||||
s[t][e] &&
|
||||
(delete s[t][e],
|
||||
(o = s[t]),
|
||||
0 === Object.keys(o).length && delete s[t]);
|
||||
}
|
||||
_clear() {
|
||||
this._data = {};
|
||||
}
|
||||
_getVal(t, e) {
|
||||
const i = this._data;
|
||||
return (i && i[t] && i[t][e]) || "";
|
||||
}
|
||||
_toArr(t, e) {
|
||||
const i = [];
|
||||
for (let s = 0; s < e; s++) {
|
||||
i.push([]);
|
||||
for (let e = 0; e < t; e++) i[s].push(this._getVal(e, s));
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
||||
function n(t, e, i, s, n, o, r) {
|
||||
if ((t += i) < s) {
|
||||
if (n === 1 / 0) return { x: s, y: e };
|
||||
if (((t = n), --e < o)) {
|
||||
if (r === 1 / 0) return { x: s, y: o };
|
||||
e = r;
|
||||
}
|
||||
}
|
||||
return (
|
||||
t > n && ((t = s), ++e > r && ((e = o), (t = s))), { x: t, y: e }
|
||||
);
|
||||
}
|
||||
function o(t) {
|
||||
return t.split('"').length - 1;
|
||||
}
|
||||
function r(t, e, i) {
|
||||
return (
|
||||
e in t
|
||||
? Object.defineProperty(t, e, {
|
||||
value: i,
|
||||
enumerable: !0,
|
||||
configurable: !0,
|
||||
writable: !0,
|
||||
})
|
||||
: (t[e] = i),
|
||||
t
|
||||
);
|
||||
}
|
||||
const h = [
|
||||
"mousedown",
|
||||
"mouseenter",
|
||||
"mouseup",
|
||||
"mouseleave",
|
||||
"touchstart",
|
||||
"touchend",
|
||||
"touchmove",
|
||||
"keydown",
|
||||
"paste",
|
||||
"cut",
|
||||
"copy",
|
||||
];
|
||||
class l {
|
||||
constructor(t) {
|
||||
r(this, "_width", 1),
|
||||
r(this, "_height", 1),
|
||||
r(this, "_data", new s()),
|
||||
r(this, "paste", (t) => {
|
||||
if (this._editing) return;
|
||||
t.preventDefault();
|
||||
const e = (function (t) {
|
||||
var e,
|
||||
i,
|
||||
s,
|
||||
n,
|
||||
r,
|
||||
h,
|
||||
l,
|
||||
a = [],
|
||||
c = 0;
|
||||
for (
|
||||
(s = t.split("\n")).length > 1 &&
|
||||
"" === s[s.length - 1] &&
|
||||
s.pop(),
|
||||
e = 0,
|
||||
i = s.length;
|
||||
e < i;
|
||||
e += 1
|
||||
) {
|
||||
for (
|
||||
s[e] = s[e].split("\t"), n = 0, r = s[e].length;
|
||||
n < r;
|
||||
n += 1
|
||||
)
|
||||
a[c] || (a[c] = []),
|
||||
h && 0 === n
|
||||
? ((l = a[c].length - 1),
|
||||
(a[c][l] = a[c][l] + "\n" + s[e][0]),
|
||||
h &&
|
||||
1 & o(s[e][0]) &&
|
||||
((h = !1),
|
||||
(a[c][l] = a[c][l]
|
||||
.substring(0, a[c][l].length - 1)
|
||||
.replace(/""/g, '"'))))
|
||||
: n === r - 1 &&
|
||||
0 === s[e][n].indexOf('"') &&
|
||||
1 & o(s[e][n])
|
||||
? (a[c].push(
|
||||
s[e][n].substring(1).replace(/""/g, '"')
|
||||
),
|
||||
(h = !0))
|
||||
: (a[c].push(s[e][n].replace(/""/g, '"')),
|
||||
(h = !1));
|
||||
h || (c += 1);
|
||||
}
|
||||
return a;
|
||||
})(
|
||||
(t.clipboardData || window.clipboardData).getData(
|
||||
"text/plain"
|
||||
)
|
||||
),
|
||||
{ rx: i, ry: s } = this._selection,
|
||||
n = { x: i[0], y: s[0] };
|
||||
for (let t = 0; t < e.length; t++)
|
||||
for (let i = 0; i < e[0].length; i++)
|
||||
this._setVal(n.x + i, n.y + t, e[t][i]);
|
||||
this._changeSelectedCellsStyle(() => {
|
||||
(this._selectionStart = n),
|
||||
(this._selectionEnd = {
|
||||
x: n.x + e[0].length - 1,
|
||||
y: n.y + e.length - 1,
|
||||
}),
|
||||
this._onDataChanged();
|
||||
});
|
||||
}),
|
||||
r(this, "copy", (t) => {
|
||||
if (this._editing) return;
|
||||
const e = this._getSelectionAsArray();
|
||||
e &&
|
||||
(t.preventDefault(),
|
||||
t.clipboardData.setData(
|
||||
"text/plain",
|
||||
(function (t) {
|
||||
var e,
|
||||
i,
|
||||
s,
|
||||
n,
|
||||
o,
|
||||
r = "";
|
||||
for (e = 0, i = t.length; e < i; e += 1) {
|
||||
for (s = 0, n = t[e].length; s < n; s += 1)
|
||||
s > 0 && (r += "\t"),
|
||||
"string" == typeof (o = t[e][s])
|
||||
? o.indexOf("\n") > -1
|
||||
? (r += '"' + o.replace(/"/g, '""') + '"')
|
||||
: (r += o)
|
||||
: (r += null == o ? "" : o);
|
||||
r += "\n";
|
||||
}
|
||||
return r;
|
||||
})(e)
|
||||
));
|
||||
}),
|
||||
r(this, "cut", (t) => {
|
||||
this._editing ||
|
||||
(this.copy(t), this._setAllSelectedCellsTo(""));
|
||||
}),
|
||||
r(this, "keydown", (t) => {
|
||||
t.ctrlKey ||
|
||||
(this._selectionStart &&
|
||||
("Escape" === t.key &&
|
||||
this._editing &&
|
||||
(t.preventDefault(),
|
||||
this._revertEdit(),
|
||||
this._stopEditing()),
|
||||
"Enter" === t.key &&
|
||||
(t.preventDefault(),
|
||||
this._tabCursorInSelection(!1, t.shiftKey ? -1 : 1)),
|
||||
"Tab" === t.key &&
|
||||
(t.preventDefault(),
|
||||
this._tabCursorInSelection(!0, t.shiftKey ? -1 : 1)),
|
||||
this._editing ||
|
||||
("F2" === t.key &&
|
||||
(t.preventDefault(), this._startEditing(this._focus)),
|
||||
("Delete" !== t.key && "Backspace" !== t.key) ||
|
||||
(t.preventDefault(), this._setAllSelectedCellsTo("")),
|
||||
"ArrowDown" === t.key &&
|
||||
(t.preventDefault(),
|
||||
this._moveCursor({ y: 1 }, t.shiftKey)),
|
||||
"ArrowUp" === t.key &&
|
||||
(t.preventDefault(),
|
||||
this._moveCursor({ y: -1 }, t.shiftKey)),
|
||||
"ArrowLeft" === t.key &&
|
||||
(t.preventDefault(),
|
||||
this._moveCursor({ x: -1 }, t.shiftKey)),
|
||||
"ArrowRight" === t.key &&
|
||||
(t.preventDefault(),
|
||||
this._moveCursor({ x: 1 }, t.shiftKey))),
|
||||
1 !== t.key.length ||
|
||||
this._editing ||
|
||||
this._changeSelectedCellsStyle(() => {
|
||||
const { x: t, y: e } = this._focus;
|
||||
this._startEditing({ x: t, y: e }),
|
||||
(this._getCell(t, e).firstChild.value = "");
|
||||
})));
|
||||
}),
|
||||
r(this, "_selecting", !1),
|
||||
r(this, "_selectionStart", null),
|
||||
r(this, "_selectionEnd", null),
|
||||
r(this, "_selection", { rx: [0, 0], ry: [0, 0] }),
|
||||
r(this, "_editing", null),
|
||||
r(this, "_focus", null),
|
||||
r(this, "mousedown", (t) => {
|
||||
if (!this.mobile) {
|
||||
if (
|
||||
3 === t.which &&
|
||||
!this._editing &&
|
||||
this._selectionSize()
|
||||
) {
|
||||
let t = new Range();
|
||||
const { rx: e, ry: i } = this._selection;
|
||||
return (
|
||||
t.setStart(this._getCell(e[0], i[0]), 0),
|
||||
t.setEnd(this._getCell(e[0], i[0]), 1),
|
||||
this.cwd.getSelection().removeAllRanges(),
|
||||
void this.cwd.getSelection().addRange(t)
|
||||
);
|
||||
}
|
||||
this._changeSelectedCellsStyle(() => {
|
||||
(this.tbody.style.userSelect = "none"),
|
||||
(this._selectionEnd =
|
||||
this._selectionStart =
|
||||
this._focus =
|
||||
this._getCoords(t)),
|
||||
(this._selecting = !0);
|
||||
});
|
||||
}
|
||||
}),
|
||||
r(this, "mouseenter", (t) => {
|
||||
this.mobile ||
|
||||
(this._selecting &&
|
||||
this._changeSelectedCellsStyle(() => {
|
||||
this._selectionEnd = this._getCoords(t);
|
||||
}));
|
||||
}),
|
||||
r(this, "_lastMouseUp", null),
|
||||
r(this, "_lastMouseUpTarget", null),
|
||||
r(this, "mouseup", (t) => {
|
||||
this.mobile ||
|
||||
(3 !== t.which &&
|
||||
this._selecting &&
|
||||
this._changeSelectedCellsStyle(() => {
|
||||
(this._selectionEnd = this._getCoords(t)),
|
||||
this._endSelection(),
|
||||
this._lastMouseUp &&
|
||||
this._lastMouseUp > Date.now() - 300 &&
|
||||
this._lastMouseUpTarget.x ===
|
||||
this._selectionEnd.x &&
|
||||
this._lastMouseUpTarget.y ===
|
||||
this._selectionEnd.y &&
|
||||
this._startEditing(this._selectionEnd),
|
||||
(this._lastMouseUp = Date.now()),
|
||||
(this._lastMouseUpTarget = this._selectionEnd);
|
||||
}));
|
||||
}),
|
||||
r(this, "mouseleave", (t) => {
|
||||
t.target === this.tbody &&
|
||||
this._selecting &&
|
||||
this._endSelection();
|
||||
}),
|
||||
r(this, "touchstart", (t) => {
|
||||
this._editing || ((this.mobile = !0), (this.moved = !1));
|
||||
}),
|
||||
r(this, "touchend", (t) => {
|
||||
this.mobile &&
|
||||
(this._editing ||
|
||||
this.moved ||
|
||||
(this._changeSelectedCellsStyle(() => {
|
||||
this._selectionEnd =
|
||||
this._selectionStart =
|
||||
this._focus =
|
||||
this._getCoords(t);
|
||||
}),
|
||||
this._startEditing(this._focus)));
|
||||
}),
|
||||
r(this, "touchmove", (t) => {
|
||||
this.mobile && (this.moved = !0);
|
||||
}),
|
||||
r(this, "_stopEditing", () => {
|
||||
if (!this._editing) return;
|
||||
const { x: t, y: e } = this._editing,
|
||||
i = this._getCell(t, e);
|
||||
(i.style.width = ""), (i.style.height = "");
|
||||
const s = i.firstChild;
|
||||
s.removeEventListener("blur", this._stopEditing),
|
||||
s.removeEventListener("keydown", this._blurIfEnter),
|
||||
this._setVal(t, e, s.value),
|
||||
this._onDataChanged(),
|
||||
i.removeChild(s),
|
||||
(this._editing = null),
|
||||
this._renderTDContent(i, t, e);
|
||||
}),
|
||||
r(this, "_blurIfEnter", (t) => {
|
||||
13 === t.keyCode && (this._stopEditing(), t.preventDefault());
|
||||
}),
|
||||
r(this, "_restyle", ({ x: t, y: e }) => {
|
||||
const i = this._getCell(t, e);
|
||||
i.className = this._classNames(t, e);
|
||||
const s = a(this.checkResults.titles, t, e);
|
||||
s ? i.setAttribute("title", s) : i.removeAttribute("title");
|
||||
}),
|
||||
r(this, "_refreshDisplayedValue", ({ x: t, y: e }) => {
|
||||
const i = this._getCell(t, e).firstChild;
|
||||
"DIV" === i.tagName &&
|
||||
(i.textContent = this._divContent(t, e)),
|
||||
this._restyle({ x: t, y: e });
|
||||
}),
|
||||
this._saveConstructorOptions(t),
|
||||
this._setupDom(),
|
||||
this._replaceDataWithArray(t.data),
|
||||
this._incrementToFit({
|
||||
x: this.columns.length - 1,
|
||||
y: this._options.minRows - 1,
|
||||
}),
|
||||
this._fillScrollSpace();
|
||||
}
|
||||
_runChecks(t) {
|
||||
const { titles: e, classNames: i } = this.checks(t);
|
||||
this.checkResults = { titles: e, classNames: i };
|
||||
}
|
||||
_saveConstructorOptions({
|
||||
data: t = [],
|
||||
node: e = null,
|
||||
onChange: i = null,
|
||||
minRows: s = 1,
|
||||
maxRows: n = 1 / 0,
|
||||
css: o = "",
|
||||
width: r = "100%",
|
||||
height: h = "80vh",
|
||||
columns: l,
|
||||
checks: a,
|
||||
}) {
|
||||
if (
|
||||
((this.columns = l),
|
||||
(this.checks = a || (() => ({}))),
|
||||
this._runChecks(t),
|
||||
!e)
|
||||
)
|
||||
throw new Error(
|
||||
"You need to pass a node argument to Importabular, like this : new Importabular({node: document.body})"
|
||||
);
|
||||
(this._parent = e),
|
||||
(this._options = {
|
||||
onChange: i,
|
||||
minRows: s,
|
||||
maxRows: n,
|
||||
css:
|
||||
"\nhtml{\n -ms-overflow-style: none;\n scrollbar-width: none;\n}\n::-webkit-scrollbar {\n width: 0;\n height:0;\n}\n*{\n box-sizing: border-box;\n}\nbody{\n padding: 0; \n margin: 0;\n}\ntable{\n border-spacing: 0;\n background: white;\n border: 1px solid #ddd;\n border-width: 0 1px 1px 0;\n font-size: 16px;\n font-family: sans-serif;\n border-collapse: separate;\n min-width:100%;\n}\ntd, th{\n padding:0;\n border: 1px solid;\n border-color: #ddd transparent transparent #ddd; \n}\ntd.selected.multi:not(.editing){\n background:#d7f2f9;\n} \ntd.focus:not(.editing){\n border-color: black;\n} \ntd>*, th>*{\n border:none;\n padding:10px;\n min-width:100px;\n min-height: 40px;\n font:inherit;\n line-height: 20px;\n color:inherit;\n white-space: normal;\n}\ntd>div::selection {\n color: none;\n background: none;\n}\n\n.placeholder div{\n user-select:none;\n color:rgba(0,0,0,0.2);\n}\n*[title] div{cursor:help;}\nth{text-align:left;}\n" +
|
||||
o,
|
||||
}),
|
||||
(this._iframeStyle = {
|
||||
width: r,
|
||||
height: h,
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
});
|
||||
}
|
||||
_fitBounds({ x: t, y: e }) {
|
||||
return (
|
||||
t >= 0 &&
|
||||
t < this.columns.length &&
|
||||
e >= 0 &&
|
||||
e < this._options.maxRows
|
||||
);
|
||||
}
|
||||
_fillScrollSpace() {
|
||||
const t = Math.ceil(this.iframe.contentWindow.innerHeight / 40),
|
||||
e = Math.ceil(this.iframe.contentWindow.innerWidth / 100);
|
||||
this._incrementToFit({ x: e - 1, y: t - 1 });
|
||||
}
|
||||
getData() {
|
||||
return this._data._toArr(this._width, this._height);
|
||||
}
|
||||
_onDataChanged() {
|
||||
const t = this.getData();
|
||||
this._options.onChange && this._options.onChange(t),
|
||||
this._runChecks(t),
|
||||
this._restyleAll();
|
||||
}
|
||||
_renderTDContent(t, e, i) {
|
||||
const s = document.createElement("div");
|
||||
t.setAttribute("x", e.toString()),
|
||||
t.setAttribute("y", i.toString());
|
||||
const n = this._divContent(e, i);
|
||||
n ? (s.textContent = n) : (s.innerHTML = " "),
|
||||
t.appendChild(s),
|
||||
this._restyle({ x: e, y: i });
|
||||
}
|
||||
_divContent(t, e) {
|
||||
return this._getVal(t, e) || this.columns[t].placeholder;
|
||||
}
|
||||
_setupDom() {
|
||||
const t = document.createElement("iframe");
|
||||
(this.iframe = t), this._parent.appendChild(t);
|
||||
const e = t.contentWindow.document;
|
||||
(this.cwd = e),
|
||||
e.open(),
|
||||
e.write(
|
||||
`<html lang="${navigator.language}"><body><style>${this._options.css}</style></body></html>`
|
||||
),
|
||||
e.close(),
|
||||
Object.assign(t.style, this._iframeStyle);
|
||||
const i = document.createElement("table"),
|
||||
s = document.createElement("tbody"),
|
||||
n = document.createElement("THEAD"),
|
||||
o = document.createElement("TR");
|
||||
n.appendChild(o),
|
||||
this.columns.forEach((t) => {
|
||||
const e = document.createElement("TH"),
|
||||
i = document.createElement("div");
|
||||
(i.innerHTML = t.label),
|
||||
t.title && e.setAttribute("title", t.title),
|
||||
e.appendChild(i),
|
||||
o.appendChild(e);
|
||||
}),
|
||||
i.appendChild(n),
|
||||
i.appendChild(s),
|
||||
e.body.appendChild(i),
|
||||
(this.tbody = s),
|
||||
(this.table = i);
|
||||
for (let t = 0; t < this._height; t++) {
|
||||
const e = document.createElement("tr");
|
||||
s.appendChild(e);
|
||||
for (let i = 0; i < this._width; i++) this._addCell(e, i, t);
|
||||
}
|
||||
h.forEach((t) => e.addEventListener(t, this[t], !0));
|
||||
}
|
||||
destroy() {
|
||||
this._destroyEditing(),
|
||||
h.forEach((t) => this.cwd.removeEventListener(t, this[t], !0)),
|
||||
this.iframe.parentElement.removeChild(this.iframe);
|
||||
}
|
||||
_addCell(t, e, i) {
|
||||
const s = document.createElement("td");
|
||||
t.appendChild(s), this._renderTDContent(s, e, i);
|
||||
}
|
||||
_incrementHeight() {
|
||||
if (!this._fitBounds({ x: 0, y: this._height })) return !1;
|
||||
this._height++;
|
||||
const t = this._height - 1,
|
||||
e = document.createElement("tr");
|
||||
this.tbody.appendChild(e);
|
||||
for (let i = 0; i < this._width; i++) this._addCell(e, i, t);
|
||||
return !0;
|
||||
}
|
||||
_incrementWidth() {
|
||||
if (!this._fitBounds({ x: this._width, y: 0 })) return !1;
|
||||
this._width++;
|
||||
const t = this._width - 1;
|
||||
return (
|
||||
Array.prototype.forEach.call(this.tbody.children, (e, i) => {
|
||||
this._addCell(e, t, i);
|
||||
}),
|
||||
!0
|
||||
);
|
||||
}
|
||||
_incrementToFit({ x: t, y: e }) {
|
||||
for (; t > this._width - 1 && this._incrementWidth(); );
|
||||
for (; e > this._height - 1 && this._incrementHeight(); );
|
||||
}
|
||||
_getSelectionAsArray() {
|
||||
const { rx: t, ry: e } = this._selection;
|
||||
if (t[0] === t[1]) return null;
|
||||
const i = t[1] - t[0],
|
||||
s = e[1] - e[0],
|
||||
n = [];
|
||||
for (let o = 0; o < s; o++) {
|
||||
n.push([]);
|
||||
for (let s = 0; s < i; s++)
|
||||
n[o].push(this._getVal(t[0] + s, e[0] + o));
|
||||
}
|
||||
return n;
|
||||
}
|
||||
_setAllSelectedCellsTo(t) {
|
||||
this._forSelectionCoord(this._selection, ({ x: e, y: i }) =>
|
||||
this._setVal(e, i, t)
|
||||
),
|
||||
this._onDataChanged(),
|
||||
this._forSelectionCoord(
|
||||
this._selection,
|
||||
this._refreshDisplayedValue
|
||||
);
|
||||
}
|
||||
_moveCursor({ x: t = 0, y: e = 0 }, i) {
|
||||
const s = i ? this._selectionEnd : this._selectionStart,
|
||||
n = { x: s.x + t, y: s.y + e };
|
||||
this._fitBounds(n) &&
|
||||
(this._stopEditing(),
|
||||
this._incrementToFit(n),
|
||||
this._changeSelectedCellsStyle(() => {
|
||||
i
|
||||
? (this._selectionEnd = n)
|
||||
: (this._selectionStart =
|
||||
this._selectionEnd =
|
||||
this._focus =
|
||||
n);
|
||||
}),
|
||||
this._scrollIntoView(n));
|
||||
}
|
||||
_tabCursorInSelection(t, e = 1) {
|
||||
let { x: i, y: s } = this._focus || { x: 0, y: 0 };
|
||||
const o = this._selectionSize(),
|
||||
{ rx: r, ry: h } =
|
||||
o > 1
|
||||
? this._selection
|
||||
: {
|
||||
rx: [0, this.columns.length],
|
||||
ry: [0, this._options.maxRows],
|
||||
};
|
||||
let l;
|
||||
if (t) l = n(i, s, e, r[0], r[1] - 1, h[0], h[1] - 1);
|
||||
else {
|
||||
const t = n(s, i, e, h[0], h[1] - 1, r[0], r[1] - 1);
|
||||
l = { x: t.y, y: t.x };
|
||||
}
|
||||
this._fitBounds(l) &&
|
||||
(this._stopEditing(),
|
||||
this._incrementToFit(l),
|
||||
this._changeSelectedCellsStyle(() => {
|
||||
(this._focus = l),
|
||||
o <= 1 && (this._selectionStart = this._selectionEnd = l);
|
||||
}),
|
||||
this._scrollIntoView(l));
|
||||
}
|
||||
_scrollIntoView({ x: t, y: e }) {
|
||||
this._getCell(t, e).scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
_endSelection() {
|
||||
(this._selecting = !1), (this.tbody.style.userSelect = "");
|
||||
}
|
||||
_startEditing({ x: t, y: e }) {
|
||||
this._editing = { x: t, y: e };
|
||||
const i = this._getCell(t, e),
|
||||
s = i.getBoundingClientRect(),
|
||||
n = i.firstChild.getBoundingClientRect();
|
||||
i.removeChild(i.firstChild);
|
||||
const o = document.createElement("input");
|
||||
(o.type = "text"),
|
||||
(o.value = this._getVal(t, e)),
|
||||
i.appendChild(o),
|
||||
Object.assign(i.style, {
|
||||
width: s.width - 2,
|
||||
height: s.height,
|
||||
}),
|
||||
Object.assign(o.style, {
|
||||
width: `${n.width}px`,
|
||||
height: `${n.height}px`,
|
||||
}),
|
||||
o.focus(),
|
||||
o.addEventListener("blur", this._stopEditing),
|
||||
o.addEventListener("keydown", this._blurIfEnter);
|
||||
}
|
||||
_destroyEditing() {
|
||||
if (this._editing) {
|
||||
const { x: t, y: e } = this._editing,
|
||||
i = this._getCell(t, e).firstChild;
|
||||
i.removeEventListener("blur", this._stopEditing),
|
||||
i.removeEventListener("keydown", this._blurIfEnter);
|
||||
}
|
||||
}
|
||||
_revertEdit() {
|
||||
if (!this._editing) return;
|
||||
const { x: t, y: e } = this._editing;
|
||||
this._getCell(t, e).firstChild.value = this._getVal(t, e);
|
||||
}
|
||||
_changeSelectedCellsStyle(t) {
|
||||
const e = this._selection;
|
||||
t(),
|
||||
(this._selection = this._getSelectionCoords()),
|
||||
this._forSelectionCoord(e, this._restyle),
|
||||
this._forSelectionCoord(this._selection, this._restyle);
|
||||
}
|
||||
_getSelectionCoords() {
|
||||
if (!this._selectionStart) return { rx: [0, 0], ry: [0, 0] };
|
||||
let t = [this._selectionStart.x, this._selectionEnd.x];
|
||||
t[0] > t[1] && t.reverse();
|
||||
let e = [this._selectionStart.y, this._selectionEnd.y];
|
||||
return (
|
||||
e[0] > e[1] && e.reverse(),
|
||||
{ rx: [t[0], t[1] + 1], ry: [e[0], e[1] + 1] }
|
||||
);
|
||||
}
|
||||
_forSelectionCoord({ rx: t, ry: e }, i) {
|
||||
for (let s = t[0]; s < t[1]; s++)
|
||||
for (let t = e[0]; t < e[1]; t++)
|
||||
this._fitBounds({ x: s, y: t }) && i({ x: s, y: t });
|
||||
}
|
||||
_restyleAll() {
|
||||
for (var t = 0; t < this._width; t++)
|
||||
for (var e = 0; e < this._height; e++)
|
||||
this._restyle({ x: t, y: e });
|
||||
}
|
||||
_selectionSize() {
|
||||
const { rx: t, ry: e } = this._selection;
|
||||
return (t[1] - t[0]) * (e[1] - e[0]);
|
||||
}
|
||||
_classNames(t, e) {
|
||||
const { rx: i, ry: s } = this._selection;
|
||||
let n = "";
|
||||
return (
|
||||
t >= i[0] &&
|
||||
t < i[1] &&
|
||||
e >= s[0] &&
|
||||
e < s[1] &&
|
||||
((n += " selected"),
|
||||
this._selectionSize() > 1 && (n += " multi")),
|
||||
this._focus &&
|
||||
this._focus.x === t &&
|
||||
this._focus.y === e &&
|
||||
(n += " focus"),
|
||||
this._editing &&
|
||||
t === this._editing.x &&
|
||||
e === this._editing.y &&
|
||||
(n += " editing"),
|
||||
this._getVal(t, e) || (n += " placeholder"),
|
||||
(n += " " + a(this.checkResults.classNames, t, e)),
|
||||
n
|
||||
);
|
||||
}
|
||||
_getCoords(t) {
|
||||
let e = t.target;
|
||||
for (; !e.getAttribute("x") && e.parentElement; )
|
||||
e = e.parentElement;
|
||||
return {
|
||||
x: parseInt(e.getAttribute("x")) || 0,
|
||||
y: parseInt(e.getAttribute("y")) || 0,
|
||||
};
|
||||
}
|
||||
setData(t) {
|
||||
this._data._clear(), this._replaceDataWithArray(t);
|
||||
for (let t = 0; t < this._width; t++)
|
||||
for (let e = 0; e < this._height; e++)
|
||||
this._refreshDisplayedValue({ x: t, y: e });
|
||||
}
|
||||
_replaceDataWithArray(t = [[]]) {
|
||||
t.forEach((t, e) => {
|
||||
t.forEach((t, i) => {
|
||||
this._setVal(i, e, t);
|
||||
});
|
||||
});
|
||||
}
|
||||
_setVal(t, e, i) {
|
||||
this._fitBounds({ x: t, y: e }) &&
|
||||
(this._data._setVal(t, e, i),
|
||||
this._incrementToFit({ x: t + 1, y: e + 1 }),
|
||||
this._refreshDisplayedValue({ x: t, y: e }));
|
||||
}
|
||||
_getVal(t, e) {
|
||||
return this._data._getVal(t, e);
|
||||
}
|
||||
_getCell(t, e) {
|
||||
return this.tbody.children[e].children[t];
|
||||
}
|
||||
}
|
||||
function a(t, e, i) {
|
||||
return (t && t[i] && t[i][e]) || "";
|
||||
}
|
||||
},
|
||||
},
|
||||
e = {};
|
||||
function i(s) {
|
||||
if (e[s]) return e[s].exports;
|
||||
var n = (e[s] = { exports: {} });
|
||||
return t[s](n, n.exports, i), n.exports;
|
||||
}
|
||||
return (
|
||||
(i.d = (t, e) => {
|
||||
for (var s in e)
|
||||
i.o(e, s) &&
|
||||
!i.o(t, s) &&
|
||||
Object.defineProperty(t, s, { enumerable: !0, get: e[s] });
|
||||
}),
|
||||
(i.o = (t, e) => Object.prototype.hasOwnProperty.call(t, e)),
|
||||
i(103)
|
||||
);
|
||||
})().default;
|
||||
});
|
|
@ -77,6 +77,7 @@
|
|||
}
|
||||
|
||||
// EDIT BUTTON RELATED (TODO move to partials)
|
||||
.quote-button {
|
||||
flex-direction: row;
|
||||
.open-popup-link {
|
||||
display: inline;
|
||||
margin-inline: 0.25em;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import { apiInitializer } from "discourse/lib/api";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import I18n from "I18n";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import { create } from "virtual-dom";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
|
||||
export default apiInitializer("0.11.1", (api) => {
|
||||
const site = api.container.lookup("site:main");
|
||||
const currentUser = api.getCurrentUser();
|
||||
|
||||
function createButton() {
|
||||
const openPopupBtn = document.createElement("button");
|
||||
openPopupBtn.classList.add(
|
||||
"open-popup-link",
|
||||
"btn-default",
|
||||
"btn",
|
||||
"btn-icon-text"
|
||||
);
|
||||
const expandIcon = create(
|
||||
iconNode("pencil-alt", { class: "edit-table-icon" })
|
||||
);
|
||||
const openPopupText = document.createTextNode(
|
||||
I18n.t(themePrefix("discourse_table_builder.edit.btn_edit"))
|
||||
);
|
||||
openPopupBtn.append(expandIcon, openPopupText);
|
||||
return openPopupBtn;
|
||||
}
|
||||
|
||||
function generateModal(event) {
|
||||
const table = event.target.nextElementSibling;
|
||||
const tempTable = table.cloneNode(true);
|
||||
const postId = this.id;
|
||||
|
||||
return ajax(`/posts/${postId}`, {
|
||||
type: "GET",
|
||||
cache: false,
|
||||
})
|
||||
.then((result) => {
|
||||
const attrs = {
|
||||
widget: this,
|
||||
raw: result.raw,
|
||||
};
|
||||
|
||||
showModal("table-editor-modal", {
|
||||
model: attrs,
|
||||
}).set("tableHtml", tempTable);
|
||||
return result.raw;
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
|
||||
function generatePopups(tables, attrs) {
|
||||
tables.forEach((table) => {
|
||||
if (site.isMobileDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const popupBtn = createButton();
|
||||
table.parentNode.classList.add("fullscreen-table-wrapper");
|
||||
table.parentNode.insertBefore(popupBtn, table);
|
||||
popupBtn.addEventListener("click", generateModal.bind(attrs), false);
|
||||
});
|
||||
}
|
||||
|
||||
api.decorateCookedElement(
|
||||
(post, helper) => {
|
||||
const postOwner = helper.widget.attrs.username;
|
||||
|
||||
if (postOwner !== currentUser.username) {
|
||||
return;
|
||||
}
|
||||
|
||||
schedule("afterRender", () => {
|
||||
const tables = post.querySelectorAll("table");
|
||||
generatePopups(tables, helper.widget.attrs);
|
||||
});
|
||||
},
|
||||
{
|
||||
onlyStream: true,
|
||||
id: "edit-table",
|
||||
}
|
||||
);
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
{{#if context._canEditPost}}
|
||||
<DButton
|
||||
@class="btn-flat"
|
||||
@title={{theme-prefix "discourse_table_builder.edit.btn_edit"}}
|
||||
@label={{theme-prefix "discourse_table_builder.edit.btn_edit"}}
|
||||
@icon="pencil-alt"
|
||||
@action={{action "showEditTableModal"}}
|
||||
/>
|
||||
{{/if}}
|
|
@ -1,32 +0,0 @@
|
|||
import { action } from "@ember/object";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
|
||||
export default {
|
||||
setupComponent(args, component) {
|
||||
console.log(args, component, this);
|
||||
},
|
||||
|
||||
@action
|
||||
showEditTableModal() {
|
||||
const selection = window.getSelection();
|
||||
const selectedTable = selection.focusNode;
|
||||
const { context } = this.args;
|
||||
|
||||
// TODO: Improve table checking logic AND change so it only shows button when selected content is a table
|
||||
if (
|
||||
selectedTable.nodeName === "DIV" &&
|
||||
selectedTable.classList.contains("md-table")
|
||||
) {
|
||||
// ? TODO: simply pass quoteState object only?
|
||||
const attrs = {
|
||||
table: context.quoteState.buffer,
|
||||
postId: context.quoteState.postId,
|
||||
};
|
||||
showModal("table-editor-modal", {
|
||||
model: attrs,
|
||||
});
|
||||
} else {
|
||||
console.warn("This is not a table:", selection);
|
||||
}
|
||||
},
|
||||
};
|
|
@ -1,9 +1,46 @@
|
|||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { A } from "@ember/array";
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import { tableToObj } from "../lib/utilities";
|
||||
|
||||
export default class extends Controller {
|
||||
onShow() {
|
||||
// ? TODO move to component (read about not allowing Controllers to do DOM manipulation)
|
||||
this._super(...arguments);
|
||||
|
||||
loadScript(settings.theme_uploads.importabular).then(() => {
|
||||
this.buildTable(this.tableHtml);
|
||||
});
|
||||
}
|
||||
|
||||
buildTable(table) {
|
||||
const tableObject = tableToObj(table);
|
||||
const headings = [];
|
||||
const tableData = [];
|
||||
tableObject.forEach((object) => {
|
||||
// Build Headings
|
||||
if (!headings.includes(...Object.keys(object))) {
|
||||
headings.push(...Object.keys(object));
|
||||
}
|
||||
|
||||
tableData.push([...Object.values(object)]);
|
||||
});
|
||||
|
||||
const columns = headings.map((heading) => {
|
||||
return {
|
||||
label: heading,
|
||||
};
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-unused-vars, no-undef
|
||||
const sheet = new Importabular({
|
||||
node: document.getElementById("table-editor-spreadsheet"),
|
||||
columns,
|
||||
data: tableData,
|
||||
width: "100vw",
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
cancelTableEdit() {
|
||||
this.send("closeModal");
|
||||
|
@ -12,7 +49,6 @@ export default class extends Controller {
|
|||
@action
|
||||
editTable() {
|
||||
// TODO: insert table edit submission logic
|
||||
console.log("Table has been successfully edited");
|
||||
this.send("closeModal");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
// SRC: https://gist.github.com/mattheo-gist/4151867
|
||||
/* eslint-disable */
|
||||
|
||||
export function tableToObj(table) {
|
||||
var rows = table.rows;
|
||||
var propCells = rows[0].cells;
|
||||
var propNames = [];
|
||||
var results = [];
|
||||
var obj, row, cells;
|
||||
|
||||
// Use the first row for the property names
|
||||
// Could use a header section but result is the same if
|
||||
// there is only one header row
|
||||
for (var i = 0, iLen = propCells.length; i < iLen; i++) {
|
||||
propNames.push(propCells[i].textContent || propCells[i].innerText);
|
||||
}
|
||||
|
||||
// Use the rows for data
|
||||
// Could use tbody rows here to exclude header & footer
|
||||
// but starting from 1 gives required result
|
||||
for (var j = 1, jLen = rows.length; j < jLen; j++) {
|
||||
cells = rows[j].cells;
|
||||
obj = {};
|
||||
|
||||
for (var k = 0; k < iLen; k++) {
|
||||
obj[propNames[k]] = cells[k].textContent || cells[k].innerText;
|
||||
}
|
||||
results.push(obj);
|
||||
}
|
||||
return results;
|
||||
}
|
|
@ -3,7 +3,10 @@
|
|||
@class="table-editor-modal"
|
||||
>
|
||||
{{! TODO: Parse .md to json and fill table }}
|
||||
{{model.table}}
|
||||
{{! TODO: Fix instances where multiple tables in a single post }}
|
||||
|
||||
{{! TODO better id name }}
|
||||
<div class="table-spreadsheet-container" id="table-editor-spreadsheet"></div>
|
||||
|
||||
</DModalBody>
|
||||
|
||||
|
@ -11,7 +14,7 @@
|
|||
<DButton
|
||||
@class="btn-primary btn-edit-table"
|
||||
@label={{theme-prefix "discourse_table_builder.edit.modal.create"}}
|
||||
@icon="plus"
|
||||
@icon="pencil-alt"
|
||||
@action={{action "editTable"}}
|
||||
/>
|
||||
|
||||
|
|
|
@ -22,6 +22,6 @@ en:
|
|||
modal:
|
||||
title: "Edit Table"
|
||||
cancel: "cancel"
|
||||
create: "Edit Table"
|
||||
create: "Update Table"
|
||||
theme_metadata:
|
||||
description: "Adds a button to the composer to easily build tables in markdown"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
extend_content_security_policy:
|
||||
type: list
|
||||
default: "script_src: http://localhost:4200/"
|
Loading…
Reference in New Issue