UNPKG

infusion

Version:

Infusion is an application framework for developing flexible stuff with JavaScript

961 lines (872 loc) 36.9 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"; fluid.registerNamespace("fluid.reorderer"); fluid.reorderer.defaultAvatarCreator = function (item, cssClass, dropWarning) { fluid.dom.cleanseScripts(fluid.unwrap(item)); var avatar = $(item).clone(); fluid.dom.iterateDom(avatar.get(0), function (node) { node.removeAttribute("id"); if (node.tagName.toLowerCase() === "input") { node.setAttribute("disabled", "disabled"); } }); avatar.removeProp("id"); avatar.removeClass("ui-droppable"); avatar.addClass(cssClass); if (dropWarning) { // Will a 'div' always be valid in this position? var avatarContainer = $(document.createElement("div")); avatarContainer.append(avatar); avatarContainer.append(dropWarning); avatar = avatarContainer; } $("body").append(avatar); if (!$.browser.safari) { // FLUID-1597: Safari appears incapable of correctly determining the dimensions of elements avatar.css("display", "block").width(item.offsetWidth).height(item.offsetHeight); } if ($.browser.opera) { // FLUID-1490. Without this detect, curCSS explodes on the avatar on Firefox. avatar.hide(); } return avatar; }; // unsupported, NON-API function fluid.reorderer.bindHandlersToContainer = function (container, keyDownHandler, keyUpHandler) { var actualKeyDown = keyDownHandler; var advancedPrevention = false; // FLUID-1598 and others: Opera will refuse to honour a "preventDefault" on a keydown. // http://forums.devshed.com/javascript-development-115/onkeydown-preventdefault-opera-485371.html if ($.browser.opera) { container.on("keypress", function (evt) { if (advancedPrevention) { advancedPrevention = false; evt.preventDefault(); return false; } }); actualKeyDown = async function (evt) { var oldret = await keyDownHandler(evt); if (oldret === false) { advancedPrevention = true; } }; } container.on("keydown", actualKeyDown); container.on("keyup", keyUpHandler); }; // unsupported, NON-API function fluid.reorderer.addRolesToContainer = function (that) { that.container.attr("role", that.options.containerRole.container); that.container.attr("aria-multiselectable", "false"); that.container.attr("aria-readonly", "false"); that.container.attr("aria-disabled", "false"); // FLUID-3707: We require to have BOTH application role as well as our named role // This however breaks the component completely under NVDA and causes it to perpetually drop back into "browse mode" //that.container.wrap("<div role=\"application\"></div>"); }; // unsupported, NON-API function fluid.reorderer.createAvatarId = function (parentId) { // Generating the avatar's id to be containerId_avatar // This is safe since there is only a single avatar at a time return parentId + "_avatar"; }; /** * Constants for key codes in events. */ fluid.reorderer.keys = { TAB: 9, ENTER: 13, SHIFT: 16, CTRL: 17, ALT: 18, META: 19, SPACE: 32, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40, i: 73, j: 74, k: 75, m: 77 }; /** * The default key sets for the Reorderer. Should be moved into the proper component defaults. */ fluid.reorderer.defaultKeysets = [ { modifier : function (evt) { return evt.ctrlKey; }, up : fluid.reorderer.keys.UP, down : fluid.reorderer.keys.DOWN, right : fluid.reorderer.keys.RIGHT, left : fluid.reorderer.keys.LEFT }, { modifier : function (evt) { return evt.ctrlKey; }, up : fluid.reorderer.keys.i, down : fluid.reorderer.keys.m, right : fluid.reorderer.keys.k, left : fluid.reorderer.keys.j } ]; fluid.reorderer.keysetsPolicy = function (target, source) { var value = source ? source : target; return fluid.makeArray(value); }; fluid.reorderer.copyDropWarning = function (dropWarning) { return dropWarning ? dropWarning.clone() : dropWarning; }; /** * @param container - A jQueryable designator for the root node of the reorderer (a selector, a DOM node, or a jQuery instance) * @param options - an object containing any of the available options: * containerRole - indicates the role, or general use, for this instance of the Reorderer * keysets - an object containing sets of keycodes to use for directional navigation. Must contain: * modifier - a function that returns a boolean, indicating whether or not the required modifier(s) are activated * up * down * right * left * styles - an object containing class names for styling the Reorderer * defaultStyle * selected * dragging * hover * dropMarker * mouseDrag * avatar * avatarCreator - a function that returns a valid DOM node to be used as the dragging avatar */ fluid.defaults("fluid.reorderer", { gradeNames: ["fluid.viewComponent"], styles: { defaultStyle: "fl-reorderer-movable-default", selected: "fl-reorderer-movable-selected", dragging: "fl-reorderer-movable-dragging", mouseDrag: "fl-reorderer-movable-dragging", hover: "fl-reorderer-movable-hover", dropMarker: "fl-reorderer-dropMarker", avatar: "fl-reorderer-avatar" }, selectors: { dropWarning: ".flc-reorderer-dropWarning", movables: ".flc-reorderer-movable", selectables: ".flc-reorderer-movable", dropTargets: ".flc-reorderer-movable", grabHandle: "" }, avatarCreator: fluid.reorderer.defaultAvatarCreator, keysets: fluid.reorderer.defaultKeysets, // These two ginger options injected "upwards" from layoutHandler and actually time its construction (before FLUID-4925) containerRole: "{that}.layoutHandler.options.containerRole", selectablesTabindex: "{that}.layoutHandler.options.selectablesTabindex", layoutHandler: "fluid.listLayoutHandler", members: { dom: "@expand:fluid.createLocalContainerDomBinder({that}.container, {that}.options.selectors)", // Uses historical contract via "localContainer" argument dropManager: "@expand:fluid.dropManager()", // TODO: this is an old-style "that" which can no longer be supported as a component activeItem: null, kbDropWarning: "{that}.dom.dropWarning", mouseDropWarning: "@expand:fluid.reorderer.copyDropWarning({that}.kbDropWarning)" }, events: { onShowKeyboardDropWarning: null, onSelect: null, onBeginMove: "preventable", onMove: null, afterMove: null, onHover: null, // item, state onRefresh: null }, listeners: { onCreate: [ { namespace: "bindKeyHandlers", funcName: "fluid.reorderer.bindHandlersToContainer", args: ["{that}.container", "{that}.handleKeyDown", "{that}.handleKeyUp"] }, { namespace: "addContainerRoles", funcName: "fluid.reorderer.addRolesToContainer", args: "{that}" }, { namespace: "makeTabbable", funcName: "fluid.tabbable", args: "{that}.container" }, { namespace: "processAfterMoveCallback", funcName: "fluid.reorderer.processAfterMoveCallbackUrl", args: "{that}" }, "{that}.refresh"], onRefresh: { listener: "fluid.reorderer.initItems", args: "{that}", priority: -1000 // TODO: Can't be "first" since moduleLayout needs to respond first }, onHover: { funcName: "fluid.reorderer.hoverStyleHandler", args: ["{that}.dom", "{that}.options.styles", "{arguments}.0", "{arguments}.1"] // item, state } }, invokers: { changeSelectedToDefault: { funcName: "fluid.reorderer.changeSelectedToDefault", args: ["{arguments}.0", "{that}.options.styles"] }, setDropEffects: { funcName: "fluid.reorderer.setDropEffects", args: ["{that}.dom", "{arguments}.0"] }, createDropMarker: { funcName: "fluid.reorderer.createDropMarker", args: ["{arguments}.0", "{that}.options.styles.dropMarker"] }, refresh: { funcName: "fluid.reorderer.refresh", args: ["{that}.dom", "{that}.events", "{that}.selectableContext", "{that}.activeItem"] }, selectItem: { funcName: "fluid.reorderer.selectItem", args: ["{that}", "{arguments}.0"] }, initSelectables: { // unsupported, NON-API function funcName: "fluid.reorderer.initSelectables", args: ["{that}"] }, initMovable: { // unsupported, NON-API function funcName: "fluid.reorderer.initMovable", args: ["{that}", "{that}.dropManager", "{arguments}.0"] }, isMove: { // unsupported, NON-API function funcName: "fluid.reorderer.isMove", args: ["{that}.options.keysets", "{arguments}.0"] // evt }, isActiveItemMovable: { // unsupported, NON-API function funcName: "fluid.reorderer.isActiveItemMovable", args: ["{that}.activeItem", "{that}.dom"] }, handleKeyDown: { // unsupported, NON-API function funcName: "fluid.reorderer.handleKeyDown", args: ["{that}", "{that}.options.styles", "{arguments}.0"] // evt }, handleDirectionKeyDown: { // unsupported, NON-API function funcName: "fluid.reorderer.handleDirectionKeyDown", args: ["{that}", "{arguments}.0"] // evt }, handleKeyUp: { // unsupported, NON-API function funcName: "fluid.reorderer.handleKeyUp", args: ["{that}", "{that}.options.styles", "{arguments}.0"] // evt }, requestMovement: { // unsupported, NON-API function funcName: "fluid.reorderer.requestMovement", args: ["{that}", "{arguments}.0", "{arguments}.1"] // requestedPosition, item } }, mergePolicy: { keysets: fluid.reorderer.keysetsPolicy, "selectors.labelSource": "selectors.grabHandle", "selectors.selectables": "selectors.movables", "selectors.dropTargets": "selectors.movables" }, components: { layoutHandler: { type: "{that}.options.layoutHandler", container: "{reorderer}.container" }, labeller: { type: "fluid.reorderer.labeller", options: { members: { dom: "{reorderer}.dom" }, getGeometricInfo: "{reorderer}.layoutHandler.getGeometricInfo", orientation: "{reorderer}.layoutHandler.options.orientation", layoutType: "{reorderer}.options.layoutHandler" } } }, // The user option to enable or disable wrapping of elements within the container disableWrap: false }); fluid.reorderer.noModifier = function (evt) { return (!evt.ctrlKey && !evt.altKey && !evt.shiftKey && !evt.metaKey); }; // unsupported, NON-API function fluid.reorderer.isMove = function (keysets, evt) { // NB, needs dynamic binding for (var i = 0; i < keysets.length; i++) { if (keysets[i].modifier(evt)) { return true; } } return false; }; // unsupported, NON-API function fluid.reorderer.isActiveItemMovable = function (activeItem, dom) { return $.inArray(activeItem, dom.fastLocate("movables")) >= 0; }; // unsupported, NON-API function fluid.reorderer.handleKeyDown = async function (thatReorderer, styles, evt) { if (!thatReorderer.activeItem || thatReorderer.activeItem !== evt.target) { return true; } // If the key pressed is ctrl, and the active item is movable we want to restyle the active item. var jActiveItem = $(thatReorderer.activeItem); if (!jActiveItem.hasClass(styles.dragging) && thatReorderer.isMove(evt)) { // Don't treat the active item as dragging unless it is a movable. if (thatReorderer.isActiveItemMovable()) { jActiveItem.removeClass(styles.selected); jActiveItem.addClass(styles.dragging); jActiveItem.attr("aria-grabbed", "true"); thatReorderer.setDropEffects("move"); } return false; } // The only other keys we listen for are the arrows. return thatReorderer.handleDirectionKeyDown(evt); }; // unsupported, NON-API function fluid.reorderer.handleDirectionKeyDown = async function (thatReorderer, evt) { var item = thatReorderer.activeItem; if (!item) { return true; } var keysets = thatReorderer.options.keysets; for (var i = 0; i < keysets.length; i++) { var keyset = keysets[i]; var keydir = fluid.keyForValue(keyset, evt.keyCode); if (!keydir) { continue; } var isMovement = keyset.modifier(evt); var dirnum = fluid.keycodeDirection[keydir]; var relativeItem = thatReorderer.layoutHandler.getRelativePosition(item, dirnum, !isMovement); if (!relativeItem) { continue; } if (isMovement) { var prevent = thatReorderer.events.onBeginMove.fire(item); if (prevent === false) { return false; } var kbDropWarning = thatReorderer.kbDropWarning; if (kbDropWarning.length > 0) { if (relativeItem.clazz === "locked") { thatReorderer.events.onShowKeyboardDropWarning.fire(item, kbDropWarning); kbDropWarning.show(); } else { kbDropWarning.hide(); } } if (relativeItem.element) { thatReorderer.requestMovement(relativeItem, item); } } else if (fluid.reorderer.noModifier(evt)) { await fluid.blur(item); await fluid.focus(relativeItem.element); } return false; } return true; }; // unsupported, NON-API function fluid.reorderer.handleKeyUp = function (thatReorderer, styles, evt) { if (!thatReorderer.activeItem || thatReorderer.activeItem !== evt.target) { return true; } var jActiveItem = $(thatReorderer.activeItem); // Handle a key up event for the modifier if (jActiveItem.hasClass(styles.dragging) && !thatReorderer.isMove(evt)) { if (thatReorderer.kbDropWarning) { thatReorderer.kbDropWarning.hide(); } jActiveItem.removeClass(styles.dragging); jActiveItem.addClass(styles.selected); jActiveItem.attr("aria-grabbed", "false"); thatReorderer.setDropEffects("none"); return false; } return false; }; // unsupported, NON-API function fluid.reorderer.requestMovement = async function (thatReorderer, requestedPosition, item) { item = fluid.unwrap(item); // Temporary censoring to get around ModuleLayout inability to update relative to self. if (!requestedPosition || fluid.unwrap(requestedPosition.element) === item) { return; } var activeItem = $(thatReorderer.activeItem); // Fixes FLUID-3288. // Need to remove the blur event as safari will call blur on movements. // This caused the user to have to double tap the arrow keys to move. activeItem.off("blur.fluid.reorderer"); thatReorderer.events.onMove.fire(item, requestedPosition); thatReorderer.dropManager.geometricMove(item, requestedPosition.element, requestedPosition.position); //$(thatReorderer.activeItem).removeClass(options.styles.selected); // refocus on the active item because moving places focus on the body await fluid.focus(activeItem); thatReorderer.refresh(); thatReorderer.dropManager.updateGeometry(thatReorderer.layoutHandler.getGeometricInfo()); thatReorderer.events.afterMove.fire(item, requestedPosition, thatReorderer.dom.fastLocate("movables")); }; // unsupported, NON-API function fluid.reorderer.hoverStyleHandler = function (dom, styles, item, state) { dom.fastLocate("grabHandle", item)[state ? "addClass" : "removeClass"](styles.hover); }; // unsupported, NON-API function fluid.reorderer.processAfterMoveCallbackUrl = function (thatReorderer) { var options = thatReorderer.options; if (options.afterMoveCallbackUrl) { thatReorderer.events.afterMove.addListener(function () { var layoutHandler = thatReorderer.layoutHandler; var model = layoutHandler.getModel ? layoutHandler.getModel() : options.acquireModel(thatReorderer); $.post(options.afterMoveCallbackUrl, JSON.stringify(model)); }, "postModel"); } }; fluid.reorderer.setDropEffects = function (dom, value) { dom.fastLocate("dropTargets").attr("aria-dropeffect", value); }; fluid.reorderer.createDropMarker = function (tagName, dropClass) { var dropMarker = $(document.createElement(tagName)); dropMarker.addClass(dropClass); dropMarker.hide(); return dropMarker; }; fluid.reorderer.changeSelectedToDefault = function (jItem, styles) { jItem.removeClass(styles.selected); jItem.removeClass(styles.dragging); jItem.addClass(styles.defaultStyle); jItem.attr("aria-selected", "false"); }; fluid.reorderer.initSelectables = function (thatReorderer) { var handleBlur = function (evt) { thatReorderer.changeSelectedToDefault($(this)); return evt.stopPropagation(); }; var handleFocus = function (evt) { thatReorderer.selectItem(this); return evt.stopPropagation(); }; var handleClick = async function (evt) { var handle = fluid.unwrap(thatReorderer.dom.fastLocate("grabHandle", this)); if (handle.contains(evt.target)) { await fluid.focus(this); } }; var selectables = thatReorderer.dom.fastLocate("selectables"); for (var i = 0; i < selectables.length; ++i) { var selectable = $(selectables[i]); if (!$.data(selectable[0], "fluid.reorderer.selectable-initialised")) { selectable.addClass(thatReorderer.options.styles.defaultStyle); selectable.on("blur.fluid.reorderer", handleBlur); selectable.on("focus", handleFocus); selectable.on("click", handleClick); selectable.attr("role", thatReorderer.options.containerRole.item); selectable.attr("aria-selected", "false"); selectable.attr("aria-disabled", "false"); $.data(selectable[0], "fluid.reorderer.selectable-initialised", true); } } if (!thatReorderer.selectableContext) { thatReorderer.selectableContext = fluid.selectable(thatReorderer.container, { selectableElements: selectables, selectablesTabindex: thatReorderer.options.selectablesTabindex, direction: null }); } }; fluid.reorderer.selectItem = function (thatReorderer, anItem) { thatReorderer.events.onSelect.fire(anItem); // Set the previous active item back to its default state. if (thatReorderer.activeItem && thatReorderer.activeItem !== anItem) { thatReorderer.changeSelectedToDefault($(thatReorderer.activeItem)); } // Then select the new item. thatReorderer.activeItem = anItem; var jItem = $(anItem); var styles = thatReorderer.options.styles; jItem.removeClass(styles.defaultStyle); jItem.addClass(styles.selected); jItem.attr("aria-selected", "true"); }; /* * Takes a $ object and adds 'movable' functionality to it */ fluid.reorderer.initMovable = function (thatReorderer, dropManager, item) { var options = thatReorderer.options; var styles = options.styles; item.attr("aria-grabbed", "false"); item.on("mouseover", function () { thatReorderer.events.onHover.fire(item, true); }); item.on("mouseout", function () { thatReorderer.events.onHover.fire(item, false); }); var avatar; var handle = thatReorderer.dom.fastLocate("grabHandle", item); item.draggable({ refreshPositions: false, scroll: true, helper: function () { var dropWarningEl; if (thatReorderer.mouseDropWarning) { dropWarningEl = thatReorderer.mouseDropWarning[0]; } avatar = $(options.avatarCreator(item[0], styles.avatar, dropWarningEl)); avatar.prop("id", fluid.reorderer.createAvatarId(thatReorderer.container.id)); return avatar; }, start: function (e) { var prevent = thatReorderer.events.onBeginMove.fire(item); if (prevent === false) { return false; } var handle = thatReorderer.dom.fastLocate("grabHandle", item)[0]; var handlePos = fluid.dom.computeAbsolutePosition(handle); var handleWidth = handle.offsetWidth; var handleHeight = handle.offsetHeight; // Typically with `fluid.focus` we'd use async/await to wait for it to complete. However, with this // function that can result in breaking the drag and drop interaction. For example with the layout // reorderer, after reordering some items, clicking outside of the orderable element retains focus on // the reoderable element, but it may no longer be reorderable. // see: https://github.com/fluid-project/infusion/pull/1008#discussion_r569525065 fluid.focus(item); item.removeClass(options.styles.selected); // all this junk should happen in handler for a new event - although note that mouseDrag style might cause display: none, // invalidating dimensions item.addClass(options.styles.mouseDrag); item.attr("aria-grabbed", "true"); thatReorderer.setDropEffects("move"); dropManager.startDrag(e, handlePos, handleWidth, handleHeight); avatar.show(); }, stop: async function (e, ui) { item.removeClass(options.styles.mouseDrag); item.addClass(options.styles.selected); $(thatReorderer.activeItem).attr("aria-grabbed", "false"); var markerNode = fluid.unwrap(thatReorderer.dropMarker); if (markerNode.parentNode) { markerNode.parentNode.removeChild(markerNode); } avatar.hide(); ui.helper = null; thatReorderer.setDropEffects("none"); dropManager.endDrag(); await thatReorderer.requestMovement(dropManager.lastPosition(), item); // refocus on the active item because moving places focus on the body await fluid.focus(thatReorderer.activeItem); }, // This explicit detection is now required for jQuery UI after version 1.10.2 since the upstream API has been broken permanently. // See https://github.com/jquery/jquery-ui/pull/963 handle: fluid.unwrap(handle) === fluid.unwrap(item) ? null : handle }); }; fluid.reorderer.initItems = function (thatReorderer) { var movables = thatReorderer.dom.fastLocate("movables"); var dropTargets = thatReorderer.dom.fastLocate("dropTargets"); thatReorderer.initSelectables(); // Setup movables for (var i = 0; i < movables.length; i++) { var item = movables[i]; if (!$.data(item, "fluid.reorderer.movable-initialised")) { thatReorderer.initMovable($(item)); $.data(item, "fluid.reorderer.movable-initialised", true); } } // In order to create valid html, the drop marker is the same type as the node being dragged. // This creates a confusing UI in cases such as an ordered list. if (movables.length > 0 && !thatReorderer.dropMarker) { thatReorderer.dropMarker = thatReorderer.createDropMarker(movables[0].tagName); } thatReorderer.dropManager.updateGeometry(thatReorderer.layoutHandler.getGeometricInfo()); var dropChangeListener = function (dropTarget) { fluid.dom.moveDom(thatReorderer.dropMarker, dropTarget.element, dropTarget.position); thatReorderer.dropMarker.css("display", ""); if (thatReorderer.mouseDropWarning) { if (dropTarget.lockedelem) { thatReorderer.mouseDropWarning.show(); } else { thatReorderer.mouseDropWarning.hide(); } } }; thatReorderer.dropManager.dropChangeFirer.addListener(dropChangeListener, "fluid.reorderer"); // Set up dropTargets dropTargets.attr("aria-dropeffect", "none"); }; fluid.reorderer.refresh = function (dom, events, selectableContext, activeItem) { dom.refresh("movables"); dom.refresh("selectables"); dom.refresh("grabHandle", dom.fastLocate("movables")); dom.refresh("dropTargets"); if (selectableContext) { // if it didn't exist on dispatch, it must be up to date now selectableContext.selectables = dom.fastLocate("selectables"); selectableContext.selectablesUpdated(activeItem); } events.onRefresh.fire(); // This should be last otherwise handlers will see stale DOM binder contents }; /** * These roles are used to add ARIA roles to orderable items. This list can be extended as needed, * but the values of the container and item roles must match ARIA-specified roles. */ fluid.reorderer.roles = { GRID: { container: "grid", item: "gridcell" }, LIST: { container: "list", item: "listitem" }, REGIONS: { container: "main", item: "article" } }; fluid.defaults("fluid.reorderList", { gradeNames: ["fluid.reorderer"], layoutHandler: "fluid.listLayoutHandler" }); fluid.defaults("fluid.reorderGrid", { gradeNames: ["fluid.reorderer"], layoutHandler: "fluid.gridLayoutHandler" }); fluid.reorderer.SHUFFLE_GEOMETRIC_STRATEGY = "shuffleProjectFrom"; fluid.reorderer.GEOMETRIC_STRATEGY = "projectFrom"; fluid.reorderer.LOGICAL_STRATEGY = "logicalFrom"; fluid.reorderer.WRAP_LOCKED_STRATEGY = "lockedWrapFrom"; fluid.reorderer.NO_STRATEGY = null; // unsupported, NON-API function fluid.reorderer.relativeInfoGetter = function (orientation, coStrategy, contraStrategy, dropManager, disableWrap) { return function (item, direction, forSelection) { var dirorient = fluid.directionOrientation(direction); var strategy = dirorient === orientation ? coStrategy : contraStrategy; return strategy !== null ? dropManager[strategy](item, direction, forSelection, disableWrap) : null; }; }; /******************* * Layout Handlers * *******************/ // unsupported, NON-API function fluid.reorderer.makeGeometricInfoGetter = function (orientation, sentinelize, dom) { var that = { sentinelize: sentinelize, extents: [{ orientation: orientation, elements: dom.fastLocate("dropTargets") }], elementMapper: function (element) { return $.inArray(element, dom.fastLocate("movables")) === -1 ? "locked" : null; }, elementIndexer: function (element) { var selectables = dom.fastLocate("selectables"); return { elementClass: that.elementMapper(element), index: $.inArray(element, selectables), length: selectables.length }; } }; return that; }; fluid.defaults("fluid.layoutHandler", { gradeNames: ["fluid.viewComponent"], disableWrap: "{reorderer}.options.disableWrap", members: { reordererDom: "{reorderer}.dom", dropManager: "{reorderer}.dropManager" }, invokers: { // overridden in moduleLayoutHandler getGeometricInfo: "fluid.reorderer.makeGeometricInfoGetter({that}.options.orientation, {that}.options.sentinelize, {that}.reordererDom)" } }); // Public layout handlers. fluid.defaults("fluid.listLayoutHandler", { gradeNames: ["fluid.layoutHandler"], orientation: fluid.orientation.VERTICAL, containerRole: fluid.reorderer.roles.LIST, selectablesTabindex: -1, sentinelize: true, members: { getRelativePosition: { // TODO: an old-fashioned function member - convert to invoker expander: { funcName: "fluid.reorderer.relativeInfoGetter", args: [ "{that}.options.orientation", fluid.reorderer.LOGICAL_STRATEGY, null, "{that}.dropManager", "{that}.options.disableWrap"] } } } }); /* * Items in the Lightbox are stored in a list, but they are visually presented as a grid that * changes dimensions when the window changes size. As a result, when the user presses the up or * down arrow key, what lies above or below depends on the current window size. * * The GridLayoutHandler is responsible for handling changes to this virtual 'grid' of items * in the window, and of informing the Lightbox of which items surround a given item. */ fluid.defaults("fluid.gridLayoutHandler", { gradeNames: ["fluid.layoutHandler"], orientation: fluid.orientation.HORIZONTAL, containerRole: fluid.reorderer.roles.GRID, selectablesTabindex: -1, sentinelize: false, coStrategy: "@expand:fluid.gridLayoutHandler.computeCoStrategy({that}.options.disableWrap)", members: { getRelativePosition: { // TODO: an old-fashioned function member - convert to invoker expander: { funcName: "fluid.reorderer.relativeInfoGetter", args: [ "{that}.options.orientation", "{that}.options.coStrategy", fluid.reorderer.SHUFFLE_GEOMETRIC_STRATEGY, "{that}.dropManager", "{that}.options.disableWrap"] } } } }); fluid.gridLayoutHandler.computeCoStrategy = function (disableWrap) { return disableWrap ? fluid.reorderer.SHUFFLE_GEOMETRIC_STRATEGY : fluid.reorderer.LOGICAL_STRATEGY; }; /************* * Labelling * *************/ /* * ARIA labeller component which decorates the reorderer with the function of announcing the current * focused position of the reorderer as well as the coordinates of any requested move */ fluid.defaults("fluid.reorderer.labeller", { gradeNames: ["fluid.component"], members: { movedMap: {}, moduleCell: { expander: { funcName: "fluid.reorderer.labeller.computeModuleCell", args: ["{that}.resolver", "{that}.options.orientation"] } }, layoutType: { expander: { funcName: "fluid.computeNickName", args: "{that}.options.layoutType" } }, positionTemplate: { expander: { funcName: "fluid.reorderer.labeller.computePositionTemplate", args: ["{that}.resolver", "{that}.layoutType"] } } }, strings: { overallTemplate: "%recentStatus %item %position %movable", position: "%index of %length", position_moduleLayoutHandler: "%index of %length in %moduleCell %moduleIndex of %moduleLength", moduleCell_0: "row", // NB, these keys must agree with fluid.a11y.orientation constants moduleCell_1: "column", movable: "movable", fixed: "fixed", recentStatus: "moved from position %position" }, components: { resolver: { type: "fluid.messageResolver", options: { messageBase: "{labeller}.options.strings" } } }, invokers: { renderLabel: { funcName: "fluid.reorderer.labeller.renderLabel", args: ["{labeller}", "{arguments}.0", "{arguments}.1"] } }, listeners: { "{reorderer}.events.onRefresh": { listener: "fluid.reorderer.labeller.onRefresh", args: "{that}" }, "{reorderer}.events.onMove": { listener: "fluid.reorderer.labeller.onMove", args: ["{that}", "{arguments}.0", "{arguments}.1"] // item, newPosition } } }); // unsupported, NON-API function fluid.reorderer.labeller.computeModuleCell = function (resolver, orientation) { return resolver.resolve("moduleCell_" + orientation); }; // unsupported, NON-API function fluid.reorderer.labeller.computePositionTemplate = function (resolver, layoutType) { return resolver.lookup(["position_" + layoutType, "position"]); }; // unsupported, NON-API function fluid.reorderer.labeller.onRefresh = function (that) { var selectables = that.dom.locate("selectables"); var movedMap = that.movedMap; fluid.each(selectables, function (selectable) { var labelOptions = {}; var id = fluid.allocateSimpleId(selectable); var moved = movedMap[id]; var label = that.renderLabel(selectable); var plainLabel = label; if (moved) { moved.newRender = plainLabel; label = that.renderLabel(selectable, moved.oldRender.position); // once we move focus out of the element which just moved, return its ARIA label to be the new plain label $(selectable).one("focusout.ariaLabeller", function () { if (movedMap[id]) { var oldLabel = movedMap[id].newRender.label; delete movedMap[id]; fluid.updateAriaLabel(selectable, oldLabel); } }); labelOptions.dynamicLabel = true; } fluid.updateAriaLabel(selectable, label.label, labelOptions); }); }; // unsupported, NON-API function fluid.reorderer.labeller.onMove = function (that, item) { fluid.clear(that.movedMap); // if we somehow were fooled into missing a defocus, at least clear the map on a 2nd move // This "off" is needed for FLUID-4693 with Chrome 18, which generates a focusOut when // simply doing the DOM manipulation to move the element to a new position. $(item).off("focusout.ariaLabeller"); var movingId = fluid.allocateSimpleId(item); that.movedMap[movingId] = { oldRender: that.renderLabel(item) }; }; // unsupported, NON-API function // Convert from 0-based to 1-based indices for announcement fluid.reorderer.indexRebaser = function (indices) { indices.index++; if (indices.moduleIndex !== undefined) { indices.moduleIndex++; } return indices; }; // unsupported, NON-API function fluid.reorderer.labeller.renderLabel = function (that, selectable, recentPosition) { var geom = that.options.getGeometricInfo(); var indices = fluid.reorderer.indexRebaser(geom.elementIndexer(selectable)); indices.moduleCell = that.moduleCell; var elementClass = geom.elementMapper(selectable); var labelSource = that.dom.locate("labelSource", selectable); var recentStatus; if (recentPosition) { recentStatus = that.resolver.resolve("recentStatus", {position: recentPosition}); } var topModel = { item: typeof(labelSource) === "string" ? labelSource : fluid.unwrap(labelSource).innerText, position: that.positionTemplate.resolveFunc(that.positionTemplate.template, indices), movable: that.resolver.resolve(elementClass === "locked" ? "fixed" : "movable"), recentStatus: recentStatus || "" }; var template = that.resolver.lookup(["overallTemplate"]); var label = template.resolveFunc(template.template, topModel); return { position: topModel.position, label: label }; };