new.html 18 KB

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