Try   HackMD

Lab 3: Editable Nodes

Released: October 12th, 2023
Due: October 18th, 2023 at 11:59 PM ET
To get checked off for this lab, meet with a TA at your lab section or TA hours

⚠️ Do not clone the Assignment 3 stencil code until you have submitted Assignment 2 for the last time to Gradescope. We will be checking to make sure you do not submit Assignment 2 after cloning Assignment 3. ⚠️

Introduction

Congratulations ~ you have successfully created and deployed your very own hypermedia system! You can create anchors and links between nodes, within the same node, and follow links, but there is so much more that you could be able to do in a hypermedia system!

In this unit, you will be turning the system from a static, view-only hypermedia system to one that is interactive and usable.

In this lab we are laying out the foundation as we get ready to implement editing the properties of the INodes that we have created. As always, if you have any questions about anything, please reach out to a TA or message us on Slack!

Objective: Introduce you to NPM packages, Prosemirror, Tiptap, and how to make your content editable!

Checklist

  • Complete Assignment 2 feedback form and student information form
  • Brainstorm ideas for your final project
  • Clone the repo for the lab
  • Get setup in your codebase with tiptap
  • Implement different ways of editing a node's title
    • Double click
    • Context menu
    • Keyboard shortcuts

Reflecting on Assignment 2

Great job on a tough assignment! Please let us know what went well and what didn't by completing this anonymous feedback form for Assignment 2.

Student Information Form

We need to link all of your lab checkoffs, hypothesis annotations, and assignment submissions in order to calculate midpoint grade reports. Please fill out this form so we can get all the necessary information.

Beginning to Brainstorm for your Final Project

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

For the final project, you will be working in groups of 3 to build your very own hypermedia system! As you are already aware, the scope of a hypermedia system is extremely broad – it can range from a system that focuses on temporal media to one that focuses on visualisations, or even just a system like Wikipedia that is primarily textual.

We want to get you thinking about what type of hypermedia system you want to work on for your final project early, because in Unit 4: Additional Hypertext Features you will need to begin making choices relevant to what hypertext system you want to build.

Task 1: Write out a list of areas that you would be interested in exploring. This can range from as broad as just text nodes to a particular technology you would like to implement like a motion tracker. This task is merely to get you thinking so don't worry if you can't come up with much!

Feel free to discuss this with a partner!

Demo

Note: The demo may take up to 30 seconds to load due to the free backend we used for hosting. Additionally, you might notice some strange behavior if there are multiple students interacting with the demo at the same time.

You can find a demo of the MyHypermedia application here.

Cloning the GitHub Repo

You can clone the repository here.

The same repo will be used for Lab 3 and Assignment 3!

Always remember to run npm install in both the frontend and backend folder as well as adding your .env file with the appropriate environment variables to the server folder. Let us know if you have any questions!

Using npm modules from the npm registry

As a npm user, you can create and publish public packages that anyone can download and use in their own projects. As a developer, writing your own code for everything can become tedious and challenging, and therefore the npm registry provides free open source code that you can use for your projects.

It is vital that you have an understanding of how to install new npm modules so we will be walking you through an example of how to install one and also giving import tips along the way to explain how you can ensure that you have a well-managed codebase.

Web applications usually use a great number of third-party packages and libraries so we don’t have to write everything on our own. Instead of manually installing the packages one-by-one, we use the package manager to automate this process. It’s really quick!

Dependencies

Project dependencies are the packages that are being used in the code of the project. We have separated our frontend and backend into two seperate folders, each with their own project dependencies. There are multiple types of project dependencies, which are outlined below.

Task 2: Look at the package.json file in the unit3-editable-nodes/client folder. Notice how there are seperate lists fordependencies and devDependencies? Each dependency has the name in the npm registry and the version that your current project is compatible with.

Production Dependencies

These are the dependencies that our web application needs to run. For example since our web application is working with React we need a react node package in our application. These dependencies are specified under the key “dependencies” in package.json file.

Development Dependencies

