new.html 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  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. bindOnPasteEvent(); // Bind paste event to the editor
  135. // Allow vertical resize for the editor
  136. document.getElementById("editor").style.resize = "vertical";
  137. /*
  138. Media Selector
  139. This function will open the media selector modal when the media button is clicked.
  140. */
  141. function fetchDirectoryContents(path) {
  142. $.ajax({
  143. url: `/api/fs/list?dir=${encodeURIComponent(path)}`,
  144. method: 'GET',
  145. success: function(data) {
  146. if (data.error == undefined){
  147. pathHistory.push(currentDir);
  148. if (path.endsWith("/")) {
  149. currentDir = path.slice(0, -1); // Remove trailing slash if present
  150. } else {
  151. currentDir = path; // Update current directory
  152. }
  153. $("#locationBar").val(currentDir); // Update location bar
  154. renderDirectoryContents(data);
  155. } else {
  156. alert("Failed to fetch directory contents: " + data.error);
  157. }
  158. },
  159. error: function() {
  160. alert("Error fetching directory contents.");
  161. }
  162. });
  163. }
  164. function renderDirectoryContents(contents) {
  165. const container = $("#mediaContent");
  166. container.empty();
  167. let folderElements = ``;
  168. let fileElements = ``;
  169. let selectableFileCounter = 0;
  170. contents.forEach(item => {
  171. if (item.IsDir) {
  172. folderElements += (`
  173. <div class="ui segment folder-item selectable" data-path="${currentDir + "/" + item.Filename}">
  174. <i class="yellow folder icon"></i> ${item.Filename}
  175. </div>
  176. `);
  177. selectableFileCounter++;
  178. } else if (!item.IsDir && isWebSafeFile(item.Filename)) {
  179. let fileIcon = getFileTypeIcons(item.Filename);
  180. fileElements += (`
  181. <div class="ui segment file-item selectable" data-path="${currentDir + "/" + item.Filename}" data-type="${getFileType(item.Filename)}">
  182. ${fileIcon} ${item.Filename}
  183. </div>
  184. `);
  185. selectableFileCounter++;
  186. }
  187. });
  188. container.append(folderElements);
  189. container.append(fileElements);
  190. if (selectableFileCounter == 0) {
  191. 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>");
  192. }
  193. // Add click handlers for folders and files
  194. $(".folder-item").on('dblclick', function() {
  195. const path = $(this).data('path');
  196. fetchDirectoryContents(path);
  197. });
  198. $(".folder-item").on("click", function(event) {
  199. const path = $(this).data('path');
  200. if (!event.ctrlKey) {
  201. // Highlight the selected folder and remove previous selection if ctrl is not held
  202. $(".folder-item.selected").removeClass("selected");
  203. }
  204. $(this).toggleClass("selected"); // Toggle selection for the clicked item
  205. });
  206. $(".file-item").on('dblclick', function() {
  207. $(".postengine_media_selector").modal("hide");
  208. const path = $(this).data('path');
  209. const type = $(this).data('type');
  210. addMediaFileToEditor(path, type);
  211. });
  212. $(".file-item").on("click", function(event) {
  213. const path = $(this).data('path');
  214. const type = $(this).data('type');
  215. if (!event.ctrlKey) {
  216. // Highlight the selected file and remove previous selection if ctrl is not held
  217. $(".file-item.selected").removeClass("selected");
  218. }
  219. $(this).toggleClass("selected"); // Toggle selection for the clicked item
  220. });
  221. //Show warning if the directory starts with /admin or /store
  222. if (currentDir.startsWith("/admin") || currentDir.startsWith("/store")) {
  223. $("#limited_access_warning").show();
  224. } else {
  225. $("#limited_access_warning").hide();
  226. }
  227. }
  228. // Bind event to the back button
  229. $('#backButton').on('click', function() {
  230. if (pathHistory.length > 1) {
  231. pathHistory.pop(); // Remove the current directory
  232. const previousPath = pathHistory.pop();
  233. fetchDirectoryContents(previousPath);
  234. }
  235. });
  236. // Bind event to the parent button
  237. $('#parentButton').on('click', function() {
  238. if (currentDir == "/") {
  239. msgbox("Already at the root directory", 3000);
  240. return;
  241. }
  242. let parentPath = currentDir.split("/");
  243. parentPath.pop(); // Remove the last element (current directory)
  244. parentPath = parentPath.join("/"); // Join the remaining elements to form the parent path
  245. if (parentPath == ""){
  246. parentPath = "/"; // Set to root if empty
  247. }
  248. fetchDirectoryContents(parentPath);
  249. });
  250. // Bind event to the import button
  251. $('#importButton').on('click', function() {
  252. const selectedFolders = $(".folder-item.selected");
  253. const selectedFiles = $(".file-item.selected");
  254. if (selectedFiles.length === 0 && selectedFolders.length === 0) {
  255. alert("No files selected for import.");
  256. return;
  257. }
  258. selectedFolders.each(function() {
  259. const path = $(this).data('path');
  260. const type = "link";
  261. addMediaFileToEditor(path, type);
  262. });
  263. selectedFiles.each(function() {
  264. const path = $(this).data('path');
  265. const type = $(this).data('type');
  266. addMediaFileToEditor(path, type);
  267. });
  268. $(".postengine_media_selector").modal("hide");
  269. });
  270. function isWebSafeFile(fileName) {
  271. const webSafeExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'webm', 'mp3', 'ogg'];
  272. const extension = fileName.split('.').pop().toLowerCase();
  273. return webSafeExtensions.includes(extension);
  274. }
  275. function getFileType(fileName) {
  276. const extension = fileName.split('.').pop().toLowerCase();
  277. if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) {
  278. return 'image';
  279. } else if (extension === 'webm') {
  280. return 'video';
  281. } else if (['mp3', 'ogg'].includes(extension)) {
  282. return 'audio';
  283. } else {
  284. return 'unknown';
  285. }
  286. }
  287. function openMediaSelector(editor){
  288. // Reset history
  289. pathHistory = []; // Reset path history
  290. fetchDirectoryContents(currentDir);
  291. $(".postengine_media_selector").modal("show");
  292. }
  293. function addMediaFileToEditor(mediaLink, mediaType){
  294. if (mediaType === 'image') {
  295. simplemde.codemirror.replaceSelection(`![Image](${mediaLink})`);
  296. } else if (mediaType === 'video') {
  297. let mimeType = mediaLink.split('.').pop().toLowerCase() === 'webm' ? 'video/webm' : 'video/mp4';
  298. simplemde.codemirror.replaceSelection(`<video style="background:black;" width="720" height="480" controls><source src="${mediaLink}" type="${mimeType}"></video>`);
  299. } else if (mediaType === 'audio') {
  300. simplemde.codemirror.replaceSelection(`<audio style="min-width: 512px;" controls><source src="${mediaLink}" type="audio/mpeg">Your browser does not support the audio element.</audio>`);
  301. } else if (mediaType === 'link') {
  302. let folderName = mediaLink.split("/").pop(); // Extract folder name from path
  303. simplemde.codemirror.replaceSelection(`[${folderName}](${mediaLink})`);
  304. } else {
  305. alert("Unsupported media type!");
  306. return;
  307. }
  308. }
  309. function getFileTypeIcons(fileName) {
  310. const extension = fileName.split('.').pop().toLowerCase();
  311. if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) {
  312. return '<i class="blue file image icon"></i>';
  313. } else if (extension === 'webm') {
  314. return '<i class="violet file video icon"></i>';
  315. } else if (['mp3', 'ogg'].includes(extension)) {
  316. return '<i class="green file audio icon"></i>';
  317. } else {
  318. return '<i class="file icon"></i>';
  319. }
  320. }
  321. /*
  322. Title Validation
  323. This function will validate the title input to ensure it does not contain unsafe characters.
  324. If the title contains unsafe characters, the save button will be disabled and an alert will be shown.
  325. */
  326. function validateTitle() {
  327. const title = $('#title').val();
  328. if (unsafeCharacters.test(title) || title.length > 19) {
  329. msgbox("Title contains unsafe characters or is too long (max 19 characters).", 3000);
  330. return false;
  331. }
  332. return true;
  333. }
  334. // Save button functionality
  335. $('#saveButton').on('click', function() {
  336. const title = $('#title').val();
  337. const content = simplemde.value();
  338. console.log("Title:", title);
  339. console.log("Content:", content);
  340. if (!validateTitle()){
  341. return; // Exit if title is invalid
  342. }
  343. createNewPost(title, content);
  344. });
  345. // Save Draft button functionality
  346. $('#saveDraftButton').on('click', function() {
  347. const title = $('#title').val();
  348. const content = simplemde.value();
  349. if (title.trim() === "" && content.trim() === "") {
  350. alert("Cannot save an empty draft.");
  351. return;
  352. }
  353. localStorage.setItem('draftTitle', title);
  354. localStorage.setItem('draftContent', content);
  355. msgbox("Draft saved");
  356. });
  357. // Discard Draft button functionality
  358. $('#discardDraftButton').on('click', function() {
  359. if (confirm("Are you sure you want to discard the draft? This action cannot be undone.")) {
  360. localStorage.removeItem('draftTitle');
  361. localStorage.removeItem('draftContent');
  362. $('#title').val('');
  363. simplemde.value('');
  364. msgbox("Draft discarded");
  365. $('#loadDraftMessage').addClass('hidden');
  366. }
  367. });
  368. if (savedTitle || savedContent) {
  369. $('#loadDraftMessage').removeClass('hidden');
  370. $('#loadDraftButton').on('click', function() {
  371. $('#title').val(savedTitle || '');
  372. simplemde.value(savedContent || '');
  373. $('#loadDraftMessage').addClass('hidden');
  374. });
  375. }
  376. // Autosave functionality
  377. autosaveInterval = setInterval(function() {
  378. if (!document.getElementById("editor")) {
  379. clearInterval(autosaveInterval);
  380. return;
  381. }
  382. const title = $('#title').val();
  383. const content = simplemde.value();
  384. if (title.trim() === "" && content.trim() === "") {
  385. return; // Don't save empty drafts
  386. }
  387. localStorage.setItem('draftTitle', title);
  388. localStorage.setItem('draftContent', content);
  389. }, 5000); // Autosave every 5 seconds
  390. /*
  391. Post Create Function
  392. */
  393. // Function to create a new post
  394. function createNewPost(title, content="# Hello World\n"){
  395. if (title.trim() === "" && content.trim() === "") {
  396. alert("Cannot create an empty post.");
  397. return;
  398. }
  399. $("#saveButton").addClass("loading disabled");
  400. //Create the markdown file at the /blog/posts folder
  401. const blob = new Blob([content], { type: 'text/plain' });
  402. let storeFilename = parseInt(Date.now()/1000) + "_" + title+'.md';
  403. const file = new File([blob], storeFilename);
  404. handleFile(file, "/site/posts", function(){
  405. //Update the post index
  406. updatePostIndex(function(){
  407. $("#confirmNewPostBtn").removeClass("loading disabled");
  408. msgbox("Post created successfully!", 3000);
  409. switchToTab("allposts"); // Switch to the all posts tab
  410. });
  411. //Clear the draft
  412. localStorage.removeItem('draftTitle');
  413. localStorage.removeItem('draftContent');
  414. });
  415. }
  416. // Error handler for AJAX requests
  417. function errorHandler(event) {
  418. msgbox("Failed to create post: " + event.target.responseText, 3000);
  419. $("#saveButton").removeClass("loading disabled");
  420. }
  421. // Function to handle file upload
  422. function handleFile(file, dir=currentPath, callback=undefined) {
  423. // Perform actions with the selected file
  424. var formdata = new FormData();
  425. formdata.append("file1", file);
  426. var ajax = new XMLHttpRequest();
  427. ajax.addEventListener("load", function(event){
  428. let responseText = event.target.responseText;
  429. try{
  430. responseText = JSON.parse(responseText);
  431. if (responseText.error != undefined){
  432. alert(responseText.error);
  433. }
  434. }catch(ex){
  435. }
  436. if (callback != undefined){
  437. callback();
  438. }
  439. }, false); // doesnt appear to ever get called even upon success
  440. ajax.addEventListener("error", errorHandler, false);
  441. //ajax.addEventListener("abort", abortHandler, false);
  442. ajax.open("POST", "/upload?dir=" + dir);
  443. ajax.send(formdata);
  444. }
  445. $(document).ready(function() {
  446. // Initialize with root directory
  447. fetchDirectoryContents(currentDir);
  448. });
  449. /*
  450. Handle image paste into the mde editor
  451. */
  452. function compressImageToJpeg(imageData, filename) {
  453. return new Promise((resolve, reject) => {
  454. const canvas = document.createElement('canvas');
  455. const ctx = canvas.getContext('2d');
  456. const img = new Image();
  457. img.onload = () => {
  458. const maxWidth = 1024; // Set maximum width for compression
  459. const maxHeight = 1024; // Set maximum height for compression
  460. let width = img.width;
  461. let height = img.height;
  462. // Maintain aspect ratio
  463. if (width > maxWidth || height > maxHeight) {
  464. if (width / height > maxWidth / maxHeight) {
  465. height = Math.round((height * maxWidth) / width);
  466. width = maxWidth;
  467. } else {
  468. width = Math.round((width * maxHeight) / height);
  469. height = maxHeight;
  470. }
  471. }
  472. canvas.width = width;
  473. canvas.height = height;
  474. ctx.drawImage(img, 0, 0, width, height);
  475. canvas.toBlob(
  476. (blob) => {
  477. if (blob) {
  478. const compressedFile = new File([blob], filename, {
  479. type: 'image/jpeg',
  480. });
  481. resolve(compressedFile);
  482. } else {
  483. reject(new Error('Compression failed.'));
  484. }
  485. },
  486. 'image/jpeg',
  487. 0.8 // Compression quality (0.0 to 1.0)
  488. );
  489. };
  490. img.onerror = () => reject(new Error('Failed to load image.'));
  491. img.src = imageData; // Set the image source to the data URL
  492. });
  493. }
  494. function uploadImage(comperssedFile, callback=undefined) {
  495. const formData = new FormData();
  496. if (comperssedFile) {
  497. formData.append("file1", comperssedFile);
  498. var ajax = new XMLHttpRequest();
  499. ajax.addEventListener("load", function(event) {
  500. let responseText = event.target.responseText;
  501. if (responseText.includes("ok")){
  502. msgbox(`<i class="ui green check icon"></i> Image uploaded`, 3000);
  503. if (callback != undefined){
  504. callback();
  505. }
  506. }
  507. }, false);
  508. ajax.addEventListener("error", function(event) {
  509. alert("Failed to upload image: " + event.target.responseText);
  510. }, false);
  511. ajax.open("POST", "/upload?dir=/site/img");
  512. ajax.send(formData);
  513. }else{
  514. console.error("No image to upload!");
  515. }
  516. }
  517. function bindOnPasteEvent(){
  518. $(".CodeMirror textarea").on("paste", function(event) {
  519. console.log(event);
  520. const items = (event.clipboardData || event.originalEvent.clipboardData).items;
  521. for (let i = 0; i < items.length; i++) {
  522. if (items[i].type.indexOf("image") !== -1) {
  523. const file = items[i].getAsFile();
  524. const reader = new FileReader();
  525. reader.onload = function(e) {
  526. const imageData = e.target.result;
  527. const fileName = Date.now() + ".jpg";
  528. compressImageToJpeg(imageData, fileName).then((compressedFile) => {
  529. console.log("Pasting compressed image:", fileName);
  530. const imgTag = `![Image](/site/img/${fileName})`;
  531. uploadImage(compressedFile, function(){
  532. console.log("Image uploaded:", fileName);
  533. simplemde.codemirror.replaceSelection(imgTag);
  534. });
  535. }).catch((error) => {
  536. msgbox(`<i class="ui red times icon"></i> Image compression failed`, 3000);
  537. console.error("Image compression failed:", error);
  538. });
  539. };
  540. reader.readAsDataURL(file);
  541. event.preventDefault();
  542. return;
  543. }
  544. }
  545. });
  546. }
  547. </script>