edit.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  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. // Allow vertical resize for the editor
  142. document.getElementById("editor").style.resize = "vertical";
  143. /*
  144. Post Loader
  145. */
  146. function loadPostContent(filename) {
  147. let filepath = "/site/posts/" + filename;
  148. $.get("/api/fs/download?file=" + filepath, function(data) {
  149. if (data.error == undefined){
  150. simplemde.value(data); // Load the content into the editor
  151. } else {
  152. alert("Failed to load post content: " + data.error);
  153. }
  154. }).fail(function() {
  155. alert("Error loading post content.");
  156. });
  157. }
  158. /*
  159. Media Selector
  160. This function will open the media selector modal when the media button is clicked.
  161. */
  162. function fetchDirectoryContents(path) {
  163. $.ajax({
  164. url: `/api/fs/list?dir=${encodeURIComponent(path)}`,
  165. method: 'GET',
  166. success: function(data) {
  167. if (data.error == undefined){
  168. pathHistory.push(currentDir);
  169. if (path.endsWith("/")) {
  170. currentDir = path.slice(0, -1); // Remove trailing slash if present
  171. } else {
  172. currentDir = path; // Update current directory
  173. }
  174. $("#locationBar").val(currentDir); // Update location bar
  175. renderDirectoryContents(data);
  176. } else {
  177. alert("Failed to fetch directory contents: " + data.error);
  178. }
  179. },
  180. error: function() {
  181. alert("Error fetching directory contents.");
  182. }
  183. });
  184. }
  185. function renderDirectoryContents(contents) {
  186. const container = $("#mediaContent");
  187. container.empty();
  188. let folderElements = ``;
  189. let fileElements = ``;
  190. let selectableFileCounter = 0;
  191. contents.forEach(item => {
  192. if (item.IsDir) {
  193. folderElements += (`
  194. <div class="ui segment folder-item selectable" data-path="${currentDir + "/" + item.Filename}">
  195. <i class="yellow folder icon"></i> ${item.Filename}
  196. </div>
  197. `);
  198. selectableFileCounter++;
  199. } else if (!item.IsDir && isWebSafeFile(item.Filename)) {
  200. let fileIcon = getFileTypeIcons(item.Filename);
  201. fileElements += (`
  202. <div class="ui segment file-item selectable" data-path="${currentDir + "/" + item.Filename}" data-type="${getFileType(item.Filename)}">
  203. ${fileIcon} ${item.Filename}
  204. </div>
  205. `);
  206. selectableFileCounter++;
  207. }
  208. });
  209. container.append(folderElements);
  210. container.append(fileElements);
  211. if (selectableFileCounter == 0) {
  212. 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>");
  213. }
  214. // Add click handlers for folders and files
  215. $(".folder-item").on('dblclick', function() {
  216. const path = $(this).data('path');
  217. fetchDirectoryContents(path);
  218. });
  219. $(".folder-item").on("click", function(event) {
  220. const path = $(this).data('path');
  221. if (!event.ctrlKey) {
  222. // Highlight the selected folder and remove previous selection if ctrl is not held
  223. $(".folder-item.selected").removeClass("selected");
  224. }
  225. $(this).toggleClass("selected"); // Toggle selection for the clicked item
  226. });
  227. $(".file-item").on('dblclick', function() {
  228. $(".postengine_edit_media_selector").modal("hide");
  229. const path = $(this).data('path');
  230. const type = $(this).data('type');
  231. addMediaFileToEditor(path, type);
  232. });
  233. $(".file-item").on("click", function(event) {
  234. const path = $(this).data('path');
  235. const type = $(this).data('type');
  236. if (!event.ctrlKey) {
  237. // Highlight the selected file and remove previous selection if ctrl is not held
  238. $(".file-item.selected").removeClass("selected");
  239. }
  240. $(this).toggleClass("selected"); // Toggle selection for the clicked item
  241. });
  242. //Show warning if the directory starts with /admin or /store
  243. if (currentDir.startsWith("/admin") || currentDir.startsWith("/store")) {
  244. $("#limited_access_warning").show();
  245. } else {
  246. $("#limited_access_warning").hide();
  247. }
  248. }
  249. // Bind event to the back button
  250. $('#backButton').on('click', function() {
  251. if (pathHistory.length > 1) {
  252. pathHistory.pop(); // Remove the current directory
  253. const previousPath = pathHistory.pop();
  254. fetchDirectoryContents(previousPath);
  255. }
  256. });
  257. // Bind event to the parent button
  258. $('#parentButton').on('click', function() {
  259. if (currentDir == "/") {
  260. msgbox("Already at the root directory", 3000);
  261. return;
  262. }
  263. let parentPath = currentDir.split("/");
  264. parentPath.pop(); // Remove the last element (current directory)
  265. parentPath = parentPath.join("/"); // Join the remaining elements to form the parent path
  266. if (parentPath == ""){
  267. parentPath = "/"; // Set to root if empty
  268. }
  269. fetchDirectoryContents(parentPath);
  270. });
  271. // Bind event to the import button
  272. $('#importButton').on('click', function() {
  273. const selectedFolders = $(".folder-item.selected");
  274. const selectedFiles = $(".file-item.selected");
  275. if (selectedFiles.length === 0 && selectedFolders.length === 0) {
  276. alert("No files selected for import.");
  277. return;
  278. }
  279. selectedFolders.each(function() {
  280. const path = $(this).data('path');
  281. const type = "link";
  282. addMediaFileToEditor(path, type);
  283. });
  284. selectedFiles.each(function() {
  285. const path = $(this).data('path');
  286. const type = $(this).data('type');
  287. addMediaFileToEditor(path, type);
  288. });
  289. $(".postengine_edit_media_selector").modal("hide");
  290. });
  291. function isWebSafeFile(fileName) {
  292. const webSafeExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'webm', 'mp3', 'ogg'];
  293. const extension = fileName.split('.').pop().toLowerCase();
  294. return webSafeExtensions.includes(extension);
  295. }
  296. function getFileType(fileName) {
  297. const extension = fileName.split('.').pop().toLowerCase();
  298. if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) {
  299. return 'image';
  300. } else if (extension === 'webm') {
  301. return 'video';
  302. } else if (['mp3', 'ogg'].includes(extension)) {
  303. return 'audio';
  304. } else {
  305. return 'unknown';
  306. }
  307. }
  308. function openMediaSelector(editor){
  309. // Reset history
  310. pathHistory = []; // Reset path history
  311. fetchDirectoryContents(currentDir);
  312. $(".postengine_edit_media_selector").modal("show");
  313. }
  314. function addMediaFileToEditor(mediaLink, mediaType){
  315. if (mediaType === 'image') {
  316. simplemde.codemirror.replaceSelection(`![Image](${mediaLink})`);
  317. } else if (mediaType === 'video') {
  318. let mimeType = mediaLink.split('.').pop().toLowerCase() === 'webm' ? 'video/webm' : 'video/mp4';
  319. simplemde.codemirror.replaceSelection(`<video style="background:black;" width="720" height="480" controls><source src="${mediaLink}" type="${mimeType}"></video>`);
  320. } else if (mediaType === 'audio') {
  321. simplemde.codemirror.replaceSelection(`<audio style="min-width: 512px;" controls><source src="${mediaLink}" type="audio/mpeg">Your browser does not support the audio element.</audio>`);
  322. } else if (mediaType === 'link') {
  323. let folderName = mediaLink.split("/").pop(); // Extract folder name from path
  324. simplemde.codemirror.replaceSelection(`[${folderName}](${mediaLink})`);
  325. } else {
  326. alert("Unsupported media type!");
  327. return;
  328. }
  329. }
  330. function getFileTypeIcons(fileName) {
  331. const extension = fileName.split('.').pop().toLowerCase();
  332. if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) {
  333. return '<i class="blue file image icon"></i>';
  334. } else if (extension === 'webm') {
  335. return '<i class="violet file video icon"></i>';
  336. } else if (['mp3', 'ogg'].includes(extension)) {
  337. return '<i class="green file audio icon"></i>';
  338. } else {
  339. return '<i class="file icon"></i>';
  340. }
  341. }
  342. // Save button functionality
  343. $('#saveButton').on('click', function() {
  344. const title = $('#title').val();
  345. const content = simplemde.value();
  346. console.log("Title:", title);
  347. console.log("Content:", content);
  348. createNewPost(title, content);
  349. });
  350. /*
  351. Post Create Function
  352. */
  353. // Function to create a new post
  354. function createNewPost(title, content="# Hello World\n"){
  355. if (title.trim() === "" && content.trim() === "") {
  356. alert("Cannot create an empty post.");
  357. return;
  358. }
  359. $("#saveButton").addClass("loading disabled");
  360. //Create the markdown file at the /blog/posts folder
  361. const blob = new Blob([content], { type: 'text/plain' });
  362. let storeFilename = editingPostFilename; //Overwrite on top of the original one
  363. const file = new File([blob], storeFilename);
  364. handleFile(file, "/site/posts", function(){
  365. //Update the post index
  366. updatePostIndex(function(){
  367. $("#confirmNewPostBtn").removeClass("loading disabled");
  368. msgbox("Post created successfully!", 3000);
  369. switchToTab("allposts"); // Switch to the all posts tab
  370. });
  371. //Clear the draft
  372. localStorage.removeItem('draftTitle');
  373. localStorage.removeItem('draftContent');
  374. });
  375. }
  376. // Error handler for AJAX requests
  377. function errorHandler(event) {
  378. msgbox("Failed to create post: " + event.target.responseText, 3000);
  379. $("#saveButton").removeClass("loading disabled");
  380. }
  381. // Function to handle file upload
  382. function handleFile(file, dir=currentPath, callback=undefined) {
  383. // Perform actions with the selected file
  384. var formdata = new FormData();
  385. formdata.append("file1", file);
  386. var ajax = new XMLHttpRequest();
  387. ajax.addEventListener("load", function(event){
  388. let responseText = event.target.responseText;
  389. try{
  390. responseText = JSON.parse(responseText);
  391. if (responseText.error != undefined){
  392. alert(responseText.error);
  393. }
  394. }catch(ex){
  395. }
  396. if (callback != undefined){
  397. callback();
  398. }
  399. }, false); // doesnt appear to ever get called even upon success
  400. ajax.addEventListener("error", errorHandler, false);
  401. //ajax.addEventListener("abort", abortHandler, false);
  402. ajax.open("POST", "/upload?dir=" + dir);
  403. ajax.send(formdata);
  404. }
  405. $(document).ready(function() {
  406. // Initialize with root directory
  407. fetchDirectoryContents(currentDir);
  408. });
  409. </script>