Эх сурвалжийг харах

HUE-25
updating side-by-side select demo to have more options (also fixing a typo)
sidebyside select's layout changed; updating css
reworking sidebyside select to have multi-select and keyboard support.

Aaron Newton 15 жил өмнө
parent
commit
f5225215bc

+ 11 - 1
apps/jframegallery/src/jframegallery/templates/side.by.side.select.html

@@ -18,7 +18,7 @@ limitations under the License.
 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
 <html>
 	<head>
-		<title>Art Button</title>
+		<title>Side-by-side Select</title>
 	</head>
 	<body>
 		<div class="jframe_padded">
@@ -27,6 +27,16 @@ limitations under the License.
 				<option value="1">one</option>
 				<option value="2">two</option>
 				<option value="3">three</option>
+				<option value="4">four</option>
+				<option value="5">five</option>
+				<option value="6">six</option>
+				<option value="7">seven</option>
+				<option value="8">eight</option>
+				<option value="9">nine</option>
+				<option value="10">ten</option>
+				<option value="11">eleven</option>
+				<option value="12">twelve</option>
+				<option value="13">thirteen</option>
 			</select>
 			
 			<h2 style="margin: 8px 0px 4px">Example in a container that has the side_by_side_select class</h2>

+ 15 - 19
desktop/core/static/css/shared.css

@@ -424,9 +424,12 @@ Takes a multi-select box and makes it so you can click to move elements from a s
 */
 
 .sideBySideSelect {
+	user-select: none;
+	-moz-user-select: none;
+	-webkit-user-select: none;
 	min-width:340px;
 }
