KA Engineering

KA Engineering

We're the engineers behind Khan Academy. We're building a free, world-class education for anyone, anywhere.

Subscribe

Latest posts

Creating Query Components with Apollo

Brian Genisio on June 12

Migrating to a Mobile Monorepo for React Native

Jared Forsyth on May 29

Memcached-Backed Content Infrastructure

Ben Kraft on May 15

Profiling App Engine Memcached

Ben Kraft on May 1

App Engine Flex Language Shootout

Amos Latteier on April 17

What's New in OSS at Khan Academy

Brian Genisio on April 3

Automating App Store Screenshots

Bryan Clark on March 27

It's Okay to Break Things: Reflections on Khan Academy's Healthy Hackathon

Kimerie Green on March 6

Interning at Khan Academy: from student to intern

Shadaj Laddad on Dec 12, 2016

Prototyping with Framer

Nick Breen on Oct 3, 2016

Evolving our content infrastructure

William Chargin on Sep 19, 2016

Building a Really, Really Small Android App

Charlie Marsh on Aug 22, 2016

A Case for Time Tracking: Data Driven Time-Management

Oliver Northwood on Aug 8, 2016

Time Management at Khan Academy

Several Authors on Jul 25, 2016

Hackathons Can Be Healthy

Tom Yedwab on Jul 11, 2016

Ensuring transaction-safety in Google App Engine

Craig Silverstein on Jun 27, 2016

The User Write Lock: an Alternative to Transactions for Google App Engine

Craig Silverstein on Jun 20, 2016

Khan Academy's Engineering Principles

Ben Kamens on Jun 6, 2016

Minimizing the length of regular expressions, in practice

Craig Silverstein on May 23, 2016

Introducing SwiftTweaks

Bryan Clark on May 9, 2016

The Autonomous Dumbledore

Evy Kassirer on Apr 25, 2016

Engineering career development at Khan Academy

Ben Eater on Apr 11, 2016

Inline CSS at Khan Academy: Aphrodite

Jamie Wong on Mar 29, 2016

Starting Android at Khan Academy

Ben Komalo on Feb 29, 2016

Automating Highly Similar Translations

Kevin Barabash on Feb 15, 2016

The weekly snippet-server: open-sourced

Craig Silverstein on Feb 1, 2016

Stories from our latest intern class

2015 Interns on Dec 21, 2015

Kanbanning the LearnStorm Dev Process

Kevin Dangoor on Dec 7, 2015

Forgo JS packaging? Not so fast

Craig Silverstein on Nov 23, 2015

Switching to Slack

Benjamin Pollack on Nov 9, 2015

Receiving feedback as an intern at Khan Academy

David Wang on Oct 26, 2015

Schrödinger's deploys no more: how we update translations

Chelsea Voss on Oct 12, 2015

i18nize-templates: Internationalization After the Fact

Craig Silverstein on Sep 28, 2015

Making thumbnails fast

William Chargin on Sep 14, 2015

Copy-pasting more than just text

Sam Lau on Aug 31, 2015

No cheating allowed!!

Phillip Lemons on Aug 17, 2015

Fun with slope fields, css and react

Marcos Ojeda on Aug 5, 2015

Khan Academy: a new employee's primer

Riley Shaw on Jul 20, 2015

How wooden puzzles can destroy dev teams

John Sullivan on Jul 6, 2015

Babel in Khan Academy's i18n Toolchain

Kevin Barabash on Jun 22, 2015

tota11y - an accessibility visualization toolkit

Jordan Scales on Jun 8, 2015

Meta

Copy-pasting more than just text

by Sam Lau on Aug 31, 2015

The Result

I'll skip to the happy ending. This little feature implements more sophisticated copy-pasting for the interactive widgets we use for our exercises and articles, making life for our content creators easier. See it in action:

copypastehooray

Notice that the copied content is just text, but when it's pasted the metadata associated with the text is copied over properly. For example, the image URL is copied over properly and the number line's starting value is still 3 after being copied and pasted back and forth between the two text boxes.

First, About Perseus

Our exercises are created using Perseus, a question editor and renderer that makes it simple to create interactive problems. Before we had Perseus, we had a framework called Khan Exercises. However, to write exercise content, content writers essentially had to know how to program. As you can imagine, this made scaling up work on content difficult.

Perseus, which is what you see in that animation above, allows for easier interactive content creation through widgets. For example, the animation above features an image and a number line widget. This live exercise features a transformer widget.

The Problem

Before, copy-pasting worked like this:

copypastesad Those are sad faces, if you couldn't tell.

