@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:
-import axios from "axios";
-
import * as constants from "./constants";
+
import EventBus from "./eventBus";
+import axios from "axios";
const api = axios.create();
--- /dev/null
+$off-white: #f8f9fd70;
+$form-control-border: #ced4da;
+$drop-shadow: #0000000a;
+$muted-text: #6c757d;
+$text: #222222;
+$button-background: #00000010;
+$input-highlight: #bbcdff;
-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() {
// Home Page
if (basePath == "") {
+ this.updateDocumentTitle();
this.currentView = this.views.home;
this.$nextTick(function() {
this.focusSearchInput();
// 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) {
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() {
});
},
- 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();
},
}
},
- 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.",
},
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) {
<!-- 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>
</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: {
--- /dev/null
+<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>
</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: {
</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 {
-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" };
// 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] },
};
};
-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);
@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 {
}
}
-.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 {
}
.search-input {
- box-shadow: 0 0 20px #0000000a;
+ box-shadow: 0 0 20px $drop-shadow;
}
.recently-modified {
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
}
--- /dev/null
+@mixin note-padding {
+ padding: min(2vw, 30px) min(3vw, 40px);
+}
"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",
"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",