%PDF- %PDF-
Direktori : /proc/985914/root/data/old/home/stash/atlassian-stash/static/widget/ |
Current File : //proc/985914/root/data/old/home/stash/atlassian-stash/static/widget/keyboard-controller.js |
define('widget/keyboard-controller', [ 'jquery' ], function( $ ) { 'use strict'; /** * * @param eventTarget * @param handlers * @return {KeyboardController} * @constructor */ function KeyboardController(eventTarget, handlers) { if (!(this instanceof KeyboardController)) { return new KeyboardController(eventTarget, handlers); } var $eventTarget = $(eventTarget); var rawHandler; $eventTarget.on('keydown', rawHandler = function(e) { if (e.keyCode in handlers) { if (!handlers[e.keyCode](e)) { e.preventDefault(); } } }); this.destroy = function() { $eventTarget.off('keydown', rawHandler); }; return this; } /** * TabKeyboardController overrides native browser tabbing for two reasons: * 1. to force Safari to tab to anchors just like all our other browsers. * 2. to allow us to force tabbing to cycle between focusable elements within a given context (such as a dialog). * * This is currently only used in the Branch Selector. * * NOTE: the selector used in this component is rather aggressive. Please be aware that a large number of descendant * elements will cause this control to perform poorly. * * @param context {Object} an element within which to control tabbing. * @param options {Object} an object matching the shape of TabKeyboardController.defaults * @return {KeyboardController} * @constructor */ function TabKeyboardController(context, options) { options = $.extend({}, TabKeyboardController.defaults, options); function doFocus($el) { $el.focus().addClass(options.focusedClass); } var handlers = {}; handlers[$.ui.keyCode.TAB] = function (e) { var $selectable, $first_selectable, $last_selectable, target = e.target; $selectable = $("a:visible, :input:visible:enabled, :checkbox:visible:enabled, :radio:visible:enabled, [tabindex]", context) .not("[tabindex=-1]") .filter(function() { return $(this).css('visibility') !== 'hidden'; }); $last_selectable = $selectable.last(); $first_selectable = $selectable.first(); $selectable.removeClass(options.focusedClass); if (e.shiftKey && target === $first_selectable[0]) { if (!options.wrapAround) { return true; // let it go natively outside the context } doFocus($last_selectable); } else if (!e.shiftKey && target === $last_selectable[0]) { if (!options.wrapAround) { return true; // let it go natively outside the context } doFocus($first_selectable); } else if (e.shiftKey) { doFocus($selectable.eq($selectable.index(target) - 1)); } else { doFocus($selectable.eq($selectable.index(target) + 1)); } }; return new KeyboardController(context, handlers); } TabKeyboardController.defaults = { focusedClass : 'item-focused', // necessary for IE which doesn't allow :focus styles. wrapAround : true }; /** * @param eventTarget {Element} the element to listen for events on * @param listElement {Element} the list element to traverse items within * @param options {Object} an object in the shape of ListKeyboardController.default * @return {KeyboardController} * @constructor */ function ListKeyboardController(eventTarget, listElement, options) { options = $.extend({}, ListKeyboardController.defaults, options); var $listEl = $(listElement); var selectCallbacks = $.Callbacks(); var focusCallbacks = $.Callbacks(); if (options.onSelect) { selectCallbacks.add(options.onSelect); } if (options.onFocus) { focusCallbacks.add(options.onFocus); } var reachedEnd = false, awaitingMore = false, waitingInterrupted = false; function traverseForwards($current) { do { var $firstChild = $current.find('>:first-child'); var $nextSibling = $current.next(); if ($firstChild.length) { // Attempt to traverse children first $current = $firstChild; } else if ($nextSibling.length) { // Otherwise go for the next sibling $current = $nextSibling; } else { // Otherwise find the next sibling of the closest parent that has a sibling $current = $current.parentsUntil($listEl[0]).next().first(); } } while ($current.length && !$current.is(options.itemSelector)); return $current; } function traverseBackwards($current) { do { var $previous = $current.prev(); if ($previous.length) { var $lastChild; // Work down the tree of last-children until a node with no children is reached while(($lastChild = $previous.find('>:last-child')).length) { $previous = $lastChild; } $current = $previous; } else { // If there are none before the current element, move to the parent $current = $current.parent(); // Unless it's the containing element if ($current[0] === $listEl[0]) { $current = $(); } } } while ($current.length && !$current.is(options.itemSelector)); return $current; } function getPrevNextFunc(nextPrev) { return function prevNextFunc(e) { var $items = $(options.itemSelector, $listEl); var $focusedItem = $items.filter(function() { return $(this).hasClass(options.focusedClass); }); var cycled = false; var movingDown = nextPrev === 'next'; var $next; if ($focusedItem.length) { if (options.adjacentItems) { $next = $focusedItem[nextPrev](options.itemSelector); } else { if (nextPrev === 'next') { $next = traverseForwards($focusedItem); } else { $next = traverseBackwards($focusedItem); } } } else if (nextPrev === 'next') { $next = $items.first(); } else if (options.wrapAround) { $next = $items.last(); } else { return; } //ignore any waiting requests since we have a newer one waitingInterrupted = true; if (!$next.length) { // if we reached the bottom, check if we need to load more if (nextPrev === 'next' && options.requestMore && !reachedEnd) { if (!awaitingMore) { waitingInterrupted = false; var promise = options.requestMore(); if (promise) { awaitingMore = true; promise.done(function(didReachEnd) { reachedEnd = didReachEnd; if (!waitingInterrupted) { prevNextFunc(e); } }).always(function() { awaitingMore = false; waitingInterrupted = false; }); return; } else { reachedEnd = true; prevNextFunc(e); return; } } else { waitingInterrupted = false; return; } } else if (options.wrapAround) { //cycle from last to first/first to last when then press down/up. cycled = true; movingDown = !movingDown; $next = $items[nextPrev === 'next' ? 'first' : 'last'](); } else { return; } } $focusedItem.removeClass(options.focusedClass); $next.addClass(options.focusedClass); var $nextNext = $next[nextPrev](), $toScrollTo = !cycled && $nextNext.length ? $nextNext : $next; var activeEl = document.activeElement; if (options.focusIntoView && activeEl) { // A hack for scrolling the virtually focused item into view - Used by the branch-selector var oldTabindex = $toScrollTo.attr('tabindex'); $toScrollTo.attr('tabindex', '0'); $toScrollTo.focus(); activeEl.focus(); if (oldTabindex == null) { $toScrollTo.removeAttr('tabindex'); } else { $toScrollTo.attr('tabindex', oldTabindex); } } else { $next[0].scrollIntoView(false); } focusCallbacks.fire($next, e); }; } var handlers = {}; var prevHandler = handlers[$.ui.keyCode.UP] = getPrevNextFunc('prev'); var nextHandler = handlers[$.ui.keyCode.DOWN] = getPrevNextFunc('next'); handlers[$.ui.keyCode.ENTER] = function(e) { var $items = $(options.itemSelector, $listEl); var $focusedItem = $items.filter(function() { return $(this).hasClass(options.focusedClass); }); if ($focusedItem.length) { selectCallbacks.fire($focusedItem, e); } }; var controller = new KeyboardController(eventTarget, handlers); controller.setListElement = function(listEl) { $listEl = $(listEl); }; controller.blur = function() { $(options.itemSelector, $listEl).removeClass(options.focusedClass); }; controller.focus = function() { focusCallbacks.add.apply(focusCallbacks, arguments); return this; }; controller.select = function() { selectCallbacks.add.apply(selectCallbacks, arguments); return this; }; controller.moveToNext = nextHandler; controller.moveToPrev = prevHandler; return controller; } ListKeyboardController.defaults = { itemSelector : 'li', focusedClass : 'item-focused', wrapAround : false, adjacentItems: true, requestMore : undefined, onFocus : undefined, onSelect : undefined }; return { KeyboardController : KeyboardController, TabKeyboardController : TabKeyboardController, ListKeyboardController : ListKeyboardController }; });