edit.html 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. <link rel="stylesheet" href="mde/script/mde.css">
  2. <script src="mde/script/mde.js"></script>
  3. <style>
  4. .selectable{
  5. cursor: pointer;
  6. }
  7. .selectable:hover{
  8. background-color: rgba(0, 0, 0, 0.1);
  9. }
  10. #mediaContent{
  11. max-height: 50vh;
  12. overflow-y: auto;
  13. }
  14. .editor-preview img {
  15. max-width: 100%;
  16. height: auto;
  17. }
  18. .CodeMirror,
  19. .CodeMirror-scroll {
  20. height: 520px !important;
  21. min-height: 520px !important;
  22. max-height: 520px !important;
  23. }
  24. .file-item.selected {
  25. background-color: rgba(0, 0, 0, 0.1) !important;
  26. }
  27. .folder-item.selected {
  28. background-color: rgba(0, 0, 0, 0.1) !important;
  29. }
  30. </style>
  31. <br>
  32. <script>
  33. if ($(".postengine_edit_media_selector").length > 1) {
  34. $(".postengine_edit_media_selector").first().remove();
  35. }
  36. </script>
  37. <div class="ui container" style="margin-top: 20px;">
  38. <div class="ui header">
  39. Edit Post
  40. </div>
  41. <!-- Post Editor -->
  42. <div class="ui form">
  43. <div class="disabled field">
  44. <label for="title">Title</label>
  45. <input type="text" id="title" placeholder="Enter title here" readonly="readonly">
  46. </div>
  47. <div class="field">
  48. <label for="editor">Post Content</label>
  49. <textarea id="editor"></textarea>
  50. </div>
  51. <button class="ui basic button" id="saveButton"><i class="ui green save icon"></i> Update</button>
  52. <button class="ui basic button" id="cancelButton" onclick="handleBackToPostList();">
  53. <i class="ui red cancel icon"></i> Discard Changes
  54. </button>
  55. </div>
  56. </div>
  57. <!-- Media Selector -->
  58. <div class="ui modal postengine_edit_media_selector">
  59. <i class="close icon"></i>
  60. <div class="content">
  61. <div class="ui fluid input">
  62. <button class="ui basic circular icon button" id="backButton">
  63. <i class="arrow left icon"></i>
  64. </button>
  65. <button class="ui basic circular icon button" id="parentButton" style="margin-left: 0.4em;">
  66. <i class="arrow up icon"></i>
  67. </button>
  68. <input type="text" placeholder="/" id="locationBar" style="margin-left: 0.4em;" value="/">
  69. </div>
  70. <div id="mediaContent" class="ui segments" style="margin-top: 1em;">
  71. <div class="ui basic segment" style="pointer-events: none; user-select: none; opacity: 0.5;">
  72. <i class="ui green circle check icon"></i> Loading media files...
  73. </div>
  74. </div>
  75. <div id="limited_access_warning" class="ui hidden red message" style="display:none;">
  76. This folder is not accessible by guest visitors but only logged-in users.
  77. </div>
  78. </div>
  79. <div class="actions">
  80. <div class="ui black deny button">
  81. Cancel
  82. </div>
  83. <button class="ui basic button" id="importButton">
  84. <i class="green download icon"></i> Import Selected
  85. </button>
  86. </div>
  87. </div>
  88. <br><br><br>
  89. <script>
  90. var currentDir = "/"; // Initialize current path
  91. var pathHistory = []; // Initialize path history
  92. var unsafeCharacters = /[<>:"/\\|?*\x00-\x1F]/; // Regex for unsafe characters
  93. if (typeof(autosaveInterval) != "undefined"){
  94. clearInterval(autosaveInterval); // Clear any existing autosave interval
  95. }
  96. //Load the post content if editingPost is not empty
  97. var editingPostFilename = editingPost;
  98. if (loadPostContent == ""){
  99. alert("No post selected for editing.");
  100. switchToTab("allposts");
  101. }else{
  102. loadPostContent(editingPost); // Load the content of the post into the editor
  103. }
  104. //Generate the title from the filename
  105. var editingPostTitle = editingPost.split("_").slice(1).join("_").split(".")[0]; // Extract the post title from the filename
  106. if (editingPostTitle.endsWith(".md")) {
  107. editingPostTitle = editingPostTitle.slice(0, -3); // Remove the ".md" suffix
  108. }
  109. $("#title").val(editingPostTitle);
  110. function handleBackToPostList(){
  111. if (confirm("Are you sure you want to discard the editing content?")) {
  112. switchToTab("allposts"); // Switch to the all posts tab
  113. }
  114. }
  115. // Initialize SimpleMDE
  116. var simplemde = new SimpleMDE({
  117. element: document.getElementById("editor"),
  118. toolbar: [
  119. "bold",
  120. "italic",
  121. "heading",
  122. "|",
  123. "quote",
  124. "unordered-list",
  125. "ordered-list",
  126. "|",
  127. "link",
  128. "image",
  129. {
  130. name: "mediaSelect",
  131. action: function customFunction(editor) {
  132. openMediaSelector(editor);
  133. },
  134. className: "fa fa-folder", // Font Awesome icon
  135. title: "Select Media"
  136. },
  137. "|",
  138. "preview",
  139. ]
  140. });
  141. bindOnPasteEvent(); // Bind paste event to the editor
  142. // Allow vertical resize for the editor
  143. document.getElementById("editor").style.resize = "vertical";
  144. /*
  145. Post Loader
  146. */
  147. function loadPostContent(filename) {
  148. let filepath = "/site/posts/" + filename;
  149. $.get("/api/fs/download?file=" + filepath, function(data) {
  150. if (data.error == undefined){
  151. simplemde.value(data); // Load the content into the editor
  152. } else {
  153. alert("Failed to load post content: " + data.error);
  154. }
  155. }).fail(function() {
  156. alert("Error loading post content.");
  157. });
  158. }
  159. /*
  160. Media Selector
  161. This function will open the media selector modal when the media button is clicked.
  162. */
  163. function fetchDirectoryContents(path) {
  164. $.ajax({
  165. url: `/api/fs/list?dir=${encodeURIComponent(path)}`,
  166. method: 'GET',
  167. success: function(data) {
  168. if (data.error == undefined){
  169. pathHistory.push(currentDir);
  170. if (path.endsWith("/")) {
  171. currentDir = path.slice(0, -1); // Remove trailing slash if present
  172. } else {
  173. currentDir = path; // Update current directory
  174. }
  175. $("#locationBar").val(currentDir); // Update location bar
  176. renderDirectoryContents(data);
  177. } else {
  178. alert("Failed to fetch directory contents: " + data.error);
  179. }
  180. },
  181. error: function() {
  182. alert("Error fetching directory contents.");
  183. }
  184. });
  185. }
  186. function renderDirectoryContents(contents) {
  187. const container = $("#mediaContent");
  188. container.empty();
  189. let folderElements = ``;
  190. let fileElements = ``;
  191. let selectableFileCounter = 0;
  192. contents.forEach(item => {
  193. if (item.IsDir) {
  194. folderElements += (`
  195. <div class="ui segment folder-item selectable" data-path="${currentDir + "/" + item.Filename}">
  196. <i class="yellow folder icon"></i> ${item.Filename}
  197. </div>
  198. `);
  199. selectableFileCounter++;
  200. } else if (!item.IsDir && isWebSafeFile(item.Filename)) {
  201. let fileIcon = getFileTypeIcons(item.Filename);
  202. fileElements += (`
  203. <div class="ui segment file-item selectable" data-path="${currentDir + "/" + item.Filename}" data-type="${getFileType(item.Filename)}">
  204. ${fileIcon} ${item.Filename}
  205. </div>
  206. `);
  207. selectableFileCounter++;
  208. }
  209. });
  210. container.append(folderElements);
  211. container.append(fileElements);
  212. if (selectableFileCounter == 0) {
  213. container.append("<div class='ui basic segment' style='pointer-events: none; user-select: none; opacity: 0.5;'><i class='ui green circle check icon'></i> No usable files / folders found</div>");
  214. }
  215. // Add click handlers for folders and files
  216. $(".folder-item").on('dblclick', function() {
  217. const path = $(this).data('path');
  218. fetchDirectoryContents(path);
  219. });
  220. $(".folder-item").on("click", function(event) {
  221. const path = $(this).data('path');
  222. if (!event.ctrlKey) {
  223. // Highlight the selected folder and remove previous selection if ctrl is not held
  224. $(".folder-item.selected").removeClass("selected");
  225. }
  226. $(this).toggleClass("selected"); // Toggle selection for the clicked item
  227. });
  228. $(".file-item").on('dblclick', function() {
  229. $(".postengine_edit_media_selector").modal("hide");
  230. const path = $(this).data('path');
  231. const type = $(this).data('type');
  232. addMediaFileToEditor(path, type);
  233. });
  234. $(".file-item").on("click", function(event) {
  235. const path = $(this).data('path');
  236. const type = $(this).data('type');
  237. if (!event.ctrlKey) {
  238. // Highlight the selected file and remove previous selection if ctrl is not held
  239. $(".file-item.selected").removeClass("selected");
  240. }
  241. $(this).toggleClass("selected"); // Toggle selection for the clicked item
  242. });
  243. //Show warning if the directory starts with /admin or /store
  244. if (currentDir.startsWith("/admin") || currentDir.startsWith("/store")) {
  245. $("#limited_access_warning").show();
  246. } else {
  247. $("#limited_access_warning").hide();
  248. }
  249. }
  250. // Bind event to the back button
  251. $('#backButton').on('click', function() {
  252. if (pathHistory.length > 1) {
  253. pathHistory.pop(); // Remove the current directory
  254. const previousPath = pathHistory.pop();
  255. fetchDirectoryContents(previousPath);
  256. }
  257. });
  258. // Bind event to the parent button
  259. $('#parentButton').on('click', function() {
  260. if (currentDir == "/") {
  261. msgbox("Already at the root directory", 3000);
  262. return;
  263. }
  264. let parentPath = currentDir.split("/");
  265. parentPath.pop(); // Remove the last element (current directory)
  266. parentPath = parentPath.join("/"); // Join the remaining elements to form the parent path
  267. if (parentPath == ""){
  268. parentPath = "/"; // Set to root if empty
  269. }
  270. fetchDirectoryContents(parentPath);
  271. });
  272. // Bind event to the import button
  273. $('#importButton').on('click', function() {
  274. const selectedFolders = $(".folder-item.selected");
  275. const selectedFiles = $(".file-item.selected");
  276. if (selectedFiles.length === 0 && selectedFolders.length === 0) {
  277. alert("No files selected for import.");
  278. return;
  279. }
  280. selectedFolders.each(function() {
  281. const path = $(this).data('path');
  282. const type = "link";
  283. addMediaFileToEditor(path, type);
  284. });
  285. selectedFiles.each(function() {
  286. const path = $(this).data('path');
  287. const type = $(this).data('type');
  288. addMediaFileToEditor(path, type);
  289. });
  290. $(".postengine_edit_media_selector").modal("hide");
  291. });
  292. function isWebSafeFile(fileName) {
  293. const webSafeExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'webm', 'mp3', 'ogg'];
  294. const extension = fileName.split('.').pop().toLowerCase();
  295. return webSafeExtensions.includes(extension);
  296. }
  297. function getFileType(fileName) {
  298. const extension = fileName.split('.').pop().toLowerCase();
  299. if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) {
  300. return 'image';
  301. } else if (extension === 'webm') {
  302. return 'video';
  303. } else if (['mp3', 'ogg'].includes(extension)) {
  304. return 'audio';
  305. } else {
  306. return 'unknown';
  307. }
  308. }
  309. function openMediaSelector(editor){
  310. // Reset history
  311. pathHistory = []; // Reset path history
  312. fetchDirectoryContents(currentDir);
  313. $(".postengine_edit_media_selector").modal("show");
  314. }
  315. function addMediaFileToEditor(mediaLink, mediaType){
  316. if (mediaType === 'image') {
  317. simplemde.codemirror.replaceSelection(`![Image](${mediaLink})`);
  318. } else if (mediaType === 'video') {
  319. let mimeType = mediaLink.split('.').pop().toLowerCase() === 'webm' ? 'video/webm' : 'video/mp4';
  320. simplemde.codemirror.replaceSelection(`<video style="background:black;" width="720" height="480" controls><source src="${mediaLink}" type="${mimeType}"></video>`);
  321. } else if (mediaType === 'audio') {
  322. simplemde.codemirror.replaceSelection(`<audio style="min-width: 512px;" controls><source src="${mediaLink}" type="audio/mpeg">Your browser does not support the audio element.</audio>`);
  323. } else if (mediaType === 'link') {
  324. let folderName = mediaLink.split("/").pop(); // Extract folder name from path
  325. simplemde.codemirror.replaceSelection(`[${folderName}](${mediaLink})`);
  326. } else {
  327. alert("Unsupported media type!");
  328. return;
  329. }
  330. }
  331. function getFileTypeIcons(fileName) {
  332. const extension = fileName.split('.').pop().toLowerCase();
  333. if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) {
  334. return '<i class="blue file image icon"></i>';
  335. } else if (extension === 'webm') {
  336. return '<i class="violet file video icon"></i>';
  337. } else if (['mp3', 'ogg'].includes(extension)) {
  338. return '<i class="green file audio icon"></i>';
  339. } else {
  340. return '<i class="file icon"></i>';
  341. }
  342. }
  343. // Save button functionality
  344. $('#saveButton').on('click', function() {
  345. const title = $('#title').val();
  346. const content = simplemde.value();
  347. console.log("Title:", title);
  348. console.log("Content:", content);
  349. createNewPost(title, content);
  350. });
  351. /*
  352. Post Create Function
  353. */
  354. // Function to create a new post
  355. function createNewPost(title, content="# Hello World\n"){
  356. if (title.trim() === "" && content.trim() === "") {
  357. alert("Cannot create an empty post.");
  358. return;
  359. }
  360. $("#saveButton").addClass("loading disabled");
  361. //Create the markdown file at the /blog/posts folder
  362. const blob = new Blob([content], { type: 'text/plain' });
  363. let storeFilename = editingPostFilename; //Overwrite on top of the original one
  364. const file = new File([blob], storeFilename);
  365. handleFile(file, "/site/posts", function(){
  366. //Update the post index
  367. updatePostIndex(function(){
  368. $("#confirmNewPostBtn").removeClass("loading disabled");
  369. msgbox("Post created successfully!", 3000);
  370. switchToTab("allposts"); // Switch to the all posts tab
  371. });
  372. //Clear the draft
  373. localStorage.removeItem('draftTitle');
  374. localStorage.removeItem('draftContent');
  375. });
  376. }
  377. // Error handler for AJAX requests
  378. function errorHandler(event) {
  379. msgbox("Failed to create post: " + event.target.responseText, 3000);
  380. $("#saveButton").removeClass("loading disabled");
  381. }
  382. // Function to handle file upload
  383. function handleFile(file, dir=currentPath, callback=undefined) {
  384. // Perform actions with the selected file
  385. var formdata = new FormData();
  386. formdata.append("file1", file);
  387. var ajax = new XMLHttpRequest();
  388. ajax.addEventListener("load", function(event){
  389. let responseText = event.target.responseText;
  390. try{
  391. responseText = JSON.parse(responseText);
  392. if (responseText.error != undefined){
  393. alert(responseText.error);
  394. }
  395. }catch(ex){
  396. }
  397. if (callback != undefined){
  398. callback();
  399. }
  400. }, false); // doesnt appear to ever get called even upon success
  401. ajax.addEventListener("error", errorHandler, false);
  402. //ajax.addEventListener("abort", abortHandler, false);
  403. ajax.open("POST", "/upload?dir=" + dir);
  404. ajax.send(formdata);
  405. }
  406. $(document).ready(function() {
  407. // Initialize with root directory
  408. fetchDirectoryContents(currentDir);
  409. });
  410. /*
  411. Handle image paste into the mde editor
  412. */
  413. function compressImageToJpeg(imageData, filename) {
  414. return new Promise((resolve, reject) => {
  415. const canvas = document.createElement('canvas');
  416. const ctx = canvas.getContext('2d');
  417. const img = new Image();
  418. img.onload = () => {
  419. const maxWidth = 1024; // Set maximum width for compression
  420. const maxHeight = 1024; // Set maximum height for compression
  421. let width = img.width;
  422. let height = img.height;
  423. // Maintain aspect ratio
  424. if (width > maxWidth || height > maxHeight) {
  425. if (width / height > maxWidth / maxHeight) {
  426. height = Math.round((height * maxWidth) / width);
  427. width = maxWidth;
  428. } else {
  429. width = Math.round((width * maxHeight) / height);
  430. height = maxHeight;
  431. }
  432. }
  433. canvas.width = width;
  434. canvas.height = height;
  435. ctx.drawImage(img, 0, 0, width, height);
  436. canvas.toBlob(
  437. (blob) => {
  438. if (blob) {
  439. const compressedFile = new File([blob], filename, {
  440. type: 'image/jpeg',
  441. });
  442. resolve(compressedFile);
  443. } else {
  444. reject(new Error('Compression failed.'));
  445. }
  446. },
  447. 'image/jpeg',
  448. 0.8 // Compression quality (0.0 to 1.0)
  449. );
  450. };
  451. img.onerror = () => reject(new Error('Failed to load image.'));
  452. img.src = imageData; // Set the image source to the data URL
  453. });
  454. }
  455. function uploadImage(comperssedFile, callback=undefined) {
  456. const formData = new FormData();
  457. if (comperssedFile) {
  458. formData.append("file1", comperssedFile);
  459. var ajax = new XMLHttpRequest();
  460. ajax.addEventListener("load", function(event) {
  461. let responseText = event.target.responseText;
  462. if (responseText.includes("ok")){
  463. msgbox(`<i class="ui green check icon"></i> Image uploaded`, 3000);
  464. if (callback != undefined){
  465. callback();
  466. }
  467. }
  468. }, false);
  469. ajax.addEventListener("error", function(event) {
  470. alert("Failed to upload image: " + event.target.responseText);
  471. }, false);
  472. ajax.open("POST", "/upload?dir=/site/img");
  473. ajax.send(formData);
  474. }else{
  475. console.error("No image to upload!");
  476. }
  477. }
  478. function bindOnPasteEvent(){
  479. $(".CodeMirror textarea").on("paste", function(event) {
  480. console.log(event);
  481. const items = (event.clipboardData || event.originalEvent.clipboardData).items;
  482. for (let i = 0; i < items.length; i++) {
  483. if (items[i].type.indexOf("image") !== -1) {
  484. const file = items[i].getAsFile();
  485. const reader = new FileReader();
  486. reader.onload = function(e) {
  487. const imageData = e.target.result;
  488. const fileName = Date.now() + ".jpg";
  489. compressImageToJpeg(imageData, fileName).then((compressedFile) => {
  490. console.log("Pasting compressed image:", fileName);
  491. const imgTag = `![Image](/site/img/${fileName})`;
  492. uploadImage(compressedFile, function(){
  493. console.log("Image uploaded:", fileName);
  494. simplemde.codemirror.replaceSelection(imgTag);
  495. });
  496. }).catch((error) => {
  497. msgbox(`<i class="ui red times icon"></i> Image compression failed`, 3000);
  498. console.error("Image compression failed:", error);
  499. });
  500. };
  501. reader.readAsDataURL(file);
  502. event.preventDefault();
  503. return;
  504. }
  505. }
  506. });
  507. }
  508. </script>