All files / src/ce ve.ce.SelectionManager.js

81.86% Statements 167/204
45.26% Branches 43/95
79.48% Functions 31/39
82.41% Lines 164/199

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624                                                    1x   369x     369x     369x   369x 369x     369x   369x   369x     369x 369x       589x 369x 369x   369x 369x   369x 369x         1x   1x                           1x 314x 314x     314x 314x 314x   314x                   1x 1633x                                   1x 187x 187x 85x   187x   187x 187x   187x 187x   187x   187x                     187x 187x 106x 106x 87x   87x 87x 87x 87x 87x 87x 87x                         87x       87x                                 87x                                 87x                                 87x                                   106x   106x       187x                     187x     187x 187x 106x 106x       187x 41x 41x 32x 32x 32x         271x 187x                 1x                               1x 391x                                         1x 138x 138x                       1x 106x 106x 106x                               1x 246x 246x 246x 10x       246x 10x 10x   10x     10x 10x             1x 246x           1x                 1x 80x 80x   80x           1x 7x     7x           1x 114x 34x   80x 80x     80x     65x 65x 65x     65x   65x     65x 65x 65x   65x 65x     65x           15x                       1x 85x 85x         85x       85x     85x   85x   85x   85x   85x               1x 187x     187x                   1x 20x 20x 20x               1x     187x   187x   187x     187x                   1x 20x               1x                 1x 106x     106x   106x               1x 271x 107x                 1x                       1x   187x 187x                     1x 87x 87x           1x 32x 32x    
/*!
 * VisualEditor ContentEditable SelectionManager class.
 *
 * @copyright See AUTHORS.txt
 */
 
/**
 * Selection manager
 *
 * Handles rendering of fake selections on the surface:
 * - The deactivated selection stands in the user's native
 *   selection when the native selection is moved elsewhere
 *   (e.g. an inspector, or a dropdown menu).
 * - In a multi-user environment, other users' selections from
 *   the surface synchronizer are rendered here.
 * - Other tools can manually render fake selections, e.g. the
 *   FindAndReplaceDialog can highlight matched text, by calling
 *   #drawSelections directly.
 *
 * @class
 * @extends OO.ui.Element
 * @mixes OO.EventEmitter
 *
 * @constructor
 * @param {ve.ce.Surface} surface
 */
ve.ce.SelectionManager = function VeCeSelectionManager( surface ) {
	// Parent constructor
	ve.ce.SelectionManager.super.call( this );
 
	// Mixin constructors
	OO.EventEmitter.call( this );
 
	// Properties
	this.surface = surface;
 
	this.selectionGroups = new Map();
	this.selectionElementsCache = new Map();
 
	// Number of rects in a group that can be drawn before viewport clipping applies
	this.viewportClippingLimit = 50;
	// Vertical pixels above and below the viewport to load rects for when viewport clipping applies
	this.viewportClippingPadding = 50;
	// Maximum selections in a group to render (after viewport clipping)
	this.maxRenderedSelections = 50;
 
	// Deactivated selection
	this.deactivatedSelectionVisible = true;
	this.showDeactivatedAsActivated = false;
 
	// Events
	// Debounce to prevent trying to draw every cursor position in history.
	const teardownCheck = () => !!this.surface;
	this.onSurfacePositionDebounced = ve.debounceWithTest( teardownCheck, this.onSurfacePosition.bind( this ) );
	this.getSurface().connect( this, { position: this.onSurfacePositionDebounced } );
 
	this.onWindowScrollDebounced = ve.debounceWithTest( teardownCheck, this.onWindowScroll.bind( this ), 250 );
	this.getSurface().getSurface().$scrollListener[ 0 ].addEventListener( 'scroll', this.onWindowScrollDebounced, { passive: true } );
 
	this.$element.addClass( 've-ce-selectionManager' );
	this.$overlay = $( '<div>' ).addClass( 've-ce-selectionManager-overlay' );
};
 
