Home Reference Source

js/controllers/CategoryListViewController.js

import ViewController from '~/controllers/ViewController';
import Random from '~/modern/Random';
import Theme from '~/models/Theme';

const CLOSE_ICON = () => (
    <svg namespace="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1">
        <path namespace="http://www.w3.org/2000/svg" d="M 2.75 2.042969 L 2.042969 2.75 L 2.398438 3.101563 L 7.292969 8 L 2.042969 13.25 L 2.75 13.957031 L 8 8.707031 L 12.894531 13.605469 L 13.25 13.957031 L 13.957031 13.25 L 13.605469 12.894531 L 8.707031 8 L 13.957031 2.75 L 13.25 2.042969 L 8 7.292969 L 3.101563 2.398438 Z " fill="white"></path>
    </svg>
);

export default class CategoryListViewController extends ViewController {
    constructor(element, label = null) {
        super(element);

        this._managedTags = new Map();
        this._managingStack = [];

        this._id = `category-list-${Random.ofDefault()}`;
        if (label) {
            label.htmlFor = this._id;
        }

        this._managingContext = element;
        this._typingContext = <input id={this._id} class="text-input text-input--owned" autocapitalize="none" />
        this._inputContext = <div aria-hidden='true'></div>;

        this._managingContext.appendChild(this._typingContext);
        this._managingContext.appendChild(this._inputContext);

        this._typingContext.addEventListener('keydown', ::this._didInsertCharacter);
    }

    _didInsertCharacter(event) {
        let target = event.target;

        // Test if valid character
        if (!/[A-Za-z-]/.test(event.key)) {
            event.preventDefault();
        }

        // Detect if backspace at beginning
        if (event.key === "Backspace" && target.selectionStart === 0 && target.selectionEnd === 0) {
            this.popTag();
            return;
        }

        // Test if space or comma
        if (/^[ ,;]$/.test(event.key) || event.key === "Enter") {
            let content = target.value.substring(0, target.selectionStart);

            // Get previous selection and shrink current
            target.value = target.value.substring(target.selectionStart);

            this._addSelection(content);
            event.preventDefault()
            return;
        }
    }

    _addSelection(data) {
        let matches = data.match(/[A-Za-z-]+/g);
        if (matches === null) return;
        for (let i = 0; i < matches.length; i++) {
            this.addTag(matches[i].toLowerCase());
        }
    }

    /**
     * Creates tag with a name
     * @param {string} name Name of the tag.
     */
    addTag(name) {
        if (this._managedTags.has(name)) return;

        let close = CLOSE_ICON();
        let label = <span class="input-list-item">{name}{close}</span>
        let input = <input type="hidden" name="post-categories" value={name} />;

        close.addEventListener('click', () => this.removeTag(name));
        this._managingContext.insertBefore(label, this._typingContext);
        this._inputContext.appendChild(input);
        this._managedTags.set(name, { label, input, position: this._managingStack.length });
        this._managingStack.push(name);
    }

    /**
     * Removes the last tag
     */
    popTag() {
        let previousTag = this._managingStack[this._managingStack.length - 1];
        if (previousTag) this.removeTag(previousTag);
    }

    /**
     * Removes a tag. Silent if fails
     * @param {string} name The name of the tag to remove.
     */
    removeTag(name) {
        let tag;
        if (tag = this._managedTags.get(name)) {
            let { label, input, position } = tag;

            // Remove from DOM
            this._managingContext.removeChild(label);

            // Remove from input list
            this._inputContext.removeChild(input);

            // Remove from managedTags
            this._managedTags.delete(name);

            // Remove from stack
            this._managingStack.splice(position, 1);
        }
    }
}