UNPKG

file-prompt

Version:

An interactive prompt for selecting files from a directory.

479 lines (427 loc) 11.7 kB
/* eslint no-unused-vars: 0 */ import _ from 'underscore'; import EventEmitter from 'events'; /** * Create Listener * Private method to add listener * Decided to use a simple object literal to allow use of underscore collection * methods to traverse the array. * * @param {object} obj - Object to create the listener for * @param {string} event - Event name to listen for * @param {function} callback - Event handler * @param {object} context - Context to call the callback with * @returns {object} A listener object */ function createListener (obj, event, callback, context) { let handler = callback.bind(context); return { id: _.uniqueId('l'), obj, event, handler, callback, context, }; } /** * Write * A single function responsible for outputting to the screen. Just in case * I end up switching between console.log or some other means there is * only one place that needs to be changed. * * @param {string} content - Content to display * @param {Component} component - The component to read the stdout props from */ function write (content, component) { let writer = component.props.stdout || process.stdout; writer.write(content); } /** * Write Array * Iterates through the array to figure out what to display to the screen. * * @param {array} content - Array of content to iterate through * @param {Component} component - Component to read the stdout writer from */ function writeArray (content, component) { for (let element of content) { switch (typeof element) { case 'string': write(element, component); break; case 'function': // write('\n'); element(); break; } } } /** * Component * Base component class has react like functionality to make building * the CLI ui more quickly. * * @class * @property {object} state - State of the component instance * @property {object} properties - Initial properties of the component * @property {array} listeners - Collection of current listeners */ export default class Component extends EventEmitter { state = {}; props = {}; _listeners = []; _hasRendered = false; _content = null; static display (component) { let content = component._content; if (Array.isArray(content)) { writeArray(content, component); } else { write(content, component); } } static mount (component) { component.componentWillMount(); component.renderComponent(); Component.display(component); component.componentDidMount(); } /** * Constructor * Initializes the component instance * * @constructor * @param {object} props - Initial component properties */ constructor (props) { super(); this.props = this.getDefaultProps(); // If we have props extend our default props with it if (props) { Object.assign(this.props, props); } this.state = this.getInitialState(); } /** * Component Did Mount * Component was just rendered to the screen * * @method * @private * @returns {void} */ componentDidMount () { return; } /** * Component Did Update * Component has rerendered in the console * * @method * @private * @param {object} prevProps - Previous set of rendered props * @param {object} prevState - Previous component state * @returns {void} */ componentDidUpdate (prevProps, prevState) { return; } /** * Component Should Update * Determines if the component should even update * * @method * @private * @param {object} nextProps - Next set of properties * @param {object} nextState - Next component state object * @returns {boolean} Returns true if component should re-render */ componentShouldUpdate (nextProps, nextState) { return !_.isEqual(nextState, this.state); } /** * Component Will Mount * Component is about to be rendered * * @method * @private * @returns {void} */ componentWillMount () { return; } /** * Component Will Unmount * Component is about to be removed from display * * @method * @private * @returns {void} */ componentWillUnmount () { return; } /** * Component Will Update * Component is about to update * * @method * @private * @param {object} nextProps - Next set of properties * @param {object} nextState - Next component state object * @returns {void} */ componentWillUpdate (nextProps, nextState) { return; } /** * Get Default Props * Returns the default properties object for component * * @method * @private * @returns {object} Default component properties */ getDefaultProps () { return {}; } /** * Get Initial State * Returns the initial state data * * @method * @private * @returns {object} Initial component state */ getInitialState () { return {}; } /** * Force Update * A method to immediately re-render the component if outside data * has changed. * * @method * @param {function} [callback] - Optional callback to call after render */ forceUpdate (callback) { this.componentWillUpdate(this.props, this.state); this.renderComponent(); this.componentDidUpdate(this.props, this.state); /** Call the callback supplying this context */ if (typeof callback === 'function') { callback.call(this); } } /** * Listen To * Adds a listener to an observable object * * @method * @public * @param {object} obj - Observable object * @param {string} event - Name of the event to listen to * @param {function} callback - Callback to trigger from event * @param {object} [context] - Optional context to call the trigger with */ listenTo (obj, event, callback, context = this) { /** Keep track of what we're listening to */ let listener = createListener(obj, event, callback, context); this._listeners.push(listener); obj.on(event, listener.handler); } /** * Listen To Once * Adds a listener to an observable object that executes only once * * @method * @public * @param {object} obj - Observable object * @param {string} event - Name of the event to listen to * @param {function} callback - Callback to trigger from event * @param {object} [context] - Optional context to call the trigger with */ listenToOnce (obj, event, callback, context=this) { let listener = createListener(obj, event, callback, context); this._listeners.push(listener); obj.once(event, (...args) => { /** Remove this from our list of listeners */ this.stopListening(obj, event, callback); listener.handler(...args); }); } /** * Off * Removes an event listener. Calling this method with no params * removes all listeners tied to this component's context. * * @method * @public * @param {string} [event] - Event name to stop listening for * @param {function} [callback] - Event handler to remove * @param {object} [context] - Context of handlers to stop listening to */ off (event, callback, context=this) { this.stopListening(this, event, callback, context); } /** * On * Small ovveride to track the listeners this component has going on * * @method * @public * @param {string} event - Name of the event to listen to * @param {function} callback - Event handler * @param {context} [context] - Optional context to call handler with * @returns {*} Result of listener being added */ on (event, callback, context=this) { let listener = createListener(this, event, callback, context); this._listeners.push(listener); return super.on(event, listener.handler); } /** * Once * Small ovveride to track the listeners this component has going on * * @method * @public * @param {string} event - Name of the event to listen to * @param {function} callback - Event handler * @param {context} [context] - Optional context to call handler with * @returns {*} Result of listener being added */ once (event, callback, context=this) { let listener = createListener(this, event, callback, context); this._listeners.push(listener); return super.once(event, listener.handler); } /** * Remove * Removes the component from the UI * * @method * @public */ remove () { this.stopListening(); this.componentWillUnmount(); } /** * Render * Returns the string to write to the console * * @method * @private */ render () { throw new Error('Component must implement a render method.'); } /** * Render Component * Renders the output of the render method to console * * @method * @public */ renderComponent () { let content = this.render(); if (!Array.isArray(content)) { content += '\n'; } this._content = content; } /** * Set * Update either state or props and rerender * * @method * @private * @param {string} key - Either 'state' or 'props' * @param {object} data - Data to set on component * @param {function} [callback] - Callback to fire after re-rendering */ set (key, data, callback) { let prev = { props: _.clone(this.props), state: _.clone(this.state), }, next = { props: _.clone(this.props), state: _.clone(this.state), }; /** Get what the next set of data would be */ next[key] = Object.assign(next[key], data); /** See if the component should re-render or not */ if (this.componentShouldUpdate(next.props, next.state)) { this.componentWillUpdate(next.props, next.state); /** Update a */ this[key] = next[key]; this.renderComponent(); this.componentDidUpdate(prev.props, prev.state); } else { this[key] = next[key]; } /** Call the callback supplying this context */ if (typeof callback === 'function') { callback.call(this); } } /** * Set Props * A method to update the component's props and re-render * * @method * @public * @param {object} data - New data * @param {function} [callback] - Callback to fire after re-rendering */ setProps (data, callback) { this.set('props', data, callback); } /** * Set State * A method to update the component's state and re-render * * @method * @public * @param {object} data - New data * @param {function} [callback] - Callback to fire after re-rendering */ setState (data, callback) { this.set('state', data, callback); } /** * Stop Listening * Query the event listeners by the given criteria and remove them * * @method * @public * @param {...*} criteria - Obj, event, callback context to search for */ stopListening (...args) { let criteria = {}, listeners = this._listeners.slice(), names = ['obj', 'event', 'callback', 'context']; /** For truthy values attach them to the criteria object */ if (args.length > 0) { args.forEach((arg, i) => { if (arg) { criteria[names[i]] = arg; } }); } /** Filter the listeners to what matches the criteria */ if (Object.keys(criteria).length > 0) { listeners = _.where(listeners, criteria); } /** Tell the obj to stop listening for events */ listeners.forEach((listener) => { listener.obj.removeListener(listener.event, listener.handler); this._listeners.splice(this._listeners.indexOf(listener), 1); }); } }