/* Inheritance */
 
OO.inheritClass( ve.ce.SelectionManager, OO.ui.Element );
 
OO.mixinClass( ve.ce.SelectionManager, OO.EventEmitter );
 
/* Events */
 
/**
 * @event ve.ce.SelectionManager#update
 * @param {boolean} hasSelections The selection manager has some non-collapsed selections
 */
 
/* Methods */
 
/**
 * Destroy the selection manager
 */
ve.ce.SelectionManager.prototype.destroy = function () {
	const synchronizer = this.getSurface().getModel().synchronizer;
	Iif ( synchronizer ) {
		synchronizer.disconnect( this );
	}
	this.$element.remove();
	this.$overlay.remove();
	this.getSurface().getSurface().$scrollListener[ 0 ].removeEventListener( 'scroll', this.onWindowScrollDebounced );
 
	this.surface = null;
};
 
/**
 * Get the surface
 *
 * Will return null after the selectionmanager has been destroyed
 *
 * @return {ve.ce.Surface|null}
 */
ve.ce.SelectionManager.prototype.getSurface = function () {
	return this.surface;
};
 
/**
 * Draw selections.
 *
 * @param {string} name Unique name for the selection being drawn
 * @param {ve.ce.Selection[]} selections Selections to draw
 * @param {Object} [options]
 * @param {string} [options.color] CSS color for the selection. Should usually be set in a stylesheet using the generated class name.
 * @param {string} [options.wrapperClass] Additional CSS class string to add to the $selections wrapper.
 * @param {boolean} [options.showRects=true] Show individual selection rectangles (default)
 * @param {boolean} [options.showBounding=false] Show a bounding rectangle around the selection
 * @param {boolean} [options.showCursor=false] Show a separate rectangle at the cursor ('to' position in a non-collapsed selection)
 * @param {boolean} [options.showGutter=false] Show a vertical gutter bar matching the bounding rect
 * @param {boolean} [options.overlay=false] Render all of the selection above the text
 * @param {string} [options.label] Label shown above each selection
 */
