Tag: Editable

Creating an Editable Textarea That Supports Syntax-Highlighted Code

When I was working on a project that needed an editor component for source code, I really wanted a way to have that editor highlight the syntax its typed. There are projects like this, like CodeMirror, Ace, and Monaco, but they are all heavy-weight, full-featured editors, not just editable textareas with syntax highlighting like I wanted.

It took a little finagling, but I wound up making something that does the job and wanted to share how I did it, because it involves integrating a popular syntax highlighting library with HTML’s editing capabilities, as well as a few interesting edge cases to take into consideration.

Go ahead and give it a spin as we dig in!

The problem

First, I tried using the contenteditable attribute on a div. I typed some source code into the div and ran it through Prism.js, a popular syntax highlighter, on onkeyup via JavaScript. Seems like a decent idea, right? We have an element that can be edited on the front end, and Prism.js applies its syntax styling to what’s typed in the element.

Chris covers how to use Prism.js in this video.

But that was a no-go. Each time the content in the element changes, the DOM is manipulated and the user’s cursor is pushed back to the start of the code, meaning the source code appears backwards, with the last characters at the start, and the first characters at the end.

White monospaced text on a black background that does not spell a coherent word.
Backwards code: not very useful

Next, I tried about using a <textarea> but that also didn’t work, as textareas can only contain plain text. In other words, we’re unable to style the content that’s entered. A textarea seems to be the only way to edit the text without unwanted bugs — it just doesn’t let Prism.js do its thing.

Prism.js works a lot better when the source code is wrapped in a typical <pre><code> tag combo — it’s only missing the editable part of the equation.

So, neither seems to work by themselves. But, I thought, why not both?

The solution

I added both a syntax-highlighted <pre><code> and a textarea to the page, and made the innerText content of <pre><code> change onkeyup, using a JavaScript function. I also added an aria-hidden attribute to the <pre><code> result so that screen readers would only read what is entered into the <textarea> instead of being read aloud twice.

