From 1caf1e6b45eb339b34db519ce63cbede7673b058 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 26 Feb 2013 14:54:43 -0500 Subject: [PATCH] Clean up discourse.js --- .../javascripts/admin/routes/admin_routes.js | 2 +- app/assets/javascripts/discourse.js | 364 +++++------------- .../discourse/components/click_track.js | 2 +- .../discourse/components/development.js | 158 ++++++++ .../javascripts/discourse/components/url.js | 69 ++++ .../controllers/composer_controller.js | 4 +- .../controllers/header_controller.js | 8 +- .../discourse/controllers/topic_controller.js | 9 +- .../user_private_messages_controller.js | 2 +- .../discourse/models/topic_list.js | 2 +- .../discourse/routes/application_routes.js | 2 +- .../discourse/routes/discourse_route.js | 11 + .../discourse/routes/topic_route.js | 5 +- .../discourse/templates/header.js.handlebars | 24 +- .../discourse/views/header_view.js | 21 + .../views/modal/edit_category_view.js | 2 +- .../views/modal/move_selected_view.js | 4 +- .../discourse/views/search/search_view.js | 9 +- .../javascripts/discourse/views/topic_view.js | 8 +- app/assets/javascripts/external/ember.js | 336 ++++++++++------ .../javascripts/external_production/ember.js | 313 ++++++++++----- 21 files changed, 818 insertions(+), 537 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/development.js create mode 100644 app/assets/javascripts/discourse/components/url.js diff --git a/app/assets/javascripts/admin/routes/admin_routes.js b/app/assets/javascripts/admin/routes/admin_routes.js index bb583fdc95d..f3a2a09a47b 100644 --- a/app/assets/javascripts/admin/routes/admin_routes.js +++ b/app/assets/javascripts/admin/routes/admin_routes.js @@ -4,7 +4,7 @@ @method buildRoutes @for Discourse.AdminRoute **/ -Discourse.buildRoutes(function() { +Discourse.Route.buildRoutes(function() { this.resource('admin', { path: '/admin' }, function() { this.route('dashboard', { path: '/' }); this.route('site_settings', { path: '/site_settings' }); diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 70932d6799b..4607c6a740a 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -1,31 +1,28 @@ /*global Modernizr:true*/ /*global assetPath:true*/ -var csrf_token; +/** + The main Discourse Application + @class Discourse + @extends Ember.Application +**/ Discourse = Ember.Application.createWithMixins({ rootElement: '#main', // Data we want to remember for a short period transient: Em.Object.create(), + // Whether the app has focus or not hasFocus: true, + + // Are we currently scrolling? scrolling: false, // The highest seen post number by topic highestSeenByTopic: {}, - logoSmall: (function() { - var logo; - logo = Discourse.SiteSettings.logo_small_url; - if (logo && logo.length > 1) { - return ""; - } else { - return ""; - } - }).property(), - - titleChanged: (function() { + titleChanged: function() { var title; title = ""; if (this.get('title')) { @@ -37,51 +34,45 @@ Discourse = Ember.Application.createWithMixins({ title = "(*) " + title; } // chrome bug workaround see: http://stackoverflow.com/questions/2952384/changing-the-window-title-when-focussing-the-window-doesnt-work-in-chrome - window.setTimeout((function() { + window.setTimeout(function() { document.title = "."; document.title = title; - }), 200); - }).observes('title', 'hasFocus', 'notify'), + }, 200); + }.observes('title', 'hasFocus', 'notify'), - currentUserChanged: (function() { - var bus, user; - bus = Discourse.MessageBus; + currentUserChanged: function() { // We don't want to receive any previous user notifications + var bus = Discourse.MessageBus; bus.unsubscribe("/notification/*"); bus.callbackInterval = Discourse.SiteSettings.anon_polling_interval; bus.enableLongPolling = false; - user = this.get('currentUser'); + + var user = this.get('currentUser'); if (user) { bus.callbackInterval = Discourse.SiteSettings.polling_interval; bus.enableLongPolling = true; if (user.admin) { bus.subscribe("/flagged_counts", function(data) { - return user.set('site_flagged_posts_count', data.total); + user.set('site_flagged_posts_count', data.total); }); } - return bus.subscribe("/notification/" + user.id, (function(data) { + bus.subscribe("/notification/" + user.id, (function(data) { user.set('unread_notifications', data.unread_notifications); - return user.set('unread_private_messages', data.unread_private_messages); + user.set('unread_private_messages', data.unread_private_messages); }), user.notification_channel_position); } - }).observes('currentUser'), - notifyTitle: function() { - return this.set('notify', true); - }, + }.observes('currentUser'), - // Browser aware replaceState - replaceState: function(path) { - if (window.history && - window.history.pushState && - window.history.replaceState && - !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)) { - if (window.location.pathname !== path) { - return history.replaceState({ - path: path - }, null, path); - } - } + // The classes of buttons to show on a post + postButtons: function() { + return Discourse.SiteSettings.post_menu.split("|").map(function(i) { + return (i.replace(/\+/, '').capitalize()); + }); + }.property('Discourse.SiteSettings.post_menu'), + + notifyTitle: function() { + this.set('notify', true); }, openComposer: function(opts) { @@ -90,291 +81,108 @@ Discourse = Ember.Application.createWithMixins({ if (composer) composer.open(opts); }, - // Like router.route, but allow full urls rather than relative one - // HERE BE HACKS - uses the ember container for now until we can do this nicer. - routeTo: function(path) { - var newMatches, newTopicId, oldMatches, oldTopicId, opts, router, topicController, topicRegexp; - path = path.replace(/https?\:\/\/[^\/]+/, ''); - - // If we're in the same topic, don't push the state - topicRegexp = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/; - newMatches = topicRegexp.exec(path); - newTopicId = newMatches ? newMatches[2] : null; - if (newTopicId) { - oldMatches = topicRegexp.exec(window.location.pathname); - if ((oldTopicId = oldMatches ? oldMatches[2] : void 0) && (oldTopicId === newTopicId)) { - Discourse.replaceState(path); - topicController = Discourse.__container__.lookup('controller:topic'); - opts = { - trackVisit: false - }; - if (newMatches[3]) { - opts.nearPost = newMatches[3]; - } - topicController.get('content').loadPosts(opts); - return; - } - } - // Be wary of looking up the router. In this case, we have links in our - // HTML, say form compiled markdown posts, that need to be routed. - router = Discourse.__container__.lookup('router:main'); - router.router.updateURL(path); - return router.handleURL(path); - }, - - // The classes of buttons to show on a post - postButtons: (function() { - return Discourse.SiteSettings.post_menu.split("|").map(function(i) { - return "" + (i.replace(/\+/, '').capitalize()); - }); - }).property('Discourse.SiteSettings.post_menu'), + /** + Establishes global DOM events and bindings via jQuery. + @method bindDOMEvents + **/ bindDOMEvents: function() { - var $html, hasTouch, - _this = this; - $html = $('html'); + var $html, hasTouch; - /* Add the discourse touch event */ + $html = $('html'); hasTouch = false; + if ($html.hasClass('touch')) { hasTouch = true; } + if (Modernizr.prefixed("MaxTouchPoints", navigator) > 1) { hasTouch = true; } + if (hasTouch) { $html.addClass('discourse-touch'); this.touch = true; this.hasTouch = true; - } else { $html.addClass('discourse-no-touch'); this.touch = false; } + $('#main').on('click.discourse', '[data-not-implemented=true]', function(e) { e.preventDefault(); alert(Em.String.i18n('not_implemented')); return false; }); + $('#main').on('click.discourse', 'a', function(e) { - var $currentTarget, href; - if (e.isDefaultPrevented() || e.metaKey || e.ctrlKey) { - return; - } - $currentTarget = $(e.currentTarget); - href = $currentTarget.attr('href'); - if (href === void 0) { - return; - } - if (href === '#') { - return; - } - if ($currentTarget.attr('target')) { - return; - } - if ($currentTarget.data('auto-route')) { - return; - } - if ($currentTarget.hasClass('lightbox')) { - return; - } - if (href.indexOf("mailto:") === 0) { - return; - } - if (href.match(/^http[s]?:\/\//i) && !href.match(new RegExp("^http:\\/\\/" + window.location.hostname, "i"))) { - return; - } + if (e.isDefaultPrevented() || e.metaKey || e.ctrlKey) return; + + var $currentTarget = $(e.currentTarget); + var href = $currentTarget.attr('href'); + if (!href) return; + if (href === '#') return; + if ($currentTarget.attr('target')) return; + if ($currentTarget.data('auto-route')) return; + if ($currentTarget.hasClass('lightbox')) return; + if (href.indexOf("mailto:") === 0) return; + if (href.match(/^http[s]?:\/\//i) && !href.match(new RegExp("^http:\\/\\/" + window.location.hostname, "i"))) return; + e.preventDefault(); - _this.routeTo(href); + Discourse.URL.routeTo(href); return false; }); - return $(window).focus(function() { - _this.set('hasFocus', true); - return _this.set('notify', false); + + $(window).focus(function() { + Discourse.set('hasFocus', true); + Discourse.set('notify', false); }).blur(function() { - return _this.set('hasFocus', false); + Discourse.set('hasFocus', false); }); - }, - logout: function() { - var username, - _this = this; - username = this.get('currentUser.username'); - Discourse.KeyValueStore.abandonLocal(); - return jQuery.ajax("/session/" + username, { - type: 'DELETE', - success: function(result) { - /* To keep lots of our variables unbound, we can handle a redirect on logging out. - */ - return window.location.reload(); + + // Add a CSRF token to all AJAX requests + var csrfToken = $('meta[name=csrf-token]').attr('content'); + jQuery.ajaxPrefilter(function(options, originalOptions, xhr) { + if (!options.crossDomain) { + xhr.setRequestHeader('X-CSRF-Token', csrfToken); } }); }, - /* fancy probes in ember - */ - insertProbes: function() { - var topLevel; - if (typeof console === "undefined" || console === null) { - return; - } - topLevel = function(fn, name) { - return window.probes.measure(fn, { - name: name, - before: function(data, owner, args) { - if (owner) { - return window.probes.clear(); - } - }, - after: function(data, owner, args) { - var ary, f, n, v, _ref; - if (owner && data.time > 10) { - f = function(name, data) { - if (data && data.count) { - return "" + name + " - " + data.count + " calls " + ((data.time + 0.0).toFixed(2)) + "ms"; - } - }; - if (console && console.group) { - console.group(f(name, data)); - } else { - console.log(""); - console.log(f(name, data)); - } - ary = []; - _ref = window.probes; - for (n in _ref) { - v = _ref[n]; - if (n === name || v.time < 1) { - continue; - } - ary.push({ - k: n, - v: v - }); - } - ary.sortBy(function(item) { - if (item.v && item.v.time) { - return -item.v.time; - } else { - return 0; - } - }).each(function(item) { - var output = f("" + item.k, item.v); - if (output) { - return console.log(output); - } - }); - if (typeof console !== "undefined" && console !== null) { - if (typeof console.groupEnd === "function") { - console.groupEnd(); - } - } - return window.probes.clear(); - } - } - }); - }; - Ember.View.prototype.renderToBuffer = window.probes.measure(Ember.View.prototype.renderToBuffer, "renderToBuffer"); - Discourse.routeTo = topLevel(Discourse.routeTo, "Discourse.routeTo"); - Ember.run.end = topLevel(Ember.run.end, "Ember.run.end"); + /** + Log the current user out of Discourse + + @method logout + **/ + logout: function() { + Discourse.KeyValueStore.abandonLocal(); + return jQuery.ajax("/session/" + this.get('currentUser.username'), { + type: 'DELETE', + success: function(result) { + // To keep lots of our variables unbound, we can handle a redirect on logging out. + window.location.reload(); + } + }); }, + authenticationComplete: function(options) { // TODO, how to dispatch this to the view without the container? var loginView; loginView = Discourse.__container__.lookup('controller:modal').get('currentView'); return loginView.authenticationComplete(options); }, - buildRoutes: function(builder) { - var oldBuilder; - oldBuilder = Discourse.routeBuilder; - Discourse.routeBuilder = function() { - if (oldBuilder) { - oldBuilder.call(this); - } - return builder.call(this); - }; - }, + start: function() { - this.bindDOMEvents(); + Discourse.bindDOMEvents(); Discourse.SiteSettings = PreloadStore.getStatic('siteSettings'); Discourse.MessageBus.start(); Discourse.KeyValueStore.init("discourse_", Discourse.MessageBus); - Discourse.insertProbes(); - // subscribe to any site customizations that are loaded - $('link.custom-css').each(function() { - var id, split, stylesheet, - _this = this; - split = this.href.split("/"); - id = split[split.length - 1].split(".css")[0]; - stylesheet = this; - return Discourse.MessageBus.subscribe("/file-change/" + id, function(data) { - var orig, sp; - if (!$(stylesheet).data('orig')) { - $(stylesheet).data('orig', stylesheet.href); - } - orig = $(stylesheet).data('orig'); - sp = orig.split(".css?"); - stylesheet.href = sp[0] + ".css?" + data; - }); - }); - $('header.custom').each(function() { - var header; - header = $(this); - return Discourse.MessageBus.subscribe("/header-change/" + ($(this).data('key')), function(data) { - return header.html(data); - }); - }); - - // possibly move this to dev only - return Discourse.MessageBus.subscribe("/file-change", function(data) { - Ember.TEMPLATES.empty = Handlebars.compile("
"); - return data.each(function(me) { - var js; - if (me === "refresh") { - return document.location.reload(true); - } else if (me.name.substr(-10) === "handlebars") { - js = me.name.replace(".handlebars", "").replace("app/assets/javascripts", "/assets"); - return $LAB.script(js + "?hash=" + me.hash).wait(function() { - var templateName; - templateName = js.replace(".js", "").replace("/assets/", ""); - return jQuery.each(Ember.View.views, function() { - var _this = this; - if (this.get('templateName') === templateName) { - this.set('templateName', 'empty'); - this.rerender(); - return Em.run.next(function() { - _this.set('templateName', templateName); - return _this.rerender(); - }); - } - }); - }); - } else { - return $('link').each(function() { - if (this.href.match(me.name) && me.hash) { - if (!$(this).data('orig')) { - $(this).data('orig', this.href); - } - this.href = $(this).data('orig') + "&hash=" + me.hash; - } - }); - } - }); - }); + // Developer specific functions + Discourse.Development.setupProbes(); + Discourse.Development.observeLiveChanges(); } + }); -Discourse.Router = Discourse.Router.reopen({ - location: 'discourse_location' -}); - -// since we have no jquery-rails these days, hook up csrf token -csrf_token = $('meta[name=csrf-token]').attr('content'); - -jQuery.ajaxPrefilter(function(options, originalOptions, xhr) { - if (!options.crossDomain) { - xhr.setRequestHeader('X-CSRF-Token', csrf_token); - } -}); - - +Discourse.Router = Discourse.Router.reopen({ location: 'discourse_location' }); \ No newline at end of file diff --git a/app/assets/javascripts/discourse/components/click_track.js b/app/assets/javascripts/discourse/components/click_track.js index 7f2e66cdf07..df9b68943f8 100644 --- a/app/assets/javascripts/discourse/components/click_track.js +++ b/app/assets/javascripts/discourse/components/click_track.js @@ -88,7 +88,7 @@ Discourse.ClickTrack = { topic_id: topicId, redirect: false }); - Discourse.routeTo(href); + Discourse.URL.routeTo(href); return false; } diff --git a/app/assets/javascripts/discourse/components/development.js b/app/assets/javascripts/discourse/components/development.js new file mode 100644 index 00000000000..7e1d314fa87 --- /dev/null +++ b/app/assets/javascripts/discourse/components/development.js @@ -0,0 +1,158 @@ +/** + Functions to help development of Discourse, such as inserting probes + + @class Development + @namespace Discourse + @module Discourse +**/ +Discourse.Development = { + + + /** + Set up probes for performance measurements. + + @method setupProbes + **/ + setupProbes: function() { + + // Don't probe if we don't have a console + if (typeof console === "undefined" || console === null) return; + + var topLevel = function(fn, name) { + return window.probes.measure(fn, { + name: name, + + before: function(data, owner, args) { + if (owner) { + return window.probes.clear(); + } + }, + + after: function(data, owner, args) { + var ary, f, n, v, _ref; + if (owner && data.time > 10) { + + f = function(name, data) { + if (data && data.count) return name + " - " + data.count + " calls " + ((data.time + 0.0).toFixed(2)) + "ms"; + }; + + if (console && console.group) { + console.group(f(name, data)); + } else { + console.log(""); + console.log(f(name, data)); + } + + ary = []; + _ref = window.probes; + for (n in _ref) { + v = _ref[n]; + if (n === name || v.time < 1) { + continue; + } + ary.push({ + k: n, + v: v + }); + } + ary.sortBy(function(item) { + if (item.v && item.v.time) { + return -item.v.time; + } else { + return 0; + } + }).each(function(item) { + var output = f("" + item.k, item.v); + if (output) { + return console.log(output); + } + }); + if (typeof console !== "undefined" && console !== null) { + if (typeof console.groupEnd === "function") { + console.groupEnd(); + } + } + return window.probes.clear(); + } + } + }); + }; + + Ember.View.prototype.renderToBuffer = window.probes.measure(Ember.View.prototype.renderToBuffer, "renderToBuffer"); + Discourse.URL.routeTo = topLevel(Discourse.URL.routeTo, "Discourse.URL.routeTo"); + Ember.run.end = topLevel(Ember.run.end, "Ember.run.end"); + }, + + /** + Use the message bus for live reloading of components for faster development. + + @method observeLiveChanges + **/ + observeLiveChanges: function() { + + // subscribe to any site customizations that are loaded + $('link.custom-css').each(function() { + var id, split, stylesheet, + _this = this; + split = this.href.split("/"); + id = split[split.length - 1].split(".css")[0]; + stylesheet = this; + return Discourse.MessageBus.subscribe("/file-change/" + id, function(data) { + var orig, sp; + if (!$(stylesheet).data('orig')) { + $(stylesheet).data('orig', stylesheet.href); + } + orig = $(stylesheet).data('orig'); + sp = orig.split(".css?"); + stylesheet.href = sp[0] + ".css?" + data; + }); + }); + + // Custom header changes + $('header.custom').each(function() { + var header; + header = $(this); + return Discourse.MessageBus.subscribe("/header-change/" + ($(this).data('key')), function(data) { + return header.html(data); + }); + }); + + // Observe file changes + return Discourse.MessageBus.subscribe("/file-change", function(data) { + Ember.TEMPLATES.empty = Handlebars.compile("
"); + return data.each(function(me) { + var js; + if (me === "refresh") { + return document.location.reload(true); + } else if (me.name.substr(-10) === "handlebars") { + js = me.name.replace(".handlebars", "").replace("app/assets/javascripts", "/assets"); + return $LAB.script(js + "?hash=" + me.hash).wait(function() { + var templateName; + templateName = js.replace(".js", "").replace("/assets/", ""); + return jQuery.each(Ember.View.views, function() { + var _this = this; + if (this.get('templateName') === templateName) { + this.set('templateName', 'empty'); + this.rerender(); + return Em.run.next(function() { + _this.set('templateName', templateName); + return _this.rerender(); + }); + } + }); + }); + } else { + return $('link').each(function() { + if (this.href.match(me.name) && me.hash) { + if (!$(this).data('orig')) { + $(this).data('orig', this.href); + } + this.href = $(this).data('orig') + "&hash=" + me.hash; + } + }); + } + }); + }); + } + +}; \ No newline at end of file diff --git a/app/assets/javascripts/discourse/components/url.js b/app/assets/javascripts/discourse/components/url.js new file mode 100644 index 00000000000..b4ff5b4e6cf --- /dev/null +++ b/app/assets/javascripts/discourse/components/url.js @@ -0,0 +1,69 @@ +/** + URL related functions. + + @class URL + @namespace Discourse + @module Discourse +**/ +Discourse.URL = { + + /** + Browser aware replaceState. Will only be invoked if the browser supports it. + + @method replaceState + @param {String} path The path we are replacing our history state with. + **/ + replaceState: function(path) { + if (window.history && + window.history.pushState && + window.history.replaceState && + !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/) && + (window.location.pathname !== path)) { + return history.replaceState({ path: path }, null, path); + } + }, + + /** + Our custom routeTo method is used to intelligently overwrite default routing + behavior. + + It contains the logic necessary to route within a topic using replaceState to + keep the history intact. + + Note that currently it uses `__container__` which is not advised + but there is no other way to access the router. + + @method routeTo + @param {String} path The path we are routing to. + **/ + routeTo: function(path) { + var newMatches, newTopicId, oldMatches, oldTopicId, opts, router, topicController, topicRegexp; + path = path.replace(/https?\:\/\/[^\/]+/, ''); + + console.log("route to: " + path); + + // If we're in the same topic, don't push the state + topicRegexp = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/; + newMatches = topicRegexp.exec(path); + newTopicId = newMatches ? newMatches[2] : null; + if (newTopicId) { + oldMatches = topicRegexp.exec(window.location.pathname); + if ((oldTopicId = oldMatches ? oldMatches[2] : void 0) && (oldTopicId === newTopicId)) { + Discourse.URL.replaceState(path); + topicController = Discourse.__container__.lookup('controller:topic'); + opts = { trackVisit: false }; + if (newMatches[3]) { + opts.nearPost = newMatches[3]; + } + topicController.get('content').loadPosts(opts); + return; + } + } + // Be wary of looking up the router. In this case, we have links in our + // HTML, say form compiled markdown posts, that need to be routed. + router = Discourse.__container__.lookup('router:main'); + router.router.updateURL(path); + return router.handleURL(path); + } + +}; \ No newline at end of file diff --git a/app/assets/javascripts/discourse/controllers/composer_controller.js b/app/assets/javascripts/discourse/controllers/composer_controller.js index d19b946a855..7666505be5b 100644 --- a/app/assets/javascripts/discourse/controllers/composer_controller.js +++ b/app/assets/javascripts/discourse/controllers/composer_controller.js @@ -42,7 +42,7 @@ Discourse.ComposerController = Discourse.Controller.extend({ } else { Discourse.set('currentUser.reply_count', Discourse.get('currentUser.reply_count') + 1); } - Discourse.routeTo(opts.post.get('url')); + Discourse.URL.routeTo(opts.post.get('url')); }, function(error) { composer.set('disableDrafts', false); bootbox.alert(error); @@ -159,7 +159,7 @@ Discourse.ComposerController = Discourse.Controller.extend({ // View a new reply we've made viewNewReply: function() { - Discourse.routeTo(this.get('createdPost.url')); + Discourse.URL.routeTo(this.get('createdPost.url')); this.close(); return false; }, diff --git a/app/assets/javascripts/discourse/controllers/header_controller.js b/app/assets/javascripts/discourse/controllers/header_controller.js index 716282bebe7..6d60c8437db 100644 --- a/app/assets/javascripts/discourse/controllers/header_controller.js +++ b/app/assets/javascripts/discourse/controllers/header_controller.js @@ -1,21 +1,21 @@ /** This controller supports actions on the site header - @class HeaderController + @class HeaderController @extends Discourse.Controller @namespace Discourse @module Discourse -**/ +**/ Discourse.HeaderController = Discourse.Controller.extend({ topic: null, - showExtraInfo: false, + showExtraInfo: null, toggleStar: function() { var topic = this.get('topic'); if (topic) topic.toggleStar(); return false; } - + }); diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js b/app/assets/javascripts/discourse/controllers/topic_controller.js index 50c7caa6669..f1c503b0d85 100644 --- a/app/assets/javascripts/discourse/controllers/topic_controller.js +++ b/app/assets/javascripts/discourse/controllers/topic_controller.js @@ -10,7 +10,6 @@ Discourse.TopicController = Discourse.ObjectController.extend({ userFilters: new Em.Set(), multiSelect: false, bestOf: false, - showExtraHeaderInfo: false, needs: ['header', 'modal', 'composer', 'quoteButton'], filter: (function() { @@ -42,10 +41,6 @@ Discourse.TopicController = Discourse.ObjectController.extend({ return this.get('canDeleteSelected'); }).property('canDeleteSelected'), - showExtraHeaderInfoChanged: (function() { - this.set('controllers.header.showExtraInfo', this.get('showExtraHeaderInfo')); - }).observes('showExtraHeaderInfo'), - canDeleteSelected: (function() { var canDelete, selectedPosts; selectedPosts = this.get('selectedPosts'); @@ -109,11 +104,11 @@ Discourse.TopicController = Discourse.ObjectController.extend({ }, jumpTop: function() { - Discourse.routeTo(this.get('content.url')); + Discourse.URL.routeTo(this.get('content.url')); }, jumpBottom: function() { - Discourse.routeTo(this.get('content.lastPostUrl')); + Discourse.URL.routeTo(this.get('content.lastPostUrl')); }, cancelFilter: function() { diff --git a/app/assets/javascripts/discourse/controllers/user_private_messages_controller.js b/app/assets/javascripts/discourse/controllers/user_private_messages_controller.js index 608bc2b54fc..eea4d849bcc 100644 --- a/app/assets/javascripts/discourse/controllers/user_private_messages_controller.js +++ b/app/assets/javascripts/discourse/controllers/user_private_messages_controller.js @@ -9,7 +9,7 @@ Discourse.UserPrivateMessagesController = Discourse.ObjectController.extend({ editPreferences: function() { - return Discourse.routeTo("/users/" + (this.get('content.username_lower')) + "/preferences"); + return Discourse.URL.routeTo("/users/" + (this.get('content.username_lower')) + "/preferences"); }, composePrivateMessage: function() { diff --git a/app/assets/javascripts/discourse/models/topic_list.js b/app/assets/javascripts/discourse/models/topic_list.js index c5781bac165..32f40ebc629 100644 --- a/app/assets/javascripts/discourse/models/topic_list.js +++ b/app/assets/javascripts/discourse/models/topic_list.js @@ -14,7 +14,7 @@ Discourse.TopicList = Discourse.Model.extend({ _this = this; promise = new RSVP.Promise(); if (moreUrl = this.get('more_topics_url')) { - Discourse.replaceState("/" + (this.get('filter')) + "/more"); + Discourse.URL.replaceState("/" + (this.get('filter')) + "/more"); jQuery.ajax(moreUrl, { success: function(result) { var newTopics, topicIds, topics; diff --git a/app/assets/javascripts/discourse/routes/application_routes.js b/app/assets/javascripts/discourse/routes/application_routes.js index 744d89af7bb..a491962d1d6 100644 --- a/app/assets/javascripts/discourse/routes/application_routes.js +++ b/app/assets/javascripts/discourse/routes/application_routes.js @@ -4,7 +4,7 @@ @method buildRoutes @for Discourse.ApplicationRoute **/ -Discourse.buildRoutes(function() { +Discourse.Route.buildRoutes(function() { var router = this; // Topic routes diff --git a/app/assets/javascripts/discourse/routes/discourse_route.js b/app/assets/javascripts/discourse/routes/discourse_route.js index c76fc682f89..7489189465f 100644 --- a/app/assets/javascripts/discourse/routes/discourse_route.js +++ b/app/assets/javascripts/discourse/routes/discourse_route.js @@ -28,3 +28,14 @@ Discourse.Route = Em.Route.extend({ }); +Discourse.Route.reopenClass({ + + buildRoutes: function(builder) { + var oldBuilder = Discourse.routeBuilder; + Discourse.routeBuilder = function() { + if (oldBuilder) oldBuilder.call(this); + return builder.call(this); + }; + } + +}); diff --git a/app/assets/javascripts/discourse/routes/topic_route.js b/app/assets/javascripts/discourse/routes/topic_route.js index 5145062bcb4..9c7e7436d0c 100644 --- a/app/assets/javascripts/discourse/routes/topic_route.js +++ b/app/assets/javascripts/discourse/routes/topic_route.js @@ -34,10 +34,7 @@ Discourse.TopicRoute = Discourse.Route.extend({ }, setupController: function(controller, model) { - var headerController; - controller.set('showExtraHeaderInfo', false); - headerController = this.controllerFor('header'); - headerController.set('topic', model); + this.controllerFor('header').set('topic', model); } }); diff --git a/app/assets/javascripts/discourse/templates/header.js.handlebars b/app/assets/javascripts/discourse/templates/header.js.handlebars index ab13bc0f1a4..98488b61568 100644 --- a/app/assets/javascripts/discourse/templates/header.js.handlebars +++ b/app/assets/javascripts/discourse/templates/header.js.handlebars @@ -1,17 +1,7 @@
-
- {{#if controller.showExtraInfo}} - - {{#linkTo list.popular}}{{{Discourse.logoSmall}}}{{/linkTo}} - - {{else}} - - {{#linkTo list.popular}}{{/linkTo}} - - {{/if}} -
+ {{view.logoHTML}} {{view Discourse.TopicExtraInfoView}}
@@ -24,7 +14,7 @@ {{/if}}
{{/unless}} -
    +
    • {{#if view.currentUser}} @@ -38,7 +28,7 @@ {{/if}}
    • -
    • +
    • +
    • {{#if view.currentUser}} {{#titledLinkTo user.activity view.currentUser titleKey="current_user" class="icon"}}{{avatar Discourse.currentUser imageSize="medium" }}{{/titledLinkTo}} @@ -58,7 +48,7 @@ {{view Discourse.SearchView currentUserBinding="view.currentUser"}} -
      +
      {{#if view.notifications}}
        {{#each view.notifications}} @@ -73,7 +63,7 @@ {{/if}}
      -
      +
        {{#if Discourse.currentUser.admin}}
      • {{i18n admin_title}}
      • @@ -99,7 +89,7 @@
      • {{#linkTo "list.categories"}}{{i18n filters.categories.title}}{{/linkTo}}
      • - + {{#each view.categories}}
      • {{categoryLink this}} diff --git a/app/assets/javascripts/discourse/views/header_view.js b/app/assets/javascripts/discourse/views/header_view.js index 5ef8cc14285..7351dc4f7d8 100644 --- a/app/assets/javascripts/discourse/views/header_view.js +++ b/app/assets/javascripts/discourse/views/header_view.js @@ -81,6 +81,27 @@ Discourse.HeaderView = Discourse.View.extend({ } }, + /** + Display the correct logo in the header, showing a custom small icon if it exists. + + @property logoHTML + **/ + logoHTML: function() { + var result = ""; + return new Handlebars.SafeString(result); + }.property('controller.showExtraInfo'), + willDestroyElement: function() { $(window).unbind('scroll.discourse-dock'); return $(document).unbind('touchmove.discourse-dock'); diff --git a/app/assets/javascripts/discourse/views/modal/edit_category_view.js b/app/assets/javascripts/discourse/views/modal/edit_category_view.js index 398062ddb09..480e850ae96 100644 --- a/app/assets/javascripts/discourse/views/modal/edit_category_view.js +++ b/app/assets/javascripts/discourse/views/modal/edit_category_view.js @@ -42,7 +42,7 @@ Discourse.EditCategoryView = Discourse.ModalBodyView.extend({ showCategoryTopic: function() { $('#discourse-modal').modal('hide'); - Discourse.routeTo(this.get('category.topic_url')); + Discourse.URL.routeTo(this.get('category.topic_url')); return false; }, diff --git a/app/assets/javascripts/discourse/views/modal/move_selected_view.js b/app/assets/javascripts/discourse/views/modal/move_selected_view.js index f485fec3aa5..132ac94d0d8 100644 --- a/app/assets/javascripts/discourse/views/modal/move_selected_view.js +++ b/app/assets/javascripts/discourse/views/modal/move_selected_view.js @@ -36,8 +36,8 @@ Discourse.MoveSelectedView = Discourse.ModalBodyView.extend({ Discourse.Topic.movePosts(this.get('topic.id'), this.get('topicName'), postIds).then(function(result) { if (result.success) { $('#discourse-modal').modal('hide'); - return Em.run.next(function() { - return Discourse.routeTo(result.url); + Em.run.next(function() { + Discourse.URL.routeTo(result.url); }); } else { _this.flash(Em.String.i18n('topic.move_selected.error')); diff --git a/app/assets/javascripts/discourse/views/search/search_view.js b/app/assets/javascripts/discourse/views/search/search_view.js index 55f4c733f41..21ad26bf212 100644 --- a/app/assets/javascripts/discourse/views/search/search_view.js +++ b/app/assets/javascripts/discourse/views/search/search_view.js @@ -139,13 +139,10 @@ window.Discourse.SearchView = Discourse.View.extend({ }, select: function() { - var href; - if (this.get('loading')) { - return; - } - href = $('#search-dropdown li.selected a').prop('href'); + if (this.get('loading')) return; + var href = $('#search-dropdown li.selected a').prop('href'); if (href) { - Discourse.routeTo(href); + Discourse.URL.routeTo(href); } return false; } diff --git a/app/assets/javascripts/discourse/views/topic_view.js b/app/assets/javascripts/discourse/views/topic_view.js index 5773732660a..8872b292bbf 100644 --- a/app/assets/javascripts/discourse/views/topic_view.js +++ b/app/assets/javascripts/discourse/views/topic_view.js @@ -84,7 +84,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { postUrl += "/best_of"; } } - Discourse.replaceState(postUrl); + Discourse.URL.replaceState(postUrl); // Show appropriate jump tools if (current === 1) { @@ -441,10 +441,12 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { this.docAt = title.offset().top; } } + + var headerController = this.get('controller.controllers.header'); if (this.docAt) { - this.set('controller.showExtraHeaderInfo', offset >= this.docAt || !firstLoaded); + headerController.set('showExtraInfo', offset >= this.docAt || !firstLoaded); } else { - this.set('controller.showExtraHeaderInfo', !firstLoaded); + headerController.set('showExtraInfo', !firstLoaded); } // there is a whole bunch of caching we could add here diff --git a/app/assets/javascripts/external/ember.js b/app/assets/javascripts/external/ember.js index 158146f5f4c..87b273b78be 100644 --- a/app/assets/javascripts/external/ember.js +++ b/app/assets/javascripts/external/ember.js @@ -1,5 +1,5 @@ -// Version: v1.0.0-pre.2-723-g052062c -// Last commit: 052062c (2013-02-18 19:32:17 -0800) +// Version: v1.0.0-pre.2-756-gb26f1f0 +// Last commit: b26f1f0 (2013-02-26 09:03:26 -0800) (function() { @@ -150,8 +150,8 @@ Ember.deprecateFunc = function(message, func) { })(); -// Version: v1.0.0-pre.2-723-g052062c -// Last commit: 052062c (2013-02-18 19:32:17 -0800) +// Version: v1.0.0-pre.2-756-gb26f1f0 +// Last commit: b26f1f0 (2013-02-26 09:03:26 -0800) (function() { @@ -297,6 +297,15 @@ Ember.LOG_STACKTRACE_ON_DEPRECATION = (Ember.ENV.LOG_STACKTRACE_ON_DEPRECATION ! */ Ember.SHIM_ES5 = (Ember.ENV.SHIM_ES5 === false) ? false : Ember.EXTEND_PROTOTYPES; +/** + Determines whether Ember logs info about version of used libraries + + @property LOG_VERSION + @type Boolean + @default true +*/ +Ember.LOG_VERSION = (Ember.ENV.LOG_VERSION === false) ? false : true; + /** Empty function. Useful for some operations. @@ -1752,7 +1761,7 @@ var MapWithDefault = Ember.MapWithDefault = function(options) { @static @param [options] @param {anything} [options.defaultValue] - @return {Ember.MapWithDefault|Ember.Map} If options are passed, returns + @return {Ember.MapWithDefault|Ember.Map} If options are passed, returns `Ember.MapWithDefault` otherwise returns `Ember.Map` */ MapWithDefault.create = function(options) { @@ -1826,7 +1835,7 @@ var FIRST_KEY = /^([^\.\*]+)/; If you plan to run on IE8 and older browsers then you should use this method anytime you want to retrieve a property on an object that you don't - know for sure is private. (Properties beginning with an underscore '_' + know for sure is private. (Properties beginning with an underscore '_' are considered private.) On all newer browsers, you only need to use this method to retrieve @@ -1888,7 +1897,7 @@ get = function get(obj, keyName) { If you plan to run on IE8 and older browsers then you should use this method anytime you want to set a property on an object that you don't - know for sure is private. (Properties beginning with an underscore '_' + know for sure is private. (Properties beginning with an underscore '_' are considered private.) On all newer browsers, you only need to use this method to set @@ -4229,7 +4238,7 @@ Ember.RunLoop = RunLoop; ```javascript Ember.run(function(){ - // code to be execute within a RunLoop + // code to be execute within a RunLoop }); ``` @@ -4268,7 +4277,7 @@ var run = Ember.run; ```javascript Ember.run.begin(); - // code to be execute within a RunLoop + // code to be execute within a RunLoop Ember.run.end(); ``` @@ -4286,7 +4295,7 @@ Ember.run.begin = function() { ```javascript Ember.run.begin(); - // code to be execute within a RunLoop + // code to be execute within a RunLoop Ember.run.end(); ``` @@ -6123,7 +6132,6 @@ define("container", register: function(type, name, factory, options) { var fullName; - if (type.indexOf(':') !== -1){ options = factory; factory = name; @@ -6133,15 +6141,23 @@ define("container", fullName = type + ":" + name; } - this.registry.set(fullName, factory); - this._options.set(fullName, options || {}); + var normalizedName = this.normalize(fullName); + + this.registry.set(normalizedName, factory); + this._options.set(normalizedName, options || {}); }, resolve: function(fullName) { return this.resolver(fullName) || this.registry.get(fullName); }, + normalize: function(fullName) { + return fullName; + }, + lookup: function(fullName, options) { + fullName = this.normalize(fullName); + options = options || {}; if (this.cache.has(fullName) && options.singleton !== false) { @@ -6270,7 +6286,8 @@ define("container", } function factoryFor(container, fullName) { - return container.resolve(fullName); + var name = container.normalize(fullName); + return container.resolve(name); } function instantiate(container, fullName) { @@ -6745,6 +6762,20 @@ Ember.Error.prototype = Ember.create(Error.prototype); +(function() { +/** + Expose RSVP implementation + + @class RSVP + @namespace Ember + @constructor +*/ +Ember.RSVP = requireModule('rsvp'); + +})(); + + + (function() { /** @module ember @@ -7358,7 +7389,7 @@ Ember.Enumerable = Ember.Mixin.create( @method nextObject @param {Number} index the current index of the iteration - @param {Object} previousObject the value returned by the last call to + @param {Object} previousObject the value returned by the last call to `nextObject`. @param {Object} context a context object you can use to maintain state. @return {Object} the next object in the iteration or undefined @@ -8425,9 +8456,9 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot @method arrayContentWillChange @param {Number} startIdx The starting index in the array that will change. - @param {Number} removeAmt The number of items that will be removed. If you + @param {Number} removeAmt The number of items that will be removed. If you pass `null` assumes 0 - @param {Number} addAmt The number of items that will be added If you + @param {Number} addAmt The number of items that will be added If you pass `null` assumes 0. @return {Ember.Array} receiver */ @@ -8901,11 +8932,11 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, passed array. You should also call `this.enumerableContentDidChange()` @method replace - @param {Number} idx Starting index in the array to replace. If + @param {Number} idx Starting index in the array to replace. If idx >= length, then append to the end of the array. - @param {Number} amt Number of elements that should be removed from + @param {Number} amt Number of elements that should be removed from the array, starting at *idx*. - @param {Array} objects An array of zero or more objects that should be + @param {Array} objects An array of zero or more objects that should be inserted into the array at *idx* */ replace: Ember.required(), @@ -10160,14 +10191,14 @@ CoreObject.PrototypeMixin = Mixin.create({ view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] ``` Adding a single property that is not an array will just add it in the array: - + ```javascript var view = App.FooBarView.create({ classNames: 'baz' }) view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] ``` - + Using the `concatenatedProperties` property, we can tell to Ember that mix the content of the properties. @@ -11125,40 +11156,8 @@ Ember.Mixin.prototype.toString = classToString; (function() { -/** -@module ember -@submodule ember-runtime -*/ - -/** - Defines a namespace that will contain an executable application. This is - very similar to a normal namespace except that it is expected to include at - least a 'ready' function which can be run to initialize the application. - - Currently `Ember.Application` is very similar to `Ember.Namespace.` However, - this class may be augmented by additional frameworks so it is important to - use this instance when building new applications. - - # Example Usage - - ```javascript - MyApp = Ember.Application.create({ - VERSION: '1.0.0', - store: Ember.Store.create().from(Ember.fixtures) - }); - - MyApp.ready = function() { - //..init code goes here... - } - ``` - - @class Application - @namespace Ember - @extends Ember.Namespace -*/ Ember.Application = Ember.Namespace.extend(); - })(); @@ -11544,6 +11543,25 @@ Ember.ObjectProxy = Ember.Object.extend( } }); +Ember.ObjectProxy.reopenClass({ + create: function () { + var mixin, prototype, i, l, properties, keyName; + if (arguments.length) { + prototype = this.proto(); + for (i = 0, l = arguments.length; i < l; i++) { + properties = arguments[i]; + for (keyName in properties) { + if (!properties.hasOwnProperty(keyName) || keyName in prototype) { continue; } + if (!mixin) mixin = {}; + mixin[keyName] = null; + } + } + if (mixin) this._initMixins([mixin]); + } + return this._super.apply(this, arguments); + } +}); + })(); @@ -11864,7 +11882,7 @@ if (ignore.length>0) { /** The NativeArray mixin contains the properties needed to to make the native Array support Ember.MutableArray and all of its dependent APIs. Unless you - have `Ember.EXTEND_PROTOTYPES or `Ember.EXTEND_PROTOTYPES.Array` set to + have `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Array` set to false, this will be applied automatically. Otherwise you can apply the mixin at anytime by calling `Ember.NativeArray.activate`. @@ -12440,7 +12458,8 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, objectAtContent: function(idx) { var length = get(this, 'length'), - object = get(this,'arrangedContent').objectAt(idx); + arrangedContent = get(this,'arrangedContent'), + object = arrangedContent && arrangedContent.objectAt(idx); if (idx >= 0 && idx < length) { var controllerClass = this.lookupItemController(object); @@ -12591,15 +12610,16 @@ Ember.$ = jQuery; @module ember @submodule ember-views */ +if (Ember.$) { + // http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#dndevents + var dragEvents = Ember.String.w('dragstart drag dragenter dragleave dragover drop dragend'); -// http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#dndevents -var dragEvents = Ember.String.w('dragstart drag dragenter dragleave dragover drop dragend'); - -// Copies the `dataTransfer` property from a browser event object onto the -// jQuery event object for the specified events -Ember.EnumerableUtils.forEach(dragEvents, function(eventName) { - Ember.$.event.fixHooks[eventName] = { props: ['dataTransfer'] }; -}); + // Copies the `dataTransfer` property from a browser event object onto the + // jQuery event object for the specified events + Ember.EnumerableUtils.forEach(dragEvents, function(eventName) { + Ember.$.event.fixHooks[eventName] = { props: ['dataTransfer'] }; + }); +} })(); @@ -12616,7 +12636,8 @@ Ember.EnumerableUtils.forEach(dragEvents, function(eventName) { // Internet Explorer prior to 9 does not allow setting innerHTML if the first element // is a "zero-scope" element. This problem can be worked around by making // the first node an invisible text node. We, like Modernizr, use ­ -var needsShy = (function(){ + +var needsShy = this.document && (function(){ var testEl = document.createElement('div'); testEl.innerHTML = "
        "; testEl.firstChild.innerHTML = ""; @@ -12626,7 +12647,7 @@ var needsShy = (function(){ // IE 8 (and likely earlier) likes to move whitespace preceeding // a script tag to appear after it. This means that we can // accidentally remove whitespace when updating a morph. -var movesWhitespace = (function() { +var movesWhitespace = this.document && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "Test: Value"; return testEl.childNodes[0].nodeValue === 'Test:' && @@ -13297,7 +13318,7 @@ Ember.EventDispatcher = Ember.Object.extend( setup: function(addedEvents) { var event, events = { touchstart : 'touchStart', - // touchmove : 'touchMove', + touchmove : 'touchMove', touchend : 'touchEnd', touchcancel : 'touchCancel', keydown : 'keyDown', @@ -13308,8 +13329,7 @@ Ember.EventDispatcher = Ember.Object.extend( contextmenu : 'contextMenu', click : 'click', dblclick : 'doubleClick', - // https://github.com/emberjs/ember.js/pull/2148 - // mousemove : 'mouseMove', + mousemove : 'mouseMove', focusin : 'focusIn', focusout : 'focusOut', mouseenter : 'mouseEnter', @@ -13459,8 +13479,9 @@ Ember.EventDispatcher = Ember.Object.extend( // Add a new named queue for rendering views that happens // after bindings have synced, and a queue for scheduling actions // that that should occur after view rendering. -var queues = Ember.run.queues; -queues.splice(Ember.$.inArray('actions', queues)+1, 0, 'render', 'afterRender'); +var queues = Ember.run.queues, + indexOf = Ember.ArrayPolyfills.indexOf; +queues.splice(indexOf.call(queues, 'actions')+1, 0, 'render', 'afterRender'); })(); @@ -14250,7 +14271,7 @@ class: * `mouseEnter` * `mouseLeave` - Form events: + Form events: * `submit` * `change` @@ -14258,7 +14279,7 @@ class: * `focusOut` * `input` - HTML5 drag and drop events: + HTML5 drag and drop events: * `dragStart` * `drag` @@ -15706,17 +15727,24 @@ Ember.View = Ember.CoreView.extend( // once the view has been inserted into the DOM, legal manipulations // are done on the DOM element. +function notifyMutationListeners() { + Ember.run.once(Ember.View, 'notifyMutationListeners'); +} + var DOMManager = { prepend: function(view, html) { view.$().prepend(html); + notifyMutationListeners(); }, after: function(view, html) { view.$().after(html); + notifyMutationListeners(); }, html: function(view, html) { view.$().html(html); + notifyMutationListeners(); }, replace: function(view) { @@ -15726,15 +15754,18 @@ var DOMManager = { view._insertElementLater(function() { Ember.$(element).replaceWith(get(view, 'element')); + notifyMutationListeners(); }); }, remove: function(view) { view.$().remove(); + notifyMutationListeners(); }, empty: function(view) { view.$().empty(); + notifyMutationListeners(); } }; @@ -15795,14 +15826,14 @@ Ember.View.reopenClass({ `className` and optional `falsyClassName`. - if a `className` or `falsyClassName` has been specified: - - if the value is truthy and `className` has been specified, + - if the value is truthy and `className` has been specified, `className` is returned - - if the value is falsy and `falsyClassName` has been specified, + - if the value is falsy and `falsyClassName` has been specified, `falsyClassName` is returned - otherwise `null` is returned - - if the value is `true`, the dasherized last part of the supplied path + - if the value is `true`, the dasherized last part of the supplied path is returned - - if the value is not `false`, `undefined` or `null`, the `value` + - if the value is not `false`, `undefined` or `null`, the `value` is returned - if none of the above rules apply, `null` is returned @@ -15849,6 +15880,20 @@ Ember.View.reopenClass({ } }); +var mutation = Ember.Object.extend(Ember.Evented).create(); + +Ember.View.addMutationListener = function(callback) { + mutation.on('change', callback); +}; + +Ember.View.removeMutationListener = function(callback) { + mutation.off('change', callback); +}; + +Ember.View.notifyMutationListeners = function() { + mutation.trigger('change'); +}; + /** Global views hash @@ -16652,7 +16697,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; Given an empty `` and the following code: - ```javascript + ```javascript someItemsView = Ember.CollectionView.create({ classNames: ['a-collection'], content: ['A','B','C'], @@ -17008,15 +17053,15 @@ define("metamorph", var K = function(){}, guid = 0, - document = window.document, + document = this.document, // Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges - supportsRange = ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, + supportsRange = document && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, // Internet Explorer prior to 9 does not allow setting innerHTML if the first element // is a "zero-scope" element. This problem can be worked around by making // the first node an invisible text node. We, like Modernizr, use ­ - needsShy = (function(){ + needsShy = document && (function(){ var testEl = document.createElement('div'); testEl.innerHTML = "
        "; testEl.firstChild.innerHTML = ""; @@ -17027,7 +17072,7 @@ define("metamorph", // IE 8 (and likely earlier) likes to move whitespace preceeding // a script tag to appear after it. This means that we can // accidentally remove whitespace when updating a morph. - movesWhitespace = (function() { + movesWhitespace = document && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "Test: Value"; return testEl.childNodes[0].nodeValue === 'Test:' && @@ -17471,7 +17516,11 @@ var objectCreate = Object.create || function(parent) { return new F(); }; -var Handlebars = this.Handlebars || Ember.imports.Handlebars; +var Handlebars = this.Handlebars || (Ember.imports && Ember.imports.Handlebars); +if(!Handlebars && typeof require === 'function') { + Handlebars = require('handlebars'); +} + Ember.assert("Ember Handlebars requires Handlebars 1.0.0-rc.3 or greater", Handlebars && Handlebars.VERSION.match(/^1\.0\.[0-9](\.rc\.[23456789]+)?/)); /** @@ -17824,7 +17873,7 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { ## Example with bound options - Bound hash options are also supported. Example: + Bound hash options are also supported. Example: ```handlebars {{repeat text countBinding="numRepeats"}} @@ -17862,15 +17911,15 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { {{concatenate prop1 prop2 prop3}}. If any of the properties change, the helpr will re-render. Note that dependency keys cannot be using in conjunction with multi-property helpers, since it is ambiguous - which property the dependent keys would belong to. - + which property the dependent keys would belong to. + ## Use with unbound helper - The {{unbound}} helper can be used with bound helper invocations + The {{unbound}} helper can be used with bound helper invocations to render them in their unbound form, e.g. ```handlebars - {{unbound capitalize name}} + {{unbound capitalize name}} ``` In this example, if the name property changes, the helper @@ -17896,7 +17945,7 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { view = data.view, currentContext = (options.contexts && options.contexts[0]) || this, normalized, - pathRoot, path, + pathRoot, path, loc, hashOption; // Detect bound options (e.g. countBinding="otherCount") @@ -18002,7 +18051,7 @@ function evaluateMultiPropertyBoundHelper(context, fn, normalizedProperties, opt // Assemble liast of watched properties that'll re-render this helper. watchedProperties = []; for (boundOption in boundOptions) { - if (boundOptions.hasOwnProperty(boundOption)) { + if (boundOptions.hasOwnProperty(boundOption)) { watchedProperties.push(normalizePath(context, boundOptions[boundOption], data)); } } @@ -18131,22 +18180,30 @@ Ember.Handlebars.resolvePaths = function(options) { var set = Ember.set, get = Ember.get; var Metamorph = requireModule('metamorph'); +function notifyMutationListeners() { + Ember.run.once(Ember.View, 'notifyMutationListeners'); +} + // DOMManager should just abstract dom manipulation between jquery and metamorph var DOMManager = { remove: function(view) { view.morph.remove(); + notifyMutationListeners(); }, prepend: function(view, html) { view.morph.prepend(html); + notifyMutationListeners(); }, after: function(view, html) { view.morph.after(html); + notifyMutationListeners(); }, html: function(view, html) { view.morph.html(html); + notifyMutationListeners(); }, // This is messed up. @@ -18169,11 +18226,13 @@ var DOMManager = { morph.replaceWith(buffer.string()); view.transitionTo('inDOM'); view.triggerRecursively('didInsertElement'); + notifyMutationListeners(); }); }, empty: function(view) { view.morph.html(""); + notifyMutationListeners(); } }; @@ -18937,14 +18996,14 @@ EmberHandlebars.registerHelper('unless', function(context, options) { Result in the following rendered output: - ```html + ```html ``` A boolean return value will insert a specified class name if the property returns `true` and remove the class name if the property returns `false`. - A class name is provided via the syntax + A class name is provided via the syntax `somePropertyName:class-name-if-true`. ```javascript @@ -19103,9 +19162,9 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { @method bindClasses @for Ember.Handlebars @param {Ember.Object} context The context from which to lookup properties - @param {String} classBindings A string, space-separated, of class bindings + @param {String} classBindings A string, space-separated, of class bindings to use - @param {Ember.View} view The view in which observers should look for the + @param {Ember.View} view The view in which observers should look for the element to update @param {Srting} bindAttrId Optional bindAttr id used to lookup elements @return {Array} An array of class names to add @@ -19795,7 +19854,7 @@ Ember.Handlebars.registerHelper('unbound', function(property, fn) { // Unbound helper call. options.data.isUnbound = true; helper = Ember.Handlebars.helpers[arguments[0]] || Ember.Handlebars.helperMissing; - out = helper.apply(this, Array.prototype.slice.call(arguments, 1)); + out = helper.apply(this, Array.prototype.slice.call(arguments, 1)); delete options.data.isUnbound; return out; } @@ -21093,7 +21152,7 @@ helpers = helpers || Ember.Handlebars.helpers; data = data || {}; var buffer = '', stack1, hashTypes, escapeExpression=this.escapeExpression, self=this; function program1(depth0,data) { - + var buffer = '', hashTypes; data.buffer.push("