ve.ce.SelectionManager.prototype.drawSelections = function ( name, selections, options ) {
	options = options || {};
	if ( !this.selectionGroups.has( name ) ) {
		this.selectionGroups.set( name, new ve.ce.SelectionManager.SelectionGroup( name, this ) );
	}
	const selectionGroup = this.selectionGroups.get( name );
 
	const oldVisibleSelections = selectionGroup.visibleSelections;
	const oldOptions = selectionGroup.options;
 
	selectionGroup.setSelections( selections );
	selectionGroup.setOptions( options );
 
	selectionGroup.cancelIdleCallbacks();
 
	Iif ( selections.length > this.viewportClippingLimit ) {
		const viewportRange = this.getSurface().getViewportRange( true, this.viewportClippingPadding );
		if ( viewportRange ) {
			selections = selections.filter( ( selection ) => viewportRange.containsRange( selection.getModel().getCoveringRange() ) );
			selectionGroup.setVisibleSelections( selections );
		} else {
			// Surface not attached - nothing to render
			selections = [];
		}
	}
 
	const renderSelections = ( selectionsToRender ) => {
		selectionsToRender.forEach( ( selection ) => {
			let selectionElements = this.getCachedSelectionElements( name, selection.getModel(), options );
			if ( !selectionElements ) {
				selectionElements = new ve.ce.SelectionManager.SelectionElements();
 
				Eif ( options.showRects !== false ) {
					let rects = selection.getSelectionRects();
					Eif ( rects ) {
						rects = ve.minimizeRects( rects );
						const $rects = $( '<div>' ).addClass( 've-ce-surface-selection-rects' );
						rects.forEach( ( rect ) => {
							$rects.append(
								$( '<div>' )
									.addClass( 've-ce-surface-selection-rect' )
									.css( {
										top: rect.top,
										left: rect.left,
										// Collapsed selections can have a width of 0, so expand
										width: Math.max( rect.width, 1 ),
										height: rect.height,
										backgroundColor: options.color || undefined
									} )
							);
						} );
						selectionElements.$selection.append( $rects );
					}
				}
 
				Iif ( options.showBounding ) {
					const boundingRect = selection.getSelectionBoundingRect();
					if ( boundingRect ) {
						selectionElements.$selection.append(
							$( '<div>' )
								.addClass( 've-ce-surface-selection-bounding' )
								.css( {
									top: boundingRect.top,
									left: boundingRect.left,
									width: boundingRect.width,
									height: boundingRect.height,
									backgroundColor: options.color || undefined
								} )
						);
					}
				}
 
				Iif ( options.showGutter ) {
					const boundingRect = selection.getSelectionBoundingRect();
					if ( boundingRect ) {
						selectionElements.$selection.append(
							$( '<div>' )
								.addClass( 've-ce-surface-selection-gutter' )
								.css( {
									top: boundingRect.top,
									height: boundingRect.height,
									backgroundColor: options.color || undefined
								} )
						);
					}
				}
 
				let cursorRect;
 
				Iif ( options.showCursor ) {
					cursorRect = selection.getSelectionFocusRect();
					if ( cursorRect ) {
						selectionElements.$selection.append(
							$( '<div>' )
								.addClass( 've-ce-surface-selection-cursor' )
								.css( {
									top: cursorRect.top,
									left: cursorRect.left,
									width: cursorRect.width,
									height: cursorRect.height,
									borderColor: options.color || undefined
								} )
						);
					}
				}
 
				Iif ( options.label ) {
					// Position the label at the cursor if shown, otherwise use the start rect.
					const labelRect = cursorRect || ( selection.getSelectionStartAndEndRects() || {} ).start;
 
					if ( labelRect ) {
						selectionElements.$overlay.append(
							$( '<div>' )
								.addClass( 've-ce-surface-selection-label' )
								.text( options.label )
								.css( {
									top: labelRect.top,
									left: labelRect.left,
									backgroundColor: options.color || undefined
								} )
						);
					}
				}
			}
			selectionGroup.append( selectionElements );
 
			this.cacheSelectionElements( selectionElements, name, selection.getModel(), options );
		} );
	};
 
	Iif ( selections.length > this.maxRenderedSelections ) {
		const renderBatch = ( start ) => {
			if ( start < selections.length ) {
				selectionGroup.addIdleCallback( () => {
					renderSelections( selections.slice( start, start + this.maxRenderedSelections ) );
					renderBatch( start + this.maxRenderedSelections );
				} );
			}
		};
		renderBatch( 0 );
	} else {
		renderSelections( selections );
	}
 
	const selectionsToShow = new Set();
	selections.forEach( ( selection ) => {
		const cacheKey = this.getSelectionElementsCacheKey( name, selection.getModel(), options );
		selectionsToShow.add( cacheKey );
	} );
 
	// Remove any selections that are no longer visible
	oldVisibleSelections.forEach( ( oldSelection ) => {
		const cacheKey = this.getSelectionElementsCacheKey( name, oldSelection.getModel(), oldOptions );
		if ( !selectionsToShow.has( cacheKey ) ) {
			const selectionElements = this.getCachedSelectionElements( name, oldSelection.getModel(), oldOptions );
			Eif ( selectionElements ) {
				selectionElements.detach();
			}
		}
	} );
 
	const hasSelections = Array.from( this.selectionGroups.values() ).some( ( group ) => group.hasSelections() );
	this.emit( 'update', hasSelections );
};
 
/**
 * Change the rendering options for a selection group, if it exists
 *
 * @param {string} name Name of selection group
 * @param {Object} options
 */
