From 3a44f15ff9020e1fef8bac92582164877e0a4b9f Mon Sep 17 00:00:00 2001 From: Nate Goldman Date: Fri, 25 Mar 2016 00:28:35 -0700 Subject: [PATCH] use yo-yo --- app.js | 44 +++++------ css/app.styl | 44 +++++++---- lib/command.js | 2 +- lib/elements/channels.js | 37 +++++---- lib/elements/composer.js | 75 ++++++++++-------- lib/elements/input-prompt.js | 66 ++++++++-------- lib/elements/messages.js | 148 ++++++++++++++++++++--------------- lib/elements/status.js | 12 +-- lib/elements/users.js | 85 +++++++++++--------- package.json | 7 +- 10 files changed, 285 insertions(+), 235 deletions(-) diff --git a/app.js b/app.js index 4163965..e1d9df0 100755 --- a/app.js +++ b/app.js @@ -5,19 +5,16 @@ module.exports = window.App = App var EventEmitter = require('events').EventEmitter var catNames = require('cat-names') -var createElement = require('virtual-dom/create-element') var delegate = require('dom-delegate') -var diff = require('virtual-dom/diff') var eos = require('end-of-stream') var githubCurrentUser = require('github-current-user') -var h = require('virtual-dom/h') var inherits = require('inherits') var leveldown = require('leveldown') // browser: level-js var levelup = require('levelup') -var patch = require('virtual-dom/patch') var subleveldown = require('subleveldown') var richMessage = require('rich-message') var Swarm = require('friends-swarm') +var yo = require('yo-yo') var config = require('./config') var util = require('./lib/util') @@ -191,14 +188,11 @@ function App (el, currentWindow) { // Initial DOM tree render var tree = self.render() - var rootNode = createElement(tree) - el.appendChild(rootNode) + el.appendChild(tree) function render () { var newTree = self.render() - var patches = diff(tree, newTree) - rootNode = patch(rootNode, patches) - tree = newTree + yo.update(tree, newTree) } self.on('render', render) @@ -304,24 +298,24 @@ App.prototype.render = function () { var views = self.views var data = self.data - return h('div.layout', [ - h('.sidebar', [ - h('.sidebar-scroll', [ - views.channels.render(data.channels), - views.users.render(data.users) - ]), - views.status.render(data.username, data.peers) - ]), - h('.content', [ - views.messages.render(data.activeChannel, data.users), - views.composer.render(data) - ]) - ]) + return yo` +
+ +
+ ${views.messages.render(data.activeChannel, data.users)} + ${views.composer.render(data)} +
+
+ ` } App.prototype.isFocused = function () { - if (this.currentWindow) { - return this.currentWindow.isFocused() - } + if (this.currentWindow) return this.currentWindow.isFocused() return true } diff --git a/css/app.styl b/css/app.styl index 90e9ab1..e5f6fc6 100644 --- a/css/app.styl +++ b/css/app.styl @@ -3,6 +3,7 @@ WHITE = rgb(255, 255, 255) +GRAY_HIGHLIGHT = rgb(240, 240, 240) GRAY_LIGHTEST = rgb(220, 220, 220) GRAY_LIGHT = rgb(200, 200, 200) GRAY = rgb(100, 100, 100) @@ -122,16 +123,26 @@ button ellipsis() color: GRAY_LIGHTEST text-decoration: none - padding: (SPACING_SIZE / 3) SPACING_SIZE + padding: (SPACING_SIZE / 3) (SPACING_SIZE * 2) margin: 0 display: block - - .addChannel - input - padding: (SPACING_SIZE / 3) SPACING_SIZE - font-size: FONT_SIZE - width: 100% - z-index: 10000 + width: 100% + text-align: left + + &:hover + color: GRAY_HIGHLIGHT + + .add-channel + margin: 0 (-1 * SPACING_SIZE) + button + padding: (SPACING_SIZE / 2) (SPACING_SIZE * 2) + .inputprompt + margin: 0 (SPACING_SIZE * 2) + input + padding: (SPACING_SIZE / 3) SPACING_SIZE + font-size: FONT_SIZE + width: 100% + z-index: 10000 ul list-style: none @@ -142,11 +153,10 @@ button ellipsis() margin: 0 padding: 0 + margin: 0 (-1 * SPACING_SIZE) &.active background-color: RED_LIGHT - margin: 0 (-1 * SPACING_SIZE) - padding: 0 SPACING_SIZE button color: WHITE @@ -193,12 +203,17 @@ button padding: SPACING_SIZE height: TOP_BAR_HEIGHT - .channelName + .channel-name display: inline-block color: BLACK font-size: FONT_SIZE_BIG font-weight: bold + .num-peers + font-size: FONT_SIZE_SMALL + font-weight: normal + color: GRAY_LIGHT + .button float: right margin-top: -3px @@ -222,23 +237,24 @@ button border-radius: BORDER_RADIUS width: AVATAR_SIZE height: AVATAR_SIZE + cursor: pointer .username, .verified, .timestamp - display: inline-block line-height: 1 .username font-weight: bold margin-left: SPACING_SIZE + text-decoration: none + color: inherit .verified + display: inline-block background-image: url('../static/verified.svg') background-size: 10px width: 10px height: 10px - margin-left: (SPACING_SIZE / 2) opacity: 0.8 .timestamp color: GRAY_LIGHT font-size: FONT_SIZE_SMALL - margin-left: (SPACING_SIZE * (2 / 3)) .text margin-left: AVATAR_SIZE + SPACING_SIZE overflow-wrap: break-word diff --git a/lib/command.js b/lib/command.js index ffa012d..6de0a7f 100644 --- a/lib/command.js +++ b/lib/command.js @@ -32,7 +32,7 @@ function command (self, db) { break default: db.aliases.get(command, function (err, alias) { - if (err == null) { + if (err === null) { self.emit('executeCommand', alias) } else { console.log('Unrecognized command: ' + command + ' (in "' + commandStr + '")') diff --git a/lib/elements/channels.js b/lib/elements/channels.js index fc4b805..0f13ae5 100644 --- a/lib/elements/channels.js +++ b/lib/elements/channels.js @@ -1,16 +1,16 @@ module.exports = Channels -var h = require('virtual-dom/h') -var InputPrompt = require('./input-prompt.js') +var InputPrompt = require('./input-prompt') var inherits = require('util').inherits var BaseElement = require('./base-element') +var yo = require('yo-yo') function Channels (target) { var self = this BaseElement.call(this, target) self.addChannelPrompt = new InputPrompt({ - className: 'addChannel', + className: 'add-channel', prompt: '+ Join Channel', placeholder: 'Channel name', onsubmit: function (channelName) { @@ -28,20 +28,23 @@ Channels.prototype.render = function (channels) { channels = channels.map(function (channel) { var className = channel.active ? 'active' : '' - return h('li', { className: className }, [ - h('button', { - onclick: function () { - self.send('selectChannel', channel.name) - } - }, '#' + channel.name) - ]) + + function onclick () { + self.send('selectChannel', channel.name) + } + + return yo` +
  • + +
  • + ` }) - return [ - h('.heading', 'Channels'), - h('ul', [ - channels - ]), - self.addChannelPrompt.render() - ] + return yo` +
    +
    Channels
    + + ${self.addChannelPrompt.render()} +
    + ` } diff --git a/lib/elements/composer.js b/lib/elements/composer.js index 2ef1e33..6cfa232 100644 --- a/lib/elements/composer.js +++ b/lib/elements/composer.js @@ -1,9 +1,9 @@ module.exports = Composer -var h = require('virtual-dom/h') var inherits = require('util').inherits var uniq = require('lodash.uniq') var BaseElement = require('./base-element') +var yo = require('yo-yo') function Composer (target) { BaseElement.call(this, target) @@ -28,36 +28,47 @@ Composer.prototype.render = function (data) { data = data || {} var ownUsername = data.username - return h('textarea.composer', { - onload: self, - rows: 1, - autofocus: true, - onkeydown: function (e) { - if (e.keyCode === TAB_KEY) { - e.preventDefault() - - // if there are no matches try matching again - if (!self.autocompleting.length) { - self.resetAutocomplete() - self.autocomplete(self.node.value, ownUsername) - } - - self.insertAutocomplete(e.target) - } else if (e.keyCode === ENTER_KEY && !e.shiftKey) { - e.preventDefault() - self.submit(e.target) - } + function onkeydown (e) { + if (e.keyCode === TAB_KEY) { + e.preventDefault() - // reset the completions if the user submits or changes the text to be - // completed - if (e.keyCode !== TAB_KEY) { + // if there are no matches try matching again + if (!self.autocompleting.length) { self.resetAutocomplete() + self.autocomplete(self.node.value, ownUsername) } - }, - oninput: function (e) { - self.resize(e.target) + + self.insertAutocomplete(e.target) + } else if (e.keyCode === ENTER_KEY && !e.shiftKey) { + e.preventDefault() + self.submit(e.target) } - }) + + // reset the completions if the user submits or changes the text to be + // completed + if (e.keyCode !== TAB_KEY) { + self.resetAutocomplete() + } + } + + function oninput (e) { + self.resize(e.target) + } + + self.node = yo` + + ` + + this.initAutoExpander() + + return self.node } Composer.prototype.autocomplete = function (text, ownUsername) { @@ -79,7 +90,7 @@ Composer.prototype.submit = function (node) { } else { self.send('sendMessage', node.value) } - node.value = '' + node.innerHTML = '' self.resize(node) } @@ -97,17 +108,13 @@ Composer.prototype.resetAutocomplete = function () { } Composer.prototype.focus = function () { - var self = this - self.node.focus() + this.node.focus() } -Composer.prototype.hook = function (node) { +Composer.prototype.initAutoExpander = function () { var self = this - self.node = node - // init for auto expander setTimeout(function () { - node.setAttribute('aria-label', 'Enter a message and press enter') var savedValue = self.node.value self.node.value = '' self.node.baseScrollHeight = self.node.scrollHeight diff --git a/lib/elements/input-prompt.js b/lib/elements/input-prompt.js index 6136c72..8b81b1f 100644 --- a/lib/elements/input-prompt.js +++ b/lib/elements/input-prompt.js @@ -1,8 +1,8 @@ module.exports = InputPrompt -var h = require('virtual-dom/h') var inherits = require('util').inherits var BaseElement = require('./base-element') +var yo = require('yo-yo') function InputPrompt (params) { BaseElement.call(this) @@ -10,49 +10,47 @@ function InputPrompt (params) { if (!params.prompt) throw new Error('param `prompt` required') this.params = params - this.showInput = false } inherits(InputPrompt, BaseElement) InputPrompt.prototype.render = function () { var self = this - var view + + function onsubmit (e) { + e.preventDefault() + var input = this.querySelector('input') + self.params.onsubmit(input.value) + self.showInput = false + self.params.onupdate() + } + + function onblur (e) { + self.showInput = false + self.params.onupdate() + } + + function onclick (e) { + e.preventDefault() + self.showInput = true + self.params.onupdate() + } + if (self.showInput) { - view = h('form.inputprompt', { - onsubmit: function (e) { - e.preventDefault() - var input = this.querySelector('input') - self.params.onsubmit(input.value) - self.showInput = false - self.params.onupdate() - } - }, [ - h('input', { - placeholder: self.params.placeholder, - onload: self, - onblur: function (e) { - self.showInput = false - self.params.onupdate() - } - }) - ]) + view = yo` +
    + +
    + ` } else { - view = h('button', { - onclick: function (e) { - e.preventDefault() - self.showInput = true - self.params.onupdate() - } - }, self.params.prompt) + view = yo`` } - return h('div', { className: this.params.className }, view) -} + self.node = yo`
    ${view}
    ` -InputPrompt.prototype.hook = function (node) { - setTimeout(function () { - node.focus() - }) + return self.node } diff --git a/lib/elements/messages.js b/lib/elements/messages.js index 671f3aa..5cdc038 100644 --- a/lib/elements/messages.js +++ b/lib/elements/messages.js @@ -1,21 +1,8 @@ module.exports = Messages -var h = require('virtual-dom/h') var inherits = require('util').inherits var BaseElement = require('./base-element') - -var htmlToVDom = require('html-to-vdom') -var VNode = require('virtual-dom/vnode/vnode') -var VText = require('virtual-dom/vnode/vtext') - -var convertHTML = htmlToVDom({ - VNode: VNode, - VText: VText -}) - -function makeVDom (html) { - return convertHTML('
    ' + html + '
    ') -} +var yo = require('yo-yo') function Messages (target) { BaseElement.call(this, target) @@ -29,60 +16,99 @@ inherits(Messages, BaseElement) Messages.prototype.render = function (channel, users) { var self = this - var childViews + var childViews = renderChildViews.call(self, channel, users) + + function leaveChannel () { + if (channel.name === 'friends') return null + + function onclick () { + if (!channel || channel.name === 'friends') return + self.send('leaveChannel', channel.name) + } + + return yo`` + } + + var channelName = `#${channel ? channel.name : 'friends'}` + var numPeers = yo`${channel.peers} peer${channel.peers === 1 ? '' : 's'}` + + return yo` +
    +
    +
    ${channelName} ${numPeers}
    + ${leaveChannel()} +
    + ${childViews} +
    + ` +} +function renderChildViews (channel, users) { if (channel && channel.messages.length === 0) { var starterMessage = 'This is a new channel. Send a message to start things off!' - childViews = h('.messages.starterMessage', starterMessage) - } else { - var messages = (channel ? channel.messages : []).map(function (msg) { - var user = users[msg.username] - if (user && user.blocked) return null - - var verified = !/Anonymous/i.test(msg.username) && msg.valid - var userUrl = verified ? 'http://github.com/' + msg.username : '#' - return h('li.message.clearfix', [ - h('img.avatar', { - src: msg.avatar, - style: { cursor: 'pointer' }, - onclick: function (e) { - if (userUrl !== '#') { - self.send('openUrl', userUrl) - } - } - }), - h(verified ? 'a.username' : '.username', { href: userUrl, style: { textDecoration: 'none', color: 'inherit' } }, msg.username), - verified ? h('.verified') : null, - h('.timestamp', msg.timeago), - makeVDom(msg.html) || h('.text', msg.text) - ]) - }) - childViews = h('.messages', { - onscroll: function () { - if (this.scrollHeight <= this.clientHeight + this.scrollTop) self.shouldAutoScroll = true - else self.shouldAutoScroll = false - }, - style: { - paddingBottom: this.composerHeight - }, - scrollTop: this.scrollTop - }, messages) + return yo`
    ${starterMessage}
    ` } - var onleave = function () { - if (!channel || channel.name === 'friends') return - self.send('leaveChannel', channel.name) - } + var self = this + var messages = (channel ? channel.messages : []).map(function (msg) { + var user = users[msg.username] + if (user && user.blocked) return null + + var isVerified = !/Anonymous/i.test(msg.username) && msg.valid + var userUrl = isVerified ? `http://github.com/${msg.username}` : '#' + + function avatar () { + function onclick (e) { + if (userUrl !== '#') { self.send('openUrl', userUrl) } + } + + return yo`` + } + + function username () { + return yo`${msg.username}` + } - var leavableChannel = channel.name !== 'friends' + function verified () { + return isVerified ? yo`` : null + } + + function timestamp () { + return yo`${msg.timeago}` + } + + function message () { + var el = yo`
    ` + el.innerHTML = msg.html || msg.text + return el + } + + return yo` +
  • + ${avatar()} +
    + ${username()} + ${verified()} + ${timestamp()} +
    + ${message()} +
  • + ` + }) + + function onscroll () { + if (this.scrollHeight <= this.clientHeight + this.scrollTop) self.shouldAutoScroll = true + else self.shouldAutoScroll = false + } - return h('.messages-container', [ - h('div', { className: 'top-bar' }, [ - h('.channelName', '#' + (channel ? channel.name : 'friends') + ' (' + channel.peers + ' peer' + (channel.peers === 1 ? '' : 's') + ')'), - leavableChannel ? h('button.button.leaveButton', {onclick: onleave}, 'Leave') : null - ]), - childViews - ]) + return yo` +
    + ${messages} +
    + ` } Messages.prototype.scrollToBottom = function (force) { diff --git a/lib/elements/status.js b/lib/elements/status.js index d15b79e..0fa50b2 100644 --- a/lib/elements/status.js +++ b/lib/elements/status.js @@ -1,8 +1,8 @@ module.exports = Status -var h = require('virtual-dom/h') var inherits = require('util').inherits var BaseElement = require('./base-element') +var yo = require('yo-yo') function Status (target) { BaseElement.call(this, target) @@ -10,8 +10,10 @@ function Status (target) { inherits(Status, BaseElement) Status.prototype.render = function (username, peers) { - return h('.status', [ - h('.username', username), - h('.peers', 'Connected to ' + peers + ' peer' + (peers === 1 ? '' : 's')) - ]) + return yo` +
    +
    ${username}
    +
    Connected to ${peers} peer${peers === 1 ? '' : 's'}
    +
    + ` } diff --git a/lib/elements/users.js b/lib/elements/users.js index 45deadf..fd92956 100644 --- a/lib/elements/users.js +++ b/lib/elements/users.js @@ -1,9 +1,9 @@ module.exports = Users -var h = require('virtual-dom/h') var inherits = require('util').inherits var BaseElement = require('./base-element') var ModalElement = require('modal-element') +var yo = require('yo-yo') function Users (target) { BaseElement.call(this, target) @@ -45,48 +45,57 @@ Users.prototype.render = function (users) { function enrichUsers (username) { var user = users[username] var className = user.blocked ? 'blocked' : '' - return h('li', { className: className }, [ - h('button', { - oncontextmenu: function (e) { - e.preventDefault() - self.showUserMenuFor = username - self.lastClickPosition = [e.clientX, e.clientY] - self.send('render') - } - }, [ - h('img.avatar', { src: user.avatar }), - username - ]) - ]) + + function oncontextmenu (e) { + e.preventDefault() + self.showUserMenuFor = username + self.lastClickPosition = [e.clientX, e.clientY] + self.send('render') + } + + return yo` +
  • + +
  • + ` } // Build user menu this.userMenu.shown = !!this.showUserMenuFor - var ignoreLabel = 'mute ' - if (users[this.showUserMenuFor] && users[this.showUserMenuFor].blocked) ignoreLabel = 'unmute ' + var ignoreLabel = 'mute' + if (users[this.showUserMenuFor] && users[this.showUserMenuFor].blocked) ignoreLabel = 'unmute' + + function onclick (e) { + e.preventDefault() + self.send('toggleBlockUser', self.showUserMenuFor) + } + this.userMenu.render( - h('ul', [ - h('li', h('a', { - href: 'https://github.com/' + this.showUserMenuFor, - target: '_blank' - }, 'open github.com/' + this.showUserMenuFor)), - h('li', h('button', { - onclick: function (e) { - e.preventDefault() - self.send('toggleBlockUser', self.showUserMenuFor) - } - }, ignoreLabel + this.showUserMenuFor)) - ]) + yo` + + ` ) - return [ - h('.heading', 'Active Users (' + activeUsers.length + ')'), - h('ul.users', [ - activeUsers - ]), - h('.heading', 'Idle Users (' + idleUsers.length + ')'), - h('ul.users', [ - idleUsers - ]) - ] + return yo` +
    +
    Active Users (${activeUsers.length})
    + +
    Idle Users (${idleUsers.length})
    + +
    + ` } diff --git a/package.json b/package.json index 8b3d75f..12776c9 100644 --- a/package.json +++ b/package.json @@ -27,16 +27,12 @@ "dependencies": { "application-config-path": "^0.1.0", "cat-names": "^1.0.2", - "ctype": "^0.5.4", "dom-delegate": "^2.0.3", "end-of-stream": "^1.1.0", "friends-swarm": "^2.0.0", - "ghlink": "^0.1.2", "ghsign": "^3.0.1", "github-current-user": "^2.5.0", "highlight.js": "^9.2.0", - "html-parser": "^0.8.0", - "html-to-vdom": "^0.7.0", "inherits": "^2.0.1", "level-js": "^2.2.1", "leveldown": "^1.4.1", @@ -48,9 +44,8 @@ "rich-message": "^1.0.0", "silence-chromium": "^2.0.0", "simple-get": "^2.0.0", - "single-line-log": "^1.0.0", "subleveldown": "^2.0.0", - "virtual-dom": "^2.0.1" + "yo-yo": "^1.1.1" }, "devDependencies": { "beefy": "^2.1.5",