mirror of
https://github.com/discourse/discourse-table-builder.git
synced 2025-02-16 08:24:42 +00:00
FEATURE: Ability to build a table in the composer
- Adds a button to the composer tools which triggers a modal window for a table builder - Table builder contains fields to add table contents, as well as buttons to align contents, and add additional tables/rows
This commit is contained in:
parent
fc9898daac
commit
4c8cfa9559
@ -9,5 +9,11 @@
|
|||||||
"minimum_discourse_version": null,
|
"minimum_discourse_version": null,
|
||||||
"maximum_discourse_version": null,
|
"maximum_discourse_version": null,
|
||||||
"assets": {},
|
"assets": {},
|
||||||
"modifiers": {}
|
"modifiers": {
|
||||||
|
"svg_icons": [
|
||||||
|
"align-left",
|
||||||
|
"align-center",
|
||||||
|
"align-right"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
.table-builder-modal-modal {
|
||||||
|
.modal-inner-container {
|
||||||
|
--modal-max-width: 90vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header-fields {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
|
.header-column {
|
||||||
|
border: 1px solid var(--primary-low-mid);
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-action-buttons {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: auto;
|
||||||
|
border-top: 1px solid var(--primary-low-mid);
|
||||||
|
padding-top: 1rem;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.column-aligned {
|
||||||
|
&-right .btn-align-right {
|
||||||
|
background: var(--primary-high);
|
||||||
|
.d-icon {
|
||||||
|
color: var(--primary-very-low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-left .btn-align-left {
|
||||||
|
background: var(--primary-high);
|
||||||
|
.d-icon {
|
||||||
|
color: var(--primary-very-low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-center .btn-align-center {
|
||||||
|
background: var(--primary-high);
|
||||||
|
.d-icon {
|
||||||
|
color: var(--primary-very-low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-inputs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,24 @@
|
|||||||
import { apiInitializer } from "discourse/lib/api";
|
import { apiInitializer } from "discourse/lib/api";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
|
||||||
export default apiInitializer("0.11.1", (api) => {});
|
export default apiInitializer("0.11.1", (api) => {
|
||||||
|
api.modifyClass("component:d-editor", {
|
||||||
|
pluginId: "discourse-table-builder",
|
||||||
|
|
||||||
|
@action
|
||||||
|
showTableBuilder(event) {
|
||||||
|
showModal("table-builder-modal").set("toolbarEvent", event);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onToolbarCreate((toolbar) => {
|
||||||
|
toolbar.addButton({
|
||||||
|
id: "table-builder",
|
||||||
|
group: "insertions",
|
||||||
|
icon: "table",
|
||||||
|
sendAction: (event) => toolbar.context.send("showTableBuilder", event),
|
||||||
|
title: themePrefix("discourse_table_builder.composer.button"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
34
javascripts/discourse/components/body-row.js
Normal file
34
javascripts/discourse/components/body-row.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import GlimmerComponent from "discourse/components/glimmer";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
|
||||||
|
export default class BodyRow extends GlimmerComponent {
|
||||||
|
get disableRemoveRow() {
|
||||||
|
if (this.args.allRows.length > 1) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
addBodyValue() {
|
||||||
|
const columnId = this.args.columnId;
|
||||||
|
const rowId = this.args.row.id;
|
||||||
|
const value = this.bodyRowValue;
|
||||||
|
|
||||||
|
this.args.setRowValue(columnId, rowId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
addRow() {
|
||||||
|
const columnId = this.args.columnId;
|
||||||
|
const rowId = this.args.allRows.length + 1;
|
||||||
|
|
||||||
|
this.args.addRow(columnId, rowId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
removeRow() {
|
||||||
|
this.args.removeRow(this.args.columnId, this.args.row);
|
||||||
|
}
|
||||||
|
}
|
53
javascripts/discourse/components/header-column.js
Normal file
53
javascripts/discourse/components/header-column.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import GlimmerComponent from "discourse/components/glimmer";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
|
||||||
|
export default class HeaderColumn extends GlimmerComponent {
|
||||||
|
@tracked alignment;
|
||||||
|
|
||||||
|
get disableRemoveColumn() {
|
||||||
|
if (this.args.tableItems.length > 1) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
addColumn() {
|
||||||
|
const newColumnId = this.args.tableItems.length + 1;
|
||||||
|
this.args.addColumn(newColumnId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
removeColumn() {
|
||||||
|
this.args.removeColumn(this.args.column);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
addColumnHeader() {
|
||||||
|
const index = this.args.columnId - 1;
|
||||||
|
this.args.setColumnHeader(index, this.columnHeaderValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
addRow(columnId, rowId) {
|
||||||
|
this.args.addRow(columnId, rowId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
removeRow(columnId, row) {
|
||||||
|
this.args.removeRow(columnId, row);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setRowValue(columnId, rowId, value) {
|
||||||
|
this.args.setRowValue(columnId, rowId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
alignColumn(alignment) {
|
||||||
|
this.args.alignColumn(this.args.columnId, alignment);
|
||||||
|
this.alignment = alignment;
|
||||||
|
}
|
||||||
|
}
|
145
javascripts/discourse/controllers/table-builder-modal.js
Normal file
145
javascripts/discourse/controllers/table-builder-modal.js
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import Controller from "@ember/controller";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { A } from "@ember/array";
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
@tracked tableItems = A([
|
||||||
|
{ column: 1, rows: A([{ id: 1 }]) },
|
||||||
|
{ column: 2, rows: A([{ id: 1 }]) },
|
||||||
|
]);
|
||||||
|
|
||||||
|
@action
|
||||||
|
cancelTableCreation() {
|
||||||
|
this.send("closeModal");
|
||||||
|
}
|
||||||
|
|
||||||
|
createDivider(alignment) {
|
||||||
|
switch (alignment) {
|
||||||
|
case "left":
|
||||||
|
return ":--";
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
return "--:";
|
||||||
|
break;
|
||||||
|
case "center":
|
||||||
|
return ":--:";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return "--";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTable(table) {
|
||||||
|
const headings = [];
|
||||||
|
const divider = [];
|
||||||
|
const rows = [];
|
||||||
|
|
||||||
|
table.forEach((item) => {
|
||||||
|
headings.push(item.header);
|
||||||
|
divider.push(this.createDivider(item.alignment));
|
||||||
|
item.rows.forEach((r) => rows.push(r));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make an object for each row rather than by column
|
||||||
|
const rowItems = rows.reduce((row, { id, content }) => {
|
||||||
|
row[id] ??= { id, content: [] };
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
row[id].content = row[id].value.concat(content);
|
||||||
|
} else {
|
||||||
|
row[id].content.push(content);
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const header = `|${headings.join("|")}|\n`;
|
||||||
|
const tableDivider = `|${divider.join("|")}|\n`;
|
||||||
|
|
||||||
|
let rowValues;
|
||||||
|
Object.values(rowItems).forEach((item) => {
|
||||||
|
item.content.forEach((line) => {
|
||||||
|
if (line === undefined) {
|
||||||
|
line = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowValues) {
|
||||||
|
rowValues += `${line}|`;
|
||||||
|
} else {
|
||||||
|
rowValues = `|${line}|`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rowValues += "\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
let tableMarkdown = header + tableDivider + rowValues;
|
||||||
|
|
||||||
|
this.toolbarEvent.addText(tableMarkdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
createTable() {
|
||||||
|
this.buildTable(this.tableItems);
|
||||||
|
this.send("closeModal");
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
removeColumn(column) {
|
||||||
|
this.tableItems.removeObject(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
addColumn(columnId) {
|
||||||
|
this.tableItems.pushObject({
|
||||||
|
column: columnId,
|
||||||
|
rows: A([{ id: 1 }]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setColumnHeader(index, value) {
|
||||||
|
this.tableItems[index].header = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
addRow(columnId, rowId) {
|
||||||
|
this.tableItems.find((item) => {
|
||||||
|
if (item.column === columnId) {
|
||||||
|
item.rows.pushObject({ id: rowId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
removeRow(columnId, row) {
|
||||||
|
this.tableItems.find((item) => {
|
||||||
|
if (item.column === columnId) {
|
||||||
|
if (item.rows.length === 1) {
|
||||||
|
// do not allow deleting if only one row left
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
item.rows.removeObject(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setRowValue(columnId, rowId, value) {
|
||||||
|
const index = columnId - 1;
|
||||||
|
this.tableItems[index].rows.find((row) => {
|
||||||
|
if (row.id === rowId) {
|
||||||
|
row.content = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
alignColumn(columnId, alignment) {
|
||||||
|
this.tableItems.find((item) => {
|
||||||
|
if (item.column === columnId) {
|
||||||
|
item.alignment = alignment;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
20
javascripts/discourse/templates/components/body-row.hbs
Normal file
20
javascripts/discourse/templates/components/body-row.hbs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<div class="body-row">
|
||||||
|
<Input
|
||||||
|
@value={{bodyRowValue}}
|
||||||
|
@class="table-builder-input"
|
||||||
|
@placeholderKey={{theme-prefix "discourse_table_builder.modal.body"}}
|
||||||
|
{{on "change" this.addBodyValue}}
|
||||||
|
/>
|
||||||
|
<DButton
|
||||||
|
@icon="plus"
|
||||||
|
@title={{theme-prefix "discourse_table_builder.modal.buttons.add_row"}}
|
||||||
|
@action={{action "addRow"}}
|
||||||
|
/>
|
||||||
|
<DButton
|
||||||
|
@icon="trash-alt"
|
||||||
|
@class="btn-danger"
|
||||||
|
@title={{theme-prefix "discourse_table_builder.modal.buttons.remove_row"}}
|
||||||
|
@action={{action "removeRow"}}
|
||||||
|
@disabled={{this.disableRemoveRow}}
|
||||||
|
/>
|
||||||
|
</div>
|
62
javascripts/discourse/templates/components/header-column.hbs
Normal file
62
javascripts/discourse/templates/components/header-column.hbs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<div class="header-column">
|
||||||
|
<div class="header-row">
|
||||||
|
<Input
|
||||||
|
@value={{columnHeaderValue}}
|
||||||
|
@class="table-builder-input"
|
||||||
|
@id="header-column-{{@columnId}}"
|
||||||
|
@placeholderKey={{theme-prefix "discourse_table_builder.modal.header"}}
|
||||||
|
{{on "change" this.addColumnHeader}}
|
||||||
|
/>
|
||||||
|
<div class="body-inputs">
|
||||||
|
{{#each @column.rows as |item|}}
|
||||||
|
<BodyRow
|
||||||
|
@addRow={{this.addRow}}
|
||||||
|
@columnId={{@columnId}}
|
||||||
|
@allRows={{@column.rows}}
|
||||||
|
@row={{item}}
|
||||||
|
@removeRow={{this.removeRow}}
|
||||||
|
@setRowValue={{this.setRowValue}}
|
||||||
|
/>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="column-action-buttons column-aligned-{{this.alignment}}">
|
||||||
|
<DButton
|
||||||
|
@icon="align-left"
|
||||||
|
@class="btn-align-left"
|
||||||
|
@title={{theme-prefix "discourse_table_builder.modal.buttons.align_left"}}
|
||||||
|
@action={{action "alignColumn" "left"}}
|
||||||
|
/>
|
||||||
|
<DButton
|
||||||
|
@icon="align-center"
|
||||||
|
@class="btn-align-center"
|
||||||
|
@title={{theme-prefix
|
||||||
|
"discourse_table_builder.modal.buttons.align_center"
|
||||||
|
}}
|
||||||
|
@action={{action "alignColumn" "center"}}
|
||||||
|
/>
|
||||||
|
<DButton
|
||||||
|
@icon="align-right"
|
||||||
|
@class="btn-align-right"
|
||||||
|
@title={{theme-prefix
|
||||||
|
"discourse_table_builder.modal.buttons.align_right"
|
||||||
|
}}
|
||||||
|
@action={{action "alignColumn" "right"}}
|
||||||
|
/>
|
||||||
|
<DButton
|
||||||
|
@icon="trash-alt"
|
||||||
|
@class="btn-danger"
|
||||||
|
@action={{action "removeColumn"}}
|
||||||
|
@title={{theme-prefix
|
||||||
|
"discourse_table_builder.modal.buttons.remove_column"
|
||||||
|
}}
|
||||||
|
@disabled={{this.disableRemoveColumn}}
|
||||||
|
/>
|
||||||
|
<DButton
|
||||||
|
@icon="plus"
|
||||||
|
@action={{action "addColumn"}}
|
||||||
|
@title={{theme-prefix "discourse_table_builder.modal.buttons.add_column"}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,40 @@
|
|||||||
|
<DModalBody
|
||||||
|
@title={{theme-prefix "discourse_table_builder.modal.title"}}
|
||||||
|
@class="table-builder-modal"
|
||||||
|
>
|
||||||
|
<form id="insert-markdown-table-form" {{action "createTable" on="submit"}}>
|
||||||
|
<section class="header-section">
|
||||||
|
<div class="table-header-fields">
|
||||||
|
{{#each this.tableItems as |item|}}
|
||||||
|
<HeaderColumn
|
||||||
|
@column={{item}}
|
||||||
|
@columnId={{item.column}}
|
||||||
|
@tableItems={{this.tableItems}}
|
||||||
|
@addColumn={{this.addColumn}}
|
||||||
|
@removeColumn={{this.removeColumn}}
|
||||||
|
@setColumnHeader={{this.setColumnHeader}}
|
||||||
|
@addRow={{this.addRow}}
|
||||||
|
@removeRow={{this.removeRow}}
|
||||||
|
@setRowValue={{this.setRowValue}}
|
||||||
|
@alignColumn={{this.alignColumn}}
|
||||||
|
/>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</DModalBody>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<DButton
|
||||||
|
@class="btn-primary"
|
||||||
|
@label={{theme-prefix "discourse_table_builder.modal.create"}}
|
||||||
|
@icon="plus"
|
||||||
|
@action={{action "createTable"}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DButton
|
||||||
|
@class="btn-flat"
|
||||||
|
@label={{theme-prefix "discourse_table_builder.modal.cancel"}}
|
||||||
|
@action={{action "cancelTableCreation"}}
|
||||||
|
/>
|
||||||
|
</div>
|
@ -1,3 +1,21 @@
|
|||||||
en:
|
en:
|
||||||
|
discourse_table_builder:
|
||||||
|
title: "Table Builder"
|
||||||
|
composer:
|
||||||
|
button: "Insert Title"
|
||||||
|
modal:
|
||||||
|
title: "Table Builder"
|
||||||
|
create: "Build Table"
|
||||||
|
cancel: "cancel"
|
||||||
|
header: "Header"
|
||||||
|
body: "Body"
|
||||||
|
buttons:
|
||||||
|
add_column: "Add Column"
|
||||||
|
remove_column: "Remove Column"
|
||||||
|
add_row: "Add Row"
|
||||||
|
remove_row: "Remove Row"
|
||||||
|
align_left: "Align Left"
|
||||||
|
align_center: "Align Center"
|
||||||
|
align_right: "Align Right"
|
||||||
theme_metadata:
|
theme_metadata:
|
||||||
description: "Adds a button to the composer to easily build tables in markdown"
|
description: "Adds a button to the composer to easily build tables in markdown"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user