ve.ce.SelectionManager.prototype.setOptions = function ( name, options ) {
	const selectionGroup = this.selectionGroups.get( name );
 
	if ( selectionGroup ) {
		selectionGroup.setOptions( options );
	}
};
 
/**
 * Get a cache key for a recently drawn selection
 *
 * @param {string} name Name of selection group
 * @param {ve.dm.Selection} selectionModel Selection model
 * @param {Object} [options] Selection options
 * @return {string} Cache key
 */
ve.ce.SelectionManager.prototype.getSelectionElementsCacheKey = function ( name, selectionModel, options = {} ) {
	return name + '-' + JSON.stringify( selectionModel ) + '-' + JSON.stringify( {
		// Normalize values for cache key
		color: options.color || '',
		showRects: !!options.showRects,
		showBounding: !!options.showBounding,
		showCursor: !!options.showCursor,
		showGutter: !!options.showGutter,
		overlay: !!options.overlay,
		label: options.label || ''
		// Excluded: wrapperClass - this can be modified dynamically without re-rendering
	} );
};
 
/**
 * Get a recently drawn selection from the cache
 *
 * @param {string} name Name of selection group
 * @param {ve.dm.Selection} selectionModel Selection model
 * @param {Object} [options] Selection options
 * @return {ve.ce.SelectionElements|null} Selection elements containing $selection and $overlay, null if not found
 */
ve.ce.SelectionManager.prototype.getCachedSelectionElements = function ( name, selectionModel, options ) {
	const cacheKey = this.getSelectionElementsCacheKey( name, selectionModel, options );
	return this.selectionElementsCache.get( cacheKey ) || null;
};
 
/**
 * Store an recently drawn selection in the cache
 *
 * @param {ve.ce.SelectionElements} selectionElements Selection elements containing $selection and $overlay
 * @param {string} name Name of selection group
 * @param {ve.dm.Selection} selectionModel Selection model
 * @param {Object} [options] Selection options
 * @return {string} Cache key
 */
ve.ce.SelectionManager.prototype.cacheSelectionElements = function ( selectionElements, name, selectionModel, options ) {
	const cacheKey = this.getSelectionElementsCacheKey( name, selectionModel, options );
	this.selectionElementsCache.set( cacheKey, selectionElements );
	return cacheKey;
};
 
/**
 * Redraw selections
 *
 * When triggered by a surface 'position' event (which fires when the surface
 * changes size, or when the document is modified), the selectionElementsCache is
 * cleared as these two things will cause any previously calculated rectangles
 * to be incorrect.
 *
 * When triggered by a scroll event, the cache is not cleared, and only
 * selection groups that are clipped to the viewport are redrawn.
 *
 * @param {boolean} [fromScroll=false] The redraw was triggered by a scroll event
 */
ve.ce.SelectionManager.prototype.redrawSelections = function ( fromScroll = false ) {
	Eif ( !fromScroll ) {
		this.selectionElementsCache.clear();
		for ( const selectionGroup of this.selectionGroups.values() ) {
			selectionGroup.empty();
		}
	}
 
	for ( const selectionGroup of this.selectionGroups.values() ) {
		Eif ( !fromScroll ) {
			selectionGroup.empty();
		}
		Iif ( fromScroll && !selectionGroup.isClipped() ) {
			continue;
		}
		const selections = selectionGroup.fragments.map( ( fragments ) => this.surface.getSelection( fragments.getSelection() ) );
		this.drawSelections( selectionGroup.name, selections, selectionGroup.options );
	}
};
 
/**
 * Handle position events from the surface
 */
ve.ce.SelectionManager.prototype.onSurfacePosition = function () {
	this.redrawSelections();
};
 
/**
 * Handle window scroll events
 */
ve.ce.SelectionManager.prototype.onWindowScroll = function () {
	this.redrawSelections( true );
};
 
/**
 * Start showing the deactivated selection
 *
 * @param {boolean} [showAsActivated=true] Selection should still show as activated
 */
