Implement NoteViewerEditor component
authorAdam Dullage <redacted>
Wed, 10 Aug 2022 20:22:53 +0000 (21:22 +0100)
committerAdam Dullage <redacted>
Wed, 10 Aug 2022 20:22:53 +0000 (21:22 +0100)
15 files changed:
flatnotes/main.py
flatnotes/src/api.js
flatnotes/src/colours.scss [new file with mode: 0644]
flatnotes/src/components/App.js
flatnotes/src/components/App.vue
flatnotes/src/components/Login.vue
flatnotes/src/components/NoteViewerEditor.vue [new file with mode: 0644]
flatnotes/src/components/RecentlyModified.vue
flatnotes/src/components/SearchInput.vue
flatnotes/src/constants.js
flatnotes/src/index.js
flatnotes/src/main.scss
flatnotes/src/mixins.scss [new file with mode: 0644]
package-lock.json
package.json

index f04fbeb25d5798caec6cad17470e9ca68ec2a947..787724159e2171cf8548004854b1fad01f7f0937 100644 (file)
@@ -47,6 +47,7 @@ async def token(data: LoginModel):
 @app.get("/")
 @app.get("/login")
 @app.get("/search")
+@app.get("/new")
 @app.get("/note/{title}")
 async def root(title: str = ""):
     with open("flatnotes/dist/index.html", "r", encoding="utf-8") as f:
index 50062541e33b08b47f36cab541afdc86ad7a6424..7f970de1ee6d54d6c68eb8a859ff8b88cd201c5e 100644 (file)
@@ -1,7 +1,7 @@
-import axios from "axios";
-
 import * as constants from "./constants";
+
 import EventBus from "./eventBus";
+import axios from "axios";
 
 const api = axios.create();
 
diff --git a/flatnotes/src/colours.scss b/flatnotes/src/colours.scss
new file mode 100644 (file)
index 0000000..bf70512
--- /dev/null
@@ -0,0 +1,7 @@
+$off-white: #f8f9fd70;
+$form-control-border: #ced4da;
+$drop-shadow: #0000000a;
+$muted-text: #6c757d;
+$text: #222222;
+$button-background: #00000010;
+$input-highlight: #bbcdff;
index 6bdb1f9b9d51879b890e33a0aa0e5147bb97b22f..27d9d427fbbaf742c988dc887bd4a19f19746e47 100644 (file)
@@ -1,31 +1,29 @@
-import { Editor } from "@toast-ui/vue-editor";
-import { Viewer } from "@toast-ui/vue-editor";
+import * as constants from "../constants";
+import * as helpers from "../helpers";
 
-import RecentlyModified from "./RecentlyModified";
+import EventBus from "../eventBus";
 import LoadingIndicator from "./LoadingIndicator";
 import Login from "./Login";
+import Logo from "./Logo";
+import Mousetrap from "mousetrap";
 import NavBar from "./NavBar";
+import NoteViewerEditor from "./NoteViewerEditor";
+import RecentlyModified from "./RecentlyModified";
 import SearchInput from "./SearchInput";
-import Logo from "./Logo"
-
+import { SearchResult } from "../classes";
 import api from "../api";
