1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2009, 2010, 2013, 2014, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2005, 2006, 2007, 2009, 2010, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * This requires an "owner" which is the object that owns the full set of items, implementing:
 26  * getItemCount() to return the number of items
 27  * getItem(index) to return the item at a given index.
 28  * 
 29  * And optionally implementing
 30  * itemSelectionChanged(item, index, isSelected) which is called
 31  *         for each item that is selected or deselected
 32  * selectionChanged() which is called after a batch of items have
 33  *         been selected or deselected with select()
 34  *
 35  * @private
 36  */
 37 AjxSelectionManager = function(anOwner) {
 38 	this._owner = anOwner;
 39 };
 40 
 41 // -----------------------------------------------------------
 42 // Constants
 43 // -----------------------------------------------------------
 44 
 45 // Actions for select()
 46 AjxSelectionManager.SELECT_ONE_CLEAR_OTHERS = 0;
 47 AjxSelectionManager.TOGGLE_ONE_LEAVE_OTHERS = 1;
 48 AjxSelectionManager.SELECT_TO_ANCHOR = 2;
 49 AjxSelectionManager.DESELECT_ALL = 3;
 50 AjxSelectionManager.SELECT_ALL = 4;
 51 
 52 // -----------------------------------------------------------
 53 // API Methods
 54 // -----------------------------------------------------------
 55 
 56 /**
 57  * returns an AjxVector
 58  */
 59 AjxSelectionManager.prototype.getItems = function() {
 60 	if (this._selectedItems == null) {
 61 		this._selectedItems = this._createItemsCollection();
 62 	}
 63 	return this._selectedItems;
 64 };
 65 
 66 /**
 67  * returns the number of selected items
 68  */	
 69 AjxSelectionManager.prototype.getLength = function() {
 70 	return this.getItems().length;
 71 };
 72 	
 73 /**
 74  * returns the anchor, unless nothing is selected
 75  */
 76 AjxSelectionManager.prototype.getAnchor = function() {
 77 	if (this._anchor == null) {
 78 		var items = this.getItems();
 79 		if (items.length > 0) {
 80 			this._anchor = items[0];
 81 		}
 82 	}
 83 	return this._anchor;
 84 };
 85     
 86 /**
 87  * The cursor probably changes when the users navigates with 
 88  * the keyboard. This returns the item that is currently the cursor,
 89  * and null if nothing is selected.
 90  */
 91 AjxSelectionManager.prototype.getCursor = function() {
 92 	if (this._cursor == null) {
 93 		this._cursor = this.getAnchor();
 94 	}
 95 	return this._cursor;
 96 };
 97     
 98     
 99 /**
100  * Returns true if the given item is selected.
101  */
102 AjxSelectionManager.prototype.isSelected = function(item) {
103 	return this.getItems().binarySearch(item) != -1;
104 };
105     
106 AjxSelectionManager.prototype.selectOneItem = function(item) {
107 	this.select(item, AjxSelectionManager.SELECT_ONE_CLEAR_OTHERS);
108 };
109     
110 AjxSelectionManager.prototype.toggleItem = function(item) {
111 	this.select(item, AjxSelectionManager.TOGGLE_ONE_LEAVE_OTHERS);
112 };
113 	
114 AjxSelectionManager.prototype.selectFromAnchorToItem = function(item) {
115 	this.select(item, AjxSelectionManager.SELECT_TO_ANCHOR);
116 };
117     
118 AjxSelectionManager.prototype.deselectAll = function() {
119 	this.select(null, AjxSelectionManager.DESELECT_ALL);
120 };
121 	
122 AjxSelectionManager.prototype.selectAll = function() {
123 	this.select(null, AjxSelectionManager.SELECT_ALL);
124 };
125     
126     
127 /**
128  * This method will notify the owner of any changes by calling
129  * itemSelectionChanged() (if the owner defines it) for each item whose
130  * selection changes and also by calling selectionChanged() (if the
131  * owner defines it) once at the end, if anything changed selection.
132  *
133  */
134 AjxSelectionManager.prototype.select = function(item, action) {
135 	
136 	// Update the anchor and cursor, if necessary
137 	this._setAnchorAndCursor(item, action);
138     
139 	// save off the old set of selected items
140 	var oldItems = this._selectedItems;
141 	var oldItemsCount = (oldItems == null) ? 0 : oldItems.length;
142 	
143 	// create a fresh set of selected items
144 	this._selectedItems = null;
145 	this._selectedItems = this._createItemsCollection();
146 	
147 	// Now update the selection
148 	var itemCount = this._owner.getItemCount();
149 	var needsSort = false;
150 	var selectionChanged = false;
151 	var selecting = false;
152 	for (var i = 0; i < itemCount; ++i) {
153 		var testItem = this._owner.getItem(i);
154 		var oldSelectionExists = this._isItemOldSelection(testItem, oldItems);
155 		var newSelectionExists = oldSelectionExists;
156 		
157 		switch (action) {
158 		case AjxSelectionManager.SELECT_TO_ANCHOR:
159 			if (this._anchor == null) {
160 				// If we have no anchor, let it be the first item
161 				// in the list
162 				this._anchor = testItem;
163 			}
164 			var atEdge = (testItem == this._anchor || testItem == item);
165 			var changed = false;
166 			// mark the beginning of the selection for the iteration
167 			if (!selecting && atEdge) {
168 				selecting = true;
169 				changed = true;
170 			}
171 			newSelectionExists = selecting;
172 			// mark the end of the selection if we're there
173 			if ((!changed || this._anchor == item) 
174 				&& selecting && atEdge) {
175 				selecting = false;
176 			}
177 
178 			break;
179 		case AjxSelectionManager.SELECT_ONE_CLEAR_OTHERS:
180 			newSelectionExists = (testItem == item);
181 			break;
182 		case AjxSelectionManager.TOGGLE_ONE_LEAVE_OTHERS:
183 			if (testItem == item) {
184 				newSelectionExists = !oldSelectionExists ;
185 			}
186 			break;
187 		case AjxSelectionManager.DESELECT_ALL:
188 			newSelectionExists = false;
189 			break;
190 		case AjxSelectionManager.SELECT_ALL:
191 			newSelectionExists = true;
192 			break;
193 		}
194 
195 		if (newSelectionExists) {
196 			this._selectedItems.add(testItem);
197 			needsSort = (this._selectedItems.length > 1);
198 		}
199 
200 		if ( newSelectionExists != oldSelectionExists) {
201 			// Something changed so notify the owner.
202 			if (this._owner.itemSelectionChanged != null) {
203 				this._owner.itemSelectionChanged(testItem, 
204 												 i, newSelectionExists);
205 			}
206 			selectionChanged = true;
207 		}
208 	}
209 	selectionChanged = selectionChanged || (oldItemsCount != 
210 											this._selectedItems.length);
211 
212 	if (needsSort) this._selectedItems.sort();
213 	
214 	if (selectionChanged && this._owner.selectionChanged != null) {
215 		this._owner.selectionChanged(item);
216 	}
217 };
218 
219 /**
220  * Remove an item from the selection managers selected items
221  * collection if it exists.
222  */
223 AjxSelectionManager.prototype.removeItem = function(item) {
224 	if (this._selectedItems) {
225 		var index = this._selectedItems.binarySearch(item);
226 		if (index > -1) this._selectedItems.removeAt(index);
227 	}
228 };
229 
230 // -----------------------------------------------------------
231 // Internal Methods
232 // -----------------------------------------------------------
233 	
234 /**
235  * Creates an array suitable for use as the sorted list of selected
236  * items and returns it.
237  */
238 AjxSelectionManager.prototype._createItemsCollection = function() {
239 	return new AjxVector();
240 };
241 
242 AjxSelectionManager.prototype._isItemOldSelection = function (testItem, oldItems) {
243 	var ret = false;
244 	if (oldItems) {
245 		var oldSelectionIndex = oldItems.binarySearch(testItem);
246 		if (oldSelectionIndex > -1) {
247 			oldItems.removeAt(oldSelectionIndex);
248 		}
249 		ret = (oldSelectionIndex != -1);
250 	}
251 	return ret;
252 };
253 
254 AjxSelectionManager.prototype._setAnchorAndCursor = function (item, action) {
255 	switch (action) {
256 	case AjxSelectionManager.SELECT_TO_ANCHOR:
257 		this._cursor = item;
258 		break;
259 	case AjxSelectionManager.SELECT_ONE_CLEAR_OTHERS:		
260 		this._anchor = item;
261 		this._cursor = item;
262 		break;
263 	case AjxSelectionManager.TOGGLE_ONE_LEAVE_OTHERS:
264 		this._anchor = item;
265 		this._cursor = item;
266 		break;
267 	case AjxSelectionManager.DESELECT_ALL:
268 		this._anchor = null;
269 		this._cursor = null;
270 		break;
271 	case AjxSelectionManager.SELECT_ALL:
272 		return;
273 	}
274 };
275