Perseus is becoming more and more useful to us. In fact, during my internship my mentor and I worked on functionality to use Perseus to write our articles. It quickly became clear that we'd have to put our rush to implement features on hold in order to iron out some annoying issues with the editor. One of these issues was the fact that if content creators wanted to move or duplicate a widget, they'd have to make a new widget and manually input all the settings they wanted.

In the case of the image above, they would have to copy over the image URL as well as other metadata, like the caption and the alt text. We thought that it'd be great if copy-pasting worked transparently for our content creators.

The Approach

There are essentially two components that I want to remember when a content creator cuts or copies a piece of text. First, is the copied text itself. This usually looks something like:

Hello, this is some text and here's an image widget.

[[☃ image 1]]

The [[☃ image 1]] is a placeholder that tells Perseus that there is an image widget there. Then, there is the metadata associated with the widget itself, such as the image URL. This metadata is stored in a Javascript object as a React prop in Perseus, which means that if we can move that metadata around properly along with the basic text we'll have what we want. For example, the metadata for an image widget can look something like:

"image 1": {
    "type": "image",
    "alignment": "block",
    "graded": true,
    "options": {
        <some more metadata>,
        "backgroundImage": {
            "url": "doge.gif",
            "width": 537,
            "height": 529
        },
        "labels": [],
        "alt": "",
        "caption": "I am a doge."
    },
}

How can we allow regular copy-pasting of plain text to work correctly as well as handle the case where there are widgets to move around?

localStorage to the Rescue

Our solution was to listen for cut, copy, and paste events. On a cut / copy, we look through the text for widgets. We grab the associated metadata of each widget we find and save 'em in localStorage. On a paste, we see if localStorage has some metadata that we've previously cut / copied. If so, then pull it in.

You can find the basic implementation in this commit. It ended up being just a few lines of Javascript and I was very pleased with how it worked. One nice bonus from using localStorage was that widgets could be copied over from different web pages entirely. For example, if a content creator wants to move widgets from one question to another, he/she can copy the widgets in one question's editor, browse to the page with the other question's editor, and paste the widgets in.

But Wait...

That commit above gave content creators some basic functionality that saved many frustrating minutes re-entering in widget settings. However, there were a number of issues and edge cases that remained. Can you think of some after looking at that commit?

Here are the ones that were most immediately obvious after we deployed this feature:

  1. Name conflicts. Ex. pasting an [[☃ image 1]] in a text box that already contained an [[☃ image 1]]. In the commit I linked above, I simply ignore the pasted widget in the case of a name conflict.
  2. localStorage data isn't cleared after a paste. The original reason for this was that we could conceivably want to paste the same widget in multiple places. However, this means that we could potentially have weird behavior if we paste a widget, then copy text from another website, then paste that text in. Since we still have the metadata in localStorage, we'll try to move that data into the exercise / article.
  3. Suppose we 1. copy some text that contains widgets in Perseus, 2. decide to go to another web page and copy some text from there, and 3. paste that text instead of the original text with widgets. Since step 1 moves metadata into localStorage and when we paste we simply look for the presence of that data, we'll erroneously pull that metadata in even though the text we're actually pasting wasn't originally from Perseus.

Making Copy-Paste Do More Things Properly

I resolved the above issues as follows:

  1. Instead of totally ignoring the pasted widget in the event of a name conflict, I rename the widgets safely. For example, if the section already contains a widget called [[☃ image 2]] and I want to paste in widgets [[☃ image 1]] and [[☃ image 2]], we'll rename the first widget to [[☃ image 3]] and the second to[[☃ image 4]]. That is, we'll look at the highest-numbered widget already in the section of the same type and make sure all of the widgets we're about to paste in are numbered higher than that one.
  2. I clear localStorage after a paste. This saves headache since content creators don't really need to paste the same widget everywhere anyway.
  3. In addition to saving the metadata in localStorage, I also save the copied plaintext itself. On a paste, I check the text that's about to be pasted and only move widget metadata in if the plaintext matches the one previously copied from Perseus. This is a bit strict but ensures that only text from Perseus will trigger widget metadata pasting.

These changes are implemented in this follow-up commit.

There are almost certainly definitely more weird edge cases but these covered the majority of use cases for content creators. Shipping beats perfection, after all. Our content creators have been loving this feature, and it's always a fun one to show others.

If you'd like to try it out for yourself go ahead and check out the Perseus demo!

PS: I had an amazing time during my Khan Academy internship. If you're interested in working with brilliant people who care about each other and the future of education please check 'em out! I'd love to personally answer any questions you have and you can find me on Github.