Added drag and drop for moving table columns and rows

Has confirmation for deleting tables
This commit is contained in:
Matthew Welch 2021-01-10 01:14:54 -08:00
parent 8d43963f31
commit 20031fb918
8 changed files with 4098 additions and 52 deletions

View File

@ -29,3 +29,17 @@ td.inline-edit > textarea, th.inline-edit > textarea {
width: 100%;
height: 100%;
}
.recent-files {
width: 300px;
height: 500px;
float: left;
overflow-y: auto;
border-right:1px solid #dee2e6;
}
#splash {
width: 500px;
height: 500px;
float: right;
}

View File

@ -45,7 +45,7 @@
<div class="form-check">
<label class="form-check-label">
<input type="checkbox" class="form-check-input" id="detail-column-sortable">
Column sortable
Column Sortable
</label>
</div>
</div>
@ -54,11 +54,28 @@
<label class="form-label">Cell Text</label>
<textarea class="form-control" id="detail-cell-text"></textarea>
</div>
<div>
<button id="position-edit" class="btn btn-secondary" data-bs-toggle=tooltip" data-bs-placement=bottom" title="move columns, rows, and tables">Start Edit Position</button>
</div>
</div>
<div id="tables" class="col-9 tab-content"></div>
</div>
</div>
<div class="modal fade" id="delete-table" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
<h5>Are you sure that you want to delete the table?</h5>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No, keep table</button>
<button id="confirm-delete-table" type="button" class="btn btn-danger" data-bs-dismiss="modal">Yes, delete table</button>
</div>
</div>
</div>
</div>
<script>
if (typeof module === 'object') {
window.module = module;
@ -68,6 +85,7 @@
<script src="js/jquery-3.5.1.min.js"></script>
<script src="js/bootstrap.bundle.min.js"></script>
<script>if (window.module) module = window.module;</script>
<script src="js/Sortable.js"></script>
<script src="js/editor.js"></script>
</body>
</html>

View File

@ -80,12 +80,14 @@ app.on('activate', () => {
function createSplashScreen() {
const splash_screen = new BrowserWindow({
width: 800,
height: 600,
width: 806,
height: 529,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true
}
},
resizable: false,
frame: true
});
if (!store.has("recent_files")) {
@ -108,7 +110,7 @@ function createEditorWindow() {
nodeIntegration: true
}
});
main_window.maximize();
updateMenuBar();
main_window.loadFile("src/editor.html");
@ -157,6 +159,7 @@ function openFile(file_path="", new_file=false) {
}
if (file_path) {
updateRecentFiles(file_path);
current_file = file_path
if (new_file) {
let old_window = BrowserWindow.getFocusedWindow();
let window = createEditorWindow();
@ -282,7 +285,7 @@ function updateRecentFiles(new_file) {
recent_files.splice(recent_files.indexOf(new_file), 1);
}
recent_files.splice(0, 0, new_file);
store.set("recent_files", recent_files)
store.set("recent_files", recent_files);
updateMenuBar();
}
@ -306,6 +309,10 @@ function updateMenuBar() {
Menu.setApplicationMenu(menu);
}
ipcMain.handle("get-store-value", (event, key) => {
return store.get(key)
})
ipcMain.on("create-new", (event) => {
createNew();
})

3721
src/js/Sortable.js Normal file

File diff suppressed because it is too large Load Diff

2
src/js/Sortable.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,7 @@ let tables_data;
let current_table;
let selected_cells = [];
let selected_cell_data;
let is_positioning = false;
let nav = $("nav");
let nav_scroll = nav.scrollLeft();
let detail_tab_name = $("#detail-tab-name");
@ -12,6 +13,7 @@ let detail_column_name = $("#detail-column-name");
let detail_column_selectable = $("#detail-column-selectable");
let detail_column_sortable = $("#detail-column-sortable");
let detail_cell_text = $("#detail-cell-text");
let position_edit_btn = $("#position-edit");
function openFile(file_name) {
$.ajax({
@ -20,6 +22,7 @@ function openFile(file_name) {
cache: false,
success: (json, textStatus, jqXHR) => {
tables_data = json;
current_table = null;
refreshPage();
}
});
@ -33,23 +36,30 @@ function clearPage() {
$("#tables").empty();
nav.empty();
$("#details input, #details textarea").val("");
selected_cells = [];
selected_cell_data = null;
}
function refreshPage() {
toggleMovement();
clearPage();
generateTables();
showTab(tables_data[0]["id"]);
showTab(current_table ? current_table: tables_data[0]["id"]);
toggleMovement()
}
function generateTable(table_data) {
function generateTable(table_data, table_index) {
const table_id = table_data["id"];
let a = $("<a></a>")
.attr("id", table_id + "-nav")
.addClass("nav-link")
.addClass("nav-link table-nav")
.attr("href", "#" + table_id)
.text(table_data["name"])
.on("click", () => {
showTab(table_id);
})
.on("contextmenu", (event) => {
navContextMenu(event, table_id);
});
let add_table_btn = nav.find("a[id='add-table']");
if (add_table_btn.length !== 0) {
@ -60,7 +70,8 @@ function generateTable(table_data) {
let table_div = $("<div></div>")
.attr("id", table_id + "-tab")
.addClass("tab-pane table-responsive");
.addClass("tab-pane table-responsive text-nowrap")
.css({"margin-right": "100px"});
let table = $("<table></table>")
.attr("id", table_id)
@ -74,7 +85,7 @@ function generateTable(table_data) {
const col_id = col["field"];
let header_id = table_id + "-" + col_id;
table.find("thead > tr")
.append(makeHeader(header_id, col["title"]));
.append(makeHeader(header_id, i, col["title"]));
})
$.each(table_data["rows"], (i, row) => {
@ -90,8 +101,14 @@ function generateTable(table_data) {
table.find("tbody").append(tr);
})
table_div.append(table)
$("#tables").append(table_div);
table_div.append(table);
let table_container = $("#tables");
let add_row_div = table_container.find("div[id='add-row-div']");
if (add_row_div.length !== 0) {
add_row_div.before(table_div);
} else {
table_container.append(table_div);
}
}
function makeCell(cell_id, cell_text) {
@ -109,9 +126,10 @@ function makeCell(cell_id, cell_text) {
});
}
function makeHeader(header_id, col_title) {
function makeHeader(header_id, header_index, col_title) {
return $("<th>")
.attr("id", header_id)
.css("user-select", "none")
.text(col_title)
.on("click", () => {
selectCell(header_id);
@ -125,13 +143,33 @@ function makeHeader(header_id, col_title) {
}
function generateTables() {
for (let table_data of tables_data) {
generateTable(table_data);
}
$("#tables").append($("<div>")
.attr("id", "add-col-div")
.css({"position": "absolute", "top": "46px", "right": "0"})
.append($("<button>")
.attr("id", "btn-add-column")
.addClass("btn btn-primary")
.text("add column")
.on("click", () => {
insertColumn(current_table, -1)
})))
$.each(tables_data, (i, table_data) => {
generateTable(table_data, i);
})
$("#tables").append($("<div>")
.attr("id", "add-row-div")
.append($("<button>")
.attr("id", "btn-add-row")
.addClass("btn btn-primary")
.text("add row")
.on("click", () => {
insertRow(current_table, -1)
})))
nav.append($("<a></a>")
.attr("id", "add-table")
.addClass("nav-link btn-success")
.addClass("nav-link btn btn-success")
.html("<i class='fa fa-plus'></i>")
.attr({"data-bs-toggle": "tooltip", "data-bs-placement": "bottom", "title": "add table"})
.on("click", () => {
addTable();
}))
@ -186,6 +224,7 @@ function getSelectedCellData(force_update=false) {
"table_id": table_match[0],
"table_nav_id": table_match[0]+"-nav",
"col_id": table_match[0]+"-"+column_match[0],
"num_tables": tables_data.length,
}
$.each(tables_data, (i, table_data) => {
if (table_data["id"] === table_match[0]) {
@ -193,6 +232,8 @@ function getSelectedCellData(force_update=false) {
cell_data["table_name"] = table_data["name"];
cell_data["tab_name"] = table_data["tab_name"];
cell_data["table_hidden"] = table_data["hidden"];
cell_data["num_rows"] = table_data["rows"].length;
cell_data["num_cols"] = table_data["col-def"].length;
$.each(table_data["col-def"], (j, col) => {
if (col["field"] === column_match[0]) {
cell_data["col_index"] = j;
@ -253,13 +294,34 @@ function addTable() {
]
}
tables_data.push(table_data);
getSelectedCellData(true);
generateTable(table_data);
}
function insertColumn(table_id, index) {
if (index < 0){return;}
function duplicateTable(table_id) {
for (let i=0; i < tables_data.length; i++) {
if (tables_data[i]["id"] === table_id) {
let new_table = JSON.parse(JSON.stringify(tables_data[i]));
let table_ids = getTableIDs();
let table_num = 0;
for (let id of table_ids) {
if (table_ids.includes(`table${table_num}`)) {
table_num++;
} else {break;}
}
new_table["id"] = `table${table_num}`;
new_table["name"] = "copy of "+new_table["name"];
tables_data.splice(i+1, 0, new_table);
break;
}
}
refreshPage();
}
function insertColumn(table_id, index=-1) {
$.each(tables_data, (i, table_data) => {
if (table_data["id"] === table_id) {
if (index === -1) {index = table_data["col-def"].length}
let col_ids = getColumnIDs(table_data["col-def"]);
let col_num = 0;
for (let id of col_ids) {
@ -288,7 +350,7 @@ function insertColumn(table_id, index) {
}
let header_id = table_id + "-" + col_id;
$(`#${table_id} > thead > tr > th:nth-child(${index + 1})`)
.insert(makeHeader(header_id, "New Column"));
.insert(makeHeader(header_id, index, "New Column"));
$(`#${table_id} > tbody > tr`).each((j, tr) => {
tr = $(tr);
@ -303,9 +365,9 @@ function insertColumn(table_id, index) {
}
function insertRow(table_id, index) {
if (index < 0){return;}
$.each(tables_data, (i, table_data) => {
if (table_data["id"] === table_id) {
if (index === -1) {index = table_data["rows"].length}
let row_ids = getRowIDs(table_data["rows"]);
let row_num = 0;
for (let id of row_ids) {
@ -346,42 +408,103 @@ function insertRow(table_id, index) {
})
}
function deleteTable(table_id) {
for (let i=0; i<tables_data.length; i++) {
if (tables_data[i]["id"] === table_id) {
tables_data.splice(i, 1);
if (table_id === current_table) {
current_table = null;
}
}
}
refreshPage();
}
function deleteColumn(table_id, col_index) {
for (let table_data of tables_data) {
if (table_data["id"] === table_id) {
let field = table_data["col-def"][col_index]["field"];
table_data["col-def"].splice(col_index, 1);
for (let row of table_data["rows"]) {
delete row[field];
}
break;
}
}
refreshPage();
}
function deleteRow(table_id, row_index) {
for (let table_data of tables_data) {
if (table_data["id"] === table_id) {
table_data["rows"].splice(row_index, 1);
break;
}
}
refreshPage();
}
function cellContextMenu(event, cell_id) {
selectCell(cell_id);
let menu = $("<div></div>")
if (is_positioning) {return;}
if (!selected_cells.includes(cell_id)) {
selectCell(cell_id);
}
let menu = $("<div>")
.attr("id", "context-menu-"+cell_id)
.addClass("border context-menu text-nowrap")
.addClass("border context-menu text-nowrap list-group")
.on("mouseleave", () => {
menu.remove();
})
.append($("<button></button>")
.addClass("btn d-block")
.append($("<button>")
.addClass("list-group-item list-group-item-action")
.on("click", () => {
let data = getSelectedCellData();
insertRow(data["table_id"], data["row_index"]);
})
.text("Insert row above"))
.append($("<button></button>")
.addClass("btn d-block")
.append($("<button>")
.addClass("list-group-item list-group-item-action")
.on("click", () => {
let data = getSelectedCellData();
insertRow(data["table_id"], data["row_index"]+1);
})
.text("Insert row below"))
.append($("<button></button>")
.addClass("btn d-block")
.append($("<button>")
.addClass("list-group-item list-group-item-action")
.on("click", () => {
let data = getSelectedCellData();
insertColumn(data["table_id"], data["col_index"]);
})
.text("Insert column left"))
.append($("<button></button>")
.addClass("btn d-block")
.append($("<button>")
.addClass("list-group-item list-group-item-action")
.on("click", () => {
let data = getSelectedCellData();
insertColumn(data["table_id"], data["col_index"]+1);
})
.text("Insert column right"))
let data = getSelectedCellData();
if (data["num_cols"] > 1) {
menu.append($("<button>")
.addClass("list-group-item list-group-item-action list-group-item-danger")
.on("click", () => {
let data = getSelectedCellData();
deleteColumn(data["table_id"], data["col_index"]);
menu.remove();
})
.text("Delete column"))
}
if (data["num_rows"] > 1) {
menu.append($("<button>")
.addClass("list-group-item list-group-item-action list-group-item-danger")
.on("click", () => {
let data = getSelectedCellData();
deleteRow(data["table_id"], data["row_index"]);
menu.remove();
})
.text("Delete row"));
}
let max_height = $(document).height();
let max_width = $(document).width();
@ -394,29 +517,42 @@ function cellContextMenu(event, cell_id) {
}
function headerContextMenu(event, header_id) {
if (is_positioning) {return;}
if (!selected_cells.includes(header_id)) {
selectCell(header_id);
}
let menu = $("<div></div>")
let menu = $("<div>")
.attr("id", "context-menu-"+header_id)
.addClass("border context-menu text-nowrap")
.addClass("border context-menu text-nowrap list-group")
.on("mouseleave", () => {
menu.remove();
})
.append($("<button></button>")
.addClass("btn d-block")
.append($("<button>")
.addClass("list-group-item list-group-item-action")
.on("click", () => {
let data = getSelectedCellData();
insertColumn(data["table_id"], data["col_index"]);
})
.text("Insert column left"))
.append($("<button></button>")
.addClass("btn d-block")
.append($("<button>")
.addClass("list-group-item list-group-item-action")
.on("click", () => {
let data = getSelectedCellData();
insertColumn(data["table_id"], data["col_index"]+1);
})
.text("Insert column right"))
.text("Insert column right"));
let data = getSelectedCellData();
if (data["num_cols"] > 1) {
menu.append($("<button>")
.addClass("list-group-item list-group-item-action list-group-item-danger")
.on("click", () => {
let data = getSelectedCellData();
deleteColumn(data["table_id"], data["col_index"]);
menu.remove();
})
.text("Delete column"))
}
let max_height = $(document).height();
let max_width = $(document).width();
@ -428,7 +564,45 @@ function headerContextMenu(event, header_id) {
menu.css({"top": y_pos, "left": x_pos});
}
function navContextMenu(event, table_id) {
if (is_positioning) {return;}
let data = getSelectedCellData();
let menu = $("<div>")
.attr("id", "context-menu-"+table_id)
.addClass("border context-menu text-nowrap list-group")
.on("mouseleave", () => {
menu.remove();
})
.append($("<button>")
.addClass("list-group-item list-group-item-action")
.text("duplicate table")
.on("click", () => {
duplicateTable(table_id);
}));
if (data["num_tables"] > 1) {
menu.append($("<button>")
.addClass("list-group-item list-group-item-action list-group-item-danger")
.text("Delete Table")
.attr({"data-bs-toggle": "modal", "data-bs-target": "#delete-table"})
.on("click", () => {
$("#confirm-delete-table").one("click", (event) => {
deleteTable(table_id);
})
menu.remove();
}));
}
let max_height = $(document).height();
let max_width = $(document).width();
let x_pos = event.pageX-5;
let y_pos = event.pageY-5;
$("body").append(menu);
y_pos = Math.min(y_pos, max_height-menu.height());
x_pos = Math.min(x_pos, max_width-menu.width());
menu.css({"top": y_pos, "left": x_pos});
}
function cellInlineEdit(event) {
if (is_positioning) {return;}
let data = getSelectedCellData()
let target = $(event.target);
let text_area = $("<textarea>")
@ -465,6 +639,7 @@ function closeCellInlineEdit(save=false) {
}
function headerInlineEdit(event) {
if (is_positioning) {return;}
let data = getSelectedCellData()
let target = $(event.target);
let text_area = $("<textarea>")
@ -500,6 +675,96 @@ function closeHeaderInlineEdit(save=false) {
}
}
function moveTable(old_index, new_index) {
let table_data = tables_data[old_index];
tables_data.splice(old_index, 1);
tables_data.splice(new_index, 0, table_data);
getSelectedCellData(true);
}
function moveRow(table_id, old_index, new_index) {
for (let table_data of tables_data) {
if (table_data["id"] === table_id) {
let row = table_data["rows"][old_index];
table_data["rows"].splice(old_index, 1);
table_data["rows"].splice(new_index, 0, row);
}
}
getSelectedCellData(true);
}
function moveCol(table_id, old_index, new_index) {
for (let table_data of tables_data) {
if (table_data["id"] === table_id) {
let col = table_data["col-def"][old_index];
table_data["col-def"].splice(old_index, 1);
table_data["col-def"].splice(new_index, 0, col);
}
}
refreshPage();
}
function enableMovement() {
is_positioning = true;
$("table > tbody").each((i, elm) => {
Sortable.create(elm, {
animation: 50,
easing: "cubic-bezier(1, 0, 0, 1)",
onEnd: function (event) {
moveRow(current_table, event.oldIndex, event.newIndex);
}
});
})
$("table > thead > tr").each((i, elm) => {
Sortable.create(elm, {
animation: 50,
easing: "cubic-bezier(1, 0, 0, 1)",
onEnd: function (event) {
moveCol(current_table, event.oldIndex, event.newIndex);
}
});
})
Sortable.create($("nav")[0], {
animation: 50,
easing: "cubic-bezier(1, 0, 0, 1)",
draggable: ".table-nav",
onend: function (event) {
moveTable(event.oldIndex, event.newIndex);
}
})
position_edit_btn.text("Stop Edit Position");
}
function disableMovement() {
is_positioning = false;
$("table > tbody").each((i, elm) => {
let sortable_tbody = Sortable.get(elm);
if (sortable_tbody) {
sortable_tbody.destroy();
}
})
$("table > thead > tr").each((i, elm) => {
let sortable_thead = Sortable.get(elm);
if (sortable_thead) {
sortable_thead.destroy();
}
})
let sortable_nav = Sortable.get($("nav")[0]);
if (sortable_nav) {
sortable_nav.destroy();
}
position_edit_btn.text("Start Edit Position");
}
function toggleMovement() {
if (is_positioning) {
disableMovement();
} else {
enableMovement();
}
}
function getColumnIDs(columns) {
let ids = [];
for (let col of columns) {
@ -600,6 +865,10 @@ detail_cell_text.on("input", (event) => {
selected_cell_data["cell_text"] = new_cell_text;
});
position_edit_btn.on("click", () => {
toggleMovement();
})
ipcRenderer.on("open-default", (event) => {
openFile("default.json");
});

View File

@ -1,6 +1,25 @@
const { ipcRenderer } = require('electron')
$("#recent-files").css("display", "none");
async function showRecentFiles() {
let recent_files = await ipcRenderer.invoke("get-store-value", "recent_files");
if (recent_files.length === 0) {
$("#recent-files").css("display", "none");
$("#splash").css("width", "100%")
} else {
for (let file of recent_files) {
let recent_file_link = $("<a>")
.addClass("btn list-group-item list-group-item-action")
.text(file.replace(/^.*[\\\/]/, ''))
.attr({"data-bs-toggle": "tooltip", "data-bs-placement": "bottom", "title": file})
.on("click", () => {
ipcRenderer.send("open", file);
});
$("#recent-files").append(recent_file_link);
}
}
}
showRecentFiles();
$("#create-new").on("click", () => {
ipcRenderer.send("create-new");

View File

@ -6,17 +6,13 @@
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="container-fluid">
<div class="row">
<div id="recent-files" class="col-4"></div>
<div class="col-12 text-center">
<h1 class="text-center">Web Editor</h1>
<button id="create-new" class="btn btn-primary mb-1">Create New!</button>
<br/>
<button id="open-file" class="btn btn-secondary">Open</button>
</div>
</div>
<body style="overflow: hidden">
<div id="recent-files" class="list-group list-group-flush recent-files"></div>
<div id="splash" class="text-center">
<h1 class="text-center">Web Editor</h1>
<button id="create-new" class="btn btn-primary mb-1">Create New!</button>
<br/>
<button id="open-file" class="btn btn-secondary">Open</button>
</div>
<script>
if (typeof module === 'object') {