diff --git a/.github/images/cover.png b/.github/images/cover.png index 6369960..4203e1c 100644 Binary files a/.github/images/cover.png and b/.github/images/cover.png differ diff --git a/common/common.scss b/common/common.scss index e170146..d97b245 100644 --- a/common/common.scss +++ b/common/common.scss @@ -2,6 +2,5 @@ @import "vendor/jspreadsheet-datatables"; @import "vendor/jspreadsheet-theme"; @import "vendor/jsuites"; - -@import "table-builder"; -@import "table-editor"; +@import "post/table-edit-decorator"; +@import "modal/insert-table-modal"; diff --git a/javascripts/discourse/api-initializers/table-builder.js b/javascripts/discourse/api-initializers/table-builder.js index 87c9775..cc9bd3c 100644 --- a/javascripts/discourse/api-initializers/table-builder.js +++ b/javascripts/discourse/api-initializers/table-builder.js @@ -3,22 +3,24 @@ import { action } from "@ember/object"; import showModal from "discourse/lib/show-modal"; export default apiInitializer("0.11.1", (api) => { - api.modifyClass("component:d-editor", { + api.modifyClass("controller:composer", { pluginId: "discourse-table-builder", @action - showTableBuilder(event) { - showModal("table-builder-modal").set("toolbarEvent", event); + showTableBuilder() { + showModal("insert-table-modal").setProperties({ + toolbarEvent: this.toolbarEvent, + tableHtml: null, + }); }, }); - api.onToolbarCreate((toolbar) => { - toolbar.addButton({ + api.addToolbarPopupMenuOptionsCallback(() => { + return { id: "table-builder", - group: "insertions", + action: "showTableBuilder", icon: "table", - sendAction: (event) => toolbar.context.send("showTableBuilder", event), - title: themePrefix("discourse_table_builder.composer.button"), - }); + label: themePrefix("discourse_table_builder.composer.button"), + }; }); }); diff --git a/javascripts/discourse/api-initializers/table-editor.js b/javascripts/discourse/api-initializers/table-editor.js index 4a16fb9..bf87cae 100644 --- a/javascripts/discourse/api-initializers/table-editor.js +++ b/javascripts/discourse/api-initializers/table-editor.js @@ -35,7 +35,7 @@ export default apiInitializer("0.11.1", (api) => { return ajax(`/posts/${this.id}`, { type: "GET" }) .then((post) => { - showModal("table-editor-modal", { + showModal("insert-table-modal", { model: post, }).setProperties({ tableHtml: tempTable, diff --git a/javascripts/discourse/components/body-row.js b/javascripts/discourse/components/body-row.js deleted file mode 100644 index 27ee33e..0000000 --- a/javascripts/discourse/components/body-row.js +++ /dev/null @@ -1,34 +0,0 @@ -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); - } -} diff --git a/javascripts/discourse/components/header-column.js b/javascripts/discourse/components/header-column.js deleted file mode 100644 index b98c1c9..0000000 --- a/javascripts/discourse/components/header-column.js +++ /dev/null @@ -1,53 +0,0 @@ -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; - } -} diff --git a/javascripts/discourse/components/spreadsheet-editor.js b/javascripts/discourse/components/spreadsheet-editor.js index 4f878be..0313473 100644 --- a/javascripts/discourse/components/spreadsheet-editor.js +++ b/javascripts/discourse/components/spreadsheet-editor.js @@ -1,4 +1,3 @@ -// import Controller from "@ember/controller"; import { action } from "@ember/object"; import loadScript from "discourse/lib/load-script"; import { arrayToTable, findTableRegex, tableToObj } from "../lib/utilities"; @@ -6,26 +5,115 @@ import Component from "@ember/component"; import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; import I18n from "I18n"; +import { schedule } from "@ember/runloop"; export default Component.extend({ tagName: "", showEditReason: false, + spreadsheet: null, + // Lifecycle Methods: didInsertElement() { this._super(...arguments); - this.loadLibraries().then(() => { - this.buildTable(this.tableHtml); + schedule("afterRender", () => { + this.loadLibraries().then(() => { + if (this.isEditingTable) { + this.buildPopulatedTable(this.tableHtml); + } else { + this.buildNewTable(); + } + }); }); }, + willDestroyElement() { + this._super(...arguments); + this.spreadsheet?.destroy(); + }, + + // Getters: + get isEditingTable() { + if (this.tableHtml) { + return true; + } + + return false; + }, + + get modalAttributes() { + if (this.isEditingTable) { + return { + title: themePrefix("discourse_table_builder.edit.modal.title"), + insertTable: { + title: themePrefix("discourse_table_builder.edit.modal.create"), + icon: "pencil-alt", + }, + }; + } else { + return { + title: themePrefix("discourse_table_builder.modal.title"), + insertTable: { + title: themePrefix("discourse_table_builder.modal.create"), + icon: "plus", + }, + }; + } + }, + + // Actions: + @action + showEditReasonField() { + if (this.showEditReason) { + return this.set("showEditReason", false); + } else { + return this.set("showEditReason", true); + } + }, + + @action + cancelTableInsertion() { + this.triggerModalClose(); + }, + + @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.toolbarEvent.addText(markdownTable); + return this.triggerModalClose(); + } else { + return this.updateTable(markdownTable); + } + }, + + // Helper Methods: loadLibraries() { return loadScript(settings.theme_uploads.jsuites).then(() => { return loadScript(settings.theme_uploads.jspreadsheet); }); }, - buildTable(table) { + buildNewTable() { + const data = [ + ["", "", ""], + ["", "", ""], + ["", "", ""], + ]; + + const columns = [ + { title: "Column 1", width: 150 }, + { title: "Column 2", width: 150 }, + { title: "Column 3", width: 150 }, + ]; + + return this.buildSpreadsheet(data, columns); + }, + + buildPopulatedTable(table) { const tableObject = tableToObj(table); const headings = []; const tableData = []; @@ -47,47 +135,20 @@ export default Component.extend({ }; }); + return this.buildSpreadsheet(tableData, columns); + }, + + buildSpreadsheet(data, columns, opts = {}) { const spreadsheetContainer = document.querySelector("#spreadsheet"); // eslint-disable-next-line no-undef this.spreadsheet = jspreadsheet(spreadsheetContainer, { - data: tableData, + data, columns, + ...opts, }); }, - @action - showEditReasonField() { - if (this.showEditReason) { - return this.set("showEditReason", false); - } else { - return this.set("showEditReason", true); - } - }, - - @action - cancelTableEdit() { - this.triggerModalClose(); - }, - - @action - editTable() { - const updatedHeaders = this.spreadsheet.getHeaders().split(","); // keys - const updatedData = this.spreadsheet.getData(); // values - - const markdownTable = this.buildTableMarkdown(updatedHeaders, updatedData); - const tableId = this.get("tableId"); - const postId = this.model.id; - const newRaw = markdownTable; - const editReason = - this.get("editReason") || - I18n.t(themePrefix("discourse_table_builder.edit.default_edit_reason")); - const raw = this.model.raw; - const newPostRaw = this.buildUpdatedPost(tableId, raw, newRaw); - - this.updateTable(postId, newPostRaw, editReason); - }, - buildUpdatedPost(tableId, raw, newRaw) { const tableToEdit = raw.match(findTableRegex()); let editedTable; @@ -101,7 +162,20 @@ export default Component.extend({ return editedTable; }, - updateTable(postId, raw, edit_reason) { + updateTable(markdownTable) { + const tableId = this.get("tableId"); + const postId = this.model.id; + const newRaw = markdownTable; + const editReason = + this.get("editReason") || + I18n.t(themePrefix("discourse_table_builder.edit.default_edit_reason")); + const raw = this.model.raw; + const newPostRaw = this.buildUpdatedPost(tableId, raw, newRaw); + + return this.sendTableUpdate(postId, newPostRaw, editReason); + }, + + sendTableUpdate(postId, raw, edit_reason) { return ajax(`/posts/${postId}.json`, { type: "PUT", data: { diff --git a/javascripts/discourse/controllers/table-editor-modal.js b/javascripts/discourse/controllers/insert-table-modal.js similarity index 100% rename from javascripts/discourse/controllers/table-editor-modal.js rename to javascripts/discourse/controllers/insert-table-modal.js diff --git a/javascripts/discourse/controllers/table-builder-modal.js b/javascripts/discourse/controllers/table-builder-modal.js deleted file mode 100644 index e61b6f7..0000000 --- a/javascripts/discourse/controllers/table-builder-modal.js +++ /dev/null @@ -1,153 +0,0 @@ -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 }]) }, - ]); - - resetData() { - this.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.resetData(); - 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; - } - }); - } -} diff --git a/javascripts/discourse/templates/components/body-row.hbs b/javascripts/discourse/templates/components/body-row.hbs deleted file mode 100644 index 3230f8c..0000000 --- a/javascripts/discourse/templates/components/body-row.hbs +++ /dev/null @@ -1,20 +0,0 @@ -
- - - -
diff --git a/javascripts/discourse/templates/components/header-column.hbs b/javascripts/discourse/templates/components/header-column.hbs deleted file mode 100644 index ee0a436..0000000 --- a/javascripts/discourse/templates/components/header-column.hbs +++ /dev/null @@ -1,62 +0,0 @@ -
-
- -
- {{#each @column.rows as |item|}} - - {{/each}} -
- -
-
- - - - - -
-
diff --git a/javascripts/discourse/templates/components/spreadsheet-editor.hbs b/javascripts/discourse/templates/components/spreadsheet-editor.hbs index 5c38d67..7404534 100644 --- a/javascripts/discourse/templates/components/spreadsheet-editor.hbs +++ b/javascripts/discourse/templates/components/spreadsheet-editor.hbs @@ -1,38 +1,62 @@ - -
- - {{#if showEditReason}} - + {{#if this.isEditingTable}} +
+ - {{/if}} -
+ {{#if showEditReason}} + + {{/if}} +
+ {{/if}} +
diff --git a/javascripts/discourse/templates/modal/table-editor-modal.hbs b/javascripts/discourse/templates/modal/insert-table-modal.hbs similarity index 80% rename from javascripts/discourse/templates/modal/table-editor-modal.hbs rename to javascripts/discourse/templates/modal/insert-table-modal.hbs index b820d55..7183b8f 100644 --- a/javascripts/discourse/templates/modal/table-editor-modal.hbs +++ b/javascripts/discourse/templates/modal/insert-table-modal.hbs @@ -3,4 +3,5 @@ @tableHtml={{this.tableHtml}} @tableId={{this.tableId}} @triggerModalClose={{action "closeEditModal"}} + @toolbarEvent={{this.toolbarEvent}} /> diff --git a/javascripts/discourse/templates/modal/table-builder-modal.hbs b/javascripts/discourse/templates/modal/table-builder-modal.hbs deleted file mode 100644 index 7ffc94f..0000000 --- a/javascripts/discourse/templates/modal/table-builder-modal.hbs +++ /dev/null @@ -1,40 +0,0 @@ - -
-
-
- {{#each this.tableItems as |item|}} - - {{/each}} -
-
-
-
- - diff --git a/locales/en.yml b/locales/en.yml index 360aba5..54512fb 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -7,16 +7,13 @@ en: 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" + 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." edit: btn_edit: "Edit Table" modal: diff --git a/scss/table-editor.scss b/scss/modal/insert-table-modal.scss similarity index 58% rename from scss/table-editor.scss rename to scss/modal/insert-table-modal.scss index 53b0671..d8347b1 100644 --- a/scss/table-editor.scss +++ b/scss/modal/insert-table-modal.scss @@ -1,9 +1,4 @@ -.open-popup-link { - display: inline; - margin-inline: 0.25em; -} - -.btn-edit-table { +.btn-insert-table { background: var(--tertiary); color: var(--secondary); @@ -23,7 +18,7 @@ } } -.table-editor-modal { +.insert-table-modal-modal { display: flex; flex-direction: column; align-items: flex-start; @@ -34,4 +29,20 @@ color: var(--primary-high); } } + + .modal-footer { + display: flex; + align-items: center; + justify-content: space-between; + + .secondary-actions .tippy-content { + h4 { + color: var(--primary); + } + li { + margin-block: 0.25rem; + color: var(--primary-high); + } + } + } } diff --git a/scss/post/table-edit-decorator.scss b/scss/post/table-edit-decorator.scss new file mode 100644 index 0000000..52ecb9d --- /dev/null +++ b/scss/post/table-edit-decorator.scss @@ -0,0 +1,4 @@ +.open-popup-link { + display: inline; + margin-inline: 0.25em; +} diff --git a/scss/table-builder.scss b/scss/table-builder.scss deleted file mode 100644 index d8bd9d3..0000000 --- a/scss/table-builder.scss +++ /dev/null @@ -1,77 +0,0 @@ -.table-builder-modal-modal { - .modal-inner-container { - --modal-max-width: 90vw; - } -} - -.table-header-fields { - display: flex; - gap: 0.5rem; - margin-bottom: 2rem; - - .table-builder-input { - font-weight: bold; - } - - .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; - - .table-builder-input { - font-weight: normal; - } - } - - .body-row { - display: flex; - align-items: center; - justify-content: flex-start; - margin-bottom: 1rem; - gap: 0.5rem; - - input { - margin-bottom: 0; - } - } -}