new.html 18 KB

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