<textarea id="editing" onkeyup="update(this.value);"></textarea>  <pre id="highlighting" aria-hidden="true">   <code class="language-html" id="highlighting-content"></code> </pre>
function update(text) {   let result_element = document.querySelector("#highlighting-content");   // Update code   result_element.innerText = text;   // Syntax Highlight   Prism.highlightElement(result_element); }
The HTML for a link element pointed to CSS-Tricks is in black monospace on a white background above the same HTML link markup, but syntax-highlighted on a black background.
Now it’s editable and highlighted!

Now, when the textarea is edited — as in, a pressed key on the keyboard comes back up — the syntax-highlighted code changes. There are a few bugs we’ll get to, but I want to focus first on making it look like you are directly editing the syntax-highlighted element, rather than a separate textarea.

Making it “feel” like a code editor

The idea is to visibly merge the elements together so it looks like we’re interacting with one element when there are actually two elements at work. We can add some CSS that basically allows the <textarea> and the <pre><code> elements to be sized and spaced consistently.

#editing, #highlighting {   /* Both elements need the same text and space styling so they are directly on top of each other */   margin: 10px;   padding: 10px;   border: 0;   width: calc(100% - 32px);   height: 150px; }  #editing, #highlighting, #highlighting * {   /* Also add text styles to highlighting tokens */   font-size: 15pt;   font-family: monospace;   line-height: 20pt; }

Then we want to position them right on top of each other:

#editing, #highlighting {   position: absolute;   top: 0;   left: 0; }

From there, z-index allows the textarea to stack in front the highlighted result:

/* Move the textarea in front of the result */ #editing {   z-index: 1; }  #highlighting {   z-index: 0; }

If we stop here, we’ll see our first bug. It doesn’t look like Prism.js is highlighting the syntax, but that is only because the textarea is covering up the result.

Where has the highlighting gone? (Clue: It’s hiding in the background.)

We can fix this with CSS! We’ll make the <textarea> completely transparent except the caret (cursor):

/* Make textarea almost completely transparent */ #editing {   color: transparent;   background: transparent;   caret-color: white; /* Or choose your favorite color */ }

Ah, much better!

HTML markup for a link element pointed to CSS tricks in a syntax-highlighted monospace font on a black background.
Where will this link take you? You decide.

More fixing!

Once I got this far, I played around with the editor a bit and was able to find a few more things that needed fixing. The good thing is that all of the issues are quite easy to fix using JavaScript, CSS, or even HTML.

Remove native spell checking

We’re making a code editor, and code has lots of words and attributes that a browser’s native spell checker will think are misspellings.

Showing the basic HTML markup for a link with syntax highlighting on a black background, where the href attribute is underlined in red.

Spell checking isn’t a bad thing; it’s just unhelpful in this situation. Is something marked incorrect because it is incorrectly spelled or because the code is invalid? It’s tough to tell. To fix this, all we need is to set the spellcheck attribute on the <textarea> to false:

<textarea id="editing" spellcheck="false" ...>

Handling new lines

Turns out that innerText doesn’t support newlines (n).

White monospace text on a black background. The first line says "Hello, World" and the second line says "World" and is highlight in blue.
Even though “World!” should be on a new line, the syntax highlighted section displays it on the same line.

The update function needs to be edited. Instead of using innerText, we can replace the open bracket character (<) with &lt;. This prevents new HTML tags from being created, allowing the actual source code displays instead of the browser attempting to render the code.

result_element.innerHTML = text.replace(new RegExp("<", "g"), "<"); /* Global RegExp */
Showing the basic HTML document boilerplate with syntax highlighting on a black background. The body element contains the markup yo a paragraph that contains a link that says "CSS-Tricks is brilliant!"

Scrolling and resizing

Here’s another thing: the highlighted code cannot scroll while the editing is taking place. And when the textarea is scrolled, the highlighted code does not scroll with it.

First, let’s make sure that both the textarea and result support scrolling:

/* Can be scrolled */ #editing, #highlighting {   overflow: auto; }

Then, to make sure that the result scrolls with the textarea, we’ll update the HTML and JavaScript like this:

<textarea id="editing" onkeyup="update(this.value); sync_scroll(this);" onscroll="sync_scroll(this);"></textarea>
function sync_scroll(element) {   /* Scroll result to scroll coords of event - sync with textarea */   let result_element = document.querySelector("#highlighting");   // Get and set x and y   result_element.scrollTop = element.scrollTop;   result_element.scrollLeft = element.scrollLeft; }

Some browsers also allow a textarea to be resized, but this means that the textarea and result could become different sizes. Can CSS fix this? Of course it can. We’ll simply disable resizing:

/* No resize on textarea */ #editing {   resize: none; }

Indenting lines

One of the trickier things to adjust is how to handle line indentations in the result. The way the editor is currently set up, indenting lines with spaces works fine. But, if you’re more into tabs than spaces, you may have noticed that those aren’t working as expected.

JavaScript can be used to make the Tab key properly work. I have added comments to make it clear what is happening in the function.

<textarea ... onkeydown="check_tab(this, event);"></textarea>
function check_tab(element, event) {   let code = element.value;   if(event.key == "Tab") {     /* Tab key pressed */     event.preventDefault(); // stop normal     let before_tab = code.slice(0, element.selectionStart); // text before tab     let after_tab = code.slice(element.selectionEnd, element.value.length); // text after tab     let cursor_pos = element.selectionEnd + 2; // where cursor moves after tab - 2 for 2 spaces     element.value = before_tab + "  " + after_tab; // add tab char - 2 spaces     // move cursor     element.selectionStart = cursor_pos;     element.selectionEnd = cursor_pos;   } }

The final result

Not too crazy, right? All we have are <textarea>, <pre> and <code> elements in the HTML, a new lines of CSS that stack them together, and a syntax highlighting library to format what’s entered. And what I like best about this is that we’re working with normal, semantic HTML elements, leveraging native attributes to get the behavior we want, leaning on CSS to create the illusion that we’re only interacting with one element, then reaching for JavaScript to solve some edge cases.

While I used Prism.js for syntax highlighting, this technique will work with others. It would even work with a syntax highlighter you create yourself, if you want it to. I hope this becomes useful, and can be used in many places, whether it’s a WYSIWYG editor for a CMS, or even a forms where the ability to enter source code is a requirement like a front-end job application or perhaps a quiz. It is a<textarea>, after all, so it’s capable of being used in any form — you can even add a placeholder if you need to!


The post Creating an Editable Textarea That Supports Syntax-Highlighted Code appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,

Creating an Editable Site with Google Sheets and Eleventy

Remember Tabletop.js? We just covered it a little bit ago in this same exact context: building editable websites. It’s a tool that turns a Google Sheet into an API, that you as a developer can hit for data when building a website. In that last article, we used that API on the client side, meaning JavaScript needed to run on every single page view, hit that URL for the data, and build the page. That might be OK in some circumstances, but let’s do it one better. Let’s hit the API during the build step so that the content is built into the HTML directly. This will be far faster and more resilient.

The situation

As a developer, you might have had to work with clients who keep bugging you with unending revisions on content, sometimes, even after months of building the site. That can be frustrating as it keeps pulling you back, preventing you from doing more productive work.

We’re going to give them the keys to updating content themselves using a tool they are probably already familiar with: Google Sheets.

A new tool

In the last article, we introduced the concept of using Google Sheets with Tabletop.js. Now let’s introduce a new tool to this party: Eleventy

We’ll be using Eleventy (a static site generator) because we want the site to be rendered as a pure static site without having to ship all of the under workings of the site in the client side JavaScript. We’ll be pulling the content from the API at build time and having Eleventy create a minified index.html that we’ll push to the server for the production website. By being static, this allows the page to load faster and is better for security reasons.

The spreadsheet

We’ll be using a demo I built, with its repo and Google Sheet to demonstrate how to replicate something similar in your own projects. First, we’ll need a Google Sheet which will be our data store.

Open a new spreadsheet and enter your own values in the columns just like mine. The first cell of each column is the reference that’ll be used later in our JavaScript, and the second cell is the actual content that gets displayed.

In the first column, “header” is the reference name and “Please edit me!” is the actual content in the first column.

Next up, we’ll publish the data to the web by clicking on File → Publish to the web in the menu bar.

A link will be provided, but it’s technically useless to us, so we can ignore it. The important thing is that the spreadsheet(and its data) is now publicly accessible so we can fetch it for our app.

Take note that we’ll need the unique ID of the sheet from its URL  as we go on.

Node is required to continue, so be sure that’s installed. If you want to cut through the process of installing all of thedependencies for this work, you can fork or download my repo and run:

npm install

Run this command next — I’ll explain why it’s important in a bit:

npm run seed

Then to run it locally:

npm run dev

Alright, let’s go into src/site/_data/prod/sheet.js. This is where we’re going to pull in data from the GoogleSheet, then turn it into an object we can easily use, and finally convert the JavaScript object back to JSON format. The JSON is stored locally for development so we don’t need to hit the API every time.

Here’s the code we want in there. Again, be sure to change the variable sheetID to the unique ID of your own sheet.

 module.exports = () => {   return new Promise((resolve, reject) => {     console.log(`Requesting content from $ {googleSheetUrl}`);     axios.get(googleSheetUrl)       .then(response => {         // massage the data from the Google Sheets API into         // a shape that will more convenient for us in our SSG.         var data = {           "content": []         };         response.data.feed.entry.forEach(item => {           data.content.push({             "header": item.gsx$ header.$ t,             "header2": item.gsx$ header2.$ t,             "body": item.gsx$ body.$ t,             "body2": item.gsx$ body2.$ t,             "body3":  item.gsx$ body3.$ t,             "body4": item.gsx$ body4.$ t,             "body5": item.gsx$ body5.$ t,             "body6":  item.gsx$ body6.$ t,             "body7": item.gsx$ body7.$ t,             "body8": item.gsx$ body8.$ t,             "body9":  item.gsx$ body9.$ t,             "body10": item.gsx$ body10.$ t,             "body11": item.gsx$ body11.$ t,             "body12":  item.gsx$ body12.$ t,             "body13": item.gsx$ body13.$ t,             "body14": item.gsx$ body14.$ t,             "body15":  item.gsx$ body15.$ t,             "body16": item.gsx$ body16.$ t,             "body17": item.gsx$ body17.$ t,                        })         });         // stash the data locally for developing without         // needing to hit the API each time.         seed(JSON.stringify(data), `$ {__dirname}/../dev/sheet.json`);         // resolve the promise and return the data         resolve(data);       })       // uh-oh. Handle any errrors we might encounter       .catch(error => {         console.log('Error :', error);         reject(error);       });   }) }

In module.exports, there’s a promise that’ll resolve our data or throw errors when necessary. You’ll notice that I’m using a axios to fetch the data from the spreadsheet. I like the it handles status error codes by rejecting the promise automatically, unlike something like Fetch that requires monitoring error codes manually.

I created a data object in there with a content array in it. Feel free to change the structure of the object, depending on what the spreadsheet looks like.

We’re using the forEach() method to loop through each spreadsheet column while equating it with the corresponding name we want to allocate to it, while pushing all of these into the data object as content. 

Remember that seed command from earlier? We’re using seed to transform what’s in the data object to JSON by way of JSON.stringify, which is then sent to src/site/_data/dev/sheet.json

Yes! Now have data in a format we can use with any templating engine, like Nunjucks, to manipulate it. But, we’re focusing on content in this project, so we’ll be using the index.md template format to communicate the data stored in the project.

For example, here’s how it looks to pull item.header through a for loop statement:

<div class="listing"> {%- for item in sheet.content -%}   <h1>{{ item.header }} </h1> {%- endfor -%} </div>

If you’re using Nunjucks, or any other templating engine, you’ll have to pull the data accordingly.

Finally, let’s build this out:

npm run build

Note that you’ll want a dist folder in the project where the build process can send the compiled assets.

But that’s not all! If we were to edit the Google Sheet, we won’t see anything update on our site. That’s where Zapier comes in. We can “zap” Google sheet and Netlify so that an update to the Google Sheet triggers a deployment from Netlify.

Assuming you have a Zapier account up and running, we can create the zap by granting permissions for Google and Netlify to talk to one another, then adding triggers.

The recipe we’re looking for? We’re connecting Google Sheets to Netlify so that when a “new or updated sheet row” takes place, Netlify starts a deploy. It’s truly a set-it-and-forget-it sort of deal.

Yay, there we go! We have a performant static site that takes its data from Google Sheets and deploys automatically when updates are made to the sheet.

The post Creating an Editable Site with Google Sheets and Eleventy appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

Creating an Editable Webpage With Google Spreadsheets and Tabletop.js

Please raise your hand if you’ve ever faced never-ending content revision requests from your clients. It’s not that the changes themselves are difficult, but wouldn’t it be less complicated if clients could just make the revisions themselves? That would save everyone valuable time, and  allow you to turn your attention to more important tasks.

In the case where the site is built on the flat files (e.g. HTML, CSS and JavaScript) instead of a CMS (e.g. WordPress), you’ll need some other sort of solution to edit the content without directly editing those files.

Tabletop.js allows us to use Google Spreadsheets as a sort of data store, by taking the spreadsheet and making it easily accessible through JavaScript. It provides the data from a Google Spreadsheet in JSON format, which can then use in an app, like pulling data from any other API. In this article, we’ll be adding data to a spreadsheet then setting up Tabletop so that it can pull data from the data source to our HTML. Let us get straight to the code!

This article is going to be based off a real-world site I built when I was initially trying to wrap my head around Tabletop. By the way, I always advise developers to build applications with any form of technology they’re trying to learn, even after watching or reading tutorials.

We’ll be using the demo I made, with its source code and spreadsheet . The first thing we’ll need is a Google account to access the spreadsheet. 

Open a new spreadsheet and input your own values in the columns just like mine. The first cell in on each column been the reference that’ll be used later in our JavaScript, and the second cell been the actual content for the website.

Next up, we’ll publish the data to the web by clicking on File → Publish to the web in the menu bar.

A link will be provided, but it’s technically useless to us, so we can ignore. The important thing is that the spreadsheet (and its data) is now publicly accessible so we can fetch it for our app.

That said, there is a link we need. Clicking the big green “Share” button in the upper-right corner of the page will trigger a modal that provides a sharable link to the spreadsheet and lets us set permissions as well. Let’s grab that link and set the permissions so that anyone with the link can view the spreadsheet. That way, the data won’t inadvertently be edited by someone else.

Now is the time to initialize Tabletop in our project. Let’s link up to their hosted minified file. Similarly, we could copy the raw minified code, drop it into our own script file and host it ourselves.

Here’s the document file linked up to Tabletop’s CDN and with code snagged from the documentation.

<script src='https://cdnjs.cloudflare.com/ajax/libs/tabletop.js/1.5.1/tabletop.min.js'></script>  <script type='text/javascript'>       var publicSpreadsheetUrl = 'https://docs.google.com/spreadsheets/d/1sbyMINQHPsJctjAtMW0lCfLrcpMqoGMOJj6AN-sNQrc/pubhtml';    function init() {     Tabletop.init( {       key: publicSpreadsheetUrl,       callback: showInfo,       simpleSheet: true      } )   }    function showInfo(data, tabletop) {     alert('Successfully processed!')     console.log(data);   }    window.addEventListener('DOMContentLoaded', init) </script>

Replace the publicSpreadsheetUrl variable in the code with the sharable link from the spreadsheet. See, told you we’d need that!

Now to the interesting part. Let’s give the HTML unique IDs and leave them empty. Then, inside the showInfo function, we’ll use the forEach() method to loop through each spreadsheet column while equating it with the corresponding ID.innerHTML method which, in turn, loads the spreadsheet’s data into the HTML tag through the ID.

function showInfo(data, tabletop) {   data.forEach(function(data) {     header.innerHTML = data.header;     header2.innerHTML = data.header2;     body.innerHTML = data.body;     body2.innerHTML = data.body2;     body3.innerHTML = data.body3;     body4.innerHTML = data.body4;     body5.innerHTML = data.body5;     body6.innerHTML = data.body6;     body7.innerHTML = data.body7;     body8.innerHTML = data.body8;     body9.innerHTML = data.body9;     body10.innerHTML = data.body10;     body11.innerHTML = data.body11;     body12.innerHTML = data.body12;     body13.innerHTML = data.body13;     body14.innerHTML = data.body14;     body15.innerHTML = data.body15;     body16.innerHTML = data.body16;     body17.innerHTML = data.body17;  }); } window.addEventListener('DOMContentLoaded', init)

This is a section of HTML from my demo showing the empty tags. This is a good way to go, but it could be abstracted further but creating the HTML elements directly from JavaScript.

<!-- Start Section One: Keep track of your snippets --> <section class="feature">   <div class="intro-text">     <h3 id="body"></h3>     <p id="body2">            </p>   </div>   <div class="track-snippets">     <div class="snippet-left"><img src="img/image-computer.png" alt="computer" /></div>     <div class="snippet-right">       <div>         <h4 id="body3"></h4>         <p id="body4">         </p>       </div>       <div>         <h4 id="body5"></h4>         <p id="body6"></p>       </div>       <div>         <h4 id="body7"></h4>         <p id="body8">         </p>       </div>     </div>   </div> </section>

Hey, look at that! Now we can change the content on a website in real-time by editing the content in the spreadsheet.

The post Creating an Editable Webpage With Google Spreadsheets and Tabletop.js appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]