ve.ce.SelectionManager.prototype.showDeactivatedSelection = function ( showAsActivated = true ) {
	this.deactivatedSelectionVisible = true;
	this.showDeactivatedAsActivated = !!showAsActivated;
 
	this.updateDeactivatedSelection();
};
 
/**
 * Hide the deactivated selection
 */
ve.ce.SelectionManager.prototype.hideDeactivatedSelection = function () {
	this.deactivatedSelectionVisible = false;
 
	// Generates ve-ce-surface-selections-deactivated CSS class
	this.drawSelections( 'deactivated', [] );
};
 
/**
 * Update the deactivated selection
 */
ve.ce.SelectionManager.prototype.updateDeactivatedSelection = function () {
	if ( !this.deactivatedSelectionVisible ) {
		return;
	}
	const surface = this.getSurface();
	const selection = surface.getSelection();
 
	// Check we have a deactivated surface and a native selection
	if ( selection.isNativeCursor() ) {
		let textColor;
		// For collapsed selections, work out the text color to use for the cursor
		const isCollapsed = selection.getModel().isCollapsed();
		Eif ( isCollapsed ) {
			const currentNode = surface.getDocument().getBranchNodeFromOffset(
				selection.getModel().getCoveringRange().start
			);
			Eif ( currentNode ) {
				// This isn't perfect as it doesn't take into account annotations.
				textColor = currentNode.$element.css( 'color' );
			}
		}
		const classes = [];
		Eif ( !this.showDeactivatedAsActivated ) {
			classes.push( 've-ce-surface-selections-deactivated-showAsDeactivated' );
		}
		Eif ( isCollapsed ) {
			classes.push( 've-ce-surface-selections-deactivated-collapsed' );
		}
		// Generates ve-ce-surface-selections-deactivated CSS class
		this.drawSelections( 'deactivated', [ selection ], {
			color: textColor,
			wrapperClass: classes.join( ' ' )
		} );
	} else {
		// Generates ve-ce-surface-selections-deactivated CSS class
		this.drawSelections( 'deactivated', [] );
	}
};
 
/**
 * SelectionGroup: Holds all data for a rendered selection group.
 *
 * @class
 * @constructor
 * @param {string} name Name of the selection group
 * @param {ve.ce.SelectionManager} selectionManager Selection manager
 */
ve.ce.SelectionManager.SelectionGroup = function VeCeSelectionManagerSelectionGroup( name, selectionManager ) {
	this.name = name;
	this.selectionManager = selectionManager;
 
	// The following classes are used here:
	// * ve-ce-surface-selections-deactivated
	// * ve-ce-surface-selections-<name>
	this.$selections = $( '<div>' ).addClass( 've-ce-surface-selections ve-ce-surface-selections-' + name ).appendTo( this.selectionManager.$element );
	// The following classes are used here:
	// * ve-ce-surface-selections-deactivated
	// * ve-ce-surface-selections-<name>
	this.$overlays = $( '<div>' ).addClass( 've-ce-surface-selections ve-ce-surface-selections-' + name ).appendTo( this.selectionManager.$overlay );
 
	/** @type {Array<ve.ce.Selection>} */
	this.selections = [];
	/** @type {Array<ve.ce.Selection>} */
	this.visibleSelections = [];
	/** @type {Array<ve.dm.SurfaceFragment>} */
	this.fragments = [];
	/** @type {Object} */
	this.options = {};
	/** @type {number[]} */
	this.idleCallbacks = [];
};
 
/**
 * Set the rendering options for this selection group
 *
 * @param {Object} options
 */
ve.ce.SelectionManager.SelectionGroup.prototype.setOptions = function ( options ) {
	this.options = options;
 
	// Always set the 'class' attribute to ensure previously-set classes are cleared.
	this.$selections.add( this.$overlays ).attr(
		'class',
		've-ce-surface-selections ve-ce-surface-selections-' + this.name + ' ' +
		( this.options.wrapperClass || '' )
	);
};
 