-.sideBySideSelect .deselected, .sideBySideSelect .selected {
+.sideBySideSelect div.deselected, .sideBySideSelect div.selected {
 	border: 1px solid #333;
 	float: left;
 	width: 45%;
@@ -435,31 +438,24 @@ Takes a multi-select box and makes it so you can click to move elements from a s
 	overflow: auto;
 }
 .sideBySideSelect .spacer {
-	background: url(/static/art/led-icons/tag.png) no-repeat 50% 50%;
+	padding-top: 50px;
 	width: 30px;
-	height: 150px;
 	float: left;
 }
-.sideBySideSelect .selected table.ccs-data_table tr > td:first-child, .sideBySideSelect .selected table.ccs-data_table tr td:first-child {
-	padding-left: 16px;
-}
-.sideBySideSelect .selected table.ccs-data_table tr:hover > td:first-child, .sideBySideSelect .selected table.ccs-data_table tr.table-tr-hovered td:first-child {
-
-	background-image: url(/static/art/icons/arrow_left_tiny.png);
-	background-repeat: no-repeat;
-	background-position: 0px 2px;
+.sideBySideSelect .spacer a {
+	display: block;
+	width: 16px;
+	height: 16px;
+	text-indent: -900px;
+	margin: 5px auto;
 }
-.sideBySideSelect .selected table.ccs-data_table tr > td:first-child, .sideBySideSelect .selected table.ccs-data_table tr td:first-child {
-	padding-right: 16px;
+.sideBySideSelect .spacer .moveRight {
+	background: url(/static/art/led-icons/arrow_right.png) no-repeat;
 }
-.sideBySideSelect .deselected table.ccs-data_table tr:hover > td:first-child, .sideBySideSelect .deselected table.ccs-data_table tr.table-tr-hovered td:first-child {
-
-	background-image: url(/static/art/icons/arrow_right_tiny.png);
-	background-repeat: no-repeat;
-	background-position: right 2px;
+.sideBySideSelect .spacer .moveLeft {
+	background: url(/static/art/led-icons/arrow_left.png) no-repeat;
 }
 
-
 /* help text links */
 .ccs-help_link, .ccs-info_link {
 	display:-moz-inline-box;		/* FF2 */

+ 292 - 37
desktop/core/static/js/Source/UI/ART.SideBySideSelect.js

@@ -17,7 +17,7 @@
 ---
 description: Turns a mutli-select input into two side-by-side areas that items move between when selected/deselected.
 provides: [ART.SideBySideSelect]
-requires: [Widgets/ART.Widget, More/HtmlTable.Zebra, Core/Fx.Tween, Core/Selectors]
+requires: [Widgets/ART.Widget, Widgets/ART.Keyboard, More/HtmlTable.Zebra, Core/Fx.Tween, Core/Selectors]
 script: ART.SideBySideSelect.js
 ...
 */
@@ -44,25 +44,44 @@ ART.SideBySideSelect = new Class({
 			},
 			headers: ['Selected']
 		},
-		makeContent: function(name, selected) {
+		makeContent: function(name, selected){
 			return new Element('span', { html: name });
 		}
 	},
 	
-	initialize: function(select, options) {
-		this.select = $(select);
+	initialize: function(select, options){
+		this.select = $(select).setStyle('display', 'none');
 		this.parent(options);
 		this._build();
 		this.element.setStyle('display', 'block');
 		this.select.store('SideBySideSelect', this);
+		new ART.Keyboard(this);
+		this._attachKeys();
 	},
 	
-	_build: function() {
+	// creates the DOM elements for the UI
+	_build: function(){
 		this.deselectedContainer = new Element('div', { 'class':'deselected' });
 		this.deselected = new HtmlTable(this.options.deselectedOptions);
 		this.deselectedContainer.adopt(this.deselected);
 		
-		this.spacer = new Element('div', {'class': 'spacer'});
+		this.spacer = new Element('div', {
+			'class': 'spacer'
+		});
+		this.moveRight = new Element('a', {
+			text: 'select',
+			'class': 'moveRight',
+			events: {
+				click: this.moveSelection.bind(this, 'right')
+			}
+		}).inject(this.spacer);
+		this.moveLeft = new Element('a', {
+			text: 'select',
+			'class': 'moveLeft',
+			events: {
+				click: this.moveSelection.bind(this, 'left')
+			}
+		}).inject(this.spacer);
 		
 		this.selectedContainer = new Element('div', { 'class':'selected' });
 		this.selected = new HtmlTable(this.options.selectedOptions);
@@ -71,72 +90,308 @@ ART.SideBySideSelect = new Class({
 		this.element.adopt(this.deselectedContainer);
 		this.element.adopt(this.spacer);
 		this.element.adopt(this.selectedContainer);
-		
+
+		this.makeRows();
+
+		//click a row, select it; double click, move it
+		this.element.addEvent('click:relay(tr)', this.clickRow.bind(this));
+		this.element.addEvent('dblclick:relay(tr)', function(e, tr){
+			e.stop();
+			this.moveRow(tr);
+		}.bind(this));
+	},
+	
+	makeRows: function(){
+		this.selected.body.empty();
+		this.deselected.body.empty();
 		this.select.getElements('option').each(function(option){
 			var name = [option.get('text') || option.get('value')];
 			this._options[name] = option;
-			if (option.get('selected')) this.selectRow(name);
-			else this.deselectRow(name);
+			var row = this._makeRow(name, option.get('selected') ? 'selected' : 'deselected');
 		}, this);
+	},
+	
+	_attachKeys: function(){
+		//selects all rows in the focused table
+		var selectAll = function(e){
+			e.preventDefault();
+			var trs;
+			if (this._rangeStart) trs = this._rangeStart.getSiblings('tr');
+			else trs = this.deselected.getElements('tbody tr');
+			trs.each(function(tr) {
+				this.selectRow(tr, true);
+			}, this);
+		}.bind(this);
+		//returns the focused row or our best guess as to which one is our starting point
+		//defaults to the left side's first row if nothing else matches
+		var getCurrentRow = function(){
+			if (this._focusedRow) return this._focusedRow;
+			if (this._rangeStart) return this._rangeStart;
+			else return this.deselected.getElement('tbody tr');
+		}.bind(this);
+		//return the sibling rows for a given row
+		var getSiblings = function(row){
+			return row.getParent('tbody').getChildren('tr');
+		};
+		//selects the next row from the current focused row
+		var selectNext = function(e){
+			e.preventDefault();
+			var row = getCurrentRow();
+			var rows = getSiblings(row);
+			var next = rows[rows.indexOf(row) + 1];
+			if (!next) return;
+			//if shift is held, don't clear the selection; add to it
+			if (!e.shift) this.clearSelection(rows);
+			if (!next.hasClass('selected')) this.selectRow(next, true);
+			else this.deselectRow(row, true);
+			this._focusedRow = next;
+		}.bind(this);
+		//same as selectNext, only the other way
+		var selectPrev = function(e){
+			e.preventDefault();
+			var row = getCurrentRow();
+			var rows = getSiblings(row);
+			var prev = rows[rows.indexOf(row) - 1];
+			if (!prev) return;
+			if (!e.shift) this.clearSelection(rows);
+			if (!prev.hasClass('selected')) this.selectRow(prev, true);
+			else this.deselectRow(row, true);
+			this._focusedRow = prev;
+		}.bind(this);
+		//move selected rows to the left
+		var moveLeft = function(e){
+			e.preventDefault();
+			$(this.selected).getElements('tr.selected').each(function(row){
+				this.moveRow(row);
+			}, this);
+		}.bind(this);
+		//move selected rows to the right
+		var moveRight = function(e){
+			e.preventDefault();
+			$(this.deselected).getElements('tr.selected').each(function(row){
+				this.moveRow(row);
+			}, this);
+		}.bind(this);
+		//add keyboard entries for the above actions
+		this.addShortcuts({
+			'Select All': {
+				keys: 'ctrl+a',
+				shortcut: 'control/command + a',
+				handler: selectAll,
+				description: 'Select the previous row in the table.'
+			},
+			'Select Previous Row': {
+				keys: 'up',
+				shortcut: 'up arrow',
+				handler: selectPrev,
+				description: 'Select the previous row in the table.'
+			},
+			'Select Next Row': {
+				keys: 'down',
+				shortcut: 'down arrow',
+				handler: selectNext,
+				description: 'Select the next row in the table.'
+			},
+			'Move Row(s) Right': {
+				keys: 'right',
+				shortcut: 'right arrow',
+				handler: moveRight,
+				description: 'Select the next row in the table.'
+			},
+			'Move Row(s) Left': {
+				keys: 'left',
+				shortcut: 'left arrow',
+				handler: moveLeft,
+				description: 'Select the next row in the table.'
+			}
+		});
 		
-		this.element.addEvent('click:relay(tr)', this.swap.bind(this));
+		this.attachKeys({
+			'meta+a': selectAll,
+			'shift+up': selectPrev,
+			'shift+down': selectNext,
+			'shift+right': moveRight,
+			'shift+left': moveLeft
+		});
 	},
 	
 	_options: {},
 	_rows: {},
 	
-	_destroyRow: function(name) {
-		var row = this._rows[name];
-		if (row) {
+	//return a row given the name (the text of the option the row correlates to)
+	_getRow: function(name){
+		return this._rows[name];
+	},
+	
+	//given a row, return the name (the option text)
+	_getName: function(row){
+		return row.retrieve('_sideBySide:name');
+	},
+	
+	//given an option name, get the value of it.
+	getValue: function(name){
+		return this._options[name].get('value') || this._options[name].get('text');
+	},
+	
+	//remove a row from the layout; pass in noEffect (boolean) as *true* to do this instantly
+	_destroyRow: function(row, noEffect){
+		if (noEffect) {
+			row.destroy();
+		} else {
 			row.fade('out', { duration: 500 }).get('tween').chain(function(){
 				row.destroy();
 			});
 		}
 	},
 	
-	getValue: function(name) {
-		return this._options[name].get('value');
+	//select a given row; pass *true* for noMove to leave it where it is, otherwise move it to the opposite side
+	selectRow: function(row, noMove){
+		if (noMove) {
+			row.addClass('selected');
+		} else {
+			this.moveRow(row, 'selected');
+			row.setStyle('opacity', 0).fade('in');
+		}
+		return row;
 	},
-	
-	selectRow: function(name) {
-		this._destroyRow(name);
-		var row = this.selected.push([this.options.makeContent(name, true)]).tr;
-		row.setStyle('opacity', 0).fade('in').store('_sideBySide:name', name);
-		this._rows[name] = row;
-		this._options[name].set('selected', true);
-		this.fireEvent('select', [name, row]);
+
+	//same as selectRow, only the other direction
+	deselectRow: function(row, noMove){
+		if (noMove) {
+			row.removeClass('selected');
+		} else {
+			this.moveRow(row, 'deselected');
+			row.setStyle('opacity', 0).fade('in');
+		}
 		return row;
 	},
-	
-	deselectRow: function(name) {
-		this._destroyRow(name);
-		var row = this.deselected.push([this.options.makeContent(name)]).tr;
-		row.setStyle('opacity', 0).fade('in').store('_sideBySide:name', name);
+
+	//given a name, create a row in the specified table; either 'selected' or 'deselected'
+	_makeRow: function(name, where){
+		var row = this[where].push([this.options.makeContent.call(this, name, true)]).tr.store('_sideBySide:name', name);
+		this._options[name].set('selected', where == 'selected');
 		this._rows[name] = row;
-		this._options[name].set('selected', false);
-		this.fireEvent('deselect', [name, row]);
+		return row;
+	},
+
+	//move a row to the specified table ('selected' or 'deselected');
+	//if no *where* value is defined, move it to whatever table it isn't in.
+	moveRow: function(row, where){
+		var name = this._getName(row);
+		if (!where) where = $(this.selected).hasChild(row) ? 'deselected' : 'selected';
+		this[where].push(row);
+		this.selected.updateZebras();
+		this.deselected.updateZebras();
+		this[where + 'Container'].scrollTo(0, 999999);
+		this._options[name].set('selected', where == 'selected');
+		this.fireEvent(where == 'selected' ? 'select' : 'deselect', [name, row]);
+		this._rangeStart = null;
 		return row;
 	},
 	
-	ignore: function(event, row) {
+	//given a click event, determine if the user intended to select/move a row.
+	//allows for rows to contain elements like links or checkboxes, for example.
+	ignore: function(event, row){
 		var target = $(event.target);
-		return !['a', 'input'].every(function(str){
+		var valid = !['a', 'input'].every(function(str){
 			return !target.match(str);
 		});
+		//if the row doesn't have a stored name, then it is the header or something so ignore it
+		if (valid && this._getName(row)) return true;
+		return false;
 	},
 	
-	swap: function(event, row) {
+	//select or deselect a row depending on its current state.
+	toggleSelect: function(row){
+		if (row.hasClass('selected')) this.deselectRow(row, true);
+		else this.selectRow(row, true);
+	},
+	
+	//select a range of rows. pass in a row element; if a start row is defined,
+	//this row will be used as the end of the selection, otherwise stored as the start.
+	selectRange: function(row){
+		if (!this._rangeStart) {
+			this._rangeStart = row;
+		} else {
+			var start = this._rangeStart;
+			//see if the start of the selection is a child of the selected table
+			var startSelected = $(this.selected).hasChild(start);
+			var end = row;
+			//see if the end is a child of the selected table
+			var endSelected = $(this.selected).hasChild(start);
+			//if both are in the same table, select the range
+			if (startSelected == endSelected) {
+				var rows;
+				//get the selected rows
+				if (startSelected) {
+					rows = $(this.selected).getElements('tr');
+					deselected.removeClass('selected');
+				} else {
+					rows = $(this.deselected).getElements('tr');
+					selected.removeClass('selected');
+				}
+				var toToggle = [], started;
+				//go through each row in the respective table and find the rows to mark and mark them
+				rows.each(function(row) {
+					if (row == start || row == end) started = !started;
+					if (started || row == start || row == end) row.addClass('selected');
+					else row.removeClass('selected');
+				}, this);
+			} else {
+				//here the selected row wasn't in the same table as the previous marker, so we clear
+				//the selection and reset the start.
+				this.clearSelection();
+				this._rangeStart = row;
+			}
+		}
+	},
+
+	//clear the selection state
+	//if you pass in a collection of rows, remove their selected class.
+	clearSelection: function(rows){
+		this._rangeStart = null;
+		if (rows) rows.removeClass('selected');
+	},
+
+	//handles a row being clicked
+	clickRow: function(event, row){
+		//check if we should ignore this click
 		if (this.ignore(event, row)) return;
-		var name = row.retrieve('_sideBySide:name');
-		if (!name) return;
-		if ($(this.selected).hasChild(row)) this.deselectRow(name);
-		else if ($(this.deselected).hasChild(row)) this.selectRow(name);
+		//store the new focus point
+		this._focusedRow = row;
+		//if the user is holding shift, select the row and then the range
+		if (event.shift) {
+			this.toggleSelect(row);
+			this.selectRange(row);
+		//if meta/control, then toggle but don't clear the selection
+		} else if (event.meta || event.control) {
+			this.toggleSelect(row);
+		} else {
+			//else it's a naked click; clear the selection and select only the clicked row
+			this.clearSelection($(this).getElements('tr'));
+			this.toggleSelect(row);
+			this._rangeStart = row;
+		}
 		return this;
 	},
 	
+	//moves all selected rows from one side to the other
+	//direction = 'left' or 'right'
+	moveSelection: function(direction){
+		var selected = $(this.selected).getElements('tr.selected');
+		var deselected = $(this.deselected).getElements('tr.selected');
+		var source = direction == 'left' ? selected : deselected;
+		source.each(function(row){
+			var name = this._getName(row);
+			this.moveRow(row, direction == 'left' ? 'deselected' : 'selected');
+		}, this);
+	},
+	
+	//destroys this widget
 	destroy: function(){
 		this.element.destroy();
 		this.fireEvent('destroy');
+		this.eject();
 		return this;
 	}