-import * as constants from "../constants";
-import { Note, SearchResult } from "../classes";
-import EventBus from "../eventBus";
-import * as helpers from "../helpers";
 
 export default {
   name: "App",
 
   components: {
-    Viewer,
-    Editor,
     RecentlyModified,
     LoadingIndicator,
     Login,
     NavBar,
     SearchInput,
     Logo,
+    NoteViewerEditor,
   },
 
   data: function() {
@@ -41,6 +39,7 @@ export default {
 
       // Home Page
       if (basePath == "") {
+        this.updateDocumentTitle();
         this.currentView = this.views.home;
         this.$nextTick(function() {
           this.focusSearchInput();
@@ -49,24 +48,30 @@ export default {
 
       // Search
       else if (basePath == constants.basePaths.search) {
+        this.updateDocumentTitle("Search");
         this.searchTerm = helpers.getSearchParam(constants.params.searchTerm);
         this.getSearchResults();
         this.currentView = this.views.search;
       }
 
+      // New Note
+      else if (basePath == constants.basePaths.new) {
+        this.updateDocumentTitle("New Note");
+        this.currentView = this.views.note;
+      }
+
       // Note
       else if (basePath == constants.basePaths.note) {
-        let noteTitle = path[2];
-        this.loadNote(noteTitle);
+        this.updateDocumentTitle();
+        this.noteTitle = path[2];
         this.currentView = this.views.note;
       }
 
       // Login
       else if (basePath == constants.basePaths.login) {
+        this.updateDocumentTitle("Log In");
         this.currentView = this.views.login;
       }
-
-      this.updateDocumentTitle();
     },
 
     navigate: function(href, e) {
@@ -83,20 +88,8 @@ export default {
       Object.assign(this.$data, constants.dataDefaults());
     },
 
-    updateDocumentTitle: function() {
-      let pageTitleSuffix = null;
-      if (this.currentView == this.views.login) {
-        pageTitleSuffix = "Login";
-      } else if (this.currentView == this.views.search) {
-        pageTitleSuffix = "Search";
-      } else if (
-        this.currentView == this.views.note &&
-        this.currentNote != null
-      ) {
-        pageTitleSuffix = this.currentNote.title;
-      }
-      window.document.title =
-        (pageTitleSuffix ? `${pageTitleSuffix} - ` : "") + "flatnotes";
+    updateDocumentTitle: function(suffix) {
+      window.document.title = (suffix ? `${suffix} - ` : "") + "flatnotes";
     },
 
     logout: function() {
@@ -131,261 +124,18 @@ export default {
         });
     },
 
-    getContentForEditor: function() {
-      let draftContent = localStorage.getItem(this.currentNote.title);
-      if (draftContent) {
-        if (confirm("Do you want to resume the saved draft?")) {
-          return draftContent;
-        } else {
-          localStorage.removeItem(this.currentNote.title);
-        }
-      }
-      return this.currentNote.content;
-    },
-
-    loadNote: function(title) {
-      let parent = this;
-      this.noteLoadFailed = false;
-      api
-        .get(`/api/notes/${title}`)
-        .then(function(response) {
-          parent.currentNote = new Note(
-            response.data.title,
-            response.data.lastModified,
-            response.data.content
-          );
-          parent.updateDocumentTitle();
-        })
-        .catch(function(error) {
-          if (error.handled) {
-            return;
-          } else if (
-            typeof error.response !== "undefined" &&
-            error.response.status == 404
-          ) {
-            parent.noteLoadFailedMessage = "Note not found ðŸ˜ž";
-            parent.noteLoadFailed = true;
-          } else {
-            parent.unhandledServerErrorToast();
-            parent.noteLoadFailed = true;
-          }
-        });
-    },
-
-    toggleEditMode: function() {
-      let parent = this;
-
-      // To Edit Mode
-      if (this.editMode == false) {
-        this.titleInput = this.currentNote.title;
-        let draftContent = localStorage.getItem(this.currentNote.title);
-
-        if (draftContent) {
-          this.$bvModal
-            .msgBoxConfirm(
-              "There is an unsaved draft of this note stored in this browser. Do you want to resume the draft version or delete it?",
-              {
-                centered: true,
-                title: "Resume Draft?",
-                okTitle: "Resume Draft",
-                cancelTitle: "Delete Draft",
-                cancelVariant: "danger",
-              }
-            )
-            .then(function(response) {
-              if (response == true) {
-                parent.initialContent = draftContent;
-              } else {
-                parent.initialContent = parent.currentNote.content;
-                localStorage.removeItem(parent.currentNote.title);
-              }
-              parent.editMode = !parent.editMode;
-            });
-        } else {
-          this.initialContent = this.currentNote.content;
-          this.editMode = !this.editMode;
-        }
-      }
-      // To View Mode
-      else {
-        this.titleInput = null;
-        this.initialContent = null;
-        this.editMode = !this.editMode;
-      }
-    },
-
     newNote: function() {
-      this.currentNote = new Note();
-      this.toggleEditMode();
-      this.currentView = this.views.note;
-    },
-
-    getEditorContent: function() {
-      return this.$refs.toastUiEditor.invoke("getMarkdown");
-    },
-
-    clearDraftSaveTimeout: function() {
-      if (this.draftSaveTimeout != null) {
-        clearTimeout(this.draftSaveTimeout);
-      }
-    },
-
-    startDraftSaveTimeout: function() {
-      this.clearDraftSaveTimeout();
-      this.draftSaveTimeout = setTimeout(this.saveDraft, 1000);
-    },
-
-    saveDraft: function() {
-      localStorage.setItem(this.currentNote.title, this.getEditorContent());
-    },
-
-    existingTitleToast: function() {
-      this.$bvToast.toast(
-        "A note with this title already exists. Please try again with a new title.",
-        {
-          title: "Duplicate âœ˜",
-          variant: "danger",
-          noCloseButton: true,
-          toaster: "b-toaster-bottom-right",
-        }
-      );
-    },
-
-    saveNote: function() {
-      let parent = this;
-      let newContent = this.getEditorContent();
-
-      // Title Validation
-      if (typeof this.titleInput == "string") {
-        this.titleInput = this.titleInput.trim();
-      }
-      if (!this.titleInput) {
-        this.$bvToast.toast("Cannot save note without a title âœ˜", {
-          variant: "danger",
-          noCloseButton: true,
-          toaster: "b-toaster-bottom-right",
-        });
-        return;
-      }
-
-      // New Note
-      if (this.currentNote.lastModified == null) {
-        api
-          .post(`/api/notes`, {
-            title: this.titleInput,
-            content: newContent,
-          })
-          .then(this.saveNoteResponseHandler)
-          .catch(function(error) {
-            if (error.handled) {
-              return;
-            } else if (
-              typeof error.response !== "undefined" &&
-              error.response.status == 409
-            ) {
-              parent.existingTitleToast();
-            } else {
-              parent.unhandledServerErrorToast();
-            }
-          });
-      }
-
-      // Modified Note
-      else if (
-        newContent != this.currentNote.content ||
-        this.titleInput != this.currentNote.title
-      ) {
-        api
-          .patch(`/api/notes/${this.currentNote.title}`, {
-            newTitle: this.titleInput,
-            newContent: newContent,
-          })
-          .then(this.saveNoteResponseHandler)
-          .catch(function(error) {
-            if (error.handled) {
-              return;
-            } else if (
-              typeof error.response !== "undefined" &&
-              error.response.status == 409
-            ) {
-              parent.existingTitleToast();
-            } else {
-              parent.unhandledServerErrorToast();
-            }
-          });
-      }
-
-      // No Change
-      else {
-        this.toggleEditMode();
-        this.saveNoteToast();
-      }
-    },
-
-    saveNoteResponseHandler: function(response) {
-      localStorage.removeItem(this.currentNote.title);
-      this.currentNote = new Note(
-        response.data.title,
-        response.data.lastModified,
-        response.data.content
-      );
-      this.updateDocumentTitle();
-      history.replaceState(null, "", this.currentNote.href);
-      this.toggleEditMode();
-      this.saveNoteToast();
+      this.navigate(`/${constants.basePaths.new}`);
     },
 
-    saveNoteToast: function() {
-      this.$bvToast.toast("Note saved âœ“", {
+    noteDeletedToast: function() {
+      this.$bvToast.toast("Note deleted âœ“", {
         variant: "success",
         noCloseButton: true,
         toaster: "b-toaster-bottom-right",
       });
     },
 
-    cancelNote: function() {
-      localStorage.removeItem(this.currentNote.title);
-      if (this.currentNote.lastModified == null) {
-        // Cancelling a new note
-        this.currentNote = null;
-        this.currentView = this.views.home;
-      }
-      this.toggleEditMode();
-    },
-
-    deleteNote: function() {
-      let parent = this;
-      this.$bvModal
-        .msgBoxConfirm(
-          `Are you sure you want to delete the note '${this.currentNote.title}'?`,
-          {
-            centered: true,
-            title: "Confirm Deletion",
-            okTitle: "Delete",
-            okVariant: "danger",
-          }
-        )
-        .then(function(response) {
-          if (response == true) {
-            api
-              .delete(`/api/notes/${parent.currentNote.title}`)
-              .then(function() {
-                parent.navigate("/");
-                parent.$bvToast.toast("Note deleted âœ“", {
-                  variant: "success",
-                  noCloseButton: true,
-                  toaster: "b-toaster-bottom-right",
-                });
-              })
-              .catch(function(error) {
-                if (!error.handled) {
-                  parent.unhandledServerErrorToast();
-                }
-              });
-          }
-        });
-    },
-
     focusSearchInput: function() {
       document.getElementById("search-input").focus();
     },
@@ -399,44 +149,6 @@ export default {
       }
     },
 
-    keyboardShortcuts: function(e) {
-      // If the user is focused on a text input or is editing a note, ignore.
-      if (
-        !["e", "/"].includes(e.key) ||
-        document.activeElement.type == "text" ||
-        (this.currentView == this.views.note && this.editMode == true)
-      ) {
-        return;
-      }
-
-      // 'e' to Edit
-      if (
-        e.key == "e" &&
-        this.currentView == this.views.note &&
-        this.editMode == false
-      ) {
-        e.preventDefault();
-        this.toggleEditMode();
-      }
-
-      // '/' to Search
-      if (e.key == "/") {
-        e.preventDefault();
-        this.openSearch();
-      }
-
-      // 'CTRL + s' to Save
-      // else if (
-      //   e.key == "s" &&
-      //   e.ctrlKey == true &&
-      //   this.currentView == this.views.note &&
-      //   this.editMode == true
-      // ) {
-      //   e.preventDefault();
-      //   this.saveNote();
-      // }
-    },
-
     unhandledServerErrorToast: function() {
       this.$bvToast.toast(
         "Unknown error communicating with the server. Please try again.",
@@ -451,9 +163,16 @@ export default {
   },
 
   created: function() {
+    let parent = this;
+
     EventBus.$on("navigate", this.navigate);
     EventBus.$on("unhandledServerError", this.unhandledServerErrorToast);
-    document.addEventListener("keydown", this.keyboardShortcuts);
+    EventBus.$on("updateDocumentTitle", this.updateDocumentTitle);
+
+    Mousetrap.bind("/", function() {
+      parent.openSearch();
+      return false;
+    });
 
     let token = localStorage.getItem("token");
     if (token != null) {
index 4d98f90e743f31727833b58a8b4da77e5da530ab..47d9a0aa4fc8e6691d5d2372e2e53074f31dc6b3 100644 (file)
     <!-- Login -->
     <Login v-if="currentView == views.login"></Login>
 
-    <!-- Buttons -->
-    <div class="d-flex justify-content-center mt-4">
-      <!-- Edit -->
-      <button
-        v-if="
-          currentView == views.note &&
-          editMode == false &&
-          noteLoadFailed == false
-        "
-        type="button"
-        class="bttn"
-        @click="toggleEditMode"
-        v-b-tooltip.hover
-        title="Keyboard Shortcut: e"
-      >
-        <b-icon icon="pencil-square"></b-icon> Edit
-      </button>
-
-      <!-- Delete -->
-      <button
-        v-if="
-          currentView == views.note &&
-          editMode == false &&
-          noteLoadFailed == false
-        "
-        type="button"
-        class="bttn"
-        @click="deleteNote"
-      >
-        <b-icon icon="trash"></b-icon> Delete
-      </button>
-
-      <!-- Cancel -->
-      <button
-        v-if="currentView == views.note && editMode == true"
-        type="button"
-        class="bttn"
-        @click="cancelNote"
-      >
-        <b-icon icon="arrow-return-left"></b-icon> Cancel
-      </button>
-
-      <!-- Save -->
-      <button
-        v-if="currentView == views.note && editMode == true"
-        type="button"
-        class="bttn"
-        @click="saveNote"
-      >
-        <b-icon icon="check-square"></b-icon> Save
-      </button>
-    </div>
-
     <!-- Home -->
     <div
       v-if="currentView == views.home"
-      v-on:submit.prevent="search"
       class="
         home-view
         d-flex
       <RecentlyModified class="recently-modified"></RecentlyModified>
     </div>
 
-    <!-- Note -->
-    <div v-if="currentView == views.note" class="w-100">
-      <!-- Loading -->
-      <div v-if="currentNote == null">
-        <loading-indicator
-          :failure-message="noteLoadFailedMessage"
-          :failed="noteLoadFailed"
-        />
-      </div>
-
-      <!-- Note Loaded -->
-      <div v-else>
-        <h2 v-if="editMode == false" class="mb-4">{{ currentNote.title }}</h2>
-        <input
-          v-else
-          type="text"
-          class="h2 title-input"
-          v-model="titleInput"
-          placeholder="Title"
-        />
-
-        <!-- Viewer -->
-        <div class="mb-4 note">
-          <div v-if="editMode == false" class="note-viewer">
-            <viewer
-              :initialValue="currentNote.content"
-              height="600px"
-              :options="viewerOptions"
-            />
-          </div>
-
-          <!-- Editor -->
-          <div v-else>
-            <editor
-              :initialValue="initialContent"
-              initialEditType="markdown"
-              previewStyle="tab"
-              height="calc(100vh - 230px)"
-              ref="toastUiEditor"
-              :options="editorOptions"
-              @change="startDraftSaveTimeout"
-            />
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- Search -->
-    <div v-if="currentView == views.search" class="w-100">
+    <!-- Search Results -->
+    <div v-if="currentView == views.search" class="w-100 pt-5">
       <!-- Searching -->
       <div v-if="searchResults == null">
         <loading-indicator
         </div>
       </div>
     </div>
+
+    <!-- Note -->
+    <NoteViewerEditor
+      v-if="currentView == this.views.note"
+      class="mt-5 flex-grow-1"
+      :titleToLoad="noteTitle"
+      @note-deleted="noteDeletedToast"
+    ></NoteViewerEditor>
   </div>
 </template>
 
index fbfd170245018ab158a638b83f12910f202d7513..20201c693fa7c8dae60f128db5d294eeb267d210 100644 (file)
 </template>
 
 <script>
-import api from "../api";
-import * as helpers from "../helpers";
 import * as constants from "../constants";
+import * as helpers from "../helpers";
+
 import EventBus from "../eventBus";
 import Logo from "./Logo";
+import api from "../api";
 
 export default {
   components: {
diff --git a/flatnotes/src/components/NoteViewerEditor.vue b/flatnotes/src/components/NoteViewerEditor.vue
new file mode 100644 (file)
index 0000000..98cdeae
--- /dev/null
@@ -0,0 +1,493 @@
+<template>
+  <!-- Note -->
+  <div class="w-100">
+    <!-- Loading -->
+    <div v-if="currentNote == null">
+      <loading-indicator
+        v-if="currentNote == null"
+        :failure-message="noteLoadFailedMessage"
+        :failed="noteLoadFailed"
+      />
+    </div>
+
+    <!-- Loaded -->
+    <div v-else class="d-flex flex-column h-100">
+      <div
+        class="d-flex justify-content-between flex-wrap align-items-end mb-3"
+      >
+        <!-- Title -->
+        <h2 v-if="editMode == false" class="title" :title="currentNote.title">
+          {{ currentNote.title }}
+        </h2>
+        <input
+          v-else
+          type="text"
+          class="h2 title-input flex-grow-1"
+          v-model="titleInput"
+          placeholder="Title"
+        />
+
+        <!-- Buttons -->
+        <div class="d-flex">
+          <!-- Edit -->
+          <button
+            v-if="editMode == false && noteLoadFailed == false"
+            type="button"
+            class="bttn"
+            @click="setEditMode(true)"
+            v-b-tooltip.hover
+            title="Keyboard Shortcut: e"
+          >
+            <b-icon icon="pencil-square"></b-icon> Edit
+          </button>
+
+          <!-- Delete -->
+          <button
+            v-if="editMode == false && noteLoadFailed == false"
+            type="button"
+            class="bttn"
+            @click="deleteNote"
+          >
+            <b-icon icon="trash"></b-icon> Delete
+          </button>
+
+          <!-- Cancel -->
+          <button
+            v-if="editMode == true"
+            type="button"
+            class="bttn"
+            @click="cancelNote"
+          >
+            <b-icon icon="arrow-return-left"></b-icon> Cancel
+          </button>
+
+          <!-- Save -->
+          <button
+            v-if="editMode == true"
+            type="button"
+            class="bttn"
+            @click="saveNote"
+          >
+            <b-icon icon="check-square"></b-icon> Save
+          </button>
+        </div>
+      </div>
+
+      <!-- Viewer -->
+      <div v-if="editMode == false" class="mb-4 note note-viewer">
+        <viewer :initialValue="currentNote.content" :options="viewerOptions" />
+      </div>
+
+      <!-- Editor -->
+      <div v-else class="mb-4 note flex-grow-1">
+        <editor
+          :initialValue="initialContent"
+          initialEditType="markdown"
+          previewStyle="tab"
+          ref="toastUiEditor"
+          :options="editorOptions"
+          height="100%"
+          @change="startDraftSaveTimeout"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+// Toast UI Markdown Editor
+@import "@toast-ui/editor/dist/toastui-editor.css";
+@import "@toast-ui/editor/dist/toastui-editor-viewer.css";
+@import "prismjs/themes/prism.css";
+@import "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css";
+
+@import "../colours";
+@import "../mixins";
+
+.title {
+  min-width: 300px;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  color: $text;
+  margin: 0;
+}
+
+.title-input {
+  border: none;
+
+  // Override user agent styling
+  background-color: transparent;
+  color: $text;
+  padding: 0;
+}
+
+.note {
+  background-color: white;
+  box-shadow: 0 0 10px $drop-shadow;
+}
+
+.note-viewer {
+  @include note-padding;
+}
+</style>
+
+<style lang="scss">
+@import "../colours";
+@import "../mixins";
+
+// Toast UI Overrides
+.toastui-editor-contents {
+  font-family: "Inter", sans-serif;
+  h1,
+  h2 {
+    border-bottom: none;
+  }
+}
+
+.toastui-editor-contents pre,
+.toastui-editor-md-code-block-line-background {
+  background-color: darken($off-white, 3%);
+}
+
+.toastui-editor-defaultUI {
+  border: none;
+}
+
+.ProseMirror {
+  font-family: "Inter", sans-serif;
+}
+
+.toastui-editor-defaultUI .ProseMirror {
+  @include note-padding;
+}
+</style>
+
+<script>
+import { Editor } from "@toast-ui/vue-editor";
+import EventBus from "../eventBus";
+import LoadingIndicator from "./LoadingIndicator";
+import Mousetrap from "mousetrap";
+import { Note } from "../classes";
+import { Viewer } from "@toast-ui/vue-editor";
+import api from "../api";
+import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js";
+
+export default {
+  components: {
+    Viewer,
+    Editor,
+    LoadingIndicator,
+  },
+
+  props: {
+    titleToLoad: { type: String, default: null },
+  },
+
+  data: function () {
+    return {
+      editMode: false,
+      draftSaveTimeout: null,
+      currentNote: null,
+      titleInput: null,
+      initialContent: null,
+      noteLoadFailed: false,
+      noteLoadFailedMessage: "Loading failed ðŸ˜ž",
+      viewerOptions: { plugins: [codeSyntaxHighlight] },
+      editorOptions: { plugins: [codeSyntaxHighlight] },
+    };
+  },
+
+  watch: {
+    titleToLoad: function () {
+      this.init();
+    },
+  },
+
+  methods: {
+    loadNote: function (title) {
+      let parent = this;
+      this.noteLoadFailed = false;
+      api
+        .get(`/api/notes/${title}`)
+        .then(function (response) {
+          parent.currentNote = new Note(
+            response.data.title,
+            response.data.lastModified,
+            response.data.content
+          );
+          EventBus.$emit("updateDocumentTitle", parent.currentNote.title);
+        })
+        .catch(function (error) {
+          if (error.handled) {
+            return;
+          } else if (
+            typeof error.response !== "undefined" &&
+            error.response.status == 404
+          ) {
+            parent.noteLoadFailedMessage = "Note not found ðŸ˜ž";
+            parent.noteLoadFailed = true;
+          } else {
+            EventBus.$emit("unhandledServerError");
+            parent.noteLoadFailed = true;
+          }
+        });
+    },
+
+    getContentForEditor: function () {
+      let draftContent = localStorage.getItem(this.currentNote.title);
+      if (draftContent) {
+        if (confirm("Do you want to resume the saved draft?")) {
+          return draftContent;
+        } else {
+          localStorage.removeItem(this.currentNote.title);
+        }
+      }
+      return this.currentNote.content;
+    },
+
+    setEditMode: function (editMode = true) {
+      let parent = this;
+
+      // To Edit Mode
+      if (editMode === true) {
+        this.titleInput = this.currentNote.title;
+        let draftContent = localStorage.getItem(this.currentNote.title);
+
+        if (draftContent) {
+          this.$bvModal
+            .msgBoxConfirm(
+              "There is an unsaved draft of this note stored in this browser. Do you want to resume the draft version or delete it?",
+              {
+                centered: true,
+                title: "Resume Draft?",
+                okTitle: "Resume Draft",
+                cancelTitle: "Delete Draft",
+                cancelVariant: "danger",
+              }
+            )
+            .then(function (response) {
+              if (response == true) {
+                parent.initialContent = draftContent;
+              } else {
+                parent.initialContent = parent.currentNote.content;
+                localStorage.removeItem(parent.currentNote.title);
+              }
+              parent.editMode = true;
+            });
+        } else {
+          this.initialContent = this.currentNote.content;
+          this.editMode = true;
+        }
+      }
+      // To View Mode
+      else {
+        this.titleInput = null;
+        this.initialContent = null;
+        this.editMode = false;
+      }
+    },
+
+    getEditorContent: function () {
+      if (typeof this.$refs.toastUiEditor != "undefined") {
+        return this.$refs.toastUiEditor.invoke("getMarkdown");
+      } else {
+        return null;
+      }
+    },
+
+    clearDraftSaveTimeout: function () {
+      if (this.draftSaveTimeout != null) {
+        clearTimeout(this.draftSaveTimeout);
+      }
+    },
+
+    startDraftSaveTimeout: function () {
+      this.clearDraftSaveTimeout();
+      this.draftSaveTimeout = setTimeout(this.saveDraft, 1000);
+    },
+
+    saveDraft: function () {
+      let content = this.getEditorContent();
+      if (content) {
+        localStorage.setItem(this.currentNote.title, content);
+      }
+    },
+
+    existingTitleToast: function () {
+      this.$bvToast.toast(
+        "A note with this title already exists. Please try again with a new title.",
+        {
+          title: "Duplicate âœ˜",
+          variant: "danger",
+          noCloseButton: true,
+          toaster: "b-toaster-bottom-right",
+        }
+      );
+    },
+
+    saveNote: function () {
+      let parent = this;
+      let newContent = this.getEditorContent();
+
+      // Title Validation
+      if (typeof this.titleInput == "string") {
+        this.titleInput = this.titleInput.trim();
+      }
+      if (!this.titleInput) {
+        this.$bvToast.toast("Cannot save note without a title âœ˜", {
+          variant: "danger",
+          noCloseButton: true,
+          toaster: "b-toaster-bottom-right",
+        });
+        return;
+      }
+
+      // New Note
+      if (this.currentNote.lastModified == null) {
+        api
+          .post(`/api/notes`, {
+            title: this.titleInput,
+            content: newContent,
+          })
+          .then(this.saveNoteResponseHandler)
+          .catch(function (error) {
+            if (error.handled) {
+              return;
+            } else if (
+              typeof error.response !== "undefined" &&
+              error.response.status == 409
+            ) {
+              parent.existingTitleToast();
+            } else {
+              EventBus.$emit("unhandledServerError");
+            }
+          });
+      }
+
+      // Modified Note
+      else if (
+        newContent != this.currentNote.content ||
+        this.titleInput != this.currentNote.title
+      ) {
+        api
+          .patch(`/api/notes/${this.currentNote.title}`, {
+            newTitle: this.titleInput,
+            newContent: newContent,
+          })
+          .then(this.saveNoteResponseHandler)
+          .catch(function (error) {
+            if (error.handled) {
+              return;
+            } else if (
+              typeof error.response !== "undefined" &&
+              error.response.status == 409
+            ) {
+              parent.existingTitleToast();
+            } else {
+              EventBus.$emit("unhandledServerError");
+            }
+          });
+      }
+
+      // No Change
+      else {
+        this.setEditMode(false);
+        this.noteSavedToast();
+      }
+    },
+
+    saveNoteResponseHandler: function (response) {
+      localStorage.removeItem(this.currentNote.title);
+      this.currentNote = new Note(
+        response.data.title,
+        response.data.lastModified,
+        response.data.content
+      );
+      EventBus.$emit("updateDocumentTitle", this.currentNote.title);
+      history.replaceState(null, "", this.currentNote.href);
+      this.setEditMode(false);
+      this.noteSavedToast();
+    },
+
+    noteSavedToast: function () {
+      this.$bvToast.toast("Note saved âœ“", {
+        variant: "success",
+        noCloseButton: true,
+        toaster: "b-toaster-bottom-right",
+      });
+    },
+
+    cancelNote: function () {
+      localStorage.removeItem(this.currentNote.title);
+      if (this.currentNote.lastModified == null) {
+        // Cancelling a new note
+        EventBus.$emit("navigate", "/");
+      } else {
+        this.setEditMode(false);
+      }
+    },
+
+    deleteNote: function () {
+      let parent = this;
+      this.$bvModal
+        .msgBoxConfirm(
+          `Are you sure you want to delete the note '${this.currentNote.title}'?`,
+          {
+            centered: true,
+            title: "Confirm Deletion",
+            okTitle: "Delete",
+            okVariant: "danger",
+          }
+        )
+        .then(function (response) {
+          if (response == true) {
+            api
+              .delete(`/api/notes/${parent.currentNote.title}`)
+              .then(function () {
+                parent.$emit("note-deleted");
+                EventBus.$emit("navigate", "/");
+              })
+              .catch(function (error) {
+                if (!error.handled) {
+                  EventBus.$emit("unhandledServerError");
+                }
+              });
+          }
+        });
+    },
+
+    init: function () {
+      this.currentNote = null;
+      if (this.titleToLoad) {
+        this.loadNote(this.titleToLoad);
+        this.setEditMode(false);
+      } else {
+        this.currentNote = new Note();
+        this.setEditMode(true);
+      }
+    },
+  },
+
+  created: function () {
+    let parent = this;
+
+    // 'e' to edit
+    Mousetrap.bind("e", function () {
+      if (parent.editMode == false) {
+        parent.setEditMode(true);
+      }
+    });
+
+    // 'ctrl+s' to save
+    // Mousetrap.bind("ctrl+s", function () {
+    //   if (parent.editMode == true) {
+    //     parent.saveNote();
+    //     return false;
+    //   }
+    // });
+
+    this.init();
+  },
+};
+</script>
index 6464ea2e43cced81feab4f01dbd7131b08c2562b..a57b946b336f2277b85c620375fbe727dd1eccf0 100644 (file)
 </template>
 
 <script>
-import api from "../api";
-import { Note } from "../classes";
 import EventBus from "../eventBus";
 import LoadingIndicator from "./LoadingIndicator.vue";
+import { Note } from "../classes";
+import api from "../api";
 
 export default {
   components: {
index b401a43174b59f94e10fdb62991b1c809508308e..63429dc6c29c56fdc7b6e96c6920fdb647d3c1a2 100644 (file)
 </template>
 
 <style lang="scss" scoped>
+@import "../colours";
+
 @keyframes highlight {
   from {
-    background-color: #bbcdff;
+    background-color: $input-highlight;
   }
   to {
     background-color: white;
 }
 
 .btn {
-  border: 1px solid #cfd4da;
+  border: 1px solid $form-control-border;
   svg {
-    color: #a7abb1;
+    color: $muted-text;
   }
 }
 </style>
 
 <script>
-import EventBus from "../eventBus";
 import * as constants from "../constants";
 
+import EventBus from "../eventBus";
+
 export default {
   data: function () {
     return {
index 329bb48f6393e53a845eb4d45b2bfb22f7fa4243..69abf2ec3714fb37318c0023e39d8dfd7cdbdbaa 100644 (file)
@@ -1,7 +1,10 @@
-import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js";
-
 // Base Paths
-export const basePaths = { login: "login", note: "note", search: "search" };
+export const basePaths = {
+  login: "login",
+  note: "note",
+  search: "search",
+  new: "new",
+};
 
 // Params
 export const params = { searchTerm: "term", redirect: "redirect" };
@@ -19,23 +22,13 @@ export const dataDefaults = function() {
 
     // State
     currentView: 1,
-    editMode: false,
-    draftSaveTimeout: null,
 
-    // Search Data
+    // Note Data
+    noteTitle: null,
+
+    // Search Result Data
     searchFailed: false,
     searchTerm: null,
     searchResults: null,
-
-    // Note Data
-    currentNote: null,
-    titleInput: null,
-    initialContent: null,
-    noteLoadFailed: false,
-    noteLoadFailedMessage: "Loading failed ðŸ˜ž",
-
-    // Toast UI Plugins
-    viewerOptions: { plugins: [codeSyntaxHighlight] },
-    editorOptions: { plugins: [codeSyntaxHighlight] },
   };
 };
index 0815f4dd0ac286e08323f16392677f9ca0d70cbd..c0ce22d0463f9a321b857322a6462ca4fe8b505d 100644 (file)
@@ -1,8 +1,9 @@
-import App from "./components/App.vue";
-import Vue from "vue";
+import "./main.scss"
+
 import { BootstrapVue, IconsPlugin } from "bootstrap-vue";
 
-import "./main.scss"
+import App from "./components/App.vue";
+import Vue from "vue";
 
 Vue.use(BootstrapVue);
 Vue.use(IconsPlugin);
index 1ab953992acc2deb3117218610ee082d642860c9..2174eb0b974244829ea8b26bd2eddfc5af181ae1 100644 (file)
@@ -5,22 +5,11 @@
 @import "node_modules/bootstrap/scss/variables";
 @import "node_modules/bootstrap/scss/mixins";
 
-// Toast UI Markdown Editor
-@import "@toast-ui/editor/dist/toastui-editor.css";
-@import "@toast-ui/editor/dist/toastui-editor-viewer.css";
-@import "prismjs/themes/prism.css";
-@import "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css";
-
 // Google Font
 @import url("https://fonts.googleapis.com/css2?family=Inter:wght@100&display=swap");
 
-// Variables
-$off-white: #f8f9fd70;
-
-// Mixins
-@mixin note-content {
-  padding: min(2vw, 30px) min(3vw, 40px);
-}
+// Colours
+@import "./colours";
 
 // Elements & Classes
 body {
@@ -39,27 +28,9 @@ body {
   }
 }
 
-.note {
-  background-color: white;
-  box-shadow: 0 0 10px #0000000a;
-}
-
-.note-viewer {
-  @include note-content;
-}
-
-.title-input {
-  border: none;
-
-  // Override user agent styling
-  background-color: $off-white;
-  color: #212529;
-  padding: 0;
-}
-
 .form-control:focus {
   box-shadow: none;
-  border-color: #ced4da;
+  border-color: $form-control-border;
 }
 
 .home-view {
@@ -67,7 +38,7 @@ body {
 }
 
 .search-input {
-  box-shadow: 0 0 20px #0000000a;
+  box-shadow: 0 0 20px $drop-shadow;
 }
 
 .recently-modified {
@@ -80,38 +51,12 @@ body {
   background-color: transparent;
   border-radius: 4px;
   padding: 4px 10px;
-  color: #6c757d;
+  color: $muted-text;
 
   svg {
-    margin-right: 4px;;
+    margin-right: 2px;
   }
 }
 .bttn:hover {
-  background-color: rgba(0, 0, 0, 0.03);
-}
-
-// Toast UI Overrides
-.toastui-editor-contents {
-  font-family: "Inter", sans-serif;
-  h1,
-  h2 {
-    border-bottom: none;
-  }
-}
-
-.toastui-editor-contents pre,
-.toastui-editor-md-code-block-line-background {
-  background-color: darken($off-white, 3%);
-}
-
-.toastui-editor-defaultUI {
-  border: none;
-}
-
-.ProseMirror {
-  font-family: "Inter", sans-serif;
-}
-
-.toastui-editor-defaultUI .ProseMirror {
-  @include note-content;
+  background-color: $button-background
 }
diff --git a/flatnotes/src/mixins.scss b/flatnotes/src/mixins.scss
new file mode 100644 (file)
index 0000000..9929232
--- /dev/null
@@ -0,0 +1,3 @@
+@mixin note-padding {
+  padding: min(2vw, 30px) min(3vw, 40px);
+}
index 22835836f553249b97139f78fbe1134ff01cebcc..a7c1d5dd11d8fc75ab509806587dd9472d2fbf21 100644 (file)
@@ -15,6 +15,7 @@
         "axios": "^0.21.1",
         "bootstrap": "4.6",
         "bootstrap-vue": "^2.21.2",
+        "mousetrap": "^1.6.5",
         "parcel-bundler": "^1.12.5",
         "portal-vue": "^2.1.7",
         "sass": "^1.37.5",
         "mkdirp": "bin/cmd.js"
       }
     },
+    "node_modules/mousetrap": {
+      "version": "1.6.5",
+      "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz",
+      "integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==",
+      "dev": true
+    },
     "node_modules/ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
         "minimist": "^1.2.5"
       }
     },
+    "mousetrap": {
+      "version": "1.6.5",
+      "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz",
+      "integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==",
+      "dev": true
+    },
     "ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
index 82161bd2e69803ffa2946cf42460ff6f5fd9ccbe..5b18bc3c9724a36264f0466daa847671bd7985a0 100644 (file)
@@ -18,6 +18,7 @@
     "axios": "^0.21.1",
     "bootstrap": "4.6",
     "bootstrap-vue": "^2.21.2",
+    "mousetrap": "^1.6.5",
     "parcel-bundler": "^1.12.5",
     "portal-vue": "^2.1.7",
     "sass": "^1.37.5",
git clone https://git.99rst.org/PROJECT