mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	input-pill: Add "input_pill" class and documentation.
This commit is contained in:
		
				
					committed by
					
						
						Tim Abbott
					
				
			
			
				
	
			
			
			
						parent
						
							d27882325c
						
					
				
				
					commit
					7b00736fa2
				
			
							
								
								
									
										281
									
								
								static/js/input_pill.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								static/js/input_pill.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,281 @@
 | 
			
		||||
var input_pill = function ($parent) {
 | 
			
		||||
    // a dictionary of the key codes that are associated with each key
 | 
			
		||||
    // to make if/else more human readable.
 | 
			
		||||
    var KEY = {
 | 
			
		||||
        ENTER: 13,
 | 
			
		||||
        BACKSPACE: 8,
 | 
			
		||||
        LEFT_ARROW: 37,
 | 
			
		||||
        RIGHT_ARROW: 39,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // a stateful object of this `pill_container` instance.
 | 
			
		||||
    // all unique instance information is stored in here.
 | 
			
		||||
    var store = {
 | 
			
		||||
        pills: [],
 | 
			
		||||
        $parent: $parent,
 | 
			
		||||
        getKeyFunction: function () {},
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // a dictionary of internal functions. Some of these are exposed as well,
 | 
			
		||||
    // and nothing in here should be assumed to be private (due to the passing)
 | 
			
		||||
    // of the `this` arg in the `Function.prototype.bind` use in the prototype.
 | 
			
		||||
    var funcs = {
 | 
			
		||||
        // return the value of the contenteditable input form.
 | 
			
		||||
        value: function (input_elem) {
 | 
			
		||||
            return input_elem.innerText.trim();
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // clear the value of the input form.
 | 
			
		||||
        clear: function (input_elem) {
 | 
			
		||||
            input_elem.innerText = "";
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // create the object that will represent the data associated with a pill.
 | 
			
		||||
        // each can have a value and an optional key value.
 | 
			
		||||
        // the value is a human readable value that is shown, whereas the key
 | 
			
		||||
        // can be a hidden ID-type value.
 | 
			
		||||
        createPillObject: function (value, optionalKey) {
 | 
			
		||||
            // we need a "global" closure variable that will be flipped if the
 | 
			
		||||
            // key or pill creation was rejected.
 | 
			
		||||
            var rejected = false;
 | 
			
		||||
 | 
			
		||||
            var reject = function () {
 | 
			
		||||
                rejected = true;
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // the user may provide a function to get a key from a value
 | 
			
		||||
            // that is entered, so return whatever value is gotten from
 | 
			
		||||
            // this function.
 | 
			
		||||
            // the default function is noop, so the return type is by
 | 
			
		||||
            // default `undefined`.
 | 
			
		||||
            if (typeof optionalKey === "undefined") {
 | 
			
		||||
                optionalKey = store.getKeyFunction(value, reject);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // if the `rejected` global is now true, it means that the user's
 | 
			
		||||
            // created pill was not accepted, and we should no longer proceed.
 | 
			
		||||
            if (rejected) {
 | 
			
		||||
                store.$parent.find(".input").addClass("shake");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var id = Math.random().toString(16);
 | 
			
		||||
 | 
			
		||||
            // the user may provide a function to get a key from a value
 | 
			
		||||
            // that is entered, so return whatever value is gotten from
 | 
			
		||||
            // this function.
 | 
			
		||||
            // the default function is noop, so the return type is by
 | 
			
		||||
            // default `undefined`.
 | 
			
		||||
            if (typeof optionalKey === "undefined") {
 | 
			
		||||
                optionalKey = store.getKeyFunction(value);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var payload = {
 | 
			
		||||
                id: id,
 | 
			
		||||
                value: value,
 | 
			
		||||
                key: optionalKey,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            store.pills.push(payload);
 | 
			
		||||
 | 
			
		||||
            return payload;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // the jQuery element representation of the data.
 | 
			
		||||
        createPillElement: function (payload) {
 | 
			
		||||
            payload.$element = $("<div class='pill' data-id='" + payload.id + "' tabindex=0>" + payload.value + "<div class='exit'>×</div></div>");
 | 
			
		||||
            return payload.$element;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // this appends a pill to the end of the container but before the
 | 
			
		||||
        // input block.
 | 
			
		||||
        appendPill: function (value, optionalKey) {
 | 
			
		||||
            var payload = this.createPillObject(value, optionalKey);
 | 
			
		||||
            // if the pill object is undefined, then it means the pill was
 | 
			
		||||
            // rejected so we should return out of this.
 | 
			
		||||
            if (!payload) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            var $pill = this.createPillElement(payload);
 | 
			
		||||
 | 
			
		||||
            store.$parent.find(".input").before($pill);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // this prepends a pill to the beginning of the container.
 | 
			
		||||
        prependPill: function (value, optionalKey) {
 | 
			
		||||
            var payload = this.createPillObject(value, optionalKey);
 | 
			
		||||
            if (!payload) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            var $pill = this.createPillElement(payload);
 | 
			
		||||
 | 
			
		||||
            store.$parent.prepend($pill);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // this searches given a particlar pill ID for it, removes the node
 | 
			
		||||
        // from the DOM, removes it from the array and returns it.
 | 
			
		||||
        // this would generally be used for DOM-provoked actions, such as a user
 | 
			
		||||
        // clicking on a pill to remove it.
 | 
			
		||||
        removePill: function (id) {
 | 
			
		||||
            var idx;
 | 
			
		||||
            for (var x = 0; x < store.pills.length; x += 1) {
 | 
			
		||||
                if (store.pills[x].id === id) {
 | 
			
		||||
                    idx = x;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (typeof idx === "number") {
 | 
			
		||||
                store.pills[idx].$element.remove();
 | 
			
		||||
                return store.pills.splice(idx, 1);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // this will remove the last pill in the container -- by defaulat tied
 | 
			
		||||
        // to the "backspace" key when the value of the input is empty.
 | 
			
		||||
        removeLastPill: function () {
 | 
			
		||||
            var pill = store.pills.pop();
 | 
			
		||||
 | 
			
		||||
            if (pill) {
 | 
			
		||||
                pill.$element.remove();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // returns all data of the pills exclusive of their elements.
 | 
			
		||||
        data: function () {
 | 
			
		||||
            return store.pills.map(function (pill) {
 | 
			
		||||
                return {
 | 
			
		||||
                    value: pill.value,
 | 
			
		||||
                    key: pill.key,
 | 
			
		||||
                };
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // returns all hidden keys.
 | 
			
		||||
        keys: function () {
 | 
			
		||||
            return store.pills.map(function (pill) {
 | 
			
		||||
                return pill.key;
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // returns all human-readable values.
 | 
			
		||||
        values: function () {
 | 
			
		||||
            return store.pills.map(function (pill) {
 | 
			
		||||
                return pill.value;
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    (function events() {
 | 
			
		||||
        store.$parent.on("keydown", ".input", function (e) {
 | 
			
		||||
            var char = e.keyCode || e.charCode;
 | 
			
		||||
 | 
			
		||||
            if (char === KEY.ENTER) {
 | 
			
		||||
                // regardless of the value of the input, the ENTER keyword
 | 
			
		||||
                // should be ignored in favor of keeping content to one line
 | 
			
		||||
                // always.
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
 | 
			
		||||
                // if there is input, grab the input, make a pill from it,
 | 
			
		||||
                // and append the pill, then clear the input.
 | 
			
		||||
                if (funcs.value(e.target).length > 0) {
 | 
			
		||||
                    var value = funcs.value(e.target);
 | 
			
		||||
 | 
			
		||||
                    // append the pill and by proxy create the pill object.
 | 
			
		||||
                    var ret = funcs.appendPill(value);
 | 
			
		||||
 | 
			
		||||
                    // if the pill to append was rejected, no need to clear the
 | 
			
		||||
                    // input; it may have just been a typo or something close but
 | 
			
		||||
                    // incorrect.
 | 
			
		||||
                    if (ret !== false) {
 | 
			
		||||
                        // clear the input.
 | 
			
		||||
                        funcs.clear(e.target);
 | 
			
		||||
                        e.stopPropagation();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // if the user backspaces and there is input, just do normal char
 | 
			
		||||
            // deletion, otherwise delete the last pill in the sequence.
 | 
			
		||||
            if (char === KEY.BACKSPACE && funcs.value(e.target).length === 0) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                funcs.removeLastPill();
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // if one is on the ".input" element and back/left arrows, then it
 | 
			
		||||
            // should switch to focus the last pill in the list.
 | 
			
		||||
            // the rest of the events then will be taken care of in the function
 | 
			
		||||
            // below that handles events on the ".pill" class.
 | 
			
		||||
            if (char === KEY.LEFT_ARROW) {
 | 
			
		||||
                if (window.getSelection().anchorOffset === 0) {
 | 
			
		||||
                    store.$parent.find(".pill").last().focus();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // handle events while hovering on ".pill" elements.
 | 
			
		||||
        // the three primary events are next, previous, and delete.
 | 
			
		||||
        store.$parent.on("keydown", ".pill", function (e) {
 | 
			
		||||
            var char = e.keyCode || e.charCode;
 | 
			
		||||
 | 
			
		||||
            var $pill = store.$parent.find(".pill:focus");
 | 
			
		||||
 | 
			
		||||
            if (char === KEY.LEFT_ARROW) {
 | 
			
		||||
                $pill.prev().focus();
 | 
			
		||||
            } else if (char === KEY.RIGHT_ARROW) {
 | 
			
		||||
                $pill.next().focus();
 | 
			
		||||
            } else if (char === KEY.BACKSPACE) {
 | 
			
		||||
                var $next = $pill.next();
 | 
			
		||||
                var id = $pill.data("id");
 | 
			
		||||
                funcs.removePill(id);
 | 
			
		||||
                $next.focus();
 | 
			
		||||
                // the "backspace" key in FireFox will go back a page if you do
 | 
			
		||||
                // not prevent it.
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // when the shake animation is applied to the ".input" on invalid input,
 | 
			
		||||
        // we want to remove the class when finished automatically.
 | 
			
		||||
        store.$parent.on("animationend", ".input", function () {
 | 
			
		||||
            $(this).removeClass("shake");
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // when the "×" is clicked on a pill, it should delete that pill and then
 | 
			
		||||
        // select the next pill (or input).
 | 
			
		||||
        store.$parent.on("click", ".exit", function () {
 | 
			
		||||
            var $pill = $(this).closest(".pill");
 | 
			
		||||
            var $next = $pill.next();
 | 
			
		||||
            var id = $pill.data("id");
 | 
			
		||||
 | 
			
		||||
            funcs.removePill(id);
 | 
			
		||||
            $next.focus();
 | 
			
		||||
        });
 | 
			
		||||
    }());
 | 
			
		||||
 | 
			
		||||
    // the external, user-accessible prototype.
 | 
			
		||||
    var prototype = {
 | 
			
		||||
        pill: {
 | 
			
		||||
            append: funcs.appendPill.bind(funcs),
 | 
			
		||||
            prepend: funcs.prependPill.bind(funcs),
 | 
			
		||||
            remove: funcs.removePill.bind(funcs),
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        data: funcs.data,
 | 
			
		||||
        keys: funcs.keys,
 | 
			
		||||
        values: funcs.values,
 | 
			
		||||
 | 
			
		||||
        onPillCreate: function (callback) {
 | 
			
		||||
            store.getKeyFunction = callback;
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return prototype;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
if (typeof module !== 'undefined') {
 | 
			
		||||
    module.exports = input_pill;
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user