edit.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  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. <div class="ui container" style="margin-top: 20px;">
  33. <div class="ui header">
  34. Edit Post
  35. </div>
  36. <!-- Post Editor -->
  37. <div class="ui form">
  38. <div class="disabled field">
  39. <label for="title">Title</label>
  40. <input type="text" id="title" placeholder="Enter title here" readonly="readonly">
  41. </div>
  42. <div class="field">
  43. <label for="editor">Post Content</label>
  44. <textarea id="editor"></textarea>
  45. </div>
  46. <button class="ui basic button" id="saveButton"><i class="ui green save icon"></i> Update</button>
  47. <button class="ui basic button" id="cancelButton" onclick="handleBackToPostList();">
  48. <i class="ui red cancel icon"></i> Discard Changes
  49. </button>
  50. </div>
  51. </div>
  52. <!-- Media Selector -->
  53. <div id="postengine_media_selector" class="ui modal">
  54. <i class="close icon"></i>
  55. <div class="content">
  56. <div class="ui fluid input">
  57. <button class="ui basic circular icon button" id="backButton">
  58. <i class="arrow left icon"></i>
  59. </button>
  60. <button class="ui basic circular icon button" id="parentButton" style="margin-left: 0.4em;">
  61. <i class="arrow up icon"></i>
  62. </button>
  63. <input type="text" placeholder="/" id="locationBar" style="margin-left: 0.4em;" value="/">
  64. </div>
  65. <div id="mediaContent" class="ui segments" style="margin-top: 1em;">
  66. <div class="ui basic segment" style="pointer-events: none; user-select: none; opacity: 0.5;">
  67. <i class="ui green circle check icon"></i> Loading media files...
  68. </div>
  69. </div>
  70. <div id="limited_access_warning" class="ui hidden red message" style="display:none;">
  71. This folder is not accessible by guest visitors but only logged-in users.
  72. </div>
  73. </div>
  74. <div class="actions">
  75. <div class="ui black deny button">
  76. Cancel
  77. </div>
  78. <button class="ui basic button" id="importButton">
  79. <i class="green download icon"></i> Import Selected
  80. </button>
  81. </div>
  82. </div>
  83. <br><br><br>
  84. <script>
  85. var currentDir = "/"; // Initialize current path
  86. var pathHistory = []; // Initialize path history
  87. var unsafeCharacters = /[<>:"/\\|?*\x00-\x1F]/; // Regex for unsafe characters
  88. if (typeof(autosaveInterval) != "undefined"){
  89. clearInterval(autosaveInterval); // Clear any existing autosave interval
  90. }
  91. //Load the post content if editingPost is not empty
  92. var editingPostFilename = editingPost;
  93. if (loadPostContent == ""){
  94. alert("No post selected for editing.");
  95. switchToTab("allposts");
  96. }else{
  97. loadPostContent(editingPost); // Load the content of the post into the editor
  98. }
  99. //Generate the title from the filename
  100. var editingPostTitle = editingPost.split("_").slice(1).join("_").split(".")[0]; // Extract the post title from the filename
  101. if (editingPostTitle.endsWith(".md")) {
  102. editingPostTitle = editingPostTitle.slice(0, -3); // Remove the ".md" suffix
  103. }
  104. $("#title").val(editingPostTitle);
  105. function handleBackToPostList(){
  106. if (confirm("Are you sure you want to discard the editing content?")) {
  107. switchToTab("allposts"); // Switch to the all posts tab
  108. }
  109. }
  110. // Initialize SimpleMDE
  111. var simplemde = new SimpleMDE({
  112. element: document.getElementById("editor"),
  113. toolbar: [
  114. "bold",
  115. "italic",
  116. "heading",
  117. "|",
  118. "quote",
  119. "unordered-list",
  120. "ordered-list",
  121. "|",
  122. "link",
  123. "image",
  124. {
  125. name: "mediaSelect",
  126. action: function customFunction(editor) {
  127. openMediaSelector(editor);
  128. },
  129. className: "fa fa-folder", // Font Awesome icon
  130. title: "Select Media"
  131. },
  132. "|",
  133. "preview",
  134. ]
  135. });
  136. // Allow vertical resize for the editor
  137. document.getElementById("editor").style.resize = "vertical";
  138. /*
  139. Post Loader
  140. */
  141. function loadPostContent(filename) {
  142. let filepath = "/site/posts/" + filename;
  143. $.get("/api/fs/download?file=" + filepath, function(data) {
  144. if (data.error == undefined){
  145. simplemde.value(data); // Load the content into the editor
  146. } else {
  147. alert("Failed to load post content: " + data.error);
  148. }
  149. }).fail(function() {
  150. alert("Error loading post content.");
  151. });
  152. }
  153. /*
  154. Media Selector
  155. This function will open the media selector modal when the media button is clicked.
  156. */
  157. function fetchDirectoryContents(path) {
  158. $.ajax({
  159. url: `/api/fs/list?dir=${encodeURIComponent(path)}`,
  160. method: 'GET',
  161. success: function(data) {
  162. if (data.error == undefined){
  163. pathHistory.push(currentDir);
  164. if (path.endsWith("/")) {
  165. currentDir = path.slice(0, -1); // Remove trailing slash if present
  166. } else {
  167. currentDir = path; // Update current directory
  168. }
  169. $("#locationBar").val(currentDir); // Update location bar
  170. renderDirectoryContents(data);
  171. } else {
  172. alert("Failed to fetch directory contents: " + data.error);
  173. }
  174. },
  175. error: function() {
  176. alert("Error fetching directory contents.");
  177. }
  178. });
  179. }
  180. function renderDirectoryContents(contents) {
  181. const container = $("#mediaContent");
  182. container.empty();
  183. let folderElements = ``;
  184. let fileElements = ``;
  185. let selectableFileCounter = 0;
  186. contents.forEach(item => {
  187. if (item.IsDir) {
  188. folderElements += (`
  189. <div class="ui segment folder-item selectable" data-path="${currentDir + "/" + item.Filename}">
  190. <i class="yellow folder icon"></i> ${item.Filename}
  191. </div>
  192. `);
  193. selectableFileCounter++;
  194. } else if (!item.IsDir && isWebSafeFile(item.Filename)) {
  195. let fileIcon = getFileTypeIcons(item.Filename);
  196. fileElements += (`
  197. <div class="ui segment file-item selectable" data-path="${currentDir + "/" + item.Filename}" data-type="${getFileType(item.Filename)}">
  198. ${fileIcon} ${item.Filename}
  199. </div>
  200. `);
  201. selectableFileCounter++;
  202. }
  203. });
  204. container.append(folderElements);
  205. container.append(fileElements);
  206. if (selectableFileCounter == 0) {
  207. 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>");
  208. }
  209. // Add click handlers for folders and files
  210. $(".folder-item").on('dblclick', function() {
  211. const path = $(this).data('path');
  212. fetchDirectoryContents(path);
  213. });
  214. $(".folder-item").on("click", function(event) {
  215. const path = $(this).data('path');
  216. if (!event.ctrlKey) {
  217. // Highlight the selected folder and remove previous selection if ctrl is not held
  218. $(".folder-item.selected").removeClass("selected");
  219. }
  220. $(this).toggleClass("selected"); // Toggle selection for the clicked item
  221. });
  222. $(".file-item").on('dblclick', function() {
  223. $("#postengine_media_selector").modal("hide");
  224. const path = $(this).data('path');
  225. const type = $(this).data('type');
  226. addMediaFileToEditor(path, type);
  227. });
  228. $(".file-item").on("click", function(event) {
  229. const path = $(this).data('path');
  230. const type = $(this).data('type');
  231. if (!event.ctrlKey) {
  232. // Highlight the selected file and remove previous selection if ctrl is not held
  233. $(".file-item.selected").removeClass("selected");
  234. }
  235. $(this).toggleClass("selected"); // Toggle selection for the clicked item
  236. });
  237. //Show warning if the directory starts with /admin or /store
  238. if (currentDir.startsWith("/admin") || currentDir.startsWith("/store")) {
  239. $("#limited_access_warning").show();
  240. } else {
  241. $("#limited_access_warning").hide();
  242. }
  243. }
  244. // Bind event to the back button
  245. $('#backButton').on('click', function() {
  246. if (pathHistory.length > 1) {
  247. pathHistory.pop(); // Remove the current directory
  248. const previousPath = pathHistory.pop();
  249. fetchDirectoryContents(previousPath);
  250. }
  251. });
  252. // Bind event to the parent button
  253. $('#parentButton').on('click', function() {
  254. if (currentDir == "/") {
  255. msgbox("Already at the root directory", 3000);
  256. return;
  257. }
  258. let parentPath = currentDir.split("/");
  259. parentPath.pop(); // Remove the last element (current directory)
  260. parentPath = parentPath.join("/"); // Join the remaining elements to form the parent path
  261. if (parentPath == ""){
  262. parentPath = "/"; // Set to root if empty
  263. }
  264. fetchDirectoryContents(parentPath);
  265. });
  266. // Bind event to the import button
  267. $('#importButton').on('click', function() {
  268. const selectedFolders = $(".folder-item.selected");
  269. const selectedFiles = $(".file-item.selected");
  270. if (selectedFiles.length === 0 && selectedFolders.length === 0) {
  271. alert("No files selected for import.");
  272. return;
  273. }
  274. selectedFolders.each(function() {
  275. const path = $(this).data('path');
  276. const type = "link";
  277. addMediaFileToEditor(path, type);
  278. });
  279. selectedFiles.each(function() {
  280. const path = $(this).data('path');
  281. const type = $(this).data('type');
  282. addMediaFileToEditor(path, type);
  283. });
  284. $("#postengine_media_selector").modal("hide");
  285. });
  286. function isWebSafeFile(fileName) {
  287. const webSafeExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'webm', 'mp3', 'ogg'];
  288. const extension = fileName.split('.').pop().toLowerCase();
  289. return webSafeExtensions.includes(extension);
  290. }
  291. function getFileType(fileName) {
  292. const extension = fileName.split('.').pop().toLowerCase();
  293. if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) {
  294. return 'image';
  295. } else if (extension === 'webm') {
  296. return 'video';
  297. } else if (['mp3', 'ogg'].includes(extension)) {
  298. return 'audio';
  299. } else {
  300. return 'unknown';
  301. }
  302. }
  303. function openMediaSelector(editor){
  304. // Reset history
  305. pathHistory = []; // Reset path history
  306. fetchDirectoryContents(currentDir);
  307. $("#postengine_media_selector").modal("show");
  308. }
  309. function addMediaFileToEditor(mediaLink, mediaType){
  310. if (mediaType === 'image') {
  311. simplemde.codemirror.replaceSelection(`![Image](${mediaLink})`);
  312. } else if (mediaType === 'video') {
  313. let mimeType = mediaLink.split('.').pop().toLowerCase() === 'webm' ? 'video/webm' : 'video/mp4';
  314. simplemde.codemirror.replaceSelection(`<video style="background:black;" width="720" height="480" controls><source src="${mediaLink}" type="${mimeType}"></video>`);
  315. } else if (mediaType === 'audio') {
  316. simplemde.codemirror.replaceSelection(`<audio style="min-width: 512px;" controls><source src="${mediaLink}" type="audio/mpeg">Your browser does not support the audio element.</audio>`);
  317. } else if (mediaType === 'link') {
  318. let folderName = mediaLink.split("/").pop(); // Extract folder name from path
  319. simplemde.codemirror.replaceSelection(`[${folderName}](${mediaLink})`);
  320. } else {
  321. alert("Unsupported media type!");
  322. return;
  323. }
  324. }
  325. function getFileTypeIcons(fileName) {
  326. const extension = fileName.split('.').pop().toLowerCase();
  327. if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) {
  328. return '<i class="blue file image icon"></i>';
  329. } else if (extension === 'webm') {
  330. return '<i class="violet file video icon"></i>';
  331. } else if (['mp3', 'ogg'].includes(extension)) {
  332. return '<i class="green file audio icon"></i>';
  333. } else {
  334. return '<i class="file icon"></i>';
  335. }
  336. }
  337. // Save button functionality
  338. $('#saveButton').on('click', function() {
  339. const title = $('#title').val();
  340. const content = simplemde.value();
  341. console.log("Title:", title);
  342. console.log("Content:", content);
  343. createNewPost(title, content);
  344. });
  345. /*
  346. Post Create Function
  347. */
  348. // Function to create a new post
  349. function createNewPost(title, content="# Hello World\n"){
  350. if (title.trim() === "" && content.trim() === "") {
  351. alert("Cannot create an empty post.");
  352. return;
  353. }
  354. $("#saveButton").addClass("loading disabled");
  355. //Create the markdown file at the /blog/posts folder
  356. const blob = new Blob([content], { type: 'text/plain' });
  357. let storeFilename = editingPostFilename; //Overwrite on top of the original one
  358. const file = new File([blob], storeFilename);
  359. handleFile(file, "/site/posts", function(){
  360. //Update the post index
  361. updatePostIndex(function(){
  362. $("#confirmNewPostBtn").removeClass("loading disabled");
  363. msgbox("Post created successfully!", 3000);
  364. switchToTab("allposts"); // Switch to the all posts tab
  365. });
  366. //Clear the draft
  367. localStorage.removeItem('draftTitle');
  368. localStorage.removeItem('draftContent');
  369. });
  370. }
  371. // Error handler for AJAX requests
  372. function errorHandler(event) {
  373. msgbox("Failed to create post: " + event.target.responseText, 3000);
  374. $("#saveButton").removeClass("loading disabled");
  375. }
  376. // Function to handle file upload
  377. function handleFile(file, dir=currentPath, callback=undefined) {
  378. // Perform actions with the selected file
  379. var formdata = new FormData();
  380. formdata.append("file1", file);
  381. var ajax = new XMLHttpRequest();
  382. ajax.addEventListener("load", function(event){
  383. let responseText = event.target.responseText;
  384. try{
  385. responseText = JSON.parse(responseText);
  386. if (responseText.error != undefined){
  387. alert(responseText.error);
  388. }
  389. }catch(ex){
  390. }
  391. if (callback != undefined){
  392. callback();
  393. }
  394. }, false); // doesnt appear to ever get called even upon success
  395. ajax.addEventListener("error", errorHandler, false);
  396. //ajax.addEventListener("abort", abortHandler, false);
  397. ajax.open("POST", "/upload?dir=" + dir);
  398. ajax.send(formdata);
  399. }
  400. $(document).ready(function() {
  401. // Initialize with root directory
  402. fetchDirectoryContents(currentDir);
  403. });
  404. </script>