DEV: Port `discourse-table-builder` theme component to core (#24441)

This commit is contained in:
Keegan George 2023-11-30 10:54:29 -08:00 committed by GitHub
parent bcca1692c6
commit d2b53ccac2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 34592 additions and 26 deletions

View File

@ -0,0 +1,389 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { schedule } from "@ember/runloop";
import { inject as service } from "@ember/service";
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
import DButton from "discourse/components/d-button";
import DModal from "discourse/components/d-modal";
import DModalCancel from "discourse/components/d-modal-cancel";
import TextField from "discourse/components/text-field";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import loadScript from "discourse/lib/load-script";
import {
arrayToTable,
findTableRegex,
tokenRange,
} from "discourse/lib/utilities";
import i18n from "discourse-common/helpers/i18n";
import I18n from "discourse-i18n";
import DTooltip from "float-kit/components/d-tooltip";
export default class SpreadsheetEditor extends Component {
@service dialog;
@tracked showEditReason = false;
@tracked loading = null;
spreadsheet = null;
defaultColWidth = 150;
isEditingTable = !!this.args.model.tableTokens;
get modalAttributes() {
if (this.isEditingTable) {
return {
title: "table_builder.edit.modal.title",
insertTable: {
title: "table_builder.edit.modal.create",
icon: "pencil-alt",
},
};
} else {
return {
title: "table_builder.modal.title",
insertTable: {
title: "table_builder.modal.create",
icon: "plus",
},
};
}
}
@action
createSpreadsheet(spreadsheet) {
this.spreadsheet = spreadsheet;
schedule("afterRender", () => {
this.loadLibraries().then(() => {
if (this.isEditingTable) {
this.buildPopulatedTable(this.args.model.tableTokens);
} else {
this.buildNewTable();
}
});
});
}
@action
showEditReasonField() {
this.showEditReason = !this.showEditReason;
}
@action
interceptCloseModal() {
if (this._hasChanges()) {
this.dialog.yesNoConfirm({
message: I18n.t("table_builder.modal.confirm_close"),
didConfirm: () => this.args.closeModal(),
});
} else {
this.args.closeModal();
}
}
@action
insertTable() {
const updatedHeaders = this.spreadsheet.getHeaders().split(","); // keys
const updatedData = this.spreadsheet.getData(); // values
const markdownTable = this.buildTableMarkdown(updatedHeaders, updatedData);
if (!this.isEditingTable) {
this.args.model.toolbarEvent.addText(markdownTable);
return this.args.closeModal();
} else {
return this.updateTable(markdownTable);
}
}
_hasChanges() {
if (this.isEditingTable) {
const originalSpreadsheetData = this.extractTableContent(
tokenRange(this.args.model.tableTokens, "tr_open", "tr_close")
);
const currentHeaders = this.spreadsheet.getHeaders().split(",");
const currentRows = this.spreadsheet.getData();
const currentSpreadsheetData = currentHeaders.concat(currentRows.flat());
return (
JSON.stringify(currentSpreadsheetData) !==
JSON.stringify(originalSpreadsheetData)
);
} else {
return this.spreadsheet
.getData()
.flat()
.some((element) => element !== "");
}
}
loadLibraries() {
this.loading = true;
return loadScript("/javascripts/jsuites/jsuites.js")
.then(() => {
return loadScript("/javascripts/jspreadsheet/jspreadsheet.js");
})
.finally(() => (this.loading = false));
}
buildNewTable() {
const data = [
["", "", ""],
["", "", ""],
["", "", ""],
["", "", ""],
["", "", ""],
["", "", ""],
];
const columns = [
{
title: I18n.t("table_builder.default_header.col_1"),
width: this.defaultColWidth,
},
{
title: I18n.t("table_builder.default_header.col_2"),
width: this.defaultColWidth,
},
{
title: I18n.t("table_builder.default_header.col_3"),
width: this.defaultColWidth,
},
{
title: I18n.t("table_builder.default_header.col_4"),
width: this.defaultColWidth,
},
];
return this.buildSpreadsheet(data, columns);
}
extractTableContent(data) {
return data
.flat()
.filter((t) => t.type === "inline")
.map((t) => t.content);
}
buildPopulatedTable(tableTokens) {
const contentRows = tokenRange(tableTokens, "tr_open", "tr_close");
const rows = [];
let headings;
const rowWidthFactor = 8;
contentRows.forEach((row, index) => {
if (index === 0) {
// headings
headings = this.extractTableContent(row).map((heading) => {
return {
title: heading,
width: Math.max(
heading.length * rowWidthFactor,
this.defaultColWidth
),
align: "left",
};
});
} else {
// rows:
rows.push(this.extractTableContent(row));
}
});
return this.buildSpreadsheet(rows, headings);
}
buildSpreadsheet(data, columns, opts = {}) {
const postNumber = this.args.model?.post_number;
const exportFileName = postNumber
? `post-${postNumber}-table-export`
: `post-table-export`;
// eslint-disable-next-line no-undef
this.spreadsheet = jspreadsheet(this.spreadsheet, {
data,
columns,
defaultColAlign: "left",
wordWrap: true,
csvFileName: exportFileName,
text: this.localeMapping(),
...opts,
});
}
buildUpdatedPost(tableIndex, raw, newRaw) {
const tableToEdit = raw.match(findTableRegex());
let editedTable;
if (tableToEdit.length) {
editedTable = raw.replace(tableToEdit[tableIndex], newRaw);
} else {
return raw;
}
// replace null characters
editedTable = editedTable.replace(/\0/g, "\ufffd");
return editedTable;
}
updateTable(markdownTable) {
const tableIndex = this.args.model.tableIndex;
const postId = this.args.model.post.id;
const newRaw = markdownTable;
const editReason =
this.editReason || I18n.t("table_builder.edit.default_edit_reason");
const raw = this.args.model.post.raw;
const newPostRaw = this.buildUpdatedPost(tableIndex, raw, newRaw);
return this.sendTableUpdate(postId, newPostRaw, editReason);
}
sendTableUpdate(postId, raw, edit_reason) {
return ajax(`/posts/${postId}.json`, {
type: "PUT",
data: {
post: {
raw,
edit_reason,
},
},
})
.catch(popupAjaxError)
.finally(() => {
this.args.closeModal();
});
}
buildTableMarkdown(headers, data) {
const table = [];
data.forEach((row) => {
const result = {};
headers.forEach((_key, index) => {
const columnKey = `col${index}`;
return (result[columnKey] = row[index]);
});
table.push(result);
});
return arrayToTable(table, headers);
}
localeMapping() {
return {
noRecordsFound: prefixedLocale("no_records_found"),
show: prefixedLocale("show"),
entries: prefixedLocale("entries"),
insertANewColumnBefore: prefixedLocale("context_menu.col.before"),
insertANewColumnAfter: prefixedLocale("context_menu.col.after"),
deleteSelectedColumns: prefixedLocale("context_menu.col.delete"),
renameThisColumn: prefixedLocale("context_menu.col.rename"),
orderAscending: prefixedLocale("context_menu.order.ascending"),
orderDescending: prefixedLocale("context_menu.order.descending"),
insertANewRowBefore: prefixedLocale("context_menu.row.before"),
insertANewRowAfter: prefixedLocale("context_menu.row.after"),
deleteSelectedRows: prefixedLocale("context_menu.row.delete"),
copy: prefixedLocale("context_menu.copy"),
paste: prefixedLocale("context_menu.paste"),
saveAs: prefixedLocale("context_menu.save"),
about: prefixedLocale("about"),
areYouSureToDeleteTheSelectedRows: prefixedLocale(
"prompts.delete_selected_rows"
),
areYouSureToDeleteTheSelectedColumns: prefixedLocale(
"prompts.delete_selected_cols"
),
thisActionWillDestroyAnyExistingMergedCellsAreYouSure: prefixedLocale(
"prompts.will_destroy_merged_cells"
),
thisActionWillClearYourSearchResultsAreYouSure: prefixedLocale(
"prompts.will_clear_search_results"
),
thereIsAConflictWithAnotherMergedCell: prefixedLocale(
"prompts.conflict_with_merged_cells"
),
invalidMergeProperties: prefixedLocale("invalid_merge_props"),
cellAlreadyMerged: prefixedLocale("cells_already_merged"),
noCellsSelected: prefixedLocale("no_cells_selected"),
};
}
<template>
<DModal
@title={{i18n this.modalAttributes.title}}
@closeModal={{this.interceptCloseModal}}
class="insert-table-modal"
>
<:body>
<ConditionalLoadingSpinner @condition={{this.loading}}>
<div
{{didInsert this.createSpreadsheet}}
tabindex="1"
class="jexcel_container"
></div>
</ConditionalLoadingSpinner>
</:body>
<:footer>
<div class="primary-actions">
<DButton
@class="btn-insert-table"
@label={{this.modalAttributes.insertTable.title}}
@icon={{this.modalAttributes.insertTable.icon}}
@action={{this.insertTable}}
/>
<DModalCancel @close={{this.interceptCloseModal}} />
</div>
<div class="secondary-actions">
{{#if this.isEditingTable}}
<div class="edit-reason">
<DButton
@icon="info-circle"
@title="table_builder.edit.modal.trigger_reason"
@action={{this.showEditReasonField}}
@class="btn-edit-reason"
/>
{{#if this.showEditReason}}
<TextField
@value={{this.editReason}}
@placeholderKey="table_builder.edit.modal.reason"
/>
{{/if}}
</div>
{{/if}}
<DTooltip
@icon="question"
@triggers="click"
@arrow={{false}}
class="btn btn-icon no-text"
>
<ul>
<h4>{{i18n "table_builder.modal.help.title"}}</h4>
<li>
<kbd>
{{i18n "table_builder.modal.help.enter_key"}}
</kbd>
{{i18n "table_builder.modal.help.new_row"}}
</li>
<li>
<kbd>
{{i18n "table_builder.modal.help.tab_key"}}
</kbd>
{{i18n "table_builder.modal.help.new_col"}}
</li>
<li>{{i18n "table_builder.modal.help.options"}}</li>
</ul>
</DTooltip>
</div>
</:footer>
</DModal>
</template>
}
function prefixedLocale(localeString) {
return I18n.t(`table_builder.spreadsheet.${localeString}`);
}

View File

@ -1,13 +1,18 @@
import { schedule } from "@ember/runloop";
import { create } from "virtual-dom";
import FullscreenTableModal from "discourse/components/modal/fullscreen-table";
import SpreadsheetEditor from "discourse/components/modal/spreadsheet-editor";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import Columns from "discourse/lib/columns";
import highlightSyntax from "discourse/lib/highlight-syntax";
import { nativeLazyLoading } from "discourse/lib/lazy-load-images";
import lightbox from "discourse/lib/lightbox";
import { SELECTORS } from "discourse/lib/lightbox/constants";
import { withPluginApi } from "discourse/lib/plugin-api";
import { parseAsync } from "discourse/lib/text";
import { setTextDirections } from "discourse/lib/text-direction";
import { tokenRange } from "discourse/lib/utilities";
import { iconHTML, iconNode } from "discourse-common/lib/icon-library";
import I18n from "discourse-i18n";
@ -106,21 +111,33 @@ export default {
});
});
function _createButton() {
function _createButton(props) {
const openPopupBtn = document.createElement("button");
openPopupBtn.classList.add(
const defaultClasses = [
"open-popup-link",
"btn-default",
"btn",
"btn-icon",
"btn-expand-table",
"no-text"
);
const expandIcon = create(
iconNode("discourse-expand", { class: "expand-table-icon" })
);
openPopupBtn.title = I18n.t("fullscreen_table.expand_btn");
openPopupBtn.append(expandIcon);
"no-text",
];
openPopupBtn.classList.add(...defaultClasses);
if (props.classes) {
openPopupBtn.classList.add(...props.classes);
}
if (props.title) {
openPopupBtn.title = I18n.t(props.title);
}
if (props.icon) {
const icon = create(
iconNode(props.icon.name, { class: props.icon?.class })
);
openPopupBtn.append(icon);
}
return openPopupBtn;
}
@ -128,14 +145,64 @@ export default {
return scrollWidth > clientWidth;
}
function generateModal(event) {
function generateFullScreenTableModal(event) {
const table = event.currentTarget.parentElement.nextElementSibling;
const tempTable = table.cloneNode(true);
modal.show(FullscreenTableModal, { model: { tableHtml: tempTable } });
}
function generatePopups(tables) {
tables.forEach((table) => {
function generateSpreadsheetModal() {
const tableIndex = this.tableIndex;
return ajax(`/posts/${this.id}`, { type: "GET" })
.then((post) => {
parseAsync(post.raw).then((tokens) => {
const allTables = tokenRange(tokens, "table_open", "table_close");
const tableTokens = allTables[tableIndex];
modal.show(SpreadsheetEditor, {
model: {
post,
tableIndex,
tableTokens,
},
});
});
})
.catch(popupAjaxError);
}
function generatePopups(tables, attrs) {
tables.forEach((table, index) => {
const buttonWrapper = document.createElement("div");
buttonWrapper.classList.add("fullscreen-table-wrapper__buttons");
const tableEditorBtn = _createButton({
classes: ["btn-edit-table"],
title: "table_builder.edit.btn_edit",
icon: {
name: "pencil-alt",
class: "edit-table-icon",
},
});
table.parentNode.setAttribute("data-table-index", index);
table.parentNode.classList.add("fullscreen-table-wrapper");
if (attrs.canEdit) {
buttonWrapper.append(tableEditorBtn);
tableEditorBtn.addEventListener(
"click",
generateSpreadsheetModal.bind({
tableIndex: index,
...attrs,
}),
false
);
}
table.parentNode.insertBefore(buttonWrapper, table);
if (!isOverflown(table.parentNode)) {
return;
}
@ -144,28 +211,50 @@ export default {
return;
}
const popupBtn = _createButton();
table.parentNode.classList.add("fullscreen-table-wrapper");
// Create a button wrapper for case of multiple buttons (i.e. table builder extension)
const buttonWrapper = document.createElement("div");
buttonWrapper.classList.add("fullscreen-table-wrapper--buttons");
buttonWrapper.append(popupBtn);
popupBtn.addEventListener("click", generateModal, false);
const expandTableBtn = _createButton({
classes: ["btn-expand-table"],
title: "fullscreen_table.expand_btn",
icon: { name: "discourse-expand", class: "expand-table-icon" },
});
buttonWrapper.append(expandTableBtn);
expandTableBtn.addEventListener(
"click",
generateFullScreenTableModal,
false
);
table.parentNode.insertBefore(buttonWrapper, table);
});
}
function cleanupPopupBtns() {
const editTableBtn = document.querySelector(
".open-popup-link.btn-edit-table"
);
const expandTableBtn = document.querySelector(
".open-popup-link.btn-expand-table"
);
expandTableBtn?.removeEventListener(
"click",
generateFullScreenTableModal
);
editTableBtn?.removeEventListener("click", generateSpreadsheetModal);
}
api.decorateCookedElement(
(post) => {
(post, helper) => {
schedule("afterRender", () => {
const tables = post.querySelectorAll("table");
generatePopups(tables);
const tables = post.querySelectorAll(".md-table table");
generatePopups(tables, helper.widget.attrs);
});
},
{
onlyStream: true,
id: "table-wrapper",
}
);
api.cleanupStream(cleanupPopupBtns);
});
},
};

View File

@ -613,3 +613,79 @@ export function getCaretPosition(element, options) {
return adjustedPosition;
}
/**
* Generate markdown table from an array of objects
* Inspired by https://github.com/Ygilany/array-to-table
*
* @param {Array} array Array of objects
* @param {Array} columns Column headings
* @param {String} colPrefix Table column prefix
*
* @return {String} Markdown table
*/
export function arrayToTable(array, cols, colPrefix = "col") {
let table = "";
// Generate table headers
table += "|";
table += cols.join(" | ");
table += "|\r\n|";
// Generate table header separator
table += cols
.map(function () {
return "---";
})
.join(" | ");
table += "|\r\n";
// Generate table body
array.forEach(function (item) {
table += "|";
table +=
cols
.map(function (_key, index) {
return String(item[`${colPrefix}${index}`] || "").replace(
/\r?\n|\r/g,
" "
);
})
.join(" | ") + "|\r\n";
});
return table;
}
/**
*
* @returns a regular expression finding all markdown tables
*/
export function findTableRegex() {
return /((\r?){2}|^)(^\|[^\r\n]*(\r?\n)?)+(?=(\r?\n){2}|$)/gm;
}
export function tokenRange(tokens, start, end) {
const contents = [];
let startPushing = false;
let items = [];
tokens.forEach((token) => {
if (token.type === start) {
startPushing = true;
}
if (token.type === end) {
contents.push(items);
items = [];
startPushing = false;
}
if (startPushing) {
items.push(token);
}
});
return contents;
}

View File

@ -9,6 +9,7 @@ import $ from "jquery";
import { Promise } from "rsvp";
import DiscardDraftModal from "discourse/components/modal/discard-draft";
import PostEnqueuedModal from "discourse/components/modal/post-enqueued";
import SpreadsheetEditor from "discourse/components/modal/spreadsheet-editor";
import { categoryBadgeHTML } from "discourse/helpers/category-link";
import {
cannotPostAgain,
@ -424,6 +425,14 @@ export default class ComposerService extends Service {
})
);
options.push(
this._setupPopupMenuOption({
action: "toggleSpreadsheet",
icon: "table",
label: "composer.insert_table",
})
);
return options.concat(
customPopupMenuOptions
.map((option) => this._setupPopupMenuOption(option))
@ -723,6 +732,16 @@ export default class ComposerService extends Service {
this.toggleProperty("model.whisper");
}
@action
toggleSpreadsheet() {
this.modal.show(SpreadsheetEditor, {
model: {
toolbarEvent: this.toolbarEvent,
tableTokens: null,
},
});
}
@action
toggleInvisible() {
this.toggleProperty("model.unlistTopic");

View File

@ -17,13 +17,13 @@ acceptance("Post Table Wrapper Test", function () {
assert.ok(
exists(
`${postWithLargeTable} .fullscreen-table-wrapper .fullscreen-table-wrapper--buttons .open-popup-link`
`${postWithLargeTable} .fullscreen-table-wrapper .fullscreen-table-wrapper__buttons .open-popup-link`
),
"buttons for the table wrapper appear inside a separate div"
);
const fullscreenButtonWrapper = query(
`${postWithLargeTable} .fullscreen-table-wrapper .fullscreen-table-wrapper--buttons`
`${postWithLargeTable} .fullscreen-table-wrapper .fullscreen-table-wrapper__buttons`
);
assert.strictEqual(

View File

@ -0,0 +1,33 @@
import { click, visit } from "@ember/test-helpers";
import { test } from "qunit";
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
import selectKit from "discourse/tests/helpers/select-kit-helper";
import I18n from "discourse-i18n";
acceptance("Table Builder", function (needs) {
needs.user();
test("Can see table builder button when creating a topic", async function (assert) {
await visit("/");
await click("#create-topic");
await click(".d-editor-button-bar .options");
await selectKit(".toolbar-popup-menu-options").expand();
assert
.dom(`.select-kit-row[data-name='${I18n.t("composer.insert_table")}']`)
.exists("it shows the builder button");
});
test("Can see table builder button when editing post", async function (assert) {
await visit("/t/internationalization-localization/280");
await click("#post_1 .show-more-actions");
await click("#post_1 .edit");
assert.ok(exists("#reply-control"));
await click(".d-editor-button-bar .options");
await selectKit(".toolbar-popup-menu-options").expand();
assert
.dom(`.select-kit-row[data-name='${I18n.t("composer.insert_table")}']`)
.exists("it shows the builder button");
});
});

View File

@ -0,0 +1,3 @@
export const mdTable = `|Make | Model | Year|\r\n|--- | --- | ---|\r\n|Toyota | Supra | 1998|\r\n|Nissan | Skyline | 1999|\r\n|Honda | S2000 | 2001|\r\n`;
export const mdTableSpecialChars = `|Make | Model | Price|\r\n|--- | --- | ---|\r\n|Toyota | Supra | $50,000|\r\n| | Celica | $20,000|\r\n|Nissan | GTR | $80,000|\r\n`;
export const mdTableNonUniqueHeadings = `|col1 | col2 | col1|\r\n|--- | --- | ---|\r\n|Col A | Col B | Col C|\r\n`;

View File

@ -6,6 +6,7 @@ import Handlebars from "handlebars";
import { module, test } from "qunit";
import sinon from "sinon";
import {
arrayToTable,
caretRowCol,
clipboardCopyAsync,
defaultHomepage,
@ -13,6 +14,7 @@ import {
escapeExpression,
extractDomainFromUrl,
fillMissingDates,
findTableRegex,
inCodeBlock,
initializeDefaultHomepage,
mergeSortedLists,
@ -22,6 +24,11 @@ import {
slugify,
toAsciiPrintable,
} from "discourse/lib/utilities";
import {
mdTable,
mdTableNonUniqueHeadings,
mdTableSpecialChars,
} from "discourse/tests/fixtures/md-table";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { chromeTest } from "discourse/tests/helpers/qunit-helpers";
@ -373,3 +380,158 @@ module("Unit | Utilities | clipboard", function (hooks) {
}
);
});
module("Unit | Utilities | table-builder", function (hooks) {
setupTest(hooks);
test("arrayToTable", function (assert) {
const tableData = [
{
col0: "Toyota",
col1: "Supra",
col2: "1998",
},
{
col0: "Nissan",
col1: "Skyline",
col2: "1999",
},
{
col0: "Honda",
col1: "S2000",
col2: "2001",
},
];
assert.strictEqual(
arrayToTable(tableData, ["Make", "Model", "Year"]),
mdTable,
"it creates a markdown table from an array of objects (with headers as keys)"
);
const specialCharsTableData = [
{
col0: "Toyota",
col1: "Supra",
col2: "$50,000",
},
{
col0: "",
col1: "Celica",
col2: "$20,000",
},
{
col0: "Nissan",
col1: "GTR",
col2: "$80,000",
},
];
assert.strictEqual(
arrayToTable(specialCharsTableData, ["Make", "Model", "Price"]),
mdTableSpecialChars,
"it creates a markdown table with special characters in correct alignment"
);
const nonUniqueColumns = ["col1", "col2", "col1"];
assert.strictEqual(
arrayToTable(
[{ col0: "Col A", col1: "Col B", col2: "Col C" }],
nonUniqueColumns
),
mdTableNonUniqueHeadings,
"it does not suppress a column if heading is the same as another column"
);
});
test("arrayToTable with custom column prefix", function (assert) {
const tableData = [
{
A0: "hey",
A1: "you",
},
{
A0: "over",
A1: "there",
},
];
assert.strictEqual(
arrayToTable(tableData, ["Col 1", "Col 2"], "A"),
`|Col 1 | Col 2|\r\n|--- | ---|\r\n|hey | you|\r\n|over | there|\r\n`,
"it works"
);
});
test("arrayToTable returns valid table with multiline cell data", function (assert) {
const tableData = [
{
col0: "Jane\nDoe",
col1: "Teri",
},
{
col0: "Finch",
col1: "Sami",
},
];
assert.strictEqual(
arrayToTable(tableData, ["Col 1", "Col 2"]),
`|Col 1 | Col 2|\r\n|--- | ---|\r\n|Jane Doe | Teri|\r\n|Finch | Sami|\r\n`,
"it creates a valid table"
);
});
test("findTableRegex", function (assert) {
const oneTable = `|Make|Model|Year|\r\n|--- | --- | ---|\r\n|Toyota|Supra|1998|`;
assert.strictEqual(
oneTable.match(findTableRegex()).length,
1,
"finds one table in markdown"
);
const threeTables = `## Heading
|Table1 | PP Port | Device | DP | Medium|
|--- | --- | --- | --- | ---|
| Something | (1+2) | Dude | Mate | Bro |
|Table2 | PP Port | Device | DP | Medium|
|--- | --- | --- | --- | ---|
| Something | (1+2) | Dude | Mate | Bro |
| | (1+2) | Dude | Mate | Bro |
| | (1+2) | Dude | Mate | Bro |
|Table3 | PP Port | Device | DP |
|--- | --- | --- | --- |
| Something | (1+2) | Dude | Sound |
| | (1+2) | Dude | OW |
| | (1+2) | Dude | OI |
Random extras
`;
assert.strictEqual(
threeTables.match(findTableRegex()).length,
3,
"finds three tables in markdown"
);
const ignoreUploads = `
:information_source: Something
[details=Example of a cross-connect in Equinix]
![image|603x500, 100%](upload://fURYa9mt00rXZITdYhhyeHFJE8J.png)
[/details]
|Table1 | PP Port | Device | DP | Medium|
|--- | --- | --- | --- | ---|
| Something | (1+2) | Dude | Mate | Bro |
`;
assert.strictEqual(
ignoreUploads.match(findTableRegex()).length,
1,
"finds on table, ignoring upload markup"
);
});
});

View File

@ -19,3 +19,4 @@
@import "common/loading-slider";
@import "common/float-kit/_index";
@import "common/login/_index";
@import "common/table-builder/_index";

View File

@ -1648,10 +1648,13 @@ iframe {
}
.open-popup-link {
display: inline;
margin-inline: 0.25em;
position: sticky;
left: 1rem;
opacity: 0%;
white-space: nowrap;
transition: 0.25s ease-in-out opacity;
}
.fullscreen-table-wrapper {
@ -1659,7 +1662,7 @@ iframe {
display: block;
position: relative;
&--buttons {
&__buttons {
position: absolute;
top: 0.5rem;
right: 0.5rem;

View File

@ -0,0 +1,5 @@
@import "vendor/jspreadsheet";
@import "vendor/jsuites";
@import "jspreadsheet-theme";
@import "table-edit-decorator";
@import "insert-table-modal";

View File

@ -0,0 +1,78 @@
.btn-insert-table {
background: var(--tertiary);
color: var(--secondary);
.d-icon {
color: var(--secondary);
}
.discourse-no-touch & {
&:hover {
background-color: var(--tertiary-hover);
color: var(--secondary);
.d-icon {
color: var(--secondary);
}
}
}
}
.insert-table-modal {
display: flex;
flex-direction: column;
align-items: flex-start;
.d-modal__container,
.modal-inner-container {
--modal-max-width: $reply-area-max-width;
width: 100%;
height: 100%;
max-height: unset;
display: grid;
grid-template-rows: auto 1fr auto;
}
.modal-body {
padding: 0;
margin: 0;
}
.d-modal__footer,
.modal-footer {
display: flex;
align-items: center;
justify-content: space-between;
.secondary-actions {
display: flex;
align-items: center;
gap: 0.5rem;
.edit-reason {
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn {
margin-right: 0;
}
input {
margin: 0;
}
}
.secondary-actions .tippy-content {
h4 {
color: var(--primary);
}
li {
margin-block: 0.25rem;
color: var(--primary-high);
}
}
}
}

View File

@ -0,0 +1,272 @@
:root {
--jexcel_header_color: var(--primary-high);
--jexcel_header_color_highlighted: var(--primary-high);
--jexcel_header_background: var(--primary-very-low);
--jexcel_header_background_highlighted: var(--primary-low);
--jexcel_content_color: var(--primary);
--jexcel_content_color_highlighted: var(--primary-high);
--jexcel_content_background: var(--secondary);
--jexcel_content_background_highlighted: var(--tertiary-very-low);
--jexcel_menu_background: var(--secondary);
--jexcel_menu_background_highlighted: var(--secondary-very-high);
--jexcel_menu_color: var(--primary-medium);
--jexcel_menu_color_highlighted: var(--primary);
--jexcel_border_color: var(--primary-low-mid);
--jexcel_border_color_highlighted: var(--tertiary-high);
--active_color: var(--primary-very-low);
--active-color: var(--active_color);
}
.jexcel {
border-bottom: 1px solid var(--jexcel_border_color);
border-right: transparent;
background-color: transparent;
}
table.jexcel > thead > tr > td {
border-top: 1px solid transparent;
border-left: 1px solid transparent;
border-right: 1px solid transparent;
border-bottom: 1px solid #000;
background-color: #fff;
padding: 10px;
font-weight: bold;
}
table.jexcel > tbody > tr > td {
padding: 8px;
border-right: 1px solid transparent;
border-left: 1px solid transparent;
}
table.jexcel {
border-bottom: 1px solid var(--jexcel_border_color);
}
.jcontextmenu.jexcel_contextmenu hr {
border-color: var(--jexcel_border_color);
}
.jexcel_container .jcontextmenu > div a {
color: var(--jexcel_menu_color);
}
.jexcel_corner {
background-color: var(--tertiary);
}
.jexcel > tbody > tr > td,
.jexcel > thead > tr > td {
border-top: 1px solid var(--jexcel_border_color);
border-left: 1px solid var(--jexcel_border_color);
background-color: var(--jexcel_content_background);
color: var(--jexcel_content_color);
}
.jexcel > tbody > tr > td:first-child,
.jexcel > thead > tr > td {
background-color: var(--jexcel_header_background);
color: var(--jexcel_header_color);
}
.jexcel > thead > tr > td.selected,
.jexcel > tbody > tr.selected > td:first-child {
background-color: var(--jexcel_header_background_highlighted);
color: var(--jexcel_header_color_highlighted);
}
table.jexcel > tbody > tr > td:first-child {
background-color: var(--jexcel_header_background);
}
table.jexcel > tbody > tr.selected > td:first-child {
background-color: var(--jexcel_header_background_highlighted);
}
.jexcel > tbody > tr > td.jexcel_cursor a {
color: var(--active-color);
}
.jexcel_pagination > div > div {
color: var(--jexcel_header_color);
background: var(--jexcel_header_background);
border: 1px solid var(--jexcel_border_color);
}
.jexcel_page,
.jexcel_container input,
.jexcel_container select {
color: var(--jexcel_header_color);
background: var(--jexcel_header_background);
border: 1px solid var(--jexcel_border_color);
}
.jexcel_contextmenu.jcontextmenu {
border: 1px solid var(--jexcel_border_color);
background: var(--jexcel_menu_background);
color: var(--jexcel_menu_color);
box-shadow: 0 12px 12px rgba(0, 0, 0, 0.15);
}
.jcontextmenu > div a {
color: var(--jexcel_menu_color);
}
.jcontextmenu > div:not(.contextmenu-line):hover a {
color: var(--jexcel_menu_color_highlighted);
}
.jcontextmenu > div:not(.contextmenu-line):hover {
background: var(--jexcel_menu_background_highlighted);
}
.jexcel_dropdown .jdropdown-container,
.jexcel_dropdown .jdropdown-content {
background-color: var(--jexcel_content_background);
color: var(--jexcel_content_color);
}
.jexcel_dropdown .jdropdown-item {
color: var(--jexcel_content_color);
}
.jexcel_dropdown .jdropdown-item:hover,
.jexcel_dropdown .jdropdown-selected,
.jexcel_dropdown .jdropdown-cursor {
background-color: var(--jexcel_content_background_highlighted);
color: var(--jexcel_content_color_highlighted);
}
.jexcel .jcalendar-content {
background-color: var(--jexcel_header_background);
color: var(--jexcel_header_color);
}
.jexcel .jcalendar-content > table {
background-color: var(--jexcel_content_background);
color: var(--jexcel_content_color);
}
.jexcel .jcalendar-weekday {
background-color: var(--jexcel_content_background_highlighted);
color: var(--jexcel_content_color_highlighted);
}
.jexcel .jcalendar-sunday {
color: var(--jexcel_header_color);
}
.jexcel .jcalendar-selected {
background-color: var(--jexcel_content_background_highlighted);
color: var(--jexcel_content_color_highlighted);
}
.jexcel_toolbar i.jexcel_toolbar_item {
color: var(--jexcel_content_color);
}
.jexcel_toolbar i.jexcel_toolbar_item:hover {
background: var(--jexcel_content_background_highlighted);
color: var(--jexcel_content_color_highlighted);
}
.jexcel_toolbar {
background: var(--jexcel_header_background);
}
.jexcel_content::-webkit-scrollbar-track {
background: var(--jexcel_background_head);
}
.jexcel_content::-webkit-scrollbar-thumb {
background: var(--jexcel_background_head_highlighted);
}
.jexcel_border_main {
border: 1px solid #000;
border-color: var(--jexcel_border_color_highlighted);
}
.jexcel .highlight {
background-color: var(--jexcel_content_background_highlighted);
}
.jexcel .highlight-bottom {
border-bottom: 1.5px solid var(--jexcel_border_color_highlighted);
}
.jexcel .highlight-right {
border-right: 1.5px solid var(--jexcel_border_color_highlighted);
}
.jexcel .highlight-left {
border-left: 1.5px solid var(--jexcel_border_color_highlighted);
}
.jexcel .highlight-top {
border-top: 1.5px solid var(--jexcel_border_color_highlighted);
}
.jexcel .copying-top {
border-top-color: var(--jexcel_border_color_highlighted);
}
.jexcel .copying-right {
border-right-color: var(--jexcel_border_color_highlighted);
}
.jexcel .copying-left {
border-left-color: var(--jexcel_border_color_highlighted);
}
.jexcel .copying-bottom {
border-bottom-color: var(--jexcel_border_color_highlighted);
}
.jexcel_border_main,
.jexcel .highlight-top.highlight-left,
.jexcel .highlight-top,
.jexcel .highlight-left {
-webkit-box-shadow: unset;
box-shadow: unset;
}
table.jexcel > thead > tr > td {
border-top: 1px solid var(--jexcel_border_color);
border-right: 1px solid var(--jexcel_border_color);
border-bottom: 1px solid var(--jexcel_border_color);
background-color: var(--jexcel_header_background);
&:first-child {
border-left: 1px solid var(--jexcel_border_color);
}
}
table.jexcel > thead > tr > td.selected {
background-color: var(--jexcel_header_background_highlighted);
color: var(--jexcel_header_color_highlighted);
}
table.jexcel > tbody > tr > td {
border-right: 1px solid var(--jexcel_border_color);
&:first-child {
border-left: 1px solid var(--jexcel_border_color);
}
}
// Hides about item in context menu
.jcontextmenu > div:not(.contextmenu-line):last-child {
display: none;
}
.jexcel_container {
padding: 0.5em;
min-width: 100%;
.jexcel_content {
min-width: 100%;
padding: 0;
table.jexcel {
min-width: 100%;
}
}
}
.jexcel_container {
padding: 0;
}

View File

@ -0,0 +1,36 @@
.btn-edit-table {
-webkit-user-select: none;
-webkit-touch-callout: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.fullscreen-table-wrapper:hover .fullscreen-table-wrapper__buttons button {
opacity: 100%;
}
.mobile-view {
.btn-edit-table {
display: none;
z-index: 2;
position: absolute;
top: 1rem;
left: 1rem;
}
.fullscreen-table-wrapper {
position: relative;
&:hover {
table {
opacity: 0.5;
}
.btn-edit-table {
display: block;
z-index: 2;
}
}
}
}

View File

@ -0,0 +1,699 @@
:root {
--jexcel-border-color: #000;
}
.jexcel_container {
display: inline-block;
padding-right: 2px;
box-sizing: border-box;
overscroll-behavior: contain;
outline: none;
}
.jexcel_container.fullscreen {
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
z-index: 21;
}
.jexcel_container.fullscreen .jexcel_content {
overflow: auto;
width: 100%;
height: 100%;
background-color: #ffffff;
}
.jexcel_container.with-toolbar .jexcel > thead > tr > td {
top: 0;
}
.jexcel_container.fullscreen.with-toolbar {
height: calc(100% - 46px);
}
.jexcel_content {
display: inline-block;
box-sizing: border-box;
padding-right: 3px;
padding-bottom: 3px;
position: relative;
scrollbar-width: thin;
scrollbar-color: #666 transparent;
}
@supports (-moz-appearance: none) {
.jexcel_content {
padding-right: 10px;
}
}
.jexcel_content::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.jexcel_content::-webkit-scrollbar-track {
background: #eee;
}
.jexcel_content::-webkit-scrollbar-thumb {
background: #666;
}
.jexcel {
border-collapse: separate;
table-layout: fixed;
white-space: nowrap;
empty-cells: show;
border: 0px;
background-color: #fff;
width: 0;
border-top: 1px solid transparent;
border-left: 1px solid transparent;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
.jexcel > thead > tr > td {
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
border-right: 1px solid transparent;
border-bottom: 1px solid transparent;
background-color: #f3f3f3;
padding: 2px;
cursor: pointer;
box-sizing: border-box;
overflow: hidden;
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 2;
}
.jexcel_container.with-toolbar .jexcel > thead > tr > td {
top: 42px;
}
.jexcel > thead > tr > td.dragging {
background-color: #fff;
opacity: 0.5;
}
.jexcel > thead > tr > td.selected {
background-color: #dcdcdc;
}
.jexcel > thead > tr > td.arrow-up {
background-repeat: no-repeat;
background-position: center right 5px;
background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' d='M0 0h24v24H0V0z'/%3E%3Cpath d='M7 14l5-5 5 5H7z' fill='gray'/%3E%3C/svg%3E");
text-decoration: underline;
}
.jexcel > thead > tr > td.arrow-down {
background-repeat: no-repeat;
background-position: center right 5px;
background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' d='M0 0h24v24H0V0z'/%3E%3Cpath d='M7 10l5 5 5-5H7z' fill='gray'/%3E%3C/svg%3E");
text-decoration: underline;
}
.jexcel > tbody > tr > td:first-child {
position: relative;
background-color: #f3f3f3;
text-align: center;
}
.jexcel > tbody.resizable > tr > td:first-child::before {
content: "\00a0";
width: 100%;
height: 3px;
position: absolute;
bottom: 0px;
left: 0px;
cursor: row-resize;
}
.jexcel > tbody.draggable > tr > td:first-child::after {
content: "\00a0";
width: 3px;
height: 100%;
position: absolute;
top: 0px;
right: 0px;
cursor: move;
}
.jexcel > tbody > tr.dragging > td {
background-color: #eee;
opacity: 0.5;
}
.jexcel > tbody > tr > td {
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
border-right: 1px solid transparent;
border-bottom: 1px solid transparent;
padding: 4px;
white-space: nowrap;
box-sizing: border-box;
line-height: 1em;
}
.jexcel_overflow > tbody > tr > td {
overflow: hidden;
}
.jexcel > tbody > tr > td:last-child {
overflow: hidden;
}
.jexcel > tbody > tr > td > img {
display: inline-block;
max-width: 100px;
}
.jexcel > tbody > tr > td.readonly {
color: rgba(0, 0, 0, 0.3);
}
.jexcel > tbody > tr.selected > td:first-child {
background-color: #dcdcdc;
}
.jexcel > tbody > tr > td > select,
.jexcel > tbody > tr > td > input,
.jexcel > tbody > tr > td > textarea {
border: 0px;
border-radius: 0px;
outline: 0px;
width: 100%;
margin: 0px;
padding: 0px;
padding-right: 2px;
background-color: transparent;
box-sizing: border-box;
}
.jexcel > tbody > tr > td > textarea {
resize: none;
padding-top: 6px !important;
}
.jexcel > tbody > tr > td > input[type="checkbox"] {
width: 12px;
margin-top: 2px;
}
.jexcel > tbody > tr > td > input[type="radio"] {
width: 12px;
margin-top: 2px;
}
.jexcel > tbody > tr > td > select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-repeat: no-repeat;
background-position-x: 100%;
background-position-y: 40%;
background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSdibGFjaycgaGVpZ2h0PScyNCcgdmlld0JveD0nMCAwIDI0IDI0JyB3aWR0aD0nMjQnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zyc+PHBhdGggZD0nTTcgMTBsNSA1IDUtNXonLz48cGF0aCBkPSdNMCAwaDI0djI0SDB6JyBmaWxsPSdub25lJy8+PC9zdmc+);
}
.jexcel > tbody > tr > td.jexcel_dropdown {
background-repeat: no-repeat;
background-position: top 50% right 5px;
background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' d='M0 0h24v24H0V0z'/%3E%3Cpath d='M7 10l5 5 5-5H7z' fill='lightgray'/%3E%3C/svg%3E");
text-overflow: ellipsis;
overflow-x: hidden;
}
.jexcel > tbody > tr > td.jexcel_dropdown.jexcel_comments {
background: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' d='M0 0h24v24H0V0z'/%3E%3Cpath d='M7 10l5 5 5-5H7z' fill='lightgray'/%3E%3C/svg%3E")
top 50% right 5px no-repeat,
url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFuGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTMxVDE4OjU1OjA4WiIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0zMVQxODo1NTowOFoiIHhtcDpNb2RpZnlEYXRlPSIyMDE5LTAxLTMxVDE4OjU1OjA4WiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDphMTlhZDJmOC1kMDI2LTI1NDItODhjOS1iZTRkYjkyMmQ0MmQiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDpkOGI5NDUyMS00ZjEwLWQ5NDktYjUwNC0wZmU1N2I3Nzk1MDEiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDplMzdjYmE1ZS1hYTMwLWNkNDUtYTAyNS1lOWYxZjk2MzUzOGUiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDplMzdjYmE1ZS1hYTMwLWNkNDUtYTAyNS1lOWYxZjk2MzUzOGUiIHN0RXZ0OndoZW49IjIwMTktMDEtMzFUMTg6NTU6MDhaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmExOWFkMmY4LWQwMjYtMjU0Mi04OGM5LWJlNGRiOTIyZDQyZCIgc3RFdnQ6d2hlbj0iMjAxOS0wMS0zMVQxODo1NTowOFoiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4En6MDAAAAX0lEQVQYlX3KOw6AIBBAwS32RpJADXfx0pTET+ERZJ8F8RODFtONsG0QAoh0CSDM82dqodaBdQXnfoLZQM7gPai+wjNNE8R4pTuAYNZSKZASqL7CMy0LxNgJp30fKYUDi3+vIqb/+rUAAAAASUVORK5CYII=")
top right no-repeat;
}
.jexcel > tbody > tr > td > .color {
width: 90%;
height: 10px;
margin: auto;
}
.jexcel > tbody > tr > td > a {
text-decoration: underline;
}
.jexcel > tbody > tr > td.highlight > a {
color: blue;
cursor: pointer;
}
.jexcel > tfoot > tr > td {
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
border-right: 1px solid transparent;
border-bottom: 1px solid transparent;
background-color: #f3f3f3;
padding: 2px;
cursor: pointer;
box-sizing: border-box;
overflow: hidden;
}
.jexcel .highlight {
background-color: rgba(0, 0, 0, 0.05);
}
.jexcel .highlight-top {
border-top: 1px solid #000; /* var(--jexcel-border-color);*/
box-shadow: 0px -1px #ccc;
}
.jexcel .highlight-left {
border-left: 1px solid #000; /* var(--jexcel-border-color);*/
box-shadow: -1px 0px #ccc;
}
.jexcel .highlight-right {
border-right: 1px solid #000; /* var(--jexcel-border-color);*/
}
.jexcel .highlight-bottom {
border-bottom: 1px solid #000; /* var(--jexcel-border-color);*/
}
.jexcel .highlight-top.highlight-left {
box-shadow: -1px -1px #ccc;
-webkit-box-shadow: -1px -1px #ccc;
-moz-box-shadow: -1px -1px #ccc;
}
.jexcel .highlight-selected {
background-color: rgba(0, 0, 0, 0);
}
.jexcel .selection {
background-color: rgba(0, 0, 0, 0.05);
}
.jexcel .selection-left {
border-left: 1px dotted #000;
}
.jexcel .selection-right {
border-right: 1px dotted #000;
}
.jexcel .selection-top {
border-top: 1px dotted #000;
}
.jexcel .selection-bottom {
border-bottom: 1px dotted #000;
}
.jexcel_corner {
position: absolute;
background-color: rgb(0, 0, 0);
height: 1px;
width: 1px;
border: 1px solid rgb(255, 255, 255);
top: -2000px;
left: -2000px;
cursor: crosshair;
box-sizing: initial;
z-index: 20;
padding: 2px;
}
.jexcel .editor {
outline: 0px solid transparent;
overflow: visible;
white-space: nowrap;
text-align: left;
padding: 0px;
box-sizing: border-box;
overflow: visible !important;
}
.jexcel .editor > input {
padding-left: 4px;
}
.jexcel .editor .jupload {
position: fixed;
top: 100%;
z-index: 40;
user-select: none;
-webkit-font-smoothing: antialiased;
font-size: 0.875rem;
letter-spacing: 0.2px;
-webkit-border-radius: 4px;
border-radius: 4px;
-webkit-box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2);
box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2);
padding: 10px;
background-color: #fff;
width: 300px;
min-height: 225px;
margin-top: 2px;
}
.jexcel .editor .jupload img {
width: 100%;
height: auto;
}
.jexcel .editor .jexcel_richtext {
position: fixed;
top: 100%;
z-index: 40;
user-select: none;
-webkit-font-smoothing: antialiased;
font-size: 0.875rem;
letter-spacing: 0.2px;
-webkit-box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2);
box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2);
padding: 10px;
background-color: #fff;
min-width: 280px;
max-width: 310px;
margin-top: 2px;
text-align: left;
}
.jexcel .editor .jclose:after {
position: absolute;
top: 0;
right: 0;
margin: 10px;
content: "close";
font-family: "Material icons";
font-size: 24px;
width: 24px;
height: 24px;
line-height: 24px;
cursor: pointer;
text-shadow: 0px 0px 5px #fff;
}
.jexcel,
.jexcel td,
.jexcel_corner {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
}
.jexcel_textarea {
position: absolute;
top: -999px;
left: -999px;
width: 1px;
height: 1px;
}
.jexcel .dragline {
position: absolute;
}
.jexcel .dragline div {
position: relative;
top: -6px;
height: 5px;
width: 22px;
}
.jexcel .dragline div:hover {
cursor: move;
}
.jexcel .onDrag {
background-color: rgba(0, 0, 0, 0.6);
}
.jexcel .error {
border: 1px solid red;
}
.jexcel thead td.resizing {
border-right-style: dotted !important;
border-right-color: red !important;
}
.jexcel tbody tr.resizing > td {
border-bottom-style: dotted !important;
border-bottom-color: red !important;
}
.jexcel tbody td.resizing {
border-right-style: dotted !important;
border-right-color: red !important;
}
.jexcel .jdropdown-header {
border: 0px !important;
outline: none !important;
width: 100% !important;
height: 100% !important;
padding: 0px !important;
padding-left: 8px !important;
}
.jexcel .jdropdown-container {
margin-top: 1px;
}
.jexcel .jdropdown-container-header {
padding: 0px;
margin: 0px;
height: inherit;
}
.jexcel .jdropdown-picker {
border: 0px !important;
padding: 0px !important;
width: inherit;
height: inherit;
}
.jexcel .jexcel_comments {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFuGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTAxLTMxVDE4OjU1OjA4WiIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wMS0zMVQxODo1NTowOFoiIHhtcDpNb2RpZnlEYXRlPSIyMDE5LTAxLTMxVDE4OjU1OjA4WiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDphMTlhZDJmOC1kMDI2LTI1NDItODhjOS1iZTRkYjkyMmQ0MmQiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDpkOGI5NDUyMS00ZjEwLWQ5NDktYjUwNC0wZmU1N2I3Nzk1MDEiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDplMzdjYmE1ZS1hYTMwLWNkNDUtYTAyNS1lOWYxZjk2MzUzOGUiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDplMzdjYmE1ZS1hYTMwLWNkNDUtYTAyNS1lOWYxZjk2MzUzOGUiIHN0RXZ0OndoZW49IjIwMTktMDEtMzFUMTg6NTU6MDhaIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmExOWFkMmY4LWQwMjYtMjU0Mi04OGM5LWJlNGRiOTIyZDQyZCIgc3RFdnQ6d2hlbj0iMjAxOS0wMS0zMVQxODo1NTowOFoiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4En6MDAAAAX0lEQVQYlX3KOw6AIBBAwS32RpJADXfx0pTET+ERZJ8F8RODFtONsG0QAoh0CSDM82dqodaBdQXnfoLZQM7gPai+wjNNE8R4pTuAYNZSKZASqL7CMy0LxNgJp30fKYUDi3+vIqb/+rUAAAAASUVORK5CYII=");
background-repeat: no-repeat;
background-position: top right;
}
.jexcel .sp-replacer {
margin: 2px;
border: 0px;
}
.jexcel > thead > tr.jexcel_filter > td > input {
border: 0px;
width: 100%;
outline: none;
}
.jexcel_about {
float: right;
font-size: 0.7em;
padding: 2px;
text-transform: uppercase;
letter-spacing: 1px;
display: none;
}
.jexcel_about a {
color: #ccc;
text-decoration: none;
}
.jexcel_about img {
display: none;
}
.jexcel_filter {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.jexcel_filter > div {
padding: 8px;
align-items: center;
}
.jexcel_pagination {
display: flex;
justify-content: space-between;
align-items: center;
}
.jexcel_pagination > div {
display: flex;
padding: 10px;
}
.jexcel_pagination > div:last-child {
padding-right: 10px;
padding-top: 10px;
}
.jexcel_pagination > div > div {
text-align: center;
width: 36px;
height: 36px;
line-height: 34px;
border: 1px solid #ccc;
box-sizing: border-box;
margin-left: 2px;
cursor: pointer;
}
.jexcel_page {
font-size: 0.8em;
}
.jexcel_page_selected {
font-weight: bold;
background-color: #f3f3f3;
}
.jexcel_toolbar {
display: flex;
background-color: #f3f3f3;
border: 1px solid #ccc;
padding: 4px;
margin: 0px 2px 4px 1px;
position: sticky;
top: 0px;
z-index: 21;
}
.jexcel_toolbar:empty {
display: none;
}
.jexcel_toolbar i.jexcel_toolbar_item {
width: 24px;
height: 24px;
padding: 4px;
cursor: pointer;
display: inline-block;
}
.jexcel_toolbar i.jexcel_toolbar_item:hover {
background-color: #ddd;
}
.jexcel_toolbar select.jexcel_toolbar_item {
margin-left: 2px;
margin-right: 2px;
display: inline-block;
border: 0px;
background-color: transparent;
padding-right: 10px;
}
.jexcel .dragging-left {
background-repeat: no-repeat;
background-position: top 50% left 0px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M14 7l-5 5 5 5V7z'/%3E%3Cpath fill='none' d='M24 0v24H0V0h24z'/%3E%3C/svg%3E");
}
.jexcel .dragging-right {
background-repeat: no-repeat;
background-position: top 50% right 0px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M10 17l5-5-5-5v10z'/%3E%3Cpath fill='none' d='M0 24V0h24v24H0z'/%3E%3C/svg%3E");
}
.jexcel_tabs .jexcel_tab {
display: none;
}
.jexcel_tabs .jexcel_tab_link {
display: inline-block;
padding: 10px;
padding-left: 20px;
padding-right: 20px;
margin-right: 5px;
margin-bottom: 5px;
background-color: #f3f3f3;
cursor: pointer;
}
.jexcel_tabs .jexcel_tab_link.selected {
background-color: #ddd;
}
.jexcel_hidden_index > tbody > tr > td:first-child,
.jexcel_hidden_index > thead > tr > td:first-child,
.jexcel_hidden_index > tfoot > tr > td:first-child,
.jexcel_hidden_index > colgroup > col:first-child {
display: none;
}
.jexcel .jrating {
display: inline-flex;
}
.jexcel .jrating > div {
zoom: 0.55;
}
.jexcel .copying-top {
border-top: 1px dashed #000;
}
.jexcel .copying-left {
border-left: 1px dashed #000;
}
.jexcel .copying-right {
border-right: 1px dashed #000;
}
.jexcel .copying-bottom {
border-bottom: 1px dashed #000;
}
.jexcel .jexcel_column_filter {
background-repeat: no-repeat;
background-position: top 50% right 5px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='gray' width='18px' height='18px'%3E%3Cpath d='M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E");
text-overflow: ellipsis;
overflow: hidden;
padding: 0px;
padding-left: 6px;
padding-right: 20px;
}
.jexcel thead .jexcel_freezed,
.jexcel tfoot .jexcel_freezed {
left: 0px;
z-index: 3 !important;
box-shadow: 2px 0px 2px 0.2px #ccc !important;
-webkit-box-shadow: 2px 0px 2px 0.2px #ccc !important;
-moz-box-shadow: 2px 0px 2px 0.2px #ccc !important;
}
.jexcel tbody .jexcel_freezed {
position: relative;
background-color: #fff;
box-shadow: 1px 1px 1px 1px #ccc !important;
-webkit-box-shadow: 2px 4px 4px 0.1px #ccc !important;
-moz-box-shadow: 2px 4px 4px 0.1px #ccc !important;
}
.red {
color: red;
}
.jexcel > tbody > tr > td.readonly > input[type="checkbox"],
.jexcel > tbody > tr > td.readonly > input[type="radio"] {
pointer-events: none;
opacity: 0.5;
}

File diff suppressed because it is too large Load Diff

View File

@ -2382,6 +2382,7 @@ en:
add_warning: "This is an official warning."
toggle_whisper: "Toggle Whisper"
toggle_unlisted: "Toggle Unlisted"
insert_table: "Insert Table"
posting_not_on_topic: "Which topic do you want to reply to?"
saved_local_draft_tip: "saved locally"
similar_topics: "Your topic is similar to…"
@ -4645,6 +4646,64 @@ en:
patternMismatch: "Please match the requested format."
badInput: "Please enter a valid input."
table_builder:
title: "Table Builder"
modal:
title: "Table Builder"
create: "Build Table"
help:
title: "Using the Spreadsheet Editor"
enter_key: "Enter"
tab_key: "Tab"
new_row: "at the end of a row to insert a new row."
new_col: "at the end of a column to insert a new column."
options: "Right-click on cells to access more options in a dropdown menu."
confirm_close: "Are you sure you want to close the table bulder? Any unsaved changes will be lost."
edit:
btn_edit: "Edit Table"
modal:
title: "Edit Table"
cancel: "cancel"
create: "Save"
reason: "why are you editing?"
trigger_reason: "Add reason for edit"
default_edit_reason: "Update Table with Table Editor"
default_header:
col_1: "Column 1"
col_2: "Column 2"
col_3: "Column 3"
col_4: "Column 4"
spreadsheet:
no_records_found: "No records found"
show: "Show"
entries: "entries"
about: "About"
prompts:
delete_selected_rows: "Are you sure you want to delete the selected rows?"
delete_selected_cols: "Are you sure you want to delete the selected columns?"
will_destroy_merged_cells: "This action will destroy any existing merged cells. Are you sure?"
will_clear_search_results: "This action will destroy any existing merged cells. Are you sure?"
conflicts_with_merged_cells: "There is a conflict with another merged cell"
invalid_merge_props: "Invalid merged properties"
cells_already_merged: "Cell already merged"
no_cells_selected: "No cells selected"
context_menu:
row:
before: "Insert a new row before"
after: "Insert a new row after"
delete: "Delete selected rows"
col:
before: "Insert a new column before"
after: "Insert a new column after"
delete: "Delete selected columns"
rename: "Rename this column"
order:
ascending: "Order ascending"
descending: "Order descending"
copy: "Copy..."
paste: "Paste..."
save: "Save as..."
# This section is exported to the javascript for i18n in the admin section
admin_js:
# This is a text input placeholder, keep the translation short

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,97 @@
#!/bin/bash
# Script used for updating depencies for the table builder/editor feature.
# Updates the JSpreadsheet and jSuites libraries to the latest available versions.
# Get the directory of the script
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# Construct paths relative to the script directory
SCSS_VENDOR="$SCRIPT_DIR/../app/assets/stylesheets/common/table-builder/vendor/"
JSPREADSHEET_VENDOR="$SCRIPT_DIR/../public/javascripts/jspreadsheet/"
JSUITES_VENDOR="$SCRIPT_DIR/../public/javascripts/jsuites/"
JSUITES_JS_URL="https://jsuites.net/v4/jsuites.js"
JSPREADSHEET_JS_URL="https://bossanova.uk/jspreadsheet/v4/jexcel.js"
JSUITES_CSS_URL="https://raw.githubusercontent.com/jsuites/jsuites/master/dist/jsuites.css"
JSPREADSHEET_CSS_URL="https://bossanova.uk/jspreadsheet/v4/jexcel.css"
JSUITES_CSS_FILE=jsuites.css
JSUITES_SCSS_FILE=jsuites.scss
JSUITES_SCSS_FILE_LOCATION=$SCSS_VENDOR$JSUITES_SCSS_FILE
JSUITES_JS_FILE=jsuites.js
JSUITES_NEW_JS_FILE=jsuites.js
JSUITES_JS_FILE_LOCATION=$JSUITES_VENDOR$JSUITES_NEW_JS_FILE
JSPREADSHEET_CSS_FILE=jexcel.css
JSPREADSHEET_SCSS_FILE=jspreadsheet.scss
JSPREADSHEET_SCSS_FILE_LOCATION=$SCSS_VENDOR$JSPREADSHEET_SCSS_FILE
JSPREADSHEET_JS_FILE=jexcel.js
JSPREADSHEET_NEW_JS_FILE=jspreadsheet.js
JSPREADSHEET_JS_FILE_LOCATION=$JSPREADSHEET_VENDOR$JSPREADSHEET_NEW_JS_FILE
# Remove all vendor related files:
rm -r ${SCSS_VENDOR}*
rm -r ${JSUITES_VENDOR}
rm -r ${JSPREADSHEET_VENDOR}
# Recreate vendor directory
mkdir $JSUITES_VENDOR
mkdir $JSPREADSHEET_VENDOR
echo "Old vendor assets have been removed."
# STYLESHEETS:
# Add JSuite vendor file
if test -f "$JSUITES_CSS_FILE"; then
echo "$JSUITES_CSS_FILE already exists."
else
# Fetch jsuite stylesheet
wget $JSUITES_CSS_URL
echo "$JSUITES_CSS_FILE has been created in $(pwd)"
# Move jsuite stylesheet to vendor as a scss file
mv $JSUITES_CSS_FILE $JSUITES_SCSS_FILE_LOCATION
echo "$JSUITES_SCSS_FILE has been placed in the scss vendor directory"
# Scope styles to jexcel_container class
sed -i '' '1s/^/.jexcel_container {\n/' $JSUITES_SCSS_FILE_LOCATION
sed -i '' '$a\
}' $JSUITES_SCSS_FILE_LOCATION
# Remove conflicting animation classes
# TODO: Improve below code to handle nested code blocks
fi
# Add JSpreadsheet vendor file
if test -f "$JSPREADSHEET_CSS_FILE"; then
echo "$JSPREADSHEET_CSS_FILE already exists."
else
# Fetch jspreadsheet stylesheet
wget $JSPREADSHEET_CSS_URL
echo "$JSPREADSHEET_CSS_FILE has been created in $(pwd)"
# Move jspreadsheet stylesheet to vendor as a scss file
mv $JSPREADSHEET_CSS_FILE $JSPREADSHEET_SCSS_FILE_LOCATION
fi
# Apply prettier to vendor files
yarn prettier --write $SCSS_VENDOR
# JAVASCRIPTS:
if test -f "$JSUITES_JS_FILE"; then
echo "$JSUITES_JS_FILE already exists."
else
wget $JSUITES_JS_URL
mv $JSUITES_JS_FILE $JSUITES_JS_FILE_LOCATION
fi
if test -f "$JSPREADSHEET_JS_FILE"; then
echo "$JSPREADSHEET_JS_FILE already exists."
else
wget $JSPREADSHEET_JS_URL
mv $JSPREADSHEET_JS_FILE $JSPREADSHEET_JS_FILE_LOCATION
fi

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
module PageObjects
module Modals
class InsertTable < PageObjects::Modals::Base
MODAL_SELECTOR = ".insert-table-modal"
SPREADSHEET_TABLE_SELECTOR = "#{MODAL_SELECTOR} .jexcel"
def click_insert_table
find("#{MODAL_SELECTOR} .btn-insert-table").click
end
def cancel
find("#{MODAL_SELECTOR} .d-modal-cancel").click
end
def click_edit_reason
find("#{MODAL_SELECTOR} .btn-edit-reason").click
end
def type_edit_reason(text)
find("#{MODAL_SELECTOR} .edit-reason input").send_keys(text)
end
def find_cell(row, col)
find("#{SPREADSHEET_TABLE_SELECTOR} tbody tr[data-y='#{row}'] td[data-x='#{col}']")
end
def select_cell(row, col)
find_cell(row, col).double_click
end
def type_in_cell(row, col, text)
select_cell(row, col)
cell = find_cell(row, col).find("textarea")
cell.send_keys(text, :return)
end
def has_content_in_cell?(row, col, content)
find_cell(row, col).text == content
end
end
end
end

View File

@ -0,0 +1,144 @@
# frozen_string_literal: true
describe "Table Builder", type: :system do
fab!(:user)
let(:composer) { PageObjects::Components::Composer.new }
let(:insert_table_modal) { PageObjects::Modals::InsertTable.new }
fab!(:topic) { Fabricate(:topic, user: user) }
fab!(:post1) { create_post(user: user, topic: topic, raw: <<~RAW) }
|Make | Model | Year|
|-------| ------- | ----|
|Toyota | Supra | 1998|
|Nissan | Skyline | 1999|
|Honda | S2000 | 2001|
RAW
let(:topic_page) { PageObjects::Pages::Topic.new }
before { sign_in(user) }
def normalize_value(content)
content.strip.gsub(/\s+/, " ").gsub(/\r\n/, "\n")
end
context "when creating a new table" do
it "should add table items created in spreadsheet to composer input" do
visit("/latest")
page.find("#create-topic").click
page.find(".toolbar-popup-menu-options").click
page.find(".select-kit-row[data-name='Insert Table']").click
insert_table_modal.type_in_cell(0, 0, "Item 1")
insert_table_modal.type_in_cell(0, 1, "Item 2")
insert_table_modal.type_in_cell(0, 2, "Item 3")
insert_table_modal.type_in_cell(0, 3, "Item 4")
insert_table_modal.click_insert_table
created_table = <<~TABLE
|Column 1 | Column 2 | Column 3 | Column 4|
|--- | --- | --- | ---|
|Item 1 | Item 2 | Item 3 | Item 4|
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
TABLE
expect(normalize_value(composer.composer_input.value)).to eq(normalize_value(created_table))
end
context "when cancelling table creation" do
it "should close the modal if there are no changes made" do
visit("/latest")
page.find("#create-topic").click
page.find(".toolbar-popup-menu-options").click
page.find(".select-kit-row[data-name='Insert Table']").click
insert_table_modal.cancel
expect(page).to have_no_css(".insert-table-modal")
end
it "should show a warning popup if there are unsaved changes" do
visit("/latest")
page.find("#create-topic").click
page.find(".toolbar-popup-menu-options").click
page.find(".select-kit-row[data-name='Insert Table']").click
insert_table_modal.type_in_cell(0, 0, "Item 1")
insert_table_modal.cancel
expect(page).to have_css(".dialog-container .dialog-content")
end
end
end
context "when editing a table" do
it "should prefill the spreadsheet with the markdown table items from the post" do
topic_page.visit_topic(topic)
topic_page.find(".btn-edit-table", visible: :all).click
expect(page).to have_selector(".insert-table-modal")
expected_table_content = [
%w[Toyota Supra 1998],
%w[Nissan Skyline 1999],
%w[Honda S2000 2001],
]
expected_table_content.each_with_index do |row, row_index|
row.each_with_index do |content, col_index|
expect(insert_table_modal).to have_content_in_cell(row_index, col_index, content)
end
end
end
it "should update the post with the new table content" do
topic_page.visit_topic(topic)
topic_page.find(".btn-edit-table", visible: :all).click
expect(page).to have_selector(".insert-table-modal")
insert_table_modal.type_in_cell(1, 1, " GTR")
insert_table_modal.click_insert_table
updated_post = <<~RAW
|Make | Model | Year|
|-------| ------- | ----|
|Toyota | Supra | 1998|
|Nissan | Skyline | 1999|
|Honda | S2000 | 2001|
RAW
expect(normalize_value(post1.reload.raw)).to eq(normalize_value(updated_post))
end
context "when adding an edit reason" do
it "should add the edit reason to the edit history" do
edit_reason = "Updated Nissan model"
topic_page.visit_topic(topic)
topic_page.find(".btn-edit-table", visible: :all).click
expect(page).to have_selector(".insert-table-modal")
insert_table_modal.type_in_cell(1, 1, " GTR")
insert_table_modal.click_edit_reason
insert_table_modal.type_edit_reason(edit_reason)
insert_table_modal.click_insert_table
wait_for { post1.reload.edit_reason == edit_reason }
expect(post1.reload.edit_reason).to eq(edit_reason)
end
end
context "when cancelling table creation" do
it "should close the modal if there are no changes made" do
topic_page.visit_topic(topic)
topic_page.find(".btn-edit-table", visible: :all).click
expect(page).to have_selector(".insert-table-modal")
insert_table_modal.cancel
expect(page).to have_no_css(".insert-table-modal")
end
it "should show a warning popup if there are unsaved changes" do
topic_page.visit_topic(topic)
topic_page.find(".btn-edit-table", visible: :all).click
expect(page).to have_selector(".insert-table-modal")
insert_table_modal.type_in_cell(1, 1, " GTR")
insert_table_modal.cancel
expect(page).to have_css(".dialog-container .dialog-content")
end
end
end
end