UNPKG

infusion

Version:

Infusion is an application framework for developing flexible stuff with JavaScript

617 lines (523 loc) 22.6 kB
/* Copyright The Infusion copyright holders See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-project/infusion/raw/main/AUTHORS.md. Licensed under the Educational Community License (ECL), Version 2.0 or the New BSD license. You may not use this file except in compliance with one these Licenses. You may obtain a copy of the ECL 2.0 License and BSD License at https://github.com/fluid-project/infusion/raw/main/Infusion-LICENSE.txt */ "use strict"; var fluid = fluid || {}; // eslint-disable-line no-redeclare (function ($) { // $().fluid("selectable", args) // $().fluid("selectable".that() // $().fluid("pager.pagerBar", args) // $().fluid("reorderer", options) /** Create a "bridge" from code written in the Fluid standard "that-ist" style, * to the standard JQuery UI plugin architecture specified at http://docs.jquery.com/UI/Guidelines . * Every Fluid component corresponding to the top-level standard signature (JQueryable, options) * will automatically convert idiomatically to the JQuery UI standard via this adapter. * Any return value which is a primitive or array type will become the return value * of the "bridged" function - however, where this function returns a general hash * (object) this is interpreted as forming part of the Fluid "return that" pattern, * and the function will instead be bridged to "return this" as per JQuery standard, * permitting chaining to occur. However, as a courtesy, the particular "this" returned * will be augmented with a function that() which will allow the original return * value to be retrieved if desired. * @param {String} name - The name under which the "plugin space" is to be injected into JQuery * @param {Object} peer - The root of the namespace corresponding to the peer object. * @return {Function} - A JQuery UI plugin function. */ fluid.thatistBridge = function (name, peer) { var togo = function (funcname) { var segs = funcname.split("."); var move = peer; for (var i = 0; i < segs.length; ++i) { move = move[segs[i]]; } var args = [this]; if (arguments.length === 2) { args = args.concat($.makeArray(arguments[1])); } var ret = move.apply(null, args); this.that = function () { return ret; }; return ret && ret.constructor && ret.constructor.name === "fluid.componentConstructor" ? this : ret; }; $.fn[name] = togo; return togo; }; fluid.thatistBridge("fluid", fluid); /************************************************************************* * Tabindex normalization - compensate for browser differences in naming * and function of "tabindex" attribute and tabbing order. *************************************************************************/ // -- Private functions -- var canHaveDefaultTabindex = function (elements) { if (elements.length <= 0) { return false; } return $(elements[0]).is("a, input, button, select, area, textarea, object"); }; var getValue = function (elements) { if (elements.length <= 0) { return undefined; } if (!fluid.tabindex.hasAttr(elements)) { return canHaveDefaultTabindex(elements) ? Number(0) : undefined; } // Get the attribute and return it as a number value. var value = elements.attr("tabindex"); return Number(value); }; var setValue = function (elements, toIndex) { return elements.each(function (i, item) { $(item).attr("tabindex", toIndex); }); }; // -- Public API -- /** * Gets the value of the tabindex attribute for the first item, or sets the tabindex value of all elements * if toIndex is specified. * * @param {jQuery} target - The target element. * @param {String|Number} toIndex - (Optional) the tabIndex value to set on the target. * @return {Any} - The result of the underlying "get" or "set" operation. */ fluid.tabindex = function (target, toIndex) { target = $(target); if (toIndex !== null && toIndex !== undefined) { return setValue(target, toIndex); } else { return getValue(target); } }; /* * Removes the tabindex attribute altogether from each element. */ fluid.tabindex.remove = function (target) { target = $(target); return target.each(function (i, item) { $(item).removeAttr("tabindex"); }); }; /* * Determines if an element actually has a tabindex attribute present. */ fluid.tabindex.hasAttr = function (target) { target = $(target); if (target.length <= 0) { return false; } var togo = target.map( function () { var attributeNode = this.getAttributeNode("tabindex"); return attributeNode ? attributeNode.specified : false; } ); return togo.length === 1 ? togo[0] : togo; }; /* * Determines if an element either has a tabindex attribute or is naturally tab-focussable. */ fluid.tabindex.has = function (target) { target = $(target); return fluid.tabindex.hasAttr(target) || canHaveDefaultTabindex(target); }; // Keyboard navigation // Public, static constants needed by the rest of the library. fluid.a11y = $.a11y || {}; fluid.a11y.orientation = { HORIZONTAL: 0, VERTICAL: 1, BOTH: 2 }; var UP_DOWN_KEYMAP = { next: $.ui.keyCode.DOWN, previous: $.ui.keyCode.UP }; var LEFT_RIGHT_KEYMAP = { next: $.ui.keyCode.RIGHT, previous: $.ui.keyCode.LEFT }; // Private functions. var unwrap = function (element) { return element.jquery ? element[0] : element; // Unwrap the element if it's a jQuery. }; var makeElementsTabFocussable = function (elements) { // If each element doesn't have a tabindex, or has one set to a negative value, set it to 0. elements.each(function (idx, item) { item = $(item); if (!item.fluid("tabindex.has") || item.fluid("tabindex") < 0) { item.fluid("tabindex", 0); } }); }; // Public API. /* * Makes all matched elements available in the tab order by setting their tabindices to "0". */ fluid.tabbable = function (target) { target = $(target); makeElementsTabFocussable(target); }; /*********************************************************************** * Selectable functionality - geometrising a set of nodes such that they * can be navigated (by setting focus) using a set of directional keys */ var CONTEXT_KEY = "selectionContext"; var NO_SELECTION = -32768; var cleanUpWhenLeavingContainer = function (selectionContext) { if (selectionContext.activeItemIndex !== NO_SELECTION) { if (selectionContext.options.onLeaveContainer) { selectionContext.options.onLeaveContainer( selectionContext.selectables[selectionContext.activeItemIndex] ); } else if (selectionContext.options.onUnselect) { selectionContext.options.onUnselect( selectionContext.selectables[selectionContext.activeItemIndex] ); } } if (!selectionContext.options.rememberSelectionState) { selectionContext.activeItemIndex = NO_SELECTION; } }; /* * Does the work of selecting an element and delegating to the client handler. */ var drawSelection = function (elementToSelect, handler) { if (handler) { handler(elementToSelect); } }; /* * Does the work of unselecting an element and delegating to the client handler. */ var eraseSelection = function (selectedElement, handler) { if (handler && selectedElement) { handler(selectedElement); } }; var unselectElement = function (selectedElement, selectionContext) { eraseSelection(selectedElement, selectionContext.options.onUnselect); }; var selectElement = function (elementToSelect, selectionContext) { // It's possible that we're being called programmatically, in which case we should clear any previous selection. unselectElement(selectionContext.selectedElement(), selectionContext); elementToSelect = unwrap(elementToSelect); var newIndex = selectionContext.selectables.index(elementToSelect); // Next check if the element is a known selectable. If not, do nothing. if (newIndex === -1) { return; } // Select the new element. selectionContext.activeItemIndex = newIndex; drawSelection(elementToSelect, selectionContext.options.onSelect); }; var selectableFocusHandler = function (selectionContext) { return function (evt) { // FLUID-3590: newer browsers (FF 3.6, Webkit 4) have a form of "bug" in that they will go bananas // on attempting to move focus off an element which has tabindex dynamically set to -1. $(evt.target).fluid("tabindex", 0); selectElement(evt.target, selectionContext); // Force focus not to bubble on some browsers. return evt.stopPropagation(); }; }; var selectableBlurHandler = function (selectionContext) { return function (evt) { $(evt.target).fluid("tabindex", selectionContext.options.selectablesTabindex); unselectElement(evt.target, selectionContext); // Force blur not to bubble on some browsers. return evt.stopPropagation(); }; }; var reifyIndex = async function (sc_that) { var elements = sc_that.selectables; if (sc_that.activeItemIndex >= elements.length) { sc_that.activeItemIndex = (sc_that.options.noWrap ? elements.length - 1 : 0); } if (sc_that.activeItemIndex < 0 && sc_that.activeItemIndex !== NO_SELECTION) { sc_that.activeItemIndex = (sc_that.options.noWrap ? 0 : elements.length - 1); } if (sc_that.activeItemIndex >= 0) { return fluid.focus(elements[sc_that.activeItemIndex]); } }; var prepareShift = async function (selectionContext) { // FLUID-3590: FF 3.6 and Safari 4.x won't fire blur() when programmatically moving focus. var selElm = selectionContext.selectedElement(); if (selElm) { await fluid.blur(selElm); } unselectElement(selectionContext.selectedElement(), selectionContext); if (selectionContext.activeItemIndex === NO_SELECTION) { selectionContext.activeItemIndex = -1; } }; var focusNextElement = async function (selectionContext) { await prepareShift(selectionContext); ++selectionContext.activeItemIndex; return reifyIndex(selectionContext); }; var focusPreviousElement = async function (selectionContext) { await prepareShift(selectionContext); --selectionContext.activeItemIndex; return reifyIndex(selectionContext); }; var arrowKeyHandler = function (selectionContext, keyMap) { return async function (evt) { if (evt.which === keyMap.next) { await focusNextElement(selectionContext); evt.preventDefault(); } else if (evt.which === keyMap.previous) { await focusPreviousElement(selectionContext); evt.preventDefault(); } }; }; var getKeyMapForDirection = function (direction) { // Determine the appropriate mapping for next and previous based on the specified direction. var keyMap; if (direction === fluid.a11y.orientation.HORIZONTAL) { keyMap = LEFT_RIGHT_KEYMAP; } else if (direction === fluid.a11y.orientation.VERTICAL) { // Assume vertical in any other case. keyMap = UP_DOWN_KEYMAP; } return keyMap; }; var tabKeyHandler = function (selectionContext) { return function (evt) { if (evt.which !== $.ui.keyCode.TAB) { return; } cleanUpWhenLeavingContainer(selectionContext); // Catch Shift-Tab and note that focus is on its way out of the container. if (evt.shiftKey) { selectionContext.focusIsLeavingContainer = true; } }; }; var containerFocusHandler = function (selectionContext) { return async function (evt) { var shouldOrig = selectionContext.options.autoSelectFirstItem; var shouldSelect = typeof(shouldOrig) === "function" ? shouldOrig() : shouldOrig; // Override the autoselection if we're on the way out of the container. if (selectionContext.focusIsLeavingContainer) { shouldSelect = false; } // This target check works around the fact that sometimes focus bubbles, even though it shouldn't. if (shouldSelect && evt.target === selectionContext.container.get(0)) { if (selectionContext.activeItemIndex === NO_SELECTION) { selectionContext.activeItemIndex = 0; } await fluid.focus(selectionContext.selectables[selectionContext.activeItemIndex]); } // Force focus not to bubble on some browsers. return evt.stopPropagation(); }; }; var containerBlurHandler = function (selectionContext) { return function (evt) { selectionContext.focusIsLeavingContainer = false; // Force blur not to bubble on some browsers. return evt.stopPropagation(); }; }; var makeElementsSelectable = function (container, defaults, userOptions) { var options = $.extend(true, {}, defaults, userOptions); var keyMap = getKeyMapForDirection(options.direction); var selectableElements = options.selectableElements ? options.selectableElements : container.find(options.selectableSelector); // Context stores the currently active item(undefined to start) and list of selectables. var that = { container: container, activeItemIndex: NO_SELECTION, selectables: selectableElements, focusIsLeavingContainer: false, options: options }; that.selectablesUpdated = async function (focusedItem) { // Remove selectables from the tab order and add focus/blur handlers if (typeof(that.options.selectablesTabindex) === "number") { that.selectables.fluid("tabindex", that.options.selectablesTabindex); } that.selectables.off("focus." + CONTEXT_KEY); that.selectables.off("blur." + CONTEXT_KEY); that.selectables.on("focus." + CONTEXT_KEY, selectableFocusHandler(that)); that.selectables.on("blur." + CONTEXT_KEY, selectableBlurHandler(that)); if (keyMap && that.options.noBubbleListeners) { that.selectables.off("keydown." + CONTEXT_KEY); that.selectables.on("keydown." + CONTEXT_KEY, arrowKeyHandler(that, keyMap)); } if (focusedItem) { selectElement(focusedItem, that); } else { return reifyIndex(that); } }; that.refresh = async function () { if (!that.options.selectableSelector) { fluid.fail("Cannot refresh selectable context which was not initialised by a selector"); } that.selectables = container.find(options.selectableSelector); return that.selectablesUpdated(); }; that.selectedElement = function () { return that.activeItemIndex < 0 ? null : that.selectables[that.activeItemIndex]; }; // Add various handlers to the container. if (keyMap && !that.options.noBubbleListeners) { container.on("keydown", arrowKeyHandler(that, keyMap)); } container.on("keydown", tabKeyHandler(that)); container.on("focus", containerFocusHandler(that)); container.on("blur", containerBlurHandler(that)); that.promise = that.selectablesUpdated(); return that; }; /* * Makes all matched elements selectable with the arrow keys. * Supply your own handlers object with onSelect: and onUnselect: properties for custom behaviour. * Options provide configurability, including direction: and autoSelectFirstItem: * Currently supported directions are jQuery.a11y.directions.HORIZONTAL and VERTICAL. */ fluid.selectable = function (target, options) { target = $(target); var that = makeElementsSelectable(target, fluid.selectable.defaults, options); fluid.setScopedData(target, CONTEXT_KEY, that); return that; }; /* * Selects the specified element. */ fluid.selectable.select = async function (target, toSelect) { return fluid.focus(toSelect); }; /* * Selects the next matched element. */ fluid.selectable.selectNext = async function (target) { target = $(target); return focusNextElement(fluid.getScopedData(target, CONTEXT_KEY)); }; /* * Selects the previous matched element. */ fluid.selectable.selectPrevious = async function (target) { target = $(target); return focusPreviousElement(fluid.getScopedData(target, CONTEXT_KEY)); }; /* * Returns the currently selected item wrapped as a jQuery object. */ fluid.selectable.currentSelection = function (target) { target = $(target); var that = fluid.getScopedData(target, CONTEXT_KEY); return $(that.selectedElement()); }; fluid.selectable.defaults = { direction: fluid.a11y.orientation.VERTICAL, selectablesTabindex: -1, autoSelectFirstItem: true, rememberSelectionState: true, selectableSelector: ".selectable", selectableElements: null, onSelect: null, onUnselect: null, onLeaveContainer: null, noWrap: false }; /******************************************************************** * Activation functionality - declaratively associating actions with * a set of keyboard bindings. */ var checkForModifier = function (binding, evt) { // If no modifier was specified, just return true. if (!binding.modifier) { return true; } var modifierKey = binding.modifier; var isCtrlKeyPresent = modifierKey && evt.ctrlKey; var isAltKeyPresent = modifierKey && evt.altKey; var isShiftKeyPresent = modifierKey && evt.shiftKey; return isCtrlKeyPresent || isAltKeyPresent || isShiftKeyPresent; }; /* Constructs a raw "keydown"-facing handler, given a binding entry. This * checks whether the key event genuinely triggers the event and forwards it * to any "activateHandler" registered in the binding. */ var makeActivationHandler = function (binding) { return function (evt) { var target = evt.target; if (!fluid.enabled(target)) { return; } // The following 'if' clause works in the real world, but there's a bug in the jQuery simulation // that causes keyboard simulation to fail in Safari, causing our tests to fail: // http://ui.jquery.com/bugs/ticket/3229 // The replacement 'if' clause works around this bug. // When this issue is resolved, we should revert to the original clause. // if (evt.which === binding.key && binding.activateHandler && checkForModifier(binding, evt)) { var code = evt.which ? evt.which : evt.keyCode; if (code === binding.key && binding.activateHandler && checkForModifier(binding, evt)) { var event = $.Event("fluid-activate"); $(target).trigger(event, [binding.activateHandler]); if (event.isDefaultPrevented()) { evt.preventDefault(); } } }; }; var makeElementsActivatable = function (elements, onActivateHandler, defaultKeys, options) { // Create bindings for each default key. var bindings = []; $(defaultKeys).each(function (index, key) { bindings.push({ modifier: null, key: key, activateHandler: onActivateHandler }); }); // Merge with any additional key bindings. if (options && options.additionalBindings) { bindings = bindings.concat(options.additionalBindings); } fluid.initEnablement(elements); // Add listeners for each key binding. for (var i = 0; i < bindings.length; ++i) { var binding = bindings[i]; elements.on("keydown", makeActivationHandler(binding)); } elements.on("fluid-activate", function (evt, handler) { handler = handler || onActivateHandler; return handler ? handler(evt) : null; }); }; /* * Makes all matched elements activatable with the Space and Enter keys. * Provide your own handler function for custom behaviour. * Options allow you to provide a list of additionalActivationKeys. */ fluid.activatable = function (target, fn, options) { target = $(target); makeElementsActivatable(target, fn, fluid.activatable.defaults.keys, options); }; /* * Activates the specified element. */ fluid.activate = function (target) { $(target).trigger("fluid-activate"); }; // Public Defaults. fluid.activatable.defaults = { keys: [$.ui.keyCode.ENTER, $.ui.keyCode.SPACE] }; })(jQuery);