These are those dependencies which are needed at the time of development but are not responsible for working of the application i.e. even if we skip these dependency our application will work just fine. An example of a dependency that would go into this list is ESLint, which checks the Java/TypeScript code we write for common problems, such as syntax errors, formatting issues, code style violations, and potential bugs.

Optional Dependencies

Our project has no optional dependencies, but as the name suggests, these dependencies are optional. If they fail to install, npm will still say the install process was successful. This is useful for dependencies that won’t necessarily work on every machine and you have a fallback plan in case they are not installed.

Peer Dependencies

Peer dependencies are primarily used when you are developing a plugin/package for a host tool or package. That means you expect the user of the plugin/package to have these dependencies installed while not necessarily using these dependencies in your plugin/package.

You won't have to worry about including peer dependencies for this web application, but you may come across the message: "some_dependency requires a peer of <another_dependency> but none is installed. You must install peer dependencies yourself".

If you see an error like this, then you should install <another_dependency> yourself!

Backend

Environment Variables

Your backend setup for the hypertext system we are building is mostly complete! Thus, this lab/assignment will mostly be devoted to frontend development inside the client folder. In terms of scope for your own projects, particularly the final project, it is likely that you will want to alter the backend types to change what the metadata of your node objects look like.

For example, if you wanted your application to handle users, you would want to create an additional MongoDB collection (user-model) to store the users.

Task 3: Time to get the backend running! You should be an expert on this by now! Use the same .env file that you did for your own MongoDB connection in Unit 2 and add it into your server folder.

It should look something like this:

DB_URI = <YOUR OWN URI> PORT=8000

You may need to change the port number. If you do, make sure the port number in the server .env file matches the one in the client endpoint.ts.

Frontend

Installing a package for editable text

Now that you have a better idea of what dependencies are and how they work, you are going to install your own package. Remember all of the tedious code that we were writing during the last lab? This package handles innerHTML manipulations for us! This package is extemely useful for editable text and we will be using it for Unit 3.

What is ProseMirror?