/**
 * Clear all rendered selections
 */
ve.ce.SelectionManager.SelectionGroup.prototype.empty = function () {
	this.$selections.empty();
	this.$overlays.empty();
	this.setVisibleSelections( [] );
};
 
/**
 * Set the selections for this selection group
 *
 * @param {ve.ce.Selection[]} selections
 */
ve.ce.SelectionManager.SelectionGroup.prototype.setSelections = function ( selections ) {
	// Store selections so we can selectively remove anything that hasn't been
	// redrawn at the exact same selection (oldSelections)
	this.selections = selections;
	// Assume all selections will be visible, unless clipped later
	this.visibleSelections = selections;
 
	const surfacemodel = this.selectionManager.getSurface().getModel();
	// Store fragments so we can automatically update selections even after
	// the document has been modified (which eventually fires a position event)
	this.fragments = selections.map( ( selection ) => surfacemodel.getFragment( selection.getModel(), true, true ) );
};
 
/**
 * Set the visible selections for this selection group
 *
 * Must be a subset of the selections set by setSelections.
 *
 * @param {ve.ce.Selection[]} visibleSelections
 */
ve.ce.SelectionManager.SelectionGroup.prototype.setVisibleSelections = function ( visibleSelections ) {
	this.visibleSelections = visibleSelections;
};
 
/**
 * Check if the selection group is clipped
 *
 * @return {boolean}
 */
ve.ce.SelectionManager.SelectionGroup.prototype.isClipped = function () {
	return this.selections.length !== this.visibleSelections.length;
};
 
/**
 * Append selection elements to the DOM
 *
 * @param {ve.ce.SelectionManager.SelectionElements} selectionElements
 */
ve.ce.SelectionManager.SelectionGroup.prototype.append = function ( selectionElements ) {
	Iif ( this.options.overlay ) {
		this.$overlays.append( selectionElements.$selection );
	} else {
		this.$selections.append( selectionElements.$selection );
	}
	this.$overlays.append( selectionElements.$overlay );
};
 
/**
 * Check if the selection group has some non-collapsed selections
 *
 * @return {boolean}
 */
ve.ce.SelectionManager.SelectionGroup.prototype.hasSelections = function () {
	return this.visibleSelections.some(
		( selection ) => !selection.getModel().isCollapsed()
	);
};
 
/**
 * Add an idle callback to be executed later
 *
 * @param {Function} callback Callback function
 */
ve.ce.SelectionManager.SelectionGroup.prototype.addIdleCallback = function ( callback ) {
	// Support: Safari
	// eslint-disable-next-line compat/compat
	const request = window.requestIdleCallback || setTimeout;
	// eslint-disable-next-line compat/compat
	const timeout = window.requestIdleCallback ? undefined : 100;
	this.idleCallbacks.push( request( callback, timeout ) );
};
 
/**
 * Cancel any pending idle callbacks
 */
ve.ce.SelectionManager.SelectionGroup.prototype.cancelIdleCallbacks = function () {
	// Support: Safari
	const cancel = window.cancelIdleCallback || clearTimeout;
	while ( this.idleCallbacks.length ) {
		cancel( this.idleCallbacks.pop() );
	}
};
 
/**
 * SelectionElements: Holds cached selection/overlay jQuery elements.
 *
 * @class
 * @constructor
 */
ve.ce.SelectionManager.SelectionElements = function VeCeSelectionManagerSelectionElements() {
	this.$selection = $( '<div>' ).addClass( 've-ce-surface-selection' );
	this.$overlay = $( '<div>' ).addClass( 've-ce-surface-selection' );
};
 
/**
 * Detach the selection elements from the DOM
 */
ve.ce.SelectionManager.SelectionElements.prototype.detach = function () {
	this.$selection.detach();
	this.$overlay.detach();
};