JSFiddle - React, Tailwind, and code Playground

by waxwing

HTML

<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-debug.js"></script>
<script src="http://www.fakeyourbeauty.com/jquery.autosize.input.debug.js"></script>
<script src="http://lorenzoongithub.github.io/completely/complete.ly.1.0.1.js"></script>
<i-expression-editor></i-expression-editor>

<script type="text/html" id="i-compound">
<span class="i-compound i-term i-box" data-bind="foreach: children, css: { 'selected': selected }"><span data-bind="template: { name: $data.template, data: $data }"></span></span>
</script>

<script type="text/html" id="i-atom">
<span class="i-atom i-term" data-bind="css: { 'selected': selected }"><span class="i-box i-text-box" data-bind="text: value, visible: !editing(), click: cellClicked"></span><span class="i-box" data-bind="visible: editing, cellEditor: { value: value, hasfocus: editing, autosizeInput: { 'space' : 0 } }"></span></span></script>

CSS

i-expression-editor {
    outline: none;
    font-size: 13px;
}

i-expression-editor .i-atom {
    font-family: monospace;
}

i-expression-editor .i-box {
    padding: 2px;
    border-radius: 3px;
    display: inline-block;
    border: 1px solid #dadada;
    /* -3px to cancel padding + border */
}

i-expression-editor .i-atom.selected .i-text-box {
    cursor: text;
}

i-expression-editor .i-atom .i-text-box {
    cursor: pointer;
}

i-expression-editor .i-atom .i-text-box:hover {
    background-color: #eee;
    
}

i-expression-editor input {
    font-size: 13px;
    font-family: monospace;
    background: transparent;
    display: inline;
    outline: none;
}

i-expression-editor .selected > .i-box,
i-expression-editor .selected.i-box {
    border: 1px solid black;
}

JavaScript

var shiftKey;
$(document).on('keyup keydown', function (e) { 
    shiftKey = e.shiftKey;
});

ko.components.register('i-expression-editor', {
    viewModel: {
        createViewModel: function (params, componentInfo) {

            function Compound(term) {
                this.children = ko.observableArray(term.map(createModel));
                this.template = 'i-compound';

                this.selected = ko.pureComputed(function () {
                    return model.multiSelect().indexOf(this) !== -1;
                }, this);
                
                this.empty = function () {
                    return this.children().length === 0;
                };
            }

            function Atom(value) {
                this.value = ko.observable(value);
                this.template = 'i-atom';

                this.selected = ko.pureComputed(function () {
                    return model.multiSelect().indexOf(this) !== -1;
                }, this);

                this.editing = ko.pureComputed({
                    read: function () {
                        return this.selected() && model.editMode();
                    },
                    owner: this
                });
                
                this.empty = function () {
                    return this.value() === '';
                };

                this.cellClicked = function () {
                    if (this.selected()) {
                        model.editMode(true);
                    } else {
                        model.editMode(false);
                        model.select(this);
                    }
                };

                this.onEnter = function () {
                    model.editMode(false);
                };
                
                this.onEscape = this.onEnter;

                this.onTab = function () {
                    if (shiftKey) {
                        model.moveSelectionLeft();
                    } else {
                       ...