ProseMirror is a toolkit for building rich text editors - it is not an out-the-box solution which some packages may provide (e.g. https://github.com/bkniffler/draft-wysiwyg). This means ProseMirror has a steep learning curve - there are many concepts and terms to learn, and it can be difficult to structure your codebase in a logical manner.

What is tiptap?

Tiptap is a wrapper library for ProseMirror, it is an abstraction layer that makes ProseMirror easier to work with, and provides React and ProseMirror integration.
Tiptap provides extensions that abstract over various ProseMirror concepts such as schemas, commands and plugins, making it much simpler to group related logic together.

In the assignment itself you will be using tiptap to make your text editable. In this lab, text nodes will not support linking and anchoring; setting those up will be explained and included in the assignment.

Task 4: Make sure to cd into your client folder in your terminal, then run the following command to install tiptap (documentation here)

npm install @tiptap/react @tiptap/starter-kit @tiptap/extension-link @tiptap/core

This will add tiptap to your package.json dependencies and also install other relevant packages. In particular, when you are installing tiptap, you are also installing the relevant ProseMirror packages.

Wondering where all of the code goes? It goes into your node_modules folder! If you can find the @tiptap directory, that means that your installation was successful!

Using tiptap in your codebase

Once you have installed it, it's ready to use - getting all of that useful code is really as easy as that!

When you have packages installed, you can access them in any of your files. Since we use ES6 (read more here), the syntax for importing packages is:

import Donut from 'PVDonuts'

In earlier version of JavaScript, require was often used to import packages, however since ES6, and throughout this codebase we will only be using import. So to import tiptap into our codebase (in particular TextContent.tsx, because that is where we want to use it) - we add the following lines:

import { Link } from '@tiptap/extension-link'
import { Editor, EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

Task 5: Go to client / src / components / NodeView / NodeContent / TextContent / TextContent.tsx and add the aforementioned tiptap imports to the top of the file

Note: At this point, you may still see some red warning messages in your code. That is fine.

Creating a tiptap editor

Task 6: Creating the tiptap editor!

Do this by creating an instance of the editor by replacing const editor = null with

const editor = useEditor({ extensions: [ StarterKit, Link.configure({ openOnClick: true, autolink: false, linkOnPaste: false, }), ], content: currentNode.content, });

Once you have added the editor, you will want to add the tiptap components that we are actually going to render. You should replace the final return statement (not the one in the !editor conditional), with the following:

return ( <div> <TextMenu editor={editor} /> <EditorContent editor={editor} onPointerUp={onPointerUp} /> </div> );

We've created a basic, editable text component using tiptap, without any buttons to apply styling.

Confirm that you're able to add and delete text from a text node as expected.

Note that we haven't done anything to save our changes to the database, so all of your changes should go away when you leave the node. We'll now update the database so your changes persist in Assignment 3!

Task 7: Follow the instructions below to add a button that can make text bold

The way that tiptap works is that it has a set of commands that you can use to alter the text in the editor. Rather than you having to go in and change the inner CSS, tiptap handles everything for you!

There are some really cool tiptap extensions - ranging from simple marks like Italic and Bold text, to getting setup with code snippets. For this lab, we'll walk you through setting up the bold extension.

The bold extension is already included in StarterKit, so we don't need to add it separately to our tiptap editor extension list. By default, we can make the text bold using the ctrl / cmmd + B keyboard shortcut, but we also want to add a button to visually show users that they can make text bold.

To add a button to make text bold, add the following into the return statement of the TextMenu.tsx file:

<button onClick={() => editor.chain().focus().toggleBold().run()} disabled={!editor.can().chain().focus().toggleBold().run()} className={ "textEditorButton" + (editor.isActive("bold") ? " activeTextEditorButton" : "") } > Bold </button>

You should end up with editable text which looks like the following image:

text editor with bold button

Tip: When we implement things it's important to make sure everything is functioning as expected before improving the user interface. In Assignment 3 you'll improve the user interface!

Task 8: Add the ability to make the text italicized! This should be a matter of doing the exact same thing we did for bold, except with toggleItalic, instead of toggleBold. You should reference the tiptap documentation as needed.

Like with bold, the italic extension is already included in StarterKit, so we don't need to add it separately to our tiptap editor extension list. If we wanted to add a different extension that isn't in StarterKit, we would need to list it in the list of extensions in useEditor.

Editing the node's title

  1. Go to client / src / components / NodeView / NodeHeader / NodeHeader.tsx where you will find the TODOs for this task!
  2. We have two state variables to keep track of the title and whether it is in an editing state or not, they are as follows:
// State variable for current node title const [title, setTitle] = useState(currentNode.title) // State variable for whether the title is being edited const [editingTitle, setEditingTitle] = useState<boolean>(false)

Task 9: We will now fill in handleUpdateTitle, which is a function that updates the text in the database

This method takes in the updated text and adds it to the database using the NodeGateway.updateNode method. Get used to using that method, because we'll be using it a lot!

It is important that we also let the user know if the update failed! This is an important thing to do in any web application because otherwise the user is misled to believe that the update successfully was written to the database when in reality it has not!

Put the following code inside the handleUpdateTitle function located in NodeHeader.tsx. Right now, this just sets up the logic for handling title changes, which we will be able to use once task 10 is complete.

setTitle(title) const nodeProperty: INodeProperty = makeINodeProperty('title', title) const titleUpdateResp = await FrontendNodeGateway.updateNode(currentNode.nodeId, [ nodeProperty, ]) if (!titleUpdateResp.success) { setAlertIsOpen(true) setAlertTitle('Title update failed') setAlertMessage(titleUpdateResp.message) } setRefresh(!refresh)

One thing to consider, and a notable design decision, is how frequently the EditableText makes a call to the database. Currently whenever any letter in title changes it updates the database by making a call to update for every onChange. This could also be done by only making a call to the database onBlur. Have a look at EditableText to see how exactly it works!

The onBlur event occurs when an object loses focus.
The onChange event occurs when the value of an element has been changed.

Task 10: We will now add three different ways to make the text editable! Note that it is not always ideal to have all three of these implementations as there may be conflicting or inaccessible behavior. However, for the purpose of this assignment, this gives us the oppurtunity to introduce different ways to update the title.

Essentially what we are doing here is looking at different ways where editingTitle is true so that the title is in editable mode.

  1. DoubleClick

The first thing that we should do is add the onDoubleClick tag to the nodeHeader-title text <div>, so that when the user right-clicks, it sets editingTitle to true.

onDoubleClick={() => setEditingTitle(true)}
  1. ContextMenu

When it comes to context menus, we do not always want to use our own context menu because the default context menu (see attached image) has some important components when it comes to accessibility. For example, someone may need to be able to Translate a selected portion of text. Using a custom context menu would prevent them from doing so.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

The first thing that we should do is add the onContextMenu tag to the nodeHeader-title text <div>, so that when the user right-clicks it calls the method that we pass in. We would do that as follows:

onContextMenu={handleTitleRightClick}

In the handleTitleRightClick method, what we want to do is make a call to the ContextMenuItems list of JSX.Elements (which is an exported list from ContextMenu.tsx). Essentially what we are doing here is saying that we should add an item to the list. You could add other context menu items if you like this way of interacting with content for your web app!

Note: There is a known bug in our usage of the context menu, where the context menu seemingly does not close after clicking away. This is okay/expected behavior. We will let you know if we make a fix for this.

Insert the following code in the handleTitleRightClick function in NodeHeader.tsx

ContextMenuItems.splice(0, ContextMenuItems.length); const menuItem: JSX.Element = ( <div key={"titleRename"} className="contextMenuItem" onClick={(e) => { ContextMenuItems.splice(0, ContextMenuItems.length); setEditingTitle(true); }} > <div className="itemTitle">Rename</div> <div className="itemShortcut">ctrl + shift + R</div> </div> ); ContextMenuItems.push(menuItem);

Tip: The following code snippet lets us determine what operating system the person is using. This could be used to help us determine what we should give as the itemShortcut. On win it would be ctrl but on mac it would be cmd. This can be useful for other reasons as well!

let os: string = '' if (navigator.userAgent.indexOf('Win') != -1) os = 'win' if (navigator.userAgent.indexOf('Mac') != -1) os = 'mac' if (navigator.userAgent.indexOf('X11') != -1) os = 'x11' if (navigator.userAgent.indexOf('Linux') != -1) os = 'linux'
  1. Keyboard shortcuts

Keyboard shortcuts are pretty snazzy, but they also have shortcomings - for example, what we do in the code snippet below is say that if Ctrl + Shift + R is pressed we should enter the editing title workflow - but the problem with that is we now lose the ability to reload the page (which Ctrl + Shift + R also handles)!

The first step is to ensure that the browser is listening for keydown events in the useEffect on load. This uses addEventListener, which you can set it up to listen for pointerdown, pointerup, and many more events!

The EventListener interface represents an object that can handle an event dispatched by an object in the HTML DOM. You can read more about it, and other EventListeners here.

document.addEventListener('keydown', nodeKeyHandlers)

Once you have added the line above, you should add the following switch statement into your nodeKeyHandlers function. It is important to call e.preventDefault() to prevent the possibility of the default shortcut (reload in this case) also being called in addition to our custom shortcut.

// key handlers with no modifiers switch (e.key) { case "Enter": if (editingTitle == true) { e.preventDefault(); setEditingTitle(false); } break; case "Escape": if (editingTitle == true) { e.preventDefault(); setEditingTitle(false); } break; } // ctrl + shift key events if (e.shiftKey && e.ctrlKey) { switch (e.key) { case "R": e.preventDefault(); setEditingTitle(true); break; } }

Checkoff

  • Show a TA that you have a tiptap text editor with buttons to make the text bold and italic.
  • Show a TA that you are able to rename a node AND that the change persists
  • Have a dazzling day 😎
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →