You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
252 lines
8.7 KiB
252 lines
8.7 KiB
import { EVENT_OPTIONS_PASSIVE } from '../../constants/events';
|
|
import { CODE_ENTER, CODE_SPACE } from '../../constants/key-codes';
|
|
import { RX_HASH, RX_HASH_ID, RX_SPACE_SPLIT } from '../../constants/regex';
|
|
import looseEqual from '../../utils/loose-equal';
|
|
import { arrayIncludes, concat } from '../../utils/array';
|
|
import { addClass, getAttr, hasAttr, isDisabled, isTag, removeAttr, removeClass, removeStyle, requestAF, setAttr, setStyle } from '../../utils/dom';
|
|
import { isBrowser } from '../../utils/env';
|
|
import { eventOn, eventOff } from '../../utils/events';
|
|
import { isString } from '../../utils/inspect';
|
|
import { keys } from '../../utils/object'; // --- Constants ---
|
|
// Classes to apply to trigger element
|
|
|
|
var CLASS_BV_TOGGLE_COLLAPSED = 'collapsed';
|
|
var CLASS_BV_TOGGLE_NOT_COLLAPSED = 'not-collapsed'; // Property key for handler storage
|
|
|
|
var BV_BASE = '__BV_toggle'; // Root event listener property (Function)
|
|
|
|
var BV_TOGGLE_ROOT_HANDLER = "".concat(BV_BASE, "_HANDLER__"); // Trigger element click handler property (Function)
|
|
|
|
var BV_TOGGLE_CLICK_HANDLER = "".concat(BV_BASE, "_CLICK__"); // Target visibility state property (Boolean)
|
|
|
|
var BV_TOGGLE_STATE = "".concat(BV_BASE, "_STATE__"); // Target ID list property (Array)
|
|
|
|
var BV_TOGGLE_TARGETS = "".concat(BV_BASE, "_TARGETS__"); // Commonly used strings
|
|
|
|
var STRING_FALSE = 'false';
|
|
var STRING_TRUE = 'true'; // Commonly used attribute names
|
|
|
|
var ATTR_ARIA_CONTROLS = 'aria-controls';
|
|
var ATTR_ARIA_EXPANDED = 'aria-expanded';
|
|
var ATTR_ROLE = 'role';
|
|
var ATTR_TABINDEX = 'tabindex'; // Commonly used style properties
|
|
|
|
var STYLE_OVERFLOW_ANCHOR = 'overflow-anchor'; // Emitted control event for collapse (emitted to collapse)
|
|
|
|
export var EVENT_TOGGLE = 'bv::toggle::collapse'; // Listen to event for toggle state update (emitted by collapse)
|
|
|
|
export var EVENT_STATE = 'bv::collapse::state'; // Private event emitted on `$root` to ensure the toggle state is always synced
|
|
// Gets emitted even if the state of b-collapse has not changed
|
|
// This event is NOT to be documented as people should not be using it
|
|
|
|
export var EVENT_STATE_SYNC = 'bv::collapse::sync::state'; // Private event we send to collapse to request state update sync event
|
|
|
|
export var EVENT_STATE_REQUEST = 'bv::request::collapse::state';
|
|
var KEYDOWN_KEY_CODES = [CODE_ENTER, CODE_SPACE]; // --- Helper methods ---
|
|
|
|
var isNonStandardTag = function isNonStandardTag(el) {
|
|
return !arrayIncludes(['button', 'a'], el.tagName.toLowerCase());
|
|
};
|
|
|
|
var getTargets = function getTargets(_ref, el) {
|
|
var modifiers = _ref.modifiers,
|
|
arg = _ref.arg,
|
|
value = _ref.value;
|
|
// Any modifiers are considered target IDs
|
|
var targets = keys(modifiers || {}); // If value is a string, split out individual targets (if space delimited)
|
|
|
|
value = isString(value) ? value.split(RX_SPACE_SPLIT) : value; // Support target ID as link href (`href="#id"`)
|
|
|
|
if (isTag(el.tagName, 'a')) {
|
|
var href = getAttr(el, 'href') || '';
|
|
|
|
if (RX_HASH_ID.test(href)) {
|
|
targets.push(href.replace(RX_HASH, ''));
|
|
}
|
|
} // Add ID from `arg` (if provided), and support value
|
|
// as a single string ID or an array of string IDs
|
|
// If `value` is not an array or string, then it gets filtered out
|
|
|
|
|
|
concat(arg, value).forEach(function (t) {
|
|
return isString(t) && targets.push(t);
|
|
}); // Return only unique and truthy target IDs
|
|
|
|
return targets.filter(function (t, index, arr) {
|
|
return t && arr.indexOf(t) === index;
|
|
});
|
|
};
|
|
|
|
var removeClickListener = function removeClickListener(el) {
|
|
var handler = el[BV_TOGGLE_CLICK_HANDLER];
|
|
|
|
if (handler) {
|
|
eventOff(el, 'click', handler, EVENT_OPTIONS_PASSIVE);
|
|
eventOff(el, 'keydown', handler, EVENT_OPTIONS_PASSIVE);
|
|
}
|
|
|
|
el[BV_TOGGLE_CLICK_HANDLER] = null;
|
|
};
|
|
|
|
var addClickListener = function addClickListener(el, vnode) {
|
|
removeClickListener(el);
|
|
|
|
if (vnode.context) {
|
|
var handler = function handler(evt) {
|
|
if (!(evt.type === 'keydown' && !arrayIncludes(KEYDOWN_KEY_CODES, evt.keyCode)) && !isDisabled(el)) {
|
|
var targets = el[BV_TOGGLE_TARGETS] || [];
|
|
targets.forEach(function (target) {
|
|
vnode.context.$root.$emit(EVENT_TOGGLE, target);
|
|
});
|
|
}
|
|
};
|
|
|
|
el[BV_TOGGLE_CLICK_HANDLER] = handler;
|
|
eventOn(el, 'click', handler, EVENT_OPTIONS_PASSIVE);
|
|
|
|
if (isNonStandardTag(el)) {
|
|
eventOn(el, 'keydown', handler, EVENT_OPTIONS_PASSIVE);
|
|
}
|
|
}
|
|
};
|
|
|
|
var removeRootListeners = function removeRootListeners(el, vnode) {
|
|
if (el[BV_TOGGLE_ROOT_HANDLER] && vnode.context) {
|
|
vnode.context.$root.$off([EVENT_STATE, EVENT_STATE_SYNC], el[BV_TOGGLE_ROOT_HANDLER]);
|
|
}
|
|
|
|
el[BV_TOGGLE_ROOT_HANDLER] = null;
|
|
};
|
|
|
|
var addRootListeners = function addRootListeners(el, vnode) {
|
|
removeRootListeners(el, vnode);
|
|
|
|
if (vnode.context) {
|
|
var handler = function handler(id, state) {
|
|
// `state` will be `true` if target is expanded
|
|
if (arrayIncludes(el[BV_TOGGLE_TARGETS] || [], id)) {
|
|
// Set/Clear 'collapsed' visibility class state
|
|
el[BV_TOGGLE_STATE] = state; // Set `aria-expanded` and class state on trigger element
|
|
|
|
setToggleState(el, state);
|
|
}
|
|
};
|
|
|
|
el[BV_TOGGLE_ROOT_HANDLER] = handler; // Listen for toggle state changes (public) and sync (private)
|
|
|
|
vnode.context.$root.$on([EVENT_STATE, EVENT_STATE_SYNC], handler);
|
|
}
|
|
};
|
|
|
|
var setToggleState = function setToggleState(el, state) {
|
|
// State refers to the visibility of the collapse/sidebar
|
|
if (state) {
|
|
removeClass(el, CLASS_BV_TOGGLE_COLLAPSED);
|
|
addClass(el, CLASS_BV_TOGGLE_NOT_COLLAPSED);
|
|
setAttr(el, ATTR_ARIA_EXPANDED, STRING_TRUE);
|
|
} else {
|
|
removeClass(el, CLASS_BV_TOGGLE_NOT_COLLAPSED);
|
|
addClass(el, CLASS_BV_TOGGLE_COLLAPSED);
|
|
setAttr(el, ATTR_ARIA_EXPANDED, STRING_FALSE);
|
|
}
|
|
}; // Reset and remove a property from the provided element
|
|
|
|
|
|
var resetProp = function resetProp(el, prop) {
|
|
el[prop] = null;
|
|
delete el[prop];
|
|
}; // Handle directive updates
|
|
|
|
|
|
var handleUpdate = function handleUpdate(el, binding, vnode) {
|
|
/* istanbul ignore next: should never happen */
|
|
if (!isBrowser || !vnode.context) {
|
|
return;
|
|
} // If element is not a button or link, we add `role="button"`
|
|
// and `tabindex="0"` for accessibility reasons
|
|
|
|
|
|
if (isNonStandardTag(el)) {
|
|
if (!hasAttr(el, ATTR_ROLE)) {
|
|
setAttr(el, ATTR_ROLE, 'button');
|
|
}
|
|
|
|
if (!hasAttr(el, ATTR_TABINDEX)) {
|
|
setAttr(el, ATTR_TABINDEX, '0');
|
|
}
|
|
} // Ensure the collapse class and `aria-*` attributes persist
|
|
// after element is updated (either by parent re-rendering
|
|
// or changes to this element or its contents)
|
|
|
|
|
|
setToggleState(el, el[BV_TOGGLE_STATE]); // Parse list of target IDs
|
|
|
|
var targets = getTargets(binding, el); // Ensure the `aria-controls` hasn't been overwritten
|
|
// or removed when vnode updates
|
|
// Also ensure to set `overflow-anchor` to `none` to prevent
|
|
// the browser's scroll anchoring behavior
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (targets.length > 0) {
|
|
setAttr(el, ATTR_ARIA_CONTROLS, targets.join(' '));
|
|
setStyle(el, STYLE_OVERFLOW_ANCHOR, 'none');
|
|
} else {
|
|
removeAttr(el, ATTR_ARIA_CONTROLS);
|
|
removeStyle(el, STYLE_OVERFLOW_ANCHOR);
|
|
} // Add/Update our click listener(s)
|
|
// Wrap in a `requestAF()` to allow any previous
|
|
// click handling to occur first
|
|
|
|
|
|
requestAF(function () {
|
|
addClickListener(el, vnode);
|
|
}); // If targets array has changed, update
|
|
|
|
if (!looseEqual(targets, el[BV_TOGGLE_TARGETS])) {
|
|
// Update targets array to element storage
|
|
el[BV_TOGGLE_TARGETS] = targets; // Ensure `aria-controls` is up to date
|
|
// Request a state update from targets so that we can
|
|
// ensure expanded state is correct (in most cases)
|
|
|
|
targets.forEach(function (target) {
|
|
vnode.context.$root.$emit(EVENT_STATE_REQUEST, target);
|
|
});
|
|
}
|
|
};
|
|
/*
|
|
* Export our directive
|
|
*/
|
|
|
|
|
|
export var VBToggle = {
|
|
bind: function bind(el, binding, vnode) {
|
|
// State is initially collapsed until we receive a state event
|
|
el[BV_TOGGLE_STATE] = false; // Assume no targets initially
|
|
|
|
el[BV_TOGGLE_TARGETS] = []; // Add our root listeners
|
|
|
|
addRootListeners(el, vnode); // Initial update of trigger
|
|
|
|
handleUpdate(el, binding, vnode);
|
|
},
|
|
componentUpdated: handleUpdate,
|
|
updated: handleUpdate,
|
|
unbind: function unbind(el, binding, vnode) {
|
|
removeClickListener(el); // Remove our $root listener
|
|
|
|
removeRootListeners(el, vnode); // Reset custom props
|
|
|
|
resetProp(el, BV_TOGGLE_ROOT_HANDLER);
|
|
resetProp(el, BV_TOGGLE_CLICK_HANDLER);
|
|
resetProp(el, BV_TOGGLE_STATE);
|
|
resetProp(el, BV_TOGGLE_TARGETS); // Reset classes/attrs/styles
|
|
|
|
removeClass(el, CLASS_BV_TOGGLE_COLLAPSED);
|
|
removeClass(el, CLASS_BV_TOGGLE_NOT_COLLAPSED);
|
|
removeAttr(el, ATTR_ARIA_EXPANDED);
|
|
removeAttr(el, ATTR_ARIA_CONTROLS);
|
|
removeAttr(el, ATTR_ROLE);
|
|
removeStyle(el, STYLE_OVERFLOW_ANCHOR);
|
|
}
|
|
};
|