ソースを参照

HUE-301. Break Partial Refresh into a stand-alone class

Aaron Newton 15 年 前
コミット
ee4cfd69b3

+ 3 - 3
apps/beeswax/src/beeswax/templates/watch_wait.mako

@@ -19,7 +19,7 @@ ${wrappers.head("Beeswax: Waiting for query...", section='query')}
 
 <meta http-equiv="refresh" content="3;${url('beeswax.views.watch_query', query.id)}?${fwd_params}" />
 
-<div class="view partial_refresh" id="watch_wait">
+<div class="view" id="watch_wait">
   <div class="resizable" data-filters="SplitView">
     <div class="left_col">
       ${util.render_query_context(query_context)}
@@ -39,7 +39,7 @@ ${wrappers.head("Beeswax: Waiting for query...", section='query')}
       %>
       <dt class="ccs-dt_cap">${mr_jobs}</dt>
         <dd class="ccs-dd_bottom bw-actions">
-          <ul data-partial-id="num_jobs">
+          <ul data-single-partial-id="num_jobs">
             % if n_jobs > 0:
               <h3 class="ccs-hidden">This query launched ${n_jobs} ${mr_jobs}:</h3>
               <ul class="beeswax_hadoop_job_links">
@@ -64,7 +64,7 @@ ${wrappers.head("Beeswax: Waiting for query...", section='query')}
         <ul class="tab_sections ccs-clear">
           <li>
             <h3 class="ccs-hidden">Server Log</h3>
-            <pre data-partial-id="log">${log}</pre>
+            <pre data-single-partial-id="log">${log}</pre>
           </li>
           <li>
             <pre>${query.query}</pre>

+ 2 - 2
apps/jframegallery/src/jframegallery/templates/html-table.treeview.ajax.mako

@@ -37,10 +37,10 @@
     <meta http-equiv="refresh" content="5" />
   </head>
   <body>
-    <div class="jframe_padded partial_refresh"> 
+    <div class="jframe_padded"> 
       <input value="you can put some text in here to verify that the whole view doesn't refresh" style="width: 500px;"/>
       <p><a href="${request_path}?show_all=true">Show everything</a> || <a href="${request_path}">back to top</a></p>
-      <table data-filters="HtmlTable" class="selectable treeView" style="border: 1px solid #999; width: 98%">
+      <table data-filters="HtmlTable" class="selectable treeView highlight-partial-updates" style="border: 1px solid #999; width: 98%">
 
       <%def name="create_row(node, depth, path)">
         <%

+ 10 - 6
apps/jframegallery/src/jframegallery/templates/html-table.treeview.html

@@ -27,9 +27,11 @@ limitations under the License.
 		<p>The table below will appear to be a tree view table immediately, even before scripts are loaded. It will become interactive once the scripts have loaded. You should be able to click a section's expand arrow to expand it. Keys should work - up and down to navigate, shift + up and down to multiselect, left and right to collapse and expand sections.</p>
 		<table id="tree2" data-filters="HtmlTable" class="selectable treeView multiselect ccs-bordered-table">
 			<thead>
-				<th>Name</th>
-				<th>Date Modified</th>
-				<th>Size</th>
+				<tr>
+					<th>Name</th>
+					<th>Date Modified</th>
+					<th>Size</th>
+				</tr>
 			</thead>
 				<tbody>
 					<tr class="table-folder table-depth-0">
@@ -136,9 +138,11 @@ limitations under the License.
 		<p>The table below will turn into a treeview on startup. Unlike the table above, none of its rows are hidden on load but only after the HtmlTable filter is applied. Both will appear the same to you, though this one will take longer to build.</p>
 		<table id="tree" data-filters="HtmlTable" class="selectable treeView buildTree ccs-bordered-table buildTree">
 			<thead>
-				<th>Name</th>
-				<th>Date Modified</th>
-				<th>Size</th>
+				<tr>
+					<th>Name</th>
+					<th>Date Modified</th>
+					<th>Size</th>
+				</tr>
 			</thead>
 				<tbody>
 					<tr class="table-depth-0">

+ 21 - 20
apps/jframegallery/src/jframegallery/templates/partial_refresh.mako

@@ -1,18 +1,18 @@
-## Licensed to Cloudera, Inc. under one
-## or more contributor license agreements.  See the NOTICE file
-## distributed with this work for additional information
-## regarding copyright ownership.  Cloudera, Inc. licenses this file
-## to you 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.
+ Licensed to Cloudera, Inc. under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  Cloudera, Inc. licenses this file
+ to you 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.
 <%!
 from datetime import datetime
 %>
@@ -23,12 +23,13 @@ count = int(get_var('count', 0))
 <html>
   <head>
     <title>Partial Refresh</title>
-    <meta http-equiv="refresh" content="2;/jframegallery/partial_refresh.mako?sleep=1&count=${count + 1}" />
+    <xmeta http-equiv="refresh" content="2;/jframegallery/partial_refresh.mako?sleep=1&count=${count + 1}" />
   </head>
   <body>
-    <div class="jframe_padded partial_refresh">
-      <h2 data-partial-id="0">The current time: ${datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</h2>
-      <a data-filters="ArtButton" data-partial-id="1">current time: ${datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</a>
+    <div class="jframe_padded highlight-partial-updates">
+      <h2 data-single-partial-id="0">The current time: ${datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</h2>
+      <a class="ccs-fake_refresh">fake refresh</a>
+      <a data-filters="ArtButton" data-single-partial-id="1">current time: ${datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</a>
       <br/>
       <textarea type="text">you can change this</textarea>
       <p>you can interact with the input above while the blocks with the time stamps update. Note that the button is rendered each time and your input changes aren't.</p>
@@ -92,7 +93,7 @@ count = int(get_var('count', 0))
        <tbody data-partial-container-id="partials-tbody-rotate" class="ordered_partial_refresh">
          <%
            rows = 0
-           show = 10
+           show = 1000
             %>
          % while rows < show:
            <%

+ 5 - 5
apps/jobsub/src/jobsub/templates/watch_submission.html

@@ -23,9 +23,9 @@ limitations under the License.
 	{% endif %}
 {% endblock %}
 {% block content %}
-	<div id="jobsub_output" class="view partial_refresh">
+	<div id="jobsub_output" class="view">
 		<h1>Job - {{ submission.name }}</h1>
-		<b class="state{% if completed %} complete {% endif %}" data-partial-id="0">
+		<b class="state{% if completed %} complete {% endif %}" data-single-partial-id="0">
 			{% if completed %}
 				status: finished
 			{% else %}
@@ -36,17 +36,17 @@ limitations under the License.
 		</b>
 		<div class="jframe_padded" data-filters="SizeTo" data-size-to-height="-74">
 			<h2 class="jobsub_output_links_header">Launched Jobs</h2>
-			<ul class="jobsub_output_links" data-partial-id="1">
+			<ul class="jobsub_output_links" data-single-partial-id="1">
 				{% for j in jobs %}
 					<li><a href="{% url single_job jobid=j %}" target="JobBrowser">{{ j }}</a>
 				{% endfor %}
 			</ul>
 
 			<h2 class="jobsub_output_stout_header">Last 10KB of stdout:</h2>
-			<pre class="jobsub_output_stout" data-partial-id="2">{{ job_data.stdout_tail }}</pre>
+			<pre class="jobsub_output_stout" data-single-partial-id="2">{{ job_data.stdout_tail }}</pre>
 
 			<h2 class="jobsub_output_sterr_header">Last 10KB of stderr:</h2>
-			<pre class="jobsub_output_sterr" data-partial-id="3">{{ job_data.stderr_tail }}</pre>
+			<pre class="jobsub_output_sterr" data-single-partial-id="3">{{ job_data.stderr_tail }}</pre>
 		</div>
 	</div>
 {% endblock %}

+ 11 - 9
desktop/core/static/css/shared.css

@@ -894,13 +894,15 @@ ul.autocompleter-choices li.autocompleter-selected span.autocompleter-queried
 	color: #9FCFFF;
 }
 
-.partial-refresh-transitionable {
-	-webkit-transition: color 1s ease-in-out, background-color 1s ease-in-out;
-	-moz-transition: color 1s ease-in-out, background-color 1s ease-in-out;
-	-o-transition: color 1s ease-in-out, background-color 1s ease-in-out;
-	transition: color 1s ease-in-out, background-color 1s ease-in-out;
-}
-.partial-refresh-updated {
-	color: #238500;
-	background-color: #FAFCAE;
+.highlight-partial-updates .partial-refresh-transitionable {
+	-webkit-transition: color 1s ease-in-out;
+	-moz-transition: color 1s ease-in-out;
+	-o-transition: color 1s ease-in-out;
+	transition: color 1s ease-in-out;
+}
+.highlight-partial-updates .partial-refresh-updated, .highlight-partial-updates .partial-refresh-updated a {
+	color: #238500 !important;
+}
+.highlight-partial-updates tr.table-tr-selected .partial-refresh-updated, .highlight-partial-updates tr.table-tr-selected .partial-refresh-updated a {
+	color: #F5FC2B !important;
 }

+ 27 - 30
desktop/core/static/js/Source/BehaviorFilters/Behavior.HtmlTableRestore.js

@@ -15,7 +15,7 @@
 // limitations under the License.
 /*
 ---
-description: Makes HtmlTable instances retain their state on refresh. 
+description: Makes HtmlTable instances retain their state on refresh.
 provides: [Behavior.HtmlTableRestore]
 requires: [Widgets/Behavior.HtmlTable]
 script: Behavior.HtmlTableRestore.js
@@ -23,33 +23,30 @@ script: Behavior.HtmlTableRestore.js
 */
 
 Behavior.addGlobalPlugin('HtmlTable', 'HtmlTableRestore', function(element, methods){
-        var table = element.retrieve('HtmlTable');
-        //Get tables in the current frame
-        var tables = methods.getContentElement().getElements('[data-filters*=HtmlTable]');
-        var tableIndex = tables.indexOf(element);
-        var tableState = methods.getBehaviorState().tableState; 
-        var loadEvent = function(loadOptions) {
-                //On load, if there is a previous page and it's the same as the current page, and there's a state array and this state array has the same number of entries as there are tables in the frame, restore the state.
-                if(loadOptions.previousPath && loadOptions.previousPath == loadOptions.requestPath && tableState && tableState.length == tables.length) {
-                        table.restore(tableState[tableIndex]);
-                } else if(tableState && tableState[tableIndex]) {
-                        delete tableState;
-                } 
-        };     
-        methods.addEvent('load', loadEvent);
-        var changeEvent = function() {
-                //On change, create the tableState array and the particular index needed, if necessary.  Then store the serialized state. 
-                if(!methods.getBehaviorState().tableState) methods.getBehaviorState().tableState = [];
-                tableState = methods.getBehaviorState().tableState;
-                if(!tableState[tableIndex]) tableState[tableIndex] = {};
-                tableState[tableIndex] = this.serialize();
-        };
-        table.addEvent('stateChanged', changeEvent); 
-        this.markForCleanup(element, function() {
-                methods.removeEvent('load', loadEvent);
-                table.removeEvent('stateChanged', changeEvent);
-        });
+	var table = element.retrieve('HtmlTable');
+	//Get tables in the current frame
+	var tables = methods.getContentElement().getElements('[data-filters*=HtmlTable]');
+	var tableIndex = tables.indexOf(element);
+	var tableState = methods.getBehaviorState().tableState;
+	var loadEvent = function(loadOptions) {
+		//On load, if there is a previous page and it's the same as the current page, and there's a state array and this state array has the same number of entries as there are tables in the frame, restore the state.
+		if(loadOptions.previousPath && loadOptions.previousPath == loadOptions.requestPath && tableState && tableState.length == tables.length && tableState[tableIndex]) {
+			table.restore(tableState[tableIndex]);
+		} else if(tableState && tableState[tableIndex]) {
+			delete tableState;
+		}
+	};
+	methods.addEvent('load', loadEvent);
+	var changeEvent = function() {
+		//On change, create the tableState array and the particular index needed, if necessary.  Then store the serialized state.
+		if(!methods.getBehaviorState().tableState) methods.getBehaviorState().tableState = [];
+		tableState = methods.getBehaviorState().tableState;
+		if(!tableState[tableIndex]) tableState[tableIndex] = {};
+		tableState[tableIndex] = this.serialize();
+	};
+	table.addEvent('stateChanged', changeEvent);
+	this.markForCleanup(element, function() {
+		methods.removeEvent('load', loadEvent);
+		table.removeEvent('stateChanged', changeEvent);
+	});
 });
-
-
-

+ 1 - 0
desktop/core/static/js/Source/CCS/CCS.JBrowser.js

@@ -44,6 +44,7 @@ requires:
  - /CCS.JFrame.PromptAndPost
  - /CCS.JFrame.Refresh
  - /CCS.JFrame.RefreshWithParams
+ - /CCS.JFrame.SplitViewLinkers
  - /CCS.JFrame.SubmitLink
  - /CCS.JFrame.Target
 script: CCS.JBrowser.js

+ 25 - 25
desktop/core/static/js/Source/CCS/CCS.JFrame.js

@@ -17,7 +17,7 @@
 ---
 description: JFrame--Configurable "container" for simple HTML pages, within the CCS framework.
 provides: [CCS.JFrame]
-requires: 
+requires:
  - Core/Request
  - More/Element.Delegation
  - More/Elements.From
@@ -89,7 +89,7 @@ CCS.JFrame = new Class({
 		redirectAfterRender: empty(redirectedTo, originalRequestedURL), //after content is rendered
 		size: {
 			width: ,
-			height: 
+			height:
 		},
 
 		**/
@@ -189,7 +189,7 @@ CCS.JFrame = new Class({
 
 /*
 	
-	the JFrame callClick event invokes the click handler for links/elements, matching against any JFrameLinkers and, 
+	the JFrame callClick event invokes the click handler for links/elements, matching against any JFrameLinkers and,
 	if none are found, running the default click handler (which is to load the link's href, if defined, into the JFrame).
 	event - (*object*) the event object that was fired; a click, usually
 	link - (*element*) typically an anchor tag, though that's not a requirement
@@ -277,7 +277,7 @@ CCS.JFrame = new Class({
 		}, options);
 		this.fireEvent('request', [options.requestPath, options.userData, options]);
 		var req = new Request();
-		this._setRequestOptions(req, 
+		this._setRequestOptions(req,
 			$merge(options, {
 				method: options.method || 'get',
 				url: new URI(options.requestPath).toString()
@@ -312,9 +312,9 @@ CCS.JFrame = new Class({
 
 	/*
 	options:
-		content: content to render (html, dom element, or $$ collection of dom elements), 
-		responsePath: the path to this content, 
-		title: the title for the frame for this content, 
+		content: content to render (html, dom element, or $$ collection of dom elements),
+		responsePath: the path to this content,
+		title: the title for the frame for this content,
 		userData: data to be passed along to the loadComplete event,
 		target: dom element or id to fill with content; defaults to this.content
 		suppressLoadComplete: (boolean) if true, the loadComplete event is not fired
@@ -341,7 +341,7 @@ CCS.JFrame = new Class({
 		everything else: used to pass along options to filters, renderers, etc.
 		
 	views:
-		The content of a JFrame request is searched for the first element with the class "view". 
+		The content of a JFrame request is searched for the first element with the class "view".
 		If found, the id of this element is treated as the current view. This id is stripped (all
 		Desktop apps do not use ids, as there may be more than one of them). The data object passed
 		to the loadComplete event and the callback in the options contains this view (the value of
@@ -364,18 +364,12 @@ CCS.JFrame = new Class({
 		These elements are referenced in the data passed to the loadComplete callback as the toolbar and footer
 		for the current view. This allows you to do special things to the navigation. By default, JBrowser
 		injects the contents of this toolbar into the area above the content and the footer content into
-		the footer. By simply putting links and other elements into a div with the class "toolbar" it 
+		the footer. By simply putting links and other elements into a div with the class "toolbar" it
 		will be added to the header (and the same for the footer).
 		You must include the toolbar / footer in every response for it to remain there.
 	*/
 	renderContent: function(options){
 		var content = {};
-		var filter = function(elements, selector){
-			if (!elements.length) return elements;
-			var first = elements[0];
-			var holder = new Element('div').adopt(elements);
-			return first.getParent().getElements(selector);
-		};
 		if ($(options.content)) {
 			//if the content is an element, cast it into an Elements array
 			content.elements = $$($(options.content));
@@ -383,11 +377,10 @@ CCS.JFrame = new Class({
 			//if it's a string, parse it
 			content = this._parseContent(options.content);
 		} else {
-			//the only other valid option is that it's an array of elements, 
+			//the only other valid option is that it's an array of elements,
 			//cast it into an Elements array in case it's just a vanilla array
 			content.elements = $$(options.content);
 		}
-		if (options.filter) content.elements = filter(content.elements, options.filter);
 		//determine view and view element
 		var view,
 		    viewElement = content.elements.filter('.view')[0] || content.elements.getElement('.view')[0];
@@ -531,7 +524,7 @@ CCS.JFrame = new Class({
 
 	//Applies all the behavior filters for an element.
 	//element - (element) an element to apply the filters registered with this Behavior instance to.
-	//behavior - (behavior object) behavior instance to use 
+	//behavior - (behavior object) behavior instance to use
 	//force - (boolean; optional) passed through to applyBehavior (see it for docs)
 	applyBehaviors: function(element, behavior, force){
 		behavior.apply(element, force);
@@ -582,7 +575,7 @@ CCS.JFrame = new Class({
 
 	/*
 		linkers are custom event handlers for links that match a specific selector. Ideally, the selector is just a classname.
-		When any link in a jFrame is clicked, it is checked against all registered linkers. If no matches are found, the link 
+		When any link in a jFrame is clicked, it is checked against all registered linkers. If no matches are found, the link
 		is handled by jFrame and loads new content. If there is a match, the matcher's function handles the event.
 		selector - (*string*) a css selector that the link is tested against.
 		fn - (*function*) callback that handles links that match the selector
@@ -815,8 +808,8 @@ CCS.JFrame = new Class({
 			error: error,
 			blankWindowWithError: blankWindowWithError,
 			previousPath: previousPath
-		}, options || {}); 
-		this.behavior.fireEvent('load', loadOptions); 
+		}, options || {});
+		this.behavior.fireEvent('load', loadOptions);
 	},
 
 	/*
@@ -858,7 +851,7 @@ CCS.JFrame = new Class({
 
 	/*
 		filters:
-		Filters are functions that are called every time the contents of the jFrame is updated. 
+		Filters are functions that are called every time the contents of the jFrame is updated.
 		The method defined is passed the container and can then apply its own logic to the contents
 		of that container. The name specified is not used, except that you can overwrite a filter
 		by using the same name.
@@ -910,8 +903,8 @@ CCS.JFrame = new Class({
 			viewElement - (*element*; optional) if defined, the element for the view
 		
 		Iterates over all the renderers for this instance (including global renderers on the JFrame prototype, which
-		includes the default renderer). Each renderer may inspect the content and elect to handle it instead of the 
-		default handler. If it handles it and wishes to prevent the default handler, the renderer returns *true*, 
+		includes the default renderer). Each renderer may inspect the content and elect to handle it instead of the
+		default handler. If it handles it and wishes to prevent the default handler, the renderer returns *true*,
 		otherwise, if it returns *false* (or nothing) the default handler will fill the contents and set up events
 		and filters, linkers, etc. as usual.
 	*/
@@ -942,7 +935,7 @@ CCS.JFrame = new Class({
 	
 	/*
 		the default renderer, if no other renderers apply
-		this is the default behavior for jframe which fills the content of the window and updates 
+		this is the default behavior for jframe which fills the content of the window and updates
 		the history (if history is enabled). It also picks out the view if there is one defined
 		as well as assigns the toolbar to the callback object for JBrowser to do with it what it will.
 		Finally, it calls the callback in the options (if specified) and fires the loadComplete event.
@@ -951,6 +944,13 @@ CCS.JFrame = new Class({
 		var options = content.options;
 		//store the path as the current one
 		if (!options.retainPath) this.currentPath = options.responsePath || this.currentPath;
+
+		if (options.filter) {
+			if (!content.elements.length) return content.elements;
+			var holder = new Element('div').adopt(content.elements);
+			content.elements = holder.getElements(options.filter);
+		}
+
 		//grab the target
 		var target = options.target ? $$(options.target)[0] || this.content : this.content;
 		this._resetOverflow(target);

+ 0 - 1
desktop/core/static/js/Source/JFrameLinkers/CCS.JFrame.LivePath.js

@@ -46,7 +46,6 @@ script: CCS.JFrame.LivePath.js
 			var okToAdd = action == "toggle" || action == "add";
 			for (path in paths){
 				var state = uri.getData(path);
-				if (window.paused) debugger;
 				if (!state) {
 					uri.setData(path, paths[path]);
 				} else if ($type(state) == "string") {

+ 124 - 394
desktop/core/static/js/Source/JFrameRenderers/CCS.JFrame.PartialRefresh.js

@@ -1,8 +1,8 @@
 /*
 ---
-description: Any JFrame response that has a root-level child element with the class .partial_refresh will find all elements that have a property defined for data-partial-id that is unique to the response and only update them. If there is a mismatch in the response such that the number of and ids of partials in the previous state do not match the return state, an alert will be shown the user that the entire view will be updated that they can cancel, if they so choose. 
+description: Any JFrame response that has elements that have a property defined for data-partial-id will have each auto-refresh response only update them.
 provides: [CCS.JFrame.PartialRefresh]
-requires: [/CCS.JFrame, Widgets/ART.Alerts, More/Table, Widgets/Element.Data]
+requires: [/CCS.JFrame, Widgets/ART.Alerts, More/Table, Widgets/Element.Data, /PartialUpdate]
 script: CCS.JFrame.PartialRefresh.js
 
 ...
@@ -24,8 +24,6 @@ script: CCS.JFrame.PartialRefresh.js
 // limitations under the License.
 (function(){
 
-	var enableLog; //set to true if you want to log messages; left here for convenience.
-
 	CCS.JFrame.addGlobalRenderers({
 
 		/*
@@ -35,16 +33,14 @@ script: CCS.JFrame.PartialRefresh.js
 			to the previous response. When there's a mis-match in the response, only those elements that have changed
 			are run through the JFrame filters to configure them for display and then replace their expired counterparts.
 
-			Each area that may have elements that should be checked is identified with the class "partial_refresh" on some
-			container; often a div wrapping the entire response.
-
-			Each individual item that might be refreshed must be marked with a data-partial-id property that is unique
-			to the JFrame (i.e. multiple app windows can have id overlap).
+			Each individual item that might be refreshed must be marked with a data-single-partial-id property that is unique
+			to the JFrame (i.e. multiple app windows can have id overlap) OR have a data-partial-id property and be the
+			child of a container with app-unique data-partial-container-id.
 
 			If you need to support the loading of additional content after the fact, such as with loading additional Ajax
 			elements within the frame or if the response may return more or fewer items than the previous response, then
 			you must add some additional instrumentation to your HTML. Specifically, you *must* define a container where
-			new elements are injected. This is typically the immediate parent of the partials, like so:
+			new elements are injected. This MUST be the immediate parent of the partials, like so:
 
 			Example w/ just partials and container
 			<div data-partial-container-id="div1">
@@ -52,8 +48,10 @@ script: CCS.JFrame.PartialRefresh.js
 				<p data-partial-id="p2">bar</p>
 			</div>
 
-			In some cases, such as with tables, you may wish to add "lines" between the partials and the container. A good
-			example here is a table which has partials for individual td elements, but new tr elements may come and go.
+			The only exception to this requirement, such as with tables, you may wish to add "lines" between
+			the partials and the container. A good example here is a table which has partials for individual
+			td elements, but new tr elements may come and go. In this case the "lines" must be immediate children
+			of the parent, but the individual partials need not be immediate children of the lines.
 
 			Example w/ partials, container, and lines
 			<tbody data-partial-container-id="tbody1">
@@ -71,6 +69,24 @@ script: CCS.JFrame.PartialRefresh.js
 			the previous row would be found there. It's id is "tr1". The *live* DOM tree - the one the user sees - would be searched
 			for an element with that line-id. The new line would be injected after it.
 
+			It is possible to support the possibility of the server returning a new sort order. This is a little more expensive
+			so it must be "turned on" by giving the container the class name "ordered_partial_refresh".
+
+			Example w/ partials, container, and lines that will obey sort order.
+			<tbody data-partial-container-id="tbody1" class="ordered_partial_refresh">
+				<tr data-partial-line-id="tr1">
+					<td data-partial-id="id1"></td>
+					<td data-partial-id="id2"></td>
+				</tr>
+				<tr data-partial-line-id="tr2">
+					<td data-partial-id="id3"></td>
+					<td data-partial-id="id4"></td>
+				</tr>
+			</tbody>
+
+			In the above example, if the server replied with tr2 preceding tr1, the order would update in the view because
+			of the "ordered_partial_refresh" class.
+
 			When using ajax to load in new content (such as with the AjaxLoad linker) you'll likely load new content in ouside
 			the scope of this refresh handler. I.e. you'll handle the response and inject the new items yourself. In this case
 			partial refresh needs only to store the original request state for future checksums. To accomplish this, set the
@@ -80,128 +96,127 @@ script: CCS.JFrame.PartialRefresh.js
 		*/
 
 		partialRefresh: function(content){
+			dbug.timeEnd('partial refresh');
+			dbug.time('partial refresh');
 			var options = content.options || {};
 			//if the request options include the ignorePartialRequest flag, exit
 			if (options.ignorePartialRefresh) return;
 			//when we load content via ajax, we don't want the response being parsed for partials
 			var jState = getJState(this);
 			//get the partial containers; containers that have elements in them to be partially refreshed
-			var partialContainers = new Element('div').adopt(content.elements).getElements('.partial_refresh');
+			var elements = new Element('div').adopt(content.elements);
+			//find all the containers in the response that have data-partial-container-id
+			var partialContainers = elements.getElements('[data-partial-container-id]');
+
+			dbug.time('create');
+			//and either fetch or create instances of PartialUpdate for them
+			var updaters = partialContainers.map(function(container){
+				return {
+					updater: getUpdater(container, jState, {
+						//when an element is destroyed, call jframe's garbage collection method
+						onElementDestroy: function(el){
+							this.collectElement(el);
+						}.bind(this),
+						//before individual elements are inserted in the DOM, run them through the jframe filters
+						onBeforeUpdate: function(checked){
+							dbug.time('update:filters');
+							this.applyFilters(checked.target);
+							dbug.timeEnd('update:filters');
+						}.bind(this)
+					}),
+					responseContainer: container
+				};
+			}, this);
+			dbug.timeEnd('create');
+
+			//find stand-alone partials; these have no containers
+			var standAlones = elements.getElements('[data-single-partial-id]'),
+			    tmpTarget = new Element('div');
+			dbug.time('stand alone');
+			
+			for (var i = 0; i < standAlones.length; i++){
+				var element = standAlones[i];
+				var id = element.get('data-single-partial-id');
+				if (!jState.standAlones[id]){
+					//create an instance of the single partial updater for each of these
+					jState.standAlones[id] = new PartialUpdate.Single(element, {
+						onElementDestroy: function(el){
+							this.collectElement(el);
+						}.bind(this),
+						partialIdProperty: 'data-single-partial-id',
+						updateStateOnStartup: false,
+						onBeforeUpdate: function(element){
+							dbug.time('update:filters');
+							this.applyFilters(tmpTarget.adopt(element));
+							dbug.timeEnd('update:filters');
+						}.bind(this)
+					});
+				}
+				updaters.push({
+					updater: jState.standAlones[id],
+					responseContainer: elements
+				});
+			}
+			dbug.timeEnd('stand alone');
+
+			//this method just updates the state of each updater to match that of the response
+			var updateState = function(){
+				dbug.time('update state');
+				updaters.each(function(updaterObj){
+					updaterObj.updater.updateState(updaterObj.responseContainer);
+				});
+				dbug.timeEnd('update state');
+			};
 
 			//for requests that want to handle the element injection themselves (ajaxload, for example)
 			//they can instruct this filter to process the partials in the response and then exit
 			if (options.onlyProcessPartials) {
-				processPartials(content, jState);
+				updateState();
 				return;
 			}
 			//store the previous path to be the current one
 			jState.prevPath = options ? options.responsePath : null;
 
-			if (!partialContainers.length) {
-				if (enableLog) dbug.log('no partials to refresh, exiting');
+			if (!updaters.length) {
 				//no partial containers, reset and fall through to other renderers
-				jState.partials = null;
+				jState.updaters = {};
+				jState.standAlones = {};
 				this.enableSpinnerUsage();
 				return;
 			}
-			//get the partials in the containers
-			var partialContainer = new Element('div').adopt(partialContainers),
-			    //we need a copy of this for later when we sort stuff
-			    untouchedPartialContainer = partialContainer.clone(true, true),
-			    //get the partials from the container
-			    partials = getPartials(partialContainer, true);
 
-			//if the options aren't defined or if we didn't auto refresh, reset and
-			//return (fall through to other renderers)
+			//disable spinner for the next autorefresh
+			this.disableSpinnerUsage();
+
+			//if the options aren't defined or if we didn't auto refresh fall through to other renderers
 			if (!options || !(options.autorefreshed || options.forcePartial)) {
 				//...then store the state and render as usual (fall through to other renderers)
-				if (enableLog) dbug.log('not auto refreshed (%s), or new path (%s != %s) and not forced (%s), existing partial refresh after setup', !options.autorefreshed, jState.prevPath, options.responsePath, options.forcePartial);
-				jState.partials = partials;
-				this.disableSpinnerUsage();
+				updateState();
 				return;
 			}
 
-			if (new URI(options.requestPath).toString() != new URI(options.responsePath).toString()) {
-				if (enableLog) dbug.warn('detected partial refresh on a possible redirect (the request path (%s) != the response path (%s)), continuing with partial refresh', new URI(options.requestPath).toString(), new URI(options.responsePath).toString());
-			}
-
-			//don't show the spinner for partial refreshes
-			this.disableSpinnerUsage();
-
 			/*******************************
 			FORM HERE ON OUT
 			this filter will handle the response; we return true and other renderers are excluded
 			UNLESS there is a partial returned that we cannot find the proper place to put it
 			(i.e. it has no partial-container).
 			*******************************/
-			if (enableLog) dbug.log('proceeding with partial refresh');
 			//store the path as the current one
 			this.currentPath = options.responsePath || this.currentPath;
 
-			var checkedPartials = checkPartials(partials, jState);
-
-			//render the content
-			if (!checkedPartials.renderedPartials.length) {
-				if (enableLog) dbug.log('no partials for render; exiting quietly');
-				//if we aren't updating anything, that's cool, but still call the autorefresh filter
-				//to ensure that the frame keeps refreshing
-				this.applyFilter('autorefresh', new Element('div'), content);
-				//if there is no new content, return true (so no other renderers are called)
-				return true;
-			}
-
-			//apply all the jframe magic to our filtered content
-			if (enableLog) dbug.log('applying filters');
-
-			//it's faster to render this stuff outside the live DOM, apparently
-			//so store the scroll offset and pop it out, leaving a placeholder
-			var container = $(this);
-			var scroll = container.getParent().getScroll();
-			var s = new Element('span').inject(container, 'after');
-			container.dispose();
+			dbug.time('update');
+			updaters.each(function(updaterObj){
+				updaterObj.updater.update(updaterObj.responseContainer);
+			});
+			dbug.timeEnd('update');
+			dbug.time('apply filters to content');
+			this.applyFilters(new Element('div'), content);
+			dbug.timeEnd('apply filters to content');
 
-
-			//filters expect to be handed an object (content) that has various properties of the
-			//response; the meta tags, the script tags, and elements that were in the body, etc.
-			//because we want to run the elements we're updating through the filters, we overwrite
-			//the elements that were passed in to this renderer with only the elements we want
-			//updated and then apply the filters.
-			content.elements = checkedPartials.renderedPartials;
-			this.applyFilters(checkedPartials.target, content);
-			//now loop through the partials again and inject them into the DOM structure from the response
-			//replacing the original partial with the cloned one
-			restorePartials(checkedPartials.partialClones, partials);
-			//finally put the partials into the real DOM
-			var success = injectPartials(partials, jState, checkedPartials.partialClones, this.behavior, container);
-
-			if (!success) {
-				//if there was a failure, we put the container back and exit to the normal renderer
-				s.parentNode.replaceChild(container, s);
-				return;
-			}
-
-			//sort the contents in the "real" container with the order from the response
-			sortPartials(untouchedPartialContainer, container);
-
-			//remove any partials that were in the previous response but not in the recent one
-			cleanRemainingParitals(partials, jState, this.behavior);
-
-			//put the container back in the live DOM
-			s.parentNode.replaceChild(container, s);
-			container.getParent().scrollTo(scroll.x, scroll.y);
 			//we've updated the display, so tell filters that are waiting that they may need to update their display, too
 			this.behavior.fireEvent('show');
 			//prevent other renderers from handling the response
-
-
-			//after a second, remove the updated designation
-			(function(){
-				var els = $(this).getElements('.partial-refresh-updated');
-				for (var i = 0; i < els.length; i++){
-					els[i].removeClass('partial-refresh-updated');
-				}
-			}).delay(1000, this);
-
+			dbug.timeEnd('partial refresh');
 			return true;
 		}
 
@@ -213,310 +228,25 @@ script: CCS.JFrame.PartialRefresh.js
 	var getJState = function(jframe) {
 		var jState = jframeStates.get(jframe);
 		if (!jState) {
-			jState = {};
+			jState = {
+				updaters: {},
+				standAlones: {}
+			};
 			jframeStates.set(jframe, jState);
 		}
 		return jState;
 	};
 
-	//gets all the partial elements to refresh from the specified container
-	//if *store* == true, then store this state on each element as the original,
-	//unaltered response
-	var getPartials = function(container, store) {
-		var partials = {};
-		//if there are no partial elements, return now
-		if (!container.innerHTML.contains('data-partial-id')) return partials;
-		//get all the elements with a partial id
-		var partial,
-		    els = container.getElements('[data-partial-id]');
-		for (var i = 0; i < els.length; i++){
-			partial = els[i];
-			//store a pointer to that element to return it
-			partials[partial.getAttribute('data-partial-id')] = partial;
-			//if instructed to, store the original state of the response before it was altered by any filter
-			if (store) partial.store('partialRefresh:unaltered', partial.innerHTML);
-		}
-		return partials;
-	};
-
-	var processPartials = function(content, jState){
-		var toProcess = getPartials(new Element('div').adopt(content.elements), true);
-		//if there are partials already, merge the result
-		if (jState.partials) {
-			for (id in toProcess) {
-				jState.partials[id] = toProcess[id].store('partialRefresh:unaltered', toProcess[id].innerHTML);
-			}
-		//else there were no partials in the previous response; so assign these.
-		} else {
-			jState.partials = toProcess;
-		}
-	};
-
-	var checkPartials = function(partials, jState){
-		var data = {
-			renderedPartials: new Elements(),
-			partialClones: {},
-			target: new Element('div')
-		};
-		//loop through the partials and figure out which ones need updating so that we can 
-		//run only those through the filters
-		for (id in partials){
-			var partial = partials[id];
-			if (enableLog) dbug.log('considering %s for update', id);
-			//get the corresponding element in the dom
-			var before = jState.partials[id];
-			//if there isn't one, or thier raw html don't match, we'll update it, so we must render it
-			if (!before || !compare(before, partial)) {
-				if (enableLog) dbug.log('preparing %s for update', id);
-				//we must preserve the DOM structure to be able to find partial containers and partial lines
-				//so clone the partial for rendering
-				var clone = document.id(partial.cloneNode(true));
-				data.target.appendChild(clone);
-				//we need an array of these elements for when we call applyFilters below, which expects
-				//content.elements to be an array of the elements returned in the response
-				data.renderedPartials.push(clone);
-				//we also need a key/value map of all the clones for quick lookups when we put them back
-				//into the response DOM
-				data.partialClones[id] = clone;
-			}
-		}
-		return data;
-	};
-
-	//given a partial, attempts to find the line it is in
-	//example: for a td that is a partial, it may have the tr as its line
-	var getPartialLine = function(partial){
-		return partial.getParent('[data-partial-line-id]');
-	};
-	//given a partial, attempts to find the container it is in
-	//for example, for a td that is a partial, it may have the tr as its line and the table as its container
-	var getPartialContainers = function(partial, container){
-		var containers = {
-			container: partial.getParent('[data-partial-container-id]')
-		};
-		if (containers.container) {
-			containers.DOMcontainer = getMatchFromDOM(containers.container, container, 'data-partial-container-id');
-		}
-		return containers;
-	};
-
-	//given two partials, compares their raw HTML before they were parsed by filters
-	var compare = function(before, after){
-		return before.retrieve('partialRefresh:unaltered') == after.retrieve('partialRefresh:unaltered');
-	};
-
-	//this method destroys a partial given its partial id
-	var destroyPartial = function(id, jState, behavior){
-		//get the element
-		var element = jState.partials[id];
-
-		clean(element, behavior);
-		//delete it from the jState
-		delete jState.partials[id];
-	};
-
-	//this method takes a group of cloned partials (that have been passed through filters)
-	//and puts them back into the DOM from which they came, replacing the elements they
-	//were cloned from. This allows us to retain the DOM structure of the response, while
-	//only running through the filters the elements that need it.
-	var restorePartials = function(clones, partials){
-		var count = 0;
-		for (id in clones) {
-			if (enableLog) dbug.log('replacing target with clone: ', id);
-			var clone = clones[id],
-			    partial = partials[id];
-			//because we're replacing, we need to copy over thier original HTML state for the checksum
-			clone.store('partialRefresh:unaltered', partial.retrieve('partialRefresh:unaltered'));
-			count++;
-			partial.parentNode.replaceChild(clone, partial);
-			//and then update the pointer as the clone is now the rendered partial
-			partials[id] = clone;
-		}
-	};
-
-	var injectPartials = function(partials, jState, rendered, behavior, container) {
-		var insertedPartials = {},
-		    partial,
-		    prevId;
-		if (enableLog) dbug.log('iterating over partials for injection');
-		//iterate over all the partials to inject them into the live DOM
-		var result = true;
-		for (id in partials) {
-			partial = partials[id];
-			if (enableLog) dbug.log('considering %s for injection', id);
-			//if it's in a line that's been injected, skip it
-			//if it was passed through the renderers, it means that it needs an update or insertion
-			if (rendered[id] && !insertedPartials[id]) {
-				//get the corresponding partial in the DOM
-				var before = jState.partials[id];
-				//if there's a corresponding partial already in the DOM, replace it
-				if (before) {
-					if (enableLog) dbug.log('performing update for %s', id);
-					// partial.addClass('partial-refresh-updated partial-refresh-transitionable').replaces(before);
-					partial.addClass('partial-refresh-updated partial-refresh-transitionable').replaces(before);
-					clean(before, behavior);
-				} else {
-					//else it's not in the DOM
-					//look to see if this partial is in a line item (for example, the tr for a td that is a partial)
-					var line = getPartialLine(partial);
-					//if there is no line, inject it into the DOM in the container
-					var success;
-					if (line) success = injectPartialLine(partial, line, container, insertedPartials);
-					else success = injectPartial(jState, partial, prevId, container);
-					if (!success) {
-						result = false;
-						break;
-					}
-				}
-			}
-			if (rendered[id]) jState.partials[id] = partial;
-			prevId = id;
-		}
-		return result;
-	};
-
-	//given a line, destroy it
-	var destroyLine = function(line, behavior){
-		if (enableLog) dbug.log('destroying line:', line);
-		behavior.cleanup(line);
-		line.destroy();
-	};
-
-	var cleanRemainingParitals = function(partials, jState, behavior){
-		var linesToDestroy = {};
-		//for any partials that were in the DOM but not in the response, remove them
-		for (id in jState.partials) {
-			partial = jState.partials[id];
-			//if the partial is in the DOM but not the response
-			if (!partials[id]) {
-				//get its line; assume that we have to remove that, too
-				var line = getPartialLine(partial);
-				if (line) linesToDestroy[line.getAttribute('data-partial-line-id')] = line;
-				if (enableLog) dbug.log('destroying %s', id, line);
-				//destroy the partial
-				destroyPartial(id, jState, behavior);
-			}
-		}
-		for (id in linesToDestroy) {
-			destroyLine(linesToDestroy[id], behavior);
-		}
-	};
-
-	var injectPartial = function(jState, partial, prevId, container){
-		var previousPartial = jState.partials[prevId];
-		if (previousPartial) {
-			if (enableLog) dbug.log('injecting line for %s after previous item (%s)', id, previousPartial);
-			//if this isn't the first one, inject it after the previous id
-			partial.inject(previousPartial, 'after');
-		} else {
-			//find the container and inject it as the first item there
-			var containers = getPartialContainers(partial, container);
-			if (containers.DOMcontainer) {
-				if (enableLog) dbug.log('injecting %s into top of container (%o)', id, containers.DOMcontainer);
-				partial.inject(containers.DOMcontainer, 'top');
-			} else {
-				//else, we don't know where to inject it
-				dbug.warn('Could not inject partial (%o); no container or previous item found.', partial);
-				return false;
-			}
-		}
-		return true;
-	};
-
-	var injectPartialLine = function(partial, line, container, insertedPartials){
-		if (enableLog) dbug.log('preparing line for injection');
-		//there is a line, so we inject it instead of the partial.
-		//get the previous line (from the response)
-		var prevLine = line.getPrevious('[data-partial-line-id]'),
-		    prevLineInDOM;
-
-		//now find it's counterpart in the live DOM
-		if (prevLine) prevLineInDOM = container.getElement('[data-partial-line-id=' + prevLine.getAttribute('data-partial-line-id') + ']');
-		//if it's there, inject this line after it
-		if (prevLineInDOM) {
-			if (enableLog) dbug.log('injecting line (%o) after previous line (%o)', line, prevLine);
-			line.inject(prevLineInDOM, 'after');
-		} else {
-			//else this is the first line, so inject it at the top of the container
-			var lineContainers = getPartialContainers(partial, container);
-			if (lineContainers.DOMcontainer) {
-				if (enableLog) dbug.log('injecting line (%o) into top of container (%o)', line, lineContainers.DOMcontainer);
-				line.inject(lineContainers.DOMcontainer, 'top');
-			} else {
-				//else, we don't know where to inject it
-				dbug.warn('Could not inject partial (%o) in line (%o); no container or previous item found.', partial, line);
-				return false;
-			}
-		}
-		//store the fact that we just injected all the partials in this line
-		line.getElements('[data-partial-id]').each(function(p){
-			p.addClass('partial-refresh-updated partial-refresh-transitionable');
-			insertedPartials[p.getAttribute('data-partial-id')] = true;
-		});
-		return true;
-	};
-
-	var getMatchFromDOM = function(element, DOM, property){
-		return DOM.getElement('[' + property + '=' + element.getAttribute(property) + ']');
-	};
-
-	var getPartialId = function(el){
-		return el.getAttribute('data-partial-line-id') || el.getAttribute('data-partial-id');
-	};
-
-
-	var sortPartials = function(partialsContainer, container) {
-		partialsContainer.getElements('.ordered_partial_refresh').each(function(ordered){
-			var DOMtwin = getMatchFromDOM(ordered, container, 'data-partial-container-id');
-			var kids = ordered.getChildren('[data-partial-line-id]', '[data-partial-id]');
-			var DOMkidIds = {};
-			var DOMkids = DOMtwin.getChildren('[data-partial-line-id]', '[data-partial-id]');
-			for (var DOMkid = 0; DOMkid < DOMkids.length; DOMkid++){
-				DOMkidIds[getPartialId(DOMkids[DOMkid])] = DOMkids[DOMkid];
-			}
-			for (var kid = 0; kid < kids.length; kid++){
-				var id = getPartialId(kids[kid]);
-				DOMkidIds[id].inject(DOMtwin);
-			}
-		});
-	};
-
-	var toGC = [], gcTimer;
-	var clean = function(element, behavior){
-		if (!gcTimer) gcTimer = gc.periodical(100);
-		toGC.push({
-			element: element, behavior: behavior
-		});
-	};
-
-	//this is a "threadsafe" garbage collection routine
-	//if we call element.destroy on, say, 1000 nodes, the MooTools gc goes through
-	//all of them at once. This version does them in batches, releasing the thread
-	//as it goes.
-	var gc = function(){
-		if (toGC.length) {
-			var i = 0;
-			if (enableLog) dbug.log("removing %s of %s elements", Math.max(100, toGC.length/10), toGC.length);
-			while (toGC.length && i < Math.max(100, toGC.length/100)) {
-				var data = toGC.shift();
-				//clean up its behaviors
-				data.behavior.cleanup(data.element);
-				//destroy the element
-				destroy(data.element);
-				i++;
-			}
-			if (enableLog) dbug.info('remaining elements to clean: ', toGC.length);
-			if (i == 0) clearInterval(gcTimer);
-		}
-	};
-
-	//implementing a custom destroy method; this is slightly faster than the MooTools version
-	//but doesn't do some of it's less-important garbage collection
-	var destroy = function(el){
-		for (var i = 0; i < el.childNodes.length; i++) {
-			destroy(el.childNodes[i]);
+	var getUpdater = function(container, jState, options){
+		var id = container.get('data-partial-container-id');
+		if (!jState.updaters[id]) {
+			jState.updaters[id] = new PartialUpdate(container, $merge({
+				sorted: container.hasClass('ordered_partial_refresh'),
+				clone: container.get('html').contains('data-filter'),
+				updateStateOnStartup: false
+			}, options));
 		}
-		if (el.removeEvents) el.removeEvents();
+		return jState.updaters[id];
 	};
 
 })();

+ 480 - 0
desktop/core/static/js/Source/JFrameRenderers/PartialUpdate.js

@@ -0,0 +1,480 @@
+/*
+---
+description: Any JFrame response that has a root-level child element with the class .partial_refresh will find all elements that have a property defined for data-partial-id that is unique to the response and only update them. If there is a mismatch in the response such that the number of and ids of partials in the previous state do not match the return state, an alert will be shown the user that the entire view will be updated that they can cancel, if they so choose.
+provides: [PartialUpdate]
+requires: [Core/Element.Dimensions, Core/Options, Core/Events, Behavior/DashSelectors]
+script: PartialUpdate.js
+
+...
+*/
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you 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.
+
+;(function(){
+
+	this.PartialUpdate = new Class({
+
+		Implements: [Options, Events],
+
+		options: {
+			// onFailure: function(){},
+			// onNoUpdate: function(){},
+			// onBeforeUpdate: function(){},
+			// initialPartials: null,
+
+			sorted: true,
+			clone: false,
+			ejectToUpdate: true,
+			updateClass: 'partial-refresh-updated partial-refresh-transitionable',
+			updateClassToRemove: 'partial-refresh-updated',
+			flagRemovalDelay: 1000,
+			partialIdProperty: 'data-partial-id',
+			partialLineIdProperty: 'data-partial-line-id',
+			updateStateOnStartup: true
+		},
+
+		initialize: function(container, options){
+			this.container = document.id(container);
+			this.setOptions(options);
+			if (this.options.updateStateOnStartup) this.updateState(this.container);
+		},
+
+		//update the current state from the container (the "live" DOM)
+		updateState: function(container){
+			var partials = this._getPartials(container);
+			if (partials) {
+				//if there are partials already, merge the result
+				if (this.partials) {
+					for (id in partials) {
+						this._validateId(id);
+						this.partials[id] = partials[id];
+					}
+				//else there were no partials in the previous response; so assign these.
+				} else {
+					this.partials = partials;
+				}
+			}
+		},
+
+		/*
+			takes the given container and updates the live DOM with the state of the given
+			container. This container must contain immediate children that either have partials
+			or partial lines.
+		*/
+		update: function(container){
+			//dbug.time('partial update completed');
+			var restore = this._eject();
+			//keep an un-altered version of the response to use for sorting
+			var untouched = this.options.sorted && container.clone(true, true);
+			//get the partials from the container
+			var partials = this._getPartials(container);
+			if (partials) {
+				//check the partials for changes
+				var checked = this._checkPartials(partials);
+				if (!checked) {
+					//if there aren't any partials in the response, then clean any in the DOM
+					this._cleanRemainingPartials(partials);
+					//put the container back in place
+					restore();
+					//exit
+					return false;
+				}
+				//if we're cloning, fire our event for jframe to run its filters
+				if (this.options.clone) {
+					//dbug.time('fireEvent: beforeUpdate');
+					this.fireEvent('beforeUpdate', checked);
+					//dbug.timeEnd('fireEvent: beforeUpdate');
+					//restore the clones to the response container
+					this._restorePartials(checked, partials);
+				}
+				//inject the response updates into the DOM
+				this._injectPartials(partials, checked);
+				//clean any partials that don't belong
+				this._cleanRemainingPartials(partials);
+				//sort if necessary
+				if (this.options.sorted) this._sortPartials(untouched);
+			} else {
+				//clean any partials that don't belong
+				this._cleanRemainingPartials(partials);
+			}
+			//return the container to the DOM
+			restore();
+			//clean up the "updated" flag after a delay
+			if (this.options.updateClassToRemove) this._cleanUpdateFlag.delay(this.options.flagRemovalDelay, this);
+			//dbug.timeEnd('partial update completed');
+			return true;
+		},
+
+		toElement: function(){
+			return this.container;
+		},
+
+		// ejects the container from the DOM, returning a function that puts it back
+		_eject: function(){
+			//dbug.time('eject');
+			if (this.container.parentNode && this.options.ejectToUpdate){
+				var scroll = this.container.getParent().getScroll();
+				var s = new Element('span').inject(this.container, 'after');
+				this.container.dispose();
+				return function(){
+					//dbug.time('restore');
+					s.parentNode.replaceChild(this.container, s);
+					this.container.getParent().scrollTo(scroll.x, scroll.y);
+					//dbug.timeEnd('restore');
+				}.bind(this);
+			} else {
+				return function(){};
+			}
+			//dbug.timeEnd('eject');
+		},
+
+		//find all elements marked as updated and remove the updated class name
+		_cleanUpdateFlag: function(){
+			var els = this.container.getElements('.' + this.options.updateClassToRemove);
+			for (var i = 0; i < els.length; i++){
+				els[i].removeClass(this.options.updateClassToRemove);
+			}
+		},
+
+		/*
+			gets the partials from the given container
+			returns a map of partial ids to objects with
+			pointers to the element and the html of that element
+			e.g. data[id] = {element: partialElement, html: partialElement.innerHTML}
+
+			if the container has no partial elements, it returns the boolean false
+		*/
+
+		_getPartials: function(container) {
+			//dbug.time('get partials');
+			var data = {};
+			//if there are no partial elements, return now
+			if (!container.innerHTML.contains(this.options.partialIdProperty)) return false;
+			//get all the elements with a partial id
+			var partial, id,
+			    els = container.getElements('[' + this.options.partialIdProperty + ']');
+			for (var i = 0; i < els.length; i++){
+				partial = els[i];
+				id = partial.getAttribute(this.options.partialIdProperty);
+				//store a pointer to that element to return it
+				data[id] = {
+					element: partial,
+					html: partial.innerHTML
+				};
+			}
+			//dbug.timeEnd('get partials');
+			return data;
+		},
+
+		/*
+			given a list of partial objects ({element:el, html:html[, line:line]})
+			go through and find which ones require updating. if options.clone is true
+			clone these into a target.
+		*/
+		_checkPartials: function(partials){
+			//dbug.time('check partials');
+			var data = {
+				updates: {},
+				target: new Element('div')
+			};
+			//loop through the partials and figure out which ones need updating so that we can
+			//run only those through the filters
+			var checked = false;
+			for (id in partials){
+				var partial = partials[id];
+				//get the corresponding element in the dom
+				var before = this.partials && this.partials[id];
+				//if there isn't one, or thier raw html don't match, we'll update it, so we must render it
+				if (!before || before.html != partial.html) {
+					//we must preserve the DOM structure to be able to find partial containers and partial lines
+					//so clone the partial for rendering
+					if (this.options.clone){
+						var clone = document.id(partial.element.cloneNode(true));
+						data.target.appendChild(clone);
+						//we also need a key/value map of all the updates for quick lookups
+						data.updates[id] = clone;
+					} else {
+						data.updates[id] = partial.element;
+					}
+					checked = true;
+				}
+			}
+			//dbug.timeEnd('check partials');
+			return checked ? data : false;
+		},
+
+		//given a partial, attempts to find the line it is in
+		//example: for a td that is a partial, it may have the tr as its line
+		_getPartialLine: function(partial, forceUpdate){
+			if (!partial.line || forceUpdate) partial.line = partial.element.getParent('[' + this.options.partialLineIdProperty + ']');
+			return partial.line;
+		},
+
+		//this method destroys a partial given its partial id
+		_destroyPartial: function(id){
+			//get the element
+			var element = this.partials[id].element;
+
+			this._clean(element);
+			//delete it from the state
+			delete this.partials[id];
+		},
+
+		//this method takes a group of cloned partials (that have been passed through filters)
+		//and puts them back into the DOM from which they came, replacing the elements they
+		//were cloned from. This allows us to retain the DOM structure of the response, while
+		//only running through the filters the elements that need it.
+		_restorePartials: function(checked, partials){
+			//dbug.time('restore partials');
+			for (id in checked.updates) {
+				var clone = checked.updates[id],
+				    partial = partials[id];
+				partial.element.parentNode.replaceChild(clone, partial.element);
+				partial.element = clone;
+			}
+			//dbug.timeEnd('restore partials');
+		},
+
+		_injectPartials: function(partials, checked) {
+			//dbug.time('inject partials');
+			var insertedPartials = {},
+			    partial,
+			    prevId;
+			//iterate over all the partials to inject them into the live DOM
+			for (id in partials) {
+				partial = partials[id];
+				//if it's in a line that's been injected, skip it
+				//if it was passed through the renderers, it means that it needs an update or insertion
+				if (checked.updates[id] && !insertedPartials[id]) {
+					//get the corresponding partial in the DOM
+					var before = this.partials && this.partials[id];
+					//if there's a corresponding partial already in the DOM, replace it
+					if (before) {
+						if (this.options.updateClass) partial.element.addClass(this.options.updateClass);
+						partial.element.replaces(before.element);
+						this._clean(before.element);
+					} else {
+						//else it's not in the DOM
+						//look to see if this partial is in a line item (for example, the tr for a td that is a partial)
+						var line = partial.line || this._getPartialLine(partial);
+						//if there is no line, inject it into the DOM in the container
+						if (line) this._injectPartialLine(partial, insertedPartials);
+						else this._injectPartial(partial, prevId);
+					}
+				}
+				if (!this.partials) this.partials = {};
+				if (checked.updates[id]) this.partials[id] = partial;
+				prevId = id;
+			}
+			//dbug.timeEnd('inject partials');
+		},
+
+		_cleanRemainingPartials: function(partials){
+			//dbug.time('clean partials');
+			var linesToDestroy = {}, partial;
+			//for any partials that were in the DOM but not in the response, remove them
+			for (id in this.partials) {
+				partial = this.partials[id];
+				//if the partial is in the DOM but not the response
+				if (!partials || !partials[id]) {
+					//get its line; assume that we have to remove that, too
+					var line = partial.line || this._getPartialLine(partial);
+					if (line) linesToDestroy[line.getAttribute(this.options.partialLineIdProperty)] = line;
+					//destroy the partial
+					this._destroyPartial(id);
+				}
+			}
+			for (id in linesToDestroy) {
+				this.fireEvent('elementDestroy', line);
+				line.destroy();
+			}
+			//dbug.timeEnd('clean partials');
+		},
+
+		//inject a partial into the container; prevId is the previous partial's id, used
+		//to inject the partial in the proper place
+		_injectPartial: function(partial, prevId){
+			this._validateId(partial.get(this.options.partialIdProperty));
+			var previousPartial = this.partials[prevId];
+			if (previousPartial) {
+				//if this isn't the first one, inject it after the previous id
+				partial.element.inject(previousPartial.element, 'after');
+			} else {
+				partial.inject(this.container, 'top');
+			}
+		},
+
+		//inject a partial line into the container; insertedPartials is a map of partial ids to
+		//partials that have been injected; used to ensure partials aren't injected twice when
+		//a line that contains several is injected into the DOM
+		_injectPartialLine: function(partial, insertedPartials){
+			//there is a line, so we inject it instead of the partial.
+			//get the previous line (from the response)
+			var line = partial.line || this._getPartialLine(partial);
+			var prevLine = line.getPrevious('[' + this.options.partialLineIdProperty + ']'),
+			    prevLineInDOM;
+			this._validateId(line.get(this.options.partialLineIdProperty));
+
+			//now find it's counterpart in the live DOM
+			var injected = false;
+			if (!this.options.sorted) {
+				if (prevLine) prevLineInDOM = this.container.getElement('[' + this.options.partialLineIdProperty + '=' + prevLine.getAttribute(this.options.partialLineIdProperty) + ']');
+				//if it's there, inject this line after it
+				if (prevLineInDOM) {
+					line.inject(prevLineInDOM, 'after');
+				} else {
+					line.inject(this.container, 'top');
+				}
+				injected = true;
+			}
+			if (!injected) line.inject(this.container);
+			//store the fact that we just injected all the partials in this line
+			var linePartials = line.getElements('[' + this.options.partialIdProperty + ']');
+			for (var p = 0; p < linePartials.length; p++) {
+				this._validateId(linePartials[p].get(this.options.partialIdProperty));
+				if (this.options.updateClass) linePartials[p].addClass(this.options.updateClass);
+				insertedPartials[linePartials[p].getAttribute(this.options.partialIdProperty)] = true;
+			};
+		},
+
+		_sortPartials: function(responseContainer) {
+			//dbug.time('sort partials');
+			this._sortCount = 0;
+			//get all the children of the update container that are partially updatable
+			var kids = responseContainer.getChildren('[' + this.options.partialIdProperty + ']');
+			//for each kid, inject it into this container
+			for (var kid = 0; kid < kids.length; kid++){
+				var id = kids[kid].getAttribute(this.options.partialIdProperty);
+				this.partials[id].element.inject(this.container);
+			}
+			//get all the lines that are children of the update container
+			var lines = responseContainer.getChildren('[' + this.options.partialLineIdProperty + ']');
+			if (lines.length){
+				//get the lines in this.container
+				var liveLines = this.container.getChildren('[' + this.options.partialLineIdProperty + ']');
+				var liveLineMap = {}, liveLineOrder = [];
+				//create a map of all the live lines to their ids
+				for (var liveLine = 0; liveLine < liveLines.length; liveLine++) {
+					var liveLineId = liveLines[liveLine].getAttribute(this.options.partialLineIdProperty);
+					liveLineMap[liveLineId] = liveLines[liveLine];
+					liveLineOrder.push(liveLineId);
+				}
+				//loop through all the lines in the response
+				var prev;
+				for (var line = 0; line < lines.length; line++){
+					//get the id of the response line
+					var lineId = lines[line].getAttribute(this.options.partialLineIdProperty);
+					if (lineId != liveLineOrder[line]) {
+						//inject the live line into that location
+						if (!prev) liveLineMap[lineId].inject(this.container, 'top');
+						else liveLineMap[lineId].inject(prev, 'after');
+						var index = liveLineOrder.indexOf(lineId);
+						liveLineOrder.splice(index, 1);
+						liveLineOrder.splice(line, 0, lineId);
+						this._sortCount++;
+					}
+					prev = liveLineMap[lineId];
+				}
+			}
+			//dbug.log('items: %s, sorted: %s', lines.length, this._sortCount);
+			//dbug.timeEnd('sort partials');
+		},
+
+		_toGC: [],
+
+		_clean: function(element){
+			this.fireEvent('elementDestroy', element);
+			if (!this._gcTimer) this._gcTimer = this._gc.periodical(100, this);
+			this._toGC.push({
+				element: element
+			});
+		},
+
+		//this is a "threadsafe" garbage collection routine
+		//if we call element.destroy on, say, 1000 nodes, the MooTools gc goes through
+		//all of them at once. This version does them in batches, releasing the thread
+		//as it goes.
+		_gc: function(){
+			if (this._toGC.length) {
+				var i = 0;
+				while (this._toGC.length && i < Math.max(100, this._toGC.length/100)) {
+					var data = this._toGC.shift();
+					//destroy the element
+					this._destroy(data.element);
+					i++;
+				}
+				if (i == 0) clearInterval(this._gcTimer);
+			}
+		},
+
+		//implementing a custom destroy method; this is slightly faster than the MooTools version
+		//but doesn't do some of it's less-important garbage collection
+		_destroy: function(el){
+			for (var i = 0; i < el.childNodes.length; i++) {
+				this._destroy(el.childNodes[i]);
+			}
+			if (el.removeEvents) el.removeEvents();
+		},
+
+		//ids must be alpha-numeric
+		_validateId: function(id){
+			if (id.test(validIdRE)) throw ('invalid partial id: "' + id + '"');
+		}
+
+	});
+
+	var validIdRE = /[^a-zA-Z0-9_\-]/;
+
+	PartialUpdate.Single = new Class({
+
+		Extends: PartialUpdate,
+
+		initialize: function(element, options){
+			this.element = document.id(element);
+			this.html = this.element.innerHTML;
+			this.setOptions(options);
+			this.partialId = this.element.get(this.options.partialIdProperty);
+			this._validateId(this.partialId);
+		},
+
+		update: function(container){
+			var partial = container.getElement('[' + this.options.partialIdProperty + '=' + this.partialId + ']');
+			var html = partial && partial.innerHTML;
+			if (partial && html != this.element.innerHTML) {
+				this.fireEvent('beforeUpdate', partial);
+				if (this.options.updateClass) partial.addClass(this.options.updateClass);
+				partial.replaces(this.element);
+				var element = this.element;
+				this.element = partial;
+				this.html = html;
+				this._clean(element);
+				this._cleanUpdateFlag.delay(this.options.flagRemovalDelay, this);
+			}
+		},
+
+		updateState: function(container){
+			var partial = container.getElement('[' + this.options.partialIdProperty + '=' + this.partialId + ']');
+			var html = partial && partial.innerHTML;
+			this.html = html;
+		},
+
+		_cleanUpdateFlag: function(){
+			this.element.removeClass(this.options.updateClassToRemove);
+		}
+
+	});
+
+})();

+ 2 - 0
desktop/core/static/js/package.yml

@@ -12,6 +12,7 @@ sources: [
   Source/JFrameRenderers/CCS.JFrame.Alert.js,
   Source/JFrameRenderers/CCS.JFrame.Prompt.js,
   Source/JFrameRenderers/CCS.JFrame.PartialRefresh.js,
+  Source/JFrameRenderers/PartialUpdate.js,
   Source/CCS/CCS.js,
   Source/CCS/CCS.ContextMenu.js,
   Source/CCS/CCS.Desktop.BackgroundManager.js,
@@ -53,6 +54,7 @@ sources: [
   Source/JFrameLinkers/CCS.JFrame.Refresh.js,
   Source/JFrameLinkers/CCS.JFrame.RefreshWithParams.js,
   Source/JFrameLinkers/CCS.JFrame.SubmitLink.js,
+  Source/JFrameLinkers/CCS.JFrame.SplitViewLinkers.js,
   Source/JFrameLinkers/CCS.JFrame.Target.js,
   Source/Native/String.CCS.js,
   Source/Native/Element.Data.js,

+ 76 - 0
ext/thirdparty/js/test-runner/Configuration.js

@@ -0,0 +1,76 @@
+// Put this file in the parent directory of the runner folder. Also rename the file to Configuration.js
+
+(function(context){
+
+var Configuration = context.Configuration = {};
+
+// Runner name
+Configuration.name = 'Hue';
+
+
+// Presets - combine the sets and the source to a preset to easily run a test
+Configuration.presets = {
+	
+	'PartialUpdate': {
+		sets: ['PartialUpdate'],
+		source: ['PartialUpdate']
+	}
+	
+};
+
+// An object with default presets
+Configuration.defaultPresets = {
+	browser: 'PartialUpdate',
+	nodejs: 'PartialUpdate',
+	jstd: 'PartialUpdate'
+};
+
+
+/*
+ * An object with sets. Each item in the object should have an path key', '
+ * that specifies where the spec files are and an array with all the files
+ * without the .js extension relative to the given path
+ */
+Configuration.sets = {
+
+	'PartialUpdate': {
+		path: 'hue/',
+		files: ['PartialUpdate']
+	}
+
+};
+
+
+/*
+ * An object with the source files. Each item should have an path key,
+ * that specifies where the source files are and an array with all the files
+ * without the .js extension relative to the given path
+ */
+Configuration.source = {
+
+	'PartialUpdate': {
+		path: '../',
+		files: [
+			'clientcide/Source/Core/dbug',
+			'core/Source/Core/Core',
+			'core/Source/Native/Array',
+			'core/Source/Native/String',
+			'core/Source/Native/Function',
+			'core/Source/Native/Number',
+			'core/Source/Native/Hash',
+			'core/Source/Class/Class',
+			'core/Source/Class/Class.Extras',
+			'core/Source/Core/Browser',
+			'core/Source/Element/Element',
+			'core/Source/Element/Element.Dimensions',
+			'core/Source/Utilities/Selectors',
+			'more/Source/Core/More',
+			'more/Source/Element/Elements.From',
+			'more/Source/Native/Array.Extras',
+			'../../../desktop/core/static/js/Source/JFrameRenderers/PartialUpdate'
+		]
+	}
+
+};
+
+})(typeof exports != 'undefined' ? exports : this);

+ 113 - 0
ext/thirdparty/js/test-runner/hue/PartialUpdate.js

@@ -0,0 +1,113 @@
+Selectors.RegExps.combined = (/\.([\w-]+)|\[([\w-]+)(?:([!*^$~|]?=)(["']?)([^\4]*?)\4)?\]|:([\w-]+)(?:\(["']?(.*?)?["']?\)|$)/g);
+
+describe('PartialUpdate', function(){
+
+	var makeTable = function(id, howbig, randomize){
+		var table = '<table id="tbl' + id + '"><tbody data-partial-container-id="tbl">';
+		var rows = [];
+		howbig.times(function(i){
+			rows.push('<tr data-partial-line-id="tr' + i + '"><td data-partial-id="tr' + i + 'td1">tbl' + id + ' tr' + i + ' td1</td><td data-partial-id="tr' + i + 'td2">tbl' + id + ' tr' + i + ' td2</td></tr>');
+		});
+		if (randomize) rows.shuffle();
+		table += rows.join('');
+		table += '</tbody></table>';
+		return Elements.from(table)[0];
+	};
+
+	var table1 = makeTable(1, 2);
+	var table2 = makeTable(2, 2);
+	var table3 = makeTable(3, 3);
+	//remove item 1
+	table3.getElements('tr')[1].dispose();
+	//move item 2 to be before item 0 (2,0)
+	table3.getElements('tr')[1].injectBefore(table3.getElements('tr')[0]);
+	var table4 = makeTable(4, 3);
+	//move item 2 before item 0 (2,0,1)
+	table4.getElements('tr')[2].injectBefore(table4.getElements('tr')[0]);
+	//move item 2 before item 1 (2,1,0)
+	table4.getElements('tr')[2].injectBefore(table4.getElements('tr')[1]);
+
+	var bigtable = makeTable(1, 1000);
+	var bigtable2 = makeTable(2, 1000);
+	var bigtable3 = makeTable(3, 1050, true);
+
+	var div = Elements.from('<div><p data-partial-id="foo">foo</p></div>')[0];
+	var div2 = Elements.from('<div><p data-partial-id="foo">bar</p></div>')[0];
+
+	var table = table1.clone(true, true);
+
+	var updater = new PartialUpdate(table.getElement('tbody'), {
+		updateClass: '',
+		updateClassToRemove: ''
+	});
+
+	var single = new PartialUpdate.Single(div.getElement('[data-partial-id]'), {
+		updateClass: '',
+		updateClassToRemove: ''
+	});
+
+	var bigTclone = bigtable.clone(true, true);
+	var bigUp = new PartialUpdate(bigTclone.getElement('tbody'), {
+		updateClass: '',
+		clone: true,
+		updateClassToRemove: ''
+	});
+
+	var test = function(tbl, up){
+		up = up || updater;
+		var tbody = tbl.getElement('tbody').clone(true, true);
+		up.update(tbody);
+		expect(document.id(up).innerHTML).toEqual(tbl.getElement('tbody').innerHTML);
+		//dbug.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
+	};
+	
+	it('Should initialize PartialUpdate', function(){
+		expect(updater.partials.tr0td1.html).toEqual('tbl1 tr0 td1');
+	});
+
+	it('Should update a PartialUpdate instance', function(){
+		//dbug.log('Should update a PartialUpdate instance');
+		test(table2);
+		expect(updater._sortCount).toEqual(0);
+	});
+	
+	it('Should update and sort elements in a PartialUpdate instance', function(){
+		//dbug.log('Should update and sort elements in a PartialUpdate instance');
+		test(table3);
+		expect(updater._sortCount).toEqual(1);
+	});
+	
+	it('Should update and add elements in a PartialUpdate instance', function(){
+		//dbug.log('Should update and add elements in a PartialUpdate instance');
+		test(table4);
+		expect(updater._sortCount).toEqual(1);
+	});
+
+	it('Should update and remove elements in a PartialUpdate instance', function(){
+		//dbug.log('Should update and remove elements in a PartialUpdate instance');
+		test(table1);
+		expect(updater._sortCount).toEqual(1);
+	});
+
+	it('Should initialize a PartialUpdate.Single', function(){
+		expect(single.html).toEqual('foo');
+	});
+
+	it('Should update a PartialUpdate.Single', function(){
+		single.update(div2.clone(true, true));
+		expect(single.html).toEqual('bar');
+	});
+
+	it('Should update a PartialUpdate instance with 1000 rows (benchmark)', function(){
+		//dbug.log('Should update a PartialUpdate instance with 1000 rows (benchmark)');
+		test(bigtable2, bigUp);
+		expect(bigUp._sortCount).toEqual(0);
+	});
+
+	it('Should update *and sort* a PartialUpdate instance with 1000 rows (benchmark)', function(){
+		//dbug.log('Should update *and sort* a PartialUpdate instance with 1000 rows (benchmark)');
+		test(bigtable3, bigUp);
+		expect(bigUp._sortCount).toBeGreaterThan(0);
+	});
+
+});