From 7151357fa5778d4e30c719cc14a64c487f97fc2c Mon Sep 17 00:00:00 2001 From: David Hirtle Date: Mon, 4 Jun 2012 00:00:04 -0700 Subject: [PATCH] =?UTF-8?q?=CE=A97:=20hover=20over=20issue=20ids=20in=20me?= =?UTF-8?q?ssage=20log=20to=20see=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/css/bootstrap.css | 134 ++++++++++++++++++ public/css/omega.css | 3 + public/js/MessageList.js | 2 +- public/js/ProjectView.js | 21 ++- public/js/lib/tooltips.js | 278 ++++++++++++++++++++++++++++++++++++++ public/js/mainProject.js | 3 +- public/js/omegaEvent.js | 14 +- public/js/util.js | 21 ++- views/project.html | 1 + 9 files changed, 459 insertions(+), 18 deletions(-) create mode 100755 public/css/bootstrap.css create mode 100755 public/js/lib/tooltips.js diff --git a/public/css/bootstrap.css b/public/css/bootstrap.css new file mode 100755 index 0000000..d711b1c --- /dev/null +++ b/public/css/bootstrap.css @@ -0,0 +1,134 @@ +/*! + * Bootstrap v2.0.4 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ +.clearfix { + *zoom: 1; +} +.clearfix:before, +.clearfix:after { + display: table; + content: ""; +} +.clearfix:after { + clear: both; +} +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} +.input-block-level { + display: block; + width: 100%; + min-height: 28px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; +} +.tooltip { + position: absolute; + z-index: 1020; + display: block; + visibility: visible; + padding: 5px; + font-size: 11px; + opacity: 0; + filter: alpha(opacity=0); +} +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} +.tooltip.top { + margin-top: -2px; +} +.tooltip.right { + margin-left: 2px; +} +.tooltip.bottom { + margin-top: 2px; +} +.tooltip.left { + margin-left: -2px; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #000000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #000000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid #000000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid #000000; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; +} +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -ms-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} +.fade.in { + opacity: 1; +} +.collapse { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -ms-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; +} +.collapse.in { + height: auto; +} diff --git a/public/css/omega.css b/public/css/omega.css index 077a49b..848a70a 100644 --- a/public/css/omega.css +++ b/public/css/omega.css @@ -269,4 +269,7 @@ input[type='text']:focus { padding: 0.3em; } +.tooltip { + font-size: 0.9em !important; +} diff --git a/public/js/MessageList.js b/public/js/MessageList.js index 001555a..ce6b5fc 100644 --- a/public/js/MessageList.js +++ b/public/js/MessageList.js @@ -52,7 +52,7 @@ define(['ko', 'underscore', 'util', 'flavour'], function (ko, _, util, flavour) }; MessageList.prototype.append = function (event) { - var text = flavour(event.type.name, util.addHtmlLinks(event.message)); + var text = flavour(event.type.name, util.addHtml(event.message)); this.messages.push(new Message(text, event.speaker, event.timestamp)); scrollToBottom(this.$element.get(0)); }; diff --git a/public/js/ProjectView.js b/public/js/ProjectView.js index 149c118..c6ba0a8 100644 --- a/public/js/ProjectView.js +++ b/public/js/ProjectView.js @@ -1,10 +1,10 @@ /* global window */ define([ - 'jquery', 'underscore', 'ko', 'timeago', + 'jquery', 'underscore', 'ko', 'timeago', 'tooltips', 'util', 'Issue', 'Notifier', 'UserManager', 'MessageList', 'IssueManager', 'error/NoSuchIssueError' ], -function ($, _, ko, timeago, util, Issue, Notifier, UserManager, MessageList, IssueManager, NoSuchIssueError) { +function ($, _, ko, timeago, tooltips, util, Issue, Notifier, UserManager, MessageList, IssueManager, NoSuchIssueError) { var BAD_COMMAND_RESPONSES = [ 'Oops.', 'This is not a Turing test.', @@ -27,6 +27,8 @@ function ($, _, ko, timeago, util, Issue, Notifier, UserManager, MessageList, Is this.disconnected = ko.observable(); this.loading = ko.observable(true); + this.initTooltips(); + $(window).bind('hashchange', _.bind(this.checkHashForBookmark, this)); this.hideClosed = ko.observable(true); @@ -50,6 +52,21 @@ function ($, _, ko, timeago, util, Issue, Notifier, UserManager, MessageList, Is that.checkHashForBookmark(); }); }; + + ProjectView.prototype.initTooltips = function () { + var that = this; + $('#messages').tooltip({ + selector: '.id', + placement: 'right', + delay: 0, + title: function () { + var issue = that.issueManager.findIssue($(this).data('id')); + var assignee = issue.assigneeLabel(); + var tooltip = assignee ? assignee + ' ' : ''; + return tooltip + issue.description(); + } + }); + }; function isCommand(input) { return input.trim().charAt(0) === "/"; diff --git a/public/js/lib/tooltips.js b/public/js/lib/tooltips.js new file mode 100755 index 0000000..c93e6a4 --- /dev/null +++ b/public/js/lib/tooltips.js @@ -0,0 +1,278 @@ +/* =========================================================== + * bootstrap-tooltip.js v2.0.4 + * http://twitter.github.com/bootstrap/javascript.html#tooltips + * Inspired by the original jQuery.tipsy by Jason Frame + * =========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + +define(['jquery'], function ($) { + +!function ($) { + + "use strict"; // jshint ;_; + + + /* TOOLTIP PUBLIC CLASS DEFINITION + * =============================== */ + + var Tooltip = function (element, options) { + this.init('tooltip', element, options) + } + + Tooltip.prototype = { + + constructor: Tooltip + + , init: function (type, element, options) { + var eventIn + , eventOut + + this.type = type + this.$element = $(element) + this.options = this.getOptions(options) + this.enabled = true + + if (this.options.trigger != 'manual') { + eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus' + eventOut = this.options.trigger == 'hover' ? 'mouseleave' : 'blur' + this.$element.on(eventIn, this.options.selector, $.proxy(this.enter, this)) + this.$element.on(eventOut, this.options.selector, $.proxy(this.leave, this)) + } + + this.options.selector ? + (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : + this.fixTitle() + } + + , getOptions: function (options) { + options = $.extend({}, $.fn[this.type].defaults, options, this.$element.data()) + + if (options.delay && typeof options.delay == 'number') { + options.delay = { + show: options.delay + , hide: options.delay + } + } + + return options + } + + , enter: function (e) { + var self = $(e.currentTarget)[this.type](this._options).data(this.type) + + if (!self.options.delay || !self.options.delay.show) return self.show() + + clearTimeout(this.timeout) + self.hoverState = 'in' + this.timeout = setTimeout(function() { + if (self.hoverState == 'in') self.show() + }, self.options.delay.show) + } + + , leave: function (e) { + var self = $(e.currentTarget)[this.type](this._options).data(this.type) + + if (this.timeout) clearTimeout(this.timeout) + if (!self.options.delay || !self.options.delay.hide) return self.hide() + + self.hoverState = 'out' + this.timeout = setTimeout(function() { + if (self.hoverState == 'out') self.hide() + }, self.options.delay.hide) + } + + , show: function () { + var $tip + , inside + , pos + , actualWidth + , actualHeight + , placement + , tp + + if (this.hasContent() && this.enabled) { + $tip = this.tip() + this.setContent() + + if (this.options.animation) { + $tip.addClass('fade') + } + + placement = typeof this.options.placement == 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement + + inside = /in/.test(placement) + + $tip + .remove() + .css({ top: 0, left: 0, display: 'block' }) + .appendTo(inside ? this.$element : document.body) + + pos = this.getPosition(inside) + + actualWidth = $tip[0].offsetWidth + actualHeight = $tip[0].offsetHeight + + switch (inside ? placement.split(' ')[1] : placement) { + case 'bottom': + tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2} + break + case 'top': + tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2} + break + case 'left': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth} + break + case 'right': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width} + break + } + + $tip + .css(tp) + .addClass(placement) + .addClass('in') + } + } + + , isHTML: function(text) { + // html string detection logic adapted from jQuery + return typeof text != 'string' + || ( text.charAt(0) === "<" + && text.charAt( text.length - 1 ) === ">" + && text.length >= 3 + ) || /^(?:[^<]*<[\w\W]+>[^>]*$)/.exec(text) + } + + , setContent: function () { + var $tip = this.tip() + , title = this.getTitle() + + $tip.find('.tooltip-inner')[this.isHTML(title) ? 'html' : 'text'](title) + $tip.removeClass('fade in top bottom left right') + } + + , hide: function () { + var that = this + , $tip = this.tip() + + $tip.removeClass('in') + + function removeWithAnimation() { + var timeout = setTimeout(function () { + $tip.off($.support.transition.end).remove() + }, 500) + + $tip.one($.support.transition.end, function () { + clearTimeout(timeout) + $tip.remove() + }) + } + + $.support.transition && this.$tip.hasClass('fade') ? + removeWithAnimation() : + $tip.remove() + } + + , fixTitle: function () { + var $e = this.$element + if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { + $e.attr('data-original-title', $e.attr('title') || '').removeAttr('title') + } + } + + , hasContent: function () { + return this.getTitle() + } + + , getPosition: function (inside) { + return $.extend({}, (inside ? {top: 0, left: 0} : this.$element.offset()), { + width: this.$element[0].offsetWidth + , height: this.$element[0].offsetHeight + }) + } + + , getTitle: function () { + var title + , $e = this.$element + , o = this.options + + title = $e.attr('data-original-title') + || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + + return title + } + + , tip: function () { + return this.$tip = this.$tip || $(this.options.template) + } + + , validate: function () { + if (!this.$element[0].parentNode) { + this.hide() + this.$element = null + this.options = null + } + } + + , enable: function () { + this.enabled = true + } + + , disable: function () { + this.enabled = false + } + + , toggleEnabled: function () { + this.enabled = !this.enabled + } + + , toggle: function () { + this[this.tip().hasClass('in') ? 'hide' : 'show']() + } + + } + + + /* TOOLTIP PLUGIN DEFINITION + * ========================= */ + + $.fn.tooltip = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('tooltip') + , options = typeof option == 'object' && option + if (!data) $this.data('tooltip', (data = new Tooltip(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.tooltip.Constructor = Tooltip + + $.fn.tooltip.defaults = { + animation: true + , placement: 'top' + , selector: false + , template: '
' + , trigger: 'hover' + , title: '' + , delay: 0 + } + +}(window.jQuery); + +}); diff --git a/public/js/mainProject.js b/public/js/mainProject.js index 5ceab1b..d83ca09 100644 --- a/public/js/mainProject.js +++ b/public/js/mainProject.js @@ -3,7 +3,8 @@ require.config({ 'jquery': 'lib/jquery-1.7.1.min', 'ko': 'lib/knockout-2.0.0.min', 'underscore': 'lib/underscore-1.2.2.min', - 'timeago': 'lib/jquery.timeago' + 'timeago': 'lib/jquery.timeago', + 'tooltips': 'lib/tooltips' } }); diff --git a/public/js/omegaEvent.js b/public/js/omegaEvent.js index fda3eff..61828de 100644 --- a/public/js/omegaEvent.js +++ b/public/js/omegaEvent.js @@ -35,13 +35,13 @@ var isNode = (typeof exports !== 'undefined'); // TODO: this should be client only OmegaEvent.Type = { UserMessage: new OmegaEventType("userMessage", "<%= message %>", "<%= speaker %> says...", "<%= message %>"), - NewIssue: new OmegaEventType("newIssue", "<%= issue.creator %> created <%= issue.id %>.", "New issue", "<%= issue.description %>"), - AssignIssue: new OmegaEventType("assignIssue", "<%= assigner %> assigned <%= issue.id %> to <%= issue.assignee %>."), - TagIssue: new OmegaEventType("tagIssue", "<%= updater %> tagged <%= issue.id %> with '<%= tag %>'."), - UntagIssue: new OmegaEventType("untagIssue", "<%= updater %> removed tags from <%= issue.id %>."), - UpdateIssue: new OmegaEventType("updateIssue", "<%= updater %> updated <%= issue.id %>."), - CloseIssue: new OmegaEventType("closeIssue", "<%= issue.closer %> closed <%= issue.id %>.", "Issue closed", "<%= issue.description %>"), - PrioritizeIssue: new OmegaEventType("prioritizeIssue", "<%= updater %> marked <%= issue.id %> as<% if (!issue.critical) print(' not'); %> critical.") + NewIssue: new OmegaEventType("newIssue", "<%= issue.creator %> created $id$<%= issue.id %>.", "New issue", "<%= issue.description %>"), + AssignIssue: new OmegaEventType("assignIssue", "<%= assigner %> assigned $id$<%= issue.id %> to <%= issue.assignee %>."), + TagIssue: new OmegaEventType("tagIssue", "<%= updater %> tagged $id$<%= issue.id %> with '<%= tag %>'."), + UntagIssue: new OmegaEventType("untagIssue", "<%= updater %> removed tags from $id$<%= issue.id %>."), + UpdateIssue: new OmegaEventType("updateIssue", "<%= updater %> updated $id$<%= issue.id %>."), + CloseIssue: new OmegaEventType("closeIssue", "<%= issue.closer %> closed $id$<%= issue.id %>.", "Issue closed", "<%= issue.description %>"), + PrioritizeIssue: new OmegaEventType("prioritizeIssue", "<%= updater %> marked $id$<%= issue.id %> as<% if (!issue.critical) print(' not'); %> critical.") }; exports.OmegaEvent = OmegaEvent; diff --git a/public/js/util.js b/public/js/util.js index e47339c..cc3fc28 100644 --- a/public/js/util.js +++ b/public/js/util.js @@ -1,12 +1,13 @@ -define(['ko'], function (ko) { +define(['ko', 'underscore'], function (ko, _) { var URL_REGEX = /(\(?\bhttps?:\/\/[-A-Z0-9+&@#\/%?=~_()|!:,.;]*[-A-Z0-9+&@#\/%=~_()|])/ig; + var ID_REGEX = /\$id\$(\d+)/g; - function escapeHtml(text) { - return text.replace(/&/g,'&').replace(//g,'>'); - } - function addHtmlLinks(text) { - text = escapeHtml(ko.utils.unwrapObservable(text)); + text = _.escape(ko.utils.unwrapObservable(text)); + return insertLinks(text); + } + + function insertLinks(text) { return text.replace(URL_REGEX, function (url) { var parens = false; if (url.charAt(0) === '(' && url.charAt(url.length - 1) === ')') { @@ -21,7 +22,12 @@ define(['ko'], function (ko) { return htmlLink.join(''); }); } - + + function addHtml(text) { + text = addHtmlLinks(text); + return text.replace(ID_REGEX, '$1'); + } + ko.bindingHandlers.fadeVisible = { init: function(element, valueAccessor) { var value = valueAccessor(); @@ -42,6 +48,7 @@ define(['ko'], function (ko) { } return { + addHtml: addHtml, addHtmlLinks: addHtmlLinks, getRandomItem: getRandomItem }; diff --git a/views/project.html b/views/project.html index 4e5f25b..5b3b91b 100644 --- a/views/project.html +++ b/views/project.html @@ -1,3 +1,4 @@ + <%= title %> - Omega Issue Tracker