views/tags: add tag input control
For now, without tag relations
This commit is contained in:
		
							parent
							
								
									5736b4adc1
								
							
						
					
					
						commit
						407848706a
					
				@ -27,3 +27,7 @@ $button-enabled-text-color = white
 | 
			
		||||
$button-enabled-background-color = $main-color
 | 
			
		||||
$button-disabled-text-color = #666
 | 
			
		||||
$button-disabled-background-color = #CCC
 | 
			
		||||
$new-tag-background-color = #DFC
 | 
			
		||||
$new-tag-text-color = black
 | 
			
		||||
$duplicate-tag-background-color = #FDC
 | 
			
		||||
$duplicate-tag-text-color = black
 | 
			
		||||
 | 
			
		||||
@ -31,14 +31,18 @@ form
 | 
			
		||||
form.tabular
 | 
			
		||||
    ul
 | 
			
		||||
        display: table
 | 
			
		||||
        table-layout: fixed
 | 
			
		||||
        width: 100%
 | 
			
		||||
        li
 | 
			
		||||
            display: table-row
 | 
			
		||||
            label:not(.radio):not(.checkbox):not(.file-dropper)
 | 
			
		||||
                display: table-cell
 | 
			
		||||
                width: 33%
 | 
			
		||||
                text-align: right
 | 
			
		||||
                padding-right: 1em
 | 
			
		||||
    .messages, .buttons
 | 
			
		||||
        margin-left: 33%
 | 
			
		||||
        padding-left: 1em
 | 
			
		||||
 | 
			
		||||
form.horizontal
 | 
			
		||||
    display: inline-block
 | 
			
		||||
@ -146,6 +150,7 @@ input[type=checkbox]:focus + .checkbox:before
 | 
			
		||||
/*
 | 
			
		||||
 * Regular inputs
 | 
			
		||||
 */
 | 
			
		||||
div.tag-input,
 | 
			
		||||
select,
 | 
			
		||||
textarea,
 | 
			
		||||
input[type=text],
 | 
			
		||||
@ -177,6 +182,30 @@ input[type=password]
 | 
			
		||||
        background: $input-disabled-background-color
 | 
			
		||||
        color: $input-disabled-text-color
 | 
			
		||||
 | 
			
		||||
div.tag-input
 | 
			
		||||
    min-height: 3.3em
 | 
			
		||||
    vertical-align: top
 | 
			
		||||
    display: inline-block
 | 
			
		||||
    cursor: text
 | 
			
		||||
    word-wrap: break-word
 | 
			
		||||
    a
 | 
			
		||||
        line-height: 110%
 | 
			
		||||
        position: relative
 | 
			
		||||
        z-index: 1
 | 
			
		||||
    span
 | 
			
		||||
        line-height: 110%
 | 
			
		||||
        &.new a
 | 
			
		||||
            background: $new-tag-background-color
 | 
			
		||||
            color: $new-tag-text-color
 | 
			
		||||
        &.duplicate a
 | 
			
		||||
            background: $duplicate-tag-background-color
 | 
			
		||||
            color: $duplicate-tag-text-color
 | 
			
		||||
        &.editable
 | 
			
		||||
            outline: 0
 | 
			
		||||
            padding-right: 0.2em
 | 
			
		||||
            &:last-child
 | 
			
		||||
                padding-left: 0.2em
 | 
			
		||||
 | 
			
		||||
label.color
 | 
			
		||||
    position: relative
 | 
			
		||||
    input[type=text]
 | 
			
		||||
 | 
			
		||||
@ -98,8 +98,8 @@ class TagsController {
 | 
			
		||||
    _show(tag, section) {
 | 
			
		||||
        topNavController.activate('tags');
 | 
			
		||||
        const categories = {};
 | 
			
		||||
        for (let [key, category] of tags.getExport().categories) {
 | 
			
		||||
            categories[key] = category.name;
 | 
			
		||||
        for (let category of tags.getAllCategories()) {
 | 
			
		||||
            categories[category.name] = category.name;
 | 
			
		||||
        }
 | 
			
		||||
        this.tagView.render({
 | 
			
		||||
            tag: tag,
 | 
			
		||||
 | 
			
		||||
@ -4,14 +4,47 @@ const request = require('superagent');
 | 
			
		||||
const util = require('./util/misc.js');
 | 
			
		||||
const events = require('./events.js');
 | 
			
		||||
 | 
			
		||||
let _export = null;
 | 
			
		||||
let _tags = null;
 | 
			
		||||
let _categories = null;
 | 
			
		||||
let _stylesheet = null;
 | 
			
		||||
 | 
			
		||||
function getTagByName(name) {
 | 
			
		||||
    return _tags.get(name.toLowerCase());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getCategoryByName(name) {
 | 
			
		||||
    return _categories.get(name.toLowerCase());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getNameToTagMap() {
 | 
			
		||||
    return _tags;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getAllTags() {
 | 
			
		||||
    return _tags.values();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getAllCategories() {
 | 
			
		||||
    return _categories.values();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getOriginalTagName(name) {
 | 
			
		||||
    const actualTag = getTagByName(name);
 | 
			
		||||
    if (actualTag) {
 | 
			
		||||
        for (let originalName of actualTag.names) {
 | 
			
		||||
            if (originalName.toLowerCase() == name.toLowerCase()) {
 | 
			
		||||
                return originalName;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return name;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _tagsToMap(tags) {
 | 
			
		||||
    let map = new Map();
 | 
			
		||||
    for (let tag of tags) {
 | 
			
		||||
        for (let name of tag.names) {
 | 
			
		||||
            map.set(name, tag);
 | 
			
		||||
            map.set(name.toLowerCase(), tag);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return map;
 | 
			
		||||
@ -20,7 +53,7 @@ function _tagsToMap(tags) {
 | 
			
		||||
function _tagCategoriesToMap(categories) {
 | 
			
		||||
    let map = new Map();
 | 
			
		||||
    for (let category of categories) {
 | 
			
		||||
        map.set(category.name, category);
 | 
			
		||||
        map.set(category.name.toLowerCase(), category);
 | 
			
		||||
    }
 | 
			
		||||
    return map;
 | 
			
		||||
}
 | 
			
		||||
@ -31,7 +64,7 @@ function _refreshStylesheet() {
 | 
			
		||||
    }
 | 
			
		||||
    _stylesheet = document.createElement('style');
 | 
			
		||||
    document.head.appendChild(_stylesheet);
 | 
			
		||||
    for (let category of _export.categories.values()) {
 | 
			
		||||
    for (let category of getAllCategories()) {
 | 
			
		||||
        _stylesheet.sheet.insertRule(
 | 
			
		||||
            '.tag-{0} { color: {1} }'.format(category.name, category.color),
 | 
			
		||||
            _stylesheet.sheet.cssRules.length);
 | 
			
		||||
@ -42,27 +75,28 @@ function refreshExport() {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
        request.get('/data/tags.json').end((error, response) => {
 | 
			
		||||
            if (error) {
 | 
			
		||||
                _export = {tags: {}, categories: {}};
 | 
			
		||||
                _tags = new Map();
 | 
			
		||||
                _categories = new Map();
 | 
			
		||||
                reject(error);
 | 
			
		||||
            }
 | 
			
		||||
            _export = response.body;
 | 
			
		||||
            _export.tags = _tagsToMap(_export.tags);
 | 
			
		||||
            _export.categories = _tagCategoriesToMap(_export.categories);
 | 
			
		||||
            _tags = _tagsToMap(response.body.tags);
 | 
			
		||||
            _categories = _tagCategoriesToMap(response.body.categories);
 | 
			
		||||
            _refreshStylesheet();
 | 
			
		||||
            resolve();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getExport() {
 | 
			
		||||
    return _export || {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
events.listen(
 | 
			
		||||
    events.TagsChange,
 | 
			
		||||
    () => { refreshExport(); return true; });
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    getExport: getExport,
 | 
			
		||||
    getAllCategories: getAllCategories,
 | 
			
		||||
    getAllTags: getAllTags,
 | 
			
		||||
    getTagByName: getTagByName,
 | 
			
		||||
    getCategoryByName: getCategoryByName,
 | 
			
		||||
    getNameToTagMap: getNameToTagMap,
 | 
			
		||||
    getOriginalTagName: getOriginalTagName,
 | 
			
		||||
    refreshExport: refreshExport,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -138,10 +138,9 @@ function makeColorInput(options) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function makeTagLink(name) {
 | 
			
		||||
    const tagExport = tags.getExport();
 | 
			
		||||
    let category = null;
 | 
			
		||||
    try {
 | 
			
		||||
        category = tagExport.tags.get(name).category;
 | 
			
		||||
        category = tags.getTagByName(name).category;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        category = 'unknown';
 | 
			
		||||
    }
 | 
			
		||||
@ -321,7 +320,8 @@ function scrollToHash() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
document.addEventListener('input', e => {
 | 
			
		||||
    if (e.target.getAttribute('type').toLowerCase() === 'color') {
 | 
			
		||||
    const type = e.target.getAttribute('type');
 | 
			
		||||
    if (type && type.toLowerCase() === 'color') {
 | 
			
		||||
        const textInput = e.target.parentNode.querySelector('input[type=text]');
 | 
			
		||||
        textInput.style.color = e.target.value;
 | 
			
		||||
        textInput.value = e.target.value;
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,7 @@ class AutoCompleteControl {
 | 
			
		||||
    constructor(input, options) {
 | 
			
		||||
        this.input = input;
 | 
			
		||||
        this.options = lodash.extend({}, {
 | 
			
		||||
            verticalShift: 2,
 | 
			
		||||
            source: null,
 | 
			
		||||
            maxResults: 15,
 | 
			
		||||
            getTextToFind: () => {
 | 
			
		||||
@ -229,7 +230,7 @@ class AutoCompleteControl {
 | 
			
		||||
        this.suggestionDiv.style.left = '-9999px';
 | 
			
		||||
        this.suggestionDiv.style.top = '-9999px';
 | 
			
		||||
        this.show();
 | 
			
		||||
        const borderSize = 2;
 | 
			
		||||
        const verticalShift = this.options.verticalShift;
 | 
			
		||||
        const inputRect = this.input.getBoundingClientRect();
 | 
			
		||||
        const bodyRect = document.body.getBoundingClientRect();
 | 
			
		||||
        const viewPortHeight = bodyRect.bottom - bodyRect.top;
 | 
			
		||||
@ -242,8 +243,8 @@ class AutoCompleteControl {
 | 
			
		||||
 | 
			
		||||
        let x = inputRect.left - bodyRect.left;
 | 
			
		||||
        let y = direction == 1 ?
 | 
			
		||||
            inputRect.bottom - bodyRect.top - borderSize :
 | 
			
		||||
            inputRect.top - bodyRect.top - listRect.height + borderSize;
 | 
			
		||||
            inputRect.bottom - bodyRect.top - verticalShift :
 | 
			
		||||
            inputRect.top - bodyRect.top - listRect.height + verticalShift;
 | 
			
		||||
 | 
			
		||||
        // remove offscreen items until whole suggestion list can fit on the
 | 
			
		||||
        // screen
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ const AutoCompleteControl = require('./auto_complete_control.js');
 | 
			
		||||
 | 
			
		||||
class TagAutoCompleteControl extends AutoCompleteControl {
 | 
			
		||||
    constructor(input, options) {
 | 
			
		||||
        const allTags = tags.getExport().tags;
 | 
			
		||||
        const allTags = tags.getNameToTagMap();
 | 
			
		||||
        const caseSensitive = false;
 | 
			
		||||
        const minLengthForPartialSearch = 3;
 | 
			
		||||
 | 
			
		||||
@ -30,7 +30,7 @@ class TagAutoCompleteControl extends AutoCompleteControl {
 | 
			
		||||
                        caption:
 | 
			
		||||
                            '<span class="tag-{0}">{1} ({2})</span>'.format(
 | 
			
		||||
                                kv[1].category,
 | 
			
		||||
                                kv[0],
 | 
			
		||||
                                tags.getOriginalTagName(kv[0]),
 | 
			
		||||
                                kv[1].usages),
 | 
			
		||||
                        value: kv[0],
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										387
									
								
								client/js/views/tag_input_control.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										387
									
								
								client/js/views/tag_input_control.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,387 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const tags = require('../tags.js');
 | 
			
		||||
const views = require('../util/views.js');
 | 
			
		||||
const TagAutoCompleteControl = require('./tag_auto_complete_control.js');
 | 
			
		||||
 | 
			
		||||
const KEY_A = 65;
 | 
			
		||||
const KEY_END = 35;
 | 
			
		||||
const KEY_HOME = 36;
 | 
			
		||||
const KEY_LEFT = 37;
 | 
			
		||||
const KEY_RIGHT = 39;
 | 
			
		||||
const KEY_SPACE = 32;
 | 
			
		||||
const KEY_RETURN = 13;
 | 
			
		||||
const KEY_BACKSPACE = 8;
 | 
			
		||||
const KEY_DELETE = 46;
 | 
			
		||||
 | 
			
		||||
class TagInputControl {
 | 
			
		||||
    constructor(sourceInputNode) {
 | 
			
		||||
        this.tags = [];
 | 
			
		||||
        this.readOnly = sourceInputNode.readOnly;
 | 
			
		||||
 | 
			
		||||
        this._autoCompleteControls = [];
 | 
			
		||||
 | 
			
		||||
        this._sourceInputNode = sourceInputNode;
 | 
			
		||||
 | 
			
		||||
        // set up main edit area
 | 
			
		||||
        this._editAreaNode = views.htmlToDom('<div class="tag-input"></div>');
 | 
			
		||||
        this._editAreaNode.autocorrect = false;
 | 
			
		||||
        this._editAreaNode.autocapitalize = false;
 | 
			
		||||
        this._editAreaNode.spellcheck = false;
 | 
			
		||||
        this._editAreaNode.addEventListener(
 | 
			
		||||
            'click', e => this._evtEditAreaClick(e));
 | 
			
		||||
 | 
			
		||||
        // set up tail editor
 | 
			
		||||
        this._tailWrapperNode = this._createWrapper();
 | 
			
		||||
        if (!this.readOnly) {
 | 
			
		||||
            this._tailInputNode = this._createInput();
 | 
			
		||||
            this._tailInputNode.tabIndex = 0;
 | 
			
		||||
            this._tailWrapperNode.appendChild(this._tailInputNode);
 | 
			
		||||
        } else {
 | 
			
		||||
            this._tailInputNode = null;
 | 
			
		||||
        }
 | 
			
		||||
        this._editAreaNode.appendChild(this._tailWrapperNode);
 | 
			
		||||
 | 
			
		||||
        // add existing tags
 | 
			
		||||
        this.addMultipleTags(sourceInputNode.value);
 | 
			
		||||
 | 
			
		||||
        // show
 | 
			
		||||
        sourceInputNode.style.display = 'none';
 | 
			
		||||
        sourceInputNode.parentNode.insertBefore(
 | 
			
		||||
            this._editAreaNode, sourceInputNode.nextSibling);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addMultipleTags(text, sourceNode) {
 | 
			
		||||
        for (let tag of text.split(/\s+/).filter(word => word)) {
 | 
			
		||||
            this.addTag(tag, sourceNode);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addTag(text, sourceNode) {
 | 
			
		||||
        text = tags.getOriginalTagName(text);
 | 
			
		||||
 | 
			
		||||
        if (!sourceNode) {
 | 
			
		||||
            sourceNode = this._tailWrapperNode;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!text) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // TODO: add implications
 | 
			
		||||
 | 
			
		||||
        if (this.tags.map(tag => tag.toLowerCase())
 | 
			
		||||
                .includes(text.toLowerCase())) {
 | 
			
		||||
            this._getWrapperFromTag(text).classList.add('duplicate');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this._hideVisualCues();
 | 
			
		||||
 | 
			
		||||
        this.tags.push(text);
 | 
			
		||||
        this._sourceInputNode.value = this.tags.join(' ');
 | 
			
		||||
 | 
			
		||||
        const sourceWrapperNode = this._getWrapperFromChild(sourceNode);
 | 
			
		||||
        const targetWrapperNode = this._createWrapper();
 | 
			
		||||
        if (!this.readOnly) {
 | 
			
		||||
            targetWrapperNode.appendChild(this._createInput());
 | 
			
		||||
        }
 | 
			
		||||
        if (!tags.getTagByName(text)) {
 | 
			
		||||
            targetWrapperNode.classList.add('new');
 | 
			
		||||
        }
 | 
			
		||||
        targetWrapperNode.appendChild(this._createLink(text));
 | 
			
		||||
        targetWrapperNode.setAttribute('data-tag', text);
 | 
			
		||||
        this._editAreaNode.insertBefore(targetWrapperNode, sourceWrapperNode);
 | 
			
		||||
        this._editAreaNode.insertBefore(this._createSpace(), sourceWrapperNode);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    deleteTag(tag) {
 | 
			
		||||
        if (!tag) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!this.tags.map(tag => tag.toLowerCase())
 | 
			
		||||
                .includes(tag.toLowerCase())) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        this._hideVisualCues();
 | 
			
		||||
        this.tags = this.tags.filter(t => t.toLowerCase() != tag.toLowerCase());
 | 
			
		||||
        this._sourceInputNode.value = this.tags.join(' ');
 | 
			
		||||
        for (let wrapperNode of this._getAllWrapperNodes()) {
 | 
			
		||||
            if (this._getTagFromWrapper(wrapperNode).toLowerCase() ==
 | 
			
		||||
                    tag.toLowerCase()) {
 | 
			
		||||
                if (wrapperNode.contains(document.activeElement)) {
 | 
			
		||||
                    const nextWrapperNode = this._getNextWrapper(wrapperNode);
 | 
			
		||||
                    const nextInputNode =
 | 
			
		||||
                        nextWrapperNode.querySelector('.editable');
 | 
			
		||||
                    if (nextInputNode) {
 | 
			
		||||
                        nextInputNode.focus();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                this._editAreaNode.removeChild(wrapperNode);
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _evtEditAreaClick(e) {
 | 
			
		||||
        if (e.target.nodeName.toLowerCase() === 'a') {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.readOnly) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
 | 
			
		||||
        let closestInputNode = null;
 | 
			
		||||
        let closestDistance = Infinity;
 | 
			
		||||
 | 
			
		||||
        const mouseX = e.clientX;
 | 
			
		||||
        const mouseY = e.clientY;
 | 
			
		||||
 | 
			
		||||
        for (let wrapperNode of this._getAllWrapperNodes()) {
 | 
			
		||||
            const inputNode = wrapperNode.querySelector('.editable');
 | 
			
		||||
            if (!inputNode) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            const inputNodeRect = inputNode.getBoundingClientRect();
 | 
			
		||||
            const inputNodeX = inputNodeRect.left;
 | 
			
		||||
            const inputNodeY = inputNodeRect.top;
 | 
			
		||||
            const distance = Math.sqrt(
 | 
			
		||||
                Math.pow(mouseX - inputNodeX, 2) +
 | 
			
		||||
                Math.pow(mouseY - inputNodeY, 2));
 | 
			
		||||
            if (distance < closestDistance) {
 | 
			
		||||
                closestDistance = distance;
 | 
			
		||||
                closestInputNode = inputNode;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (closestDistance > 25) {
 | 
			
		||||
            closestInputNode = this._tailInputNode;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        closestInputNode.focus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _evtInputPaste(e) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        const pastedText = window.clipboardData ?
 | 
			
		||||
            window.clipboardData.getData('Text') :
 | 
			
		||||
            (e.originalEvent || e).clipboardData.getData('text/plain');
 | 
			
		||||
 | 
			
		||||
        if (pastedText.length > 2000) {
 | 
			
		||||
            window.alert('Pasted text is too long.');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        this.addMultipleTags(pastedText);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _evtInputKeyDown(e) {
 | 
			
		||||
        const inputNode = e.target;
 | 
			
		||||
        const wrapperNode = this._getWrapperFromChild(inputNode);
 | 
			
		||||
        const key = e.which;
 | 
			
		||||
 | 
			
		||||
        if (key == KEY_A && e.ctrlKey) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            e.stopImmediatePropagation();
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            const selection = window.getSelection();
 | 
			
		||||
            const range = document.createRange();
 | 
			
		||||
            range.selectNode(this._editAreaNode);
 | 
			
		||||
            selection.removeAllRanges();
 | 
			
		||||
            selection.addRange(range);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (key == KEY_HOME) {
 | 
			
		||||
            if (window.getSelection().getRangeAt(0).startOffset !== 0) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            this._getAllWrapperNodes()[0].querySelector('.editable').focus();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (key == KEY_END) {
 | 
			
		||||
            if (window.getSelection().getRangeAt(0).endOffset !==
 | 
			
		||||
                    inputNode.textContent.length) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            this._getAllWrapperNodes()[this._getAllWrapperNodes().length - 1]
 | 
			
		||||
                .querySelector('.editable').focus();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (key == KEY_LEFT) {
 | 
			
		||||
            if (window.getSelection().getRangeAt(0).startOffset !== 0) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            const prevWrapperNode = this._getPreviousWrapper(wrapperNode);
 | 
			
		||||
            if (prevWrapperNode) {
 | 
			
		||||
                prevWrapperNode.querySelector('.editable').focus();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (key == KEY_RIGHT) {
 | 
			
		||||
            if (window.getSelection().getRangeAt(0).endOffset !==
 | 
			
		||||
                    inputNode.textContent.length) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            const nextWrapperNode = this._getNextWrapper(wrapperNode);
 | 
			
		||||
            if (nextWrapperNode) {
 | 
			
		||||
                nextWrapperNode.querySelector('.editable').focus();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (key == KEY_BACKSPACE) {
 | 
			
		||||
            if (inputNode.textContent !== '') {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            const prevWrapperNode = this._getPreviousWrapper(wrapperNode);
 | 
			
		||||
            this.deleteTag(this._getTagFromWrapper(prevWrapperNode));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (key == KEY_DELETE) {
 | 
			
		||||
            if (inputNode.textContent !== '') {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            if (!wrapperNode.contains(this._mainEditNode)) {
 | 
			
		||||
                this.deleteTag(this._getTagFromWrapper(wrapperNode));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (key == KEY_RETURN || key == KEY_SPACE) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            this.addTag(inputNode.textContent, inputNode);
 | 
			
		||||
            inputNode.innerHTML = '';
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _evtInputBlur(e) {
 | 
			
		||||
        const inputNode = e.target;
 | 
			
		||||
        this.addTag(inputNode.textContent, inputNode);
 | 
			
		||||
        inputNode.innerHTML = '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _evtLinkClick(e) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        // TODO: show suggestions and siblings
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _getWrapperFromChild(startNode) {
 | 
			
		||||
        let node = startNode;
 | 
			
		||||
        while (node) {
 | 
			
		||||
            if ('classList' in node && node.classList.contains('wrapper')) {
 | 
			
		||||
                return node;
 | 
			
		||||
            }
 | 
			
		||||
            node = node.parentNode;
 | 
			
		||||
        }
 | 
			
		||||
        throw Error('Wrapper node not found');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _getPreviousWrapper(wrapperNode) {
 | 
			
		||||
        let result = wrapperNode.previousSibling;
 | 
			
		||||
        while (result && result.nodeType === 3) {
 | 
			
		||||
            result = result.previousSibling;
 | 
			
		||||
        }
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _getNextWrapper(wrapperNode) {
 | 
			
		||||
        let result = wrapperNode.nextSibling;
 | 
			
		||||
        while (result && result.nodeType === 3) {
 | 
			
		||||
            result = result.nextSibling;
 | 
			
		||||
        }
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _getAllWrapperNodes() {
 | 
			
		||||
        const result = [];
 | 
			
		||||
        for (let child of this._editAreaNode.childNodes) {
 | 
			
		||||
            if (child.nodeType === 3) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            result.push(child);
 | 
			
		||||
        }
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _getTagFromWrapper(wrapperNode) {
 | 
			
		||||
        if (!wrapperNode || !wrapperNode.hasAttribute('data-tag')) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        return wrapperNode.getAttribute('data-tag');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _getWrapperFromTag(tag) {
 | 
			
		||||
        for (let wrapperNode of this._getAllWrapperNodes()) {
 | 
			
		||||
            if (this._getTagFromWrapper(wrapperNode).toLowerCase() ==
 | 
			
		||||
                    tag.toLowerCase()) {
 | 
			
		||||
                return wrapperNode;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _createWrapper() {
 | 
			
		||||
        return views.htmlToDom('<span class="wrapper"></span>');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _createSpace(text) {
 | 
			
		||||
        // space between elements serves two purposes:
 | 
			
		||||
        // - the wrappers play nicely with word-wrap: break-word
 | 
			
		||||
        // - copying the input text to clipboard shows spaces
 | 
			
		||||
        return document.createTextNode(' ');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _createInput(text) {
 | 
			
		||||
        const inputNode = views.htmlToDom(
 | 
			
		||||
            '<span class="editable" contenteditable>');
 | 
			
		||||
        const autoCompleteControl = new TagAutoCompleteControl(
 | 
			
		||||
            inputNode, {
 | 
			
		||||
                getTextToFind: () => {
 | 
			
		||||
                    return inputNode.textContent;
 | 
			
		||||
                },
 | 
			
		||||
                confirm: text => {
 | 
			
		||||
                    const wrapperNode = this._getWrapperFromChild(inputNode);
 | 
			
		||||
                    inputNode.innerHTML = '';
 | 
			
		||||
                    this.addTag(text, inputNode);
 | 
			
		||||
                },
 | 
			
		||||
                verticalShift: -2,
 | 
			
		||||
            });
 | 
			
		||||
        inputNode.addEventListener('keydown', e => this._evtInputKeyDown(e));
 | 
			
		||||
        inputNode.addEventListener('paste', e => this._evtInputPaste(e));
 | 
			
		||||
        inputNode.addEventListener('blur', e => this._evtInputBlur(e));
 | 
			
		||||
        this._autoCompleteControls.push(autoCompleteControl);
 | 
			
		||||
        return inputNode;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _createLink(text) {
 | 
			
		||||
        const actualTag = tags.getTagByName(text);
 | 
			
		||||
        const link = views.htmlToDom(
 | 
			
		||||
            views.makeNonVoidElement(
 | 
			
		||||
                'a',
 | 
			
		||||
                {
 | 
			
		||||
                    class: actualTag ? 'tag-' + actualTag.category : '',
 | 
			
		||||
                    href: '/tag/' + text,
 | 
			
		||||
                },
 | 
			
		||||
                text));
 | 
			
		||||
        link.addEventListener('click', e=> this._evtLinkClick(e));
 | 
			
		||||
        return link;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _hideVisualCues() {
 | 
			
		||||
        for (let wrapperNode of this._getAllWrapperNodes()) {
 | 
			
		||||
            wrapperNode.classList.remove('duplicate');
 | 
			
		||||
        }
 | 
			
		||||
        for (let autoCompleteControl of this._autoCompleteControls) {
 | 
			
		||||
            autoCompleteControl.hide();
 | 
			
		||||
        }
 | 
			
		||||
        // TODO: hide suggestions and siblings
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = TagInputControl;
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
const config = require('../config.js');
 | 
			
		||||
const views = require('../util/views.js');
 | 
			
		||||
const TagAutoCompleteControl = require('./tag_auto_complete_control.js');
 | 
			
		||||
const TagInputControl = require('./tag_input_control.js');
 | 
			
		||||
 | 
			
		||||
function split(str) {
 | 
			
		||||
    return str.split(/\s+/).filter(s => s);
 | 
			
		||||
@ -23,15 +23,14 @@ class TagSummaryView {
 | 
			
		||||
        const form = source.querySelector('form');
 | 
			
		||||
        const namesField = source.querySelector('.names input');
 | 
			
		||||
        const categoryField = source.querySelector('.category select');
 | 
			
		||||
        const implicationsField =
 | 
			
		||||
            source.querySelector('.implications input');
 | 
			
		||||
        const implicationsField = source.querySelector('.implications input');
 | 
			
		||||
        const suggestionsField = source.querySelector('.suggestions input');
 | 
			
		||||
 | 
			
		||||
        if (implicationsField) {
 | 
			
		||||
            new TagAutoCompleteControl(implicationsField);
 | 
			
		||||
            new TagInputControl(implicationsField);
 | 
			
		||||
        }
 | 
			
		||||
        if (suggestionsField) {
 | 
			
		||||
            new TagAutoCompleteControl(suggestionsField);
 | 
			
		||||
            new TagInputControl(suggestionsField);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        views.decorateValidator(form);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user