UNPKG

vue-tv

Version:

Spatial navigation library based on Vue js

748 lines (685 loc) 22.7 kB
'use strict';Object.defineProperty(exports,'__esModule',{value:true});var mitt=require('mitt');function _interopDefaultLegacy(e){return e&&typeof e==='object'&&'default'in e?e:{'default':e}}var mitt__default=/*#__PURE__*/_interopDefaultLegacy(mitt);function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }var focusHandler = new mitt__default['default'](); var registered = false; var actions = []; var KEY_CODE = { 37: "LEFT", 38: "UP", 39: "RIGHT", 40: "DOWN" }; var enableNavigation = function enableNavigation(actionCB) { if (registered) { var index = actions.findIndex(function (item) { return item.id === actionCB.id; }); if (index > -1) { actions[index] = actionCB; } else { actions.push(actionCB); } } else { registered = true; actions.push(actionCB); window.addEventListener("keydown", function (event) { actions.forEach(function (action) { if (!action.preCondition || action.preCondition()) return action[KEY_CODE[event.keyCode]] && action[KEY_CODE[event.keyCode]](); }); }); } }; var disableNavigation = function disableNavigation(id) { var index = actions.findIndex(function (item) { return item.id === id; }); if (index > -1) { actions.splice(index, 1); } };// var script = { name: "focusableGrid", props: { child: { type: Object, //Child component (eg: card, button) required: true }, items: { //Array to be passed to child type: Array, required: true }, isFocused: { //Condition to enable navigation type: Boolean, default: false }, disabled: { //Condition to prevent navigation type: Boolean, default: false }, shouldScroll: { type: Boolean, default: false }, maxColumn: { type: Number, default: 6 }, id: { //unique id to differentiate navigation default: Math.random().toString() } }, data: function data() { return { focusedIndex: 0, scrollAmount: 0, activeRow: 0, activeColumn: 0 }; }, computed: { style: function style() { return { transform: "translateY(".concat(this.scrollAmount, "px)") }; } }, methods: { getScrollAmount: function getScrollAmount(el, negative) { if (el) { var value = el.clientHeight; return negative ? -value : value; } return 0; }, isPrevColumnPresent: function isPrevColumnPresent() { return this.focusedIndex > 0 && this.activeColumn > 0; }, isNextColumnPresent: function isNextColumnPresent() { return this.focusedIndex < this.items.length - 1 && this.activeColumn < this.maxColumn - 1; }, isPrevRowPresent: function isPrevRowPresent() { return this.focusedIndex > 0 && this.activeRow > 0; }, isNextRowPresent: function isNextRowPresent() { return this.focusedIndex < this.items.length - 1 && this.activeRow < this.items.length / this.maxColumn - 1; }, updateColumn: function updateColumn(reverse) { var value = reverse ? -1 : 1; this.focusedIndex += value; this.activeColumn += value; }, updateRow: function updateRow(reverse) { var cardsPerColumn = reverse ? -this.maxColumn : this.maxColumn; var value = reverse ? -1 : 1; this.focusedIndex += cardsPerColumn; this.activeRow += value; if (this.isFocusIndexOutOfBound()) { //UNEVEN LAST ROW this.focusedIndex = this.items.length - 1; this.activeColumn = (this.items.length - 1) % this.maxColumn; } }, isFocusIndexOutOfBound: function isFocusIndexOutOfBound() { return this.focusedIndex > this.items.length - 1; }, updateScrollValue: function updateScrollValue(negative) { this.scrollAmount += this.getScrollAmount(this.$refs.childItem[this.focusedIndex], negative); }, handleFocusLost: function handleFocusLost() { if (this.focusedIndex > this.items.length - 1) { this.focusedIndex = this.items.length - 1; } }, resetFocus: function resetFocus(_ref) { var force = _ref.force; if (force || !this.isFocused) { this.focusedIndex = 0; this.activeColumn = 0; this.activeRow = 0; this.scrollAmount = 0; } } }, updated: function updated() { this.handleFocusLost(); }, mounted: function mounted() { var _this = this; enableNavigation({ LEFT: function LEFT() { if (_this.isPrevColumnPresent()) { _this.updateColumn("reverse"); } }, RIGHT: function RIGHT() { if (_this.isNextColumnPresent()) { _this.updateColumn(); } }, UP: function UP() { if (_this.isPrevRowPresent()) { _this.updateRow("reverse"); if (_this.shouldScroll) _this.updateScrollValue(); } }, DOWN: function DOWN() { if (_this.isNextRowPresent()) { _this.updateRow(); if (_this.shouldScroll) _this.updateScrollValue("negative"); } }, preCondition: function preCondition() { return _this.isFocused && !_this.disabled; }, id: "grid-".concat(this.id) }); focusHandler.on("RESET_FOCUS", this.resetFocus); }, destroyed: function destroyed() { disableNavigation("grid-".concat(this.id)); focusHandler.off("RESET_FOCUS", this.resetFocus); } };function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) { if (typeof shadowMode !== 'boolean') { createInjectorSSR = createInjector; createInjector = shadowMode; shadowMode = false; } // Vue.extend constructor export interop. const options = typeof script === 'function' ? script.options : script; // render functions if (template && template.render) { options.render = template.render; options.staticRenderFns = template.staticRenderFns; options._compiled = true; // functional template if (isFunctionalTemplate) { options.functional = true; } } // scopedId if (scopeId) { options._scopeId = scopeId; } let hook; if (moduleIdentifier) { // server build hook = function (context) { // 2.3 injection context = context || // cached call (this.$vnode && this.$vnode.ssrContext) || // stateful (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext); // functional // 2.2 with runInNewContext: true if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { context = __VUE_SSR_CONTEXT__; } // inject component styles if (style) { style.call(this, createInjectorSSR(context)); } // register component module identifier for async chunk inference if (context && context._registeredComponents) { context._registeredComponents.add(moduleIdentifier); } }; // used by ssr in case component is cached and beforeCreate // never gets called options._ssrRegister = hook; } else if (style) { hook = shadowMode ? function (context) { style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot)); } : function (context) { style.call(this, createInjector(context)); }; } if (hook) { if (options.functional) { // register for functional component in vue file const originalRender = options.render; options.render = function renderWithStyleInjection(h, context) { hook.call(context); return originalRender(h, context); }; } else { // inject component registration as beforeCreate hook const existing = options.beforeCreate; options.beforeCreate = existing ? [].concat(existing, hook) : [hook]; } } return script; }function createInjectorSSR(context) { if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { context = __VUE_SSR_CONTEXT__; } if (!context) return () => { }; if (!('styles' in context)) { context._styles = context._styles || {}; Object.defineProperty(context, 'styles', { enumerable: true, get: () => context._renderStyles(context._styles) }); context._renderStyles = context._renderStyles || renderStyles; } return (id, style) => addStyle(id, style, context); } function addStyle(id, css, context) { const group = css.media || 'default' ; const style = context._styles[group] || (context._styles[group] = { ids: [], css: '' }); if (!style.ids.includes(id)) { style.media = css.media; style.ids.push(id); let code = css.source; style.css += code + '\n'; } } function renderStyles(styles) { let css = ''; for (const key in styles) { const style = styles[key]; css += '<style data-vue-ssr-id="' + Array.from(style.ids).join(' ') + '"' + (style.media ? ' media="' + style.media + '"' : '') + '>' + style.css + '</style>'; } return css; }/* script */ var __vue_script__ = script; /* template */ var __vue_render__ = function __vue_render__() { var _vm = this; var _h = _vm.$createElement; var _c = _vm._self._c || _h; return _c('div', { ref: "grid", staticClass: "grid", class: { focus: _vm.isFocused }, style: _vm.style }, _vm._l(_vm.items, function (item, index) { return _vm._ssrNode("<div class=\"child\" data-v-f3e4edfc>", "</div>", [_c(_vm.child, _vm._b({ tag: "component", attrs: { "id": "child" + (item.id || index), "isFocused": _vm.isFocused && index === _vm.focusedIndex } }, 'component', item, false))], 1); }), 0); }; var __vue_staticRenderFns__ = []; /* style */ var __vue_inject_styles__ = function __vue_inject_styles__(inject) { if (!inject) return; inject("data-v-f3e4edfc_0", { source: ".grid[data-v-f3e4edfc]{display:flex;flex-wrap:wrap}.child[data-v-f3e4edfc]{display:flex}.vertical[data-v-f3e4edfc]{flex-direction:column}", map: undefined, media: undefined }); }; /* scoped */ var __vue_scope_id__ = "data-v-f3e4edfc"; /* module identifier */ var __vue_module_identifier__ = "data-v-f3e4edfc"; /* functional template */ var __vue_is_functional_template__ = false; /* style inject shadow dom */ var __vue_component__ = /*#__PURE__*/normalizeComponent({ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, __vue_inject_styles__, __vue_script__, __vue_scope_id__, __vue_is_functional_template__, __vue_module_identifier__, false, undefined, createInjectorSSR, undefined);var script$1 = { name: "focusableList", props: { child: { type: Object, //Child component (eg: card, button) required: true }, items: { //Array to be passed to child type: Array, required: true }, isFocused: { //Condition to enable navigation type: Boolean, default: false }, defaultIndex: { type: Number, default: -1 }, disabled: { //Condition to prevent navigation type: Boolean, default: false }, disabledIndex: { type: Array, default: function _default() { return [-1]; } }, orientation: { type: String, //Direction of list default: "HORIZONTAL" //'VERTICAL' }, shouldScroll: { type: Boolean, default: false }, id: { //unique id to differentiate navigation default: Math.random().toString() } }, data: function data() { return { focusedIndex: -1, scrollAmount: 0 }; }, computed: { style: function style() { return { transform: "translate".concat(this.orientation === "VERTICAL" ? "Y" : "X", "(").concat(this.scrollAmount, "px)") }; } }, methods: { isEnabledIndex: function isEnabledIndex(index) { return !this.disabledIndex.includes(index); }, setInitialvalue: function setInitialvalue() { if (this.defaultIndex > -1 && this.defaultIndex < this.items.length && this.isEnabledIndex(this.defaultIndex)) { this.focusedIndex = this.defaultIndex; } else { this.focusedIndex = this.getValidNextIndex(); } if (this.focusedIndex >= -1) this.updateScrollValue(); }, getKeysByOrientation: function getKeysByOrientation(orientation) { return { REVERSE: orientation === "VERTICAL" ? "UP" : "LEFT", FORWARD: orientation === "VERTICAL" ? "DOWN" : "RIGHT" }; }, getScrollAmountByOrientation: function getScrollAmountByOrientation(el, orientation) { if (el) { return -el[orientation === "VERTICAL" ? "clientHeight" : "clientWidth"]; } return 0; }, handleFocusLost: function handleFocusLost() { if (this.focusedIndex > this.items.length - 1 || this.disabledIndex.includes(this.focusedIndex)) { if (this.getValidPrevIndex() !== this.focusedIndex) { this.$emit("onFocusLost", { prevIndex: this.focusedIndex, newIndex: this.getValidPrevIndex() }); this.focusedIndex = this.getValidPrevIndex(); } else if (this.getValidNextIndex() !== this.focusedIndex) { this.$emit("onFocusLost", { prevIndex: this.focusedIndex, newIndex: this.getValidNextIndex() }); this.focusedIndex = this.getValidNextIndex(); } else { this.focusedIndex = -1; this.$emit("onFocusLost", { err: "No items to set focus, either disable it or provide new item to setFocus" }); } } }, isPrevItemPresent: function isPrevItemPresent() { return this.focusedIndex > 0; }, isNextItemPresent: function isNextItemPresent() { return this.focusedIndex < this.items.length - 1; }, getValidNextIndex: function getValidNextIndex() { var i = this.focusedIndex + 1; var validIndex = this.focusedIndex; while (i < this.items.length) { if (this.isEnabledIndex(i)) { validIndex = i; this.$emit("onFocusChange", { prevIndex: this.focusedIndex, newIndex: validIndex, item: this.items[validIndex] }); break; } i++; } return validIndex; }, getValidPrevIndex: function getValidPrevIndex() { var i = this.focusedIndex - 1; var validIndex = this.focusedIndex; while (i >= 0) { if (this.isEnabledIndex(i) && i < this.items.length) { validIndex = i; this.$emit("onFocusChange", { prevIndex: this.focusedIndex, newIndex: validIndex, item: this.items[validIndex] }); break; } i--; } return validIndex; }, updateFocus: function updateFocus(reverse) { if (reverse) { this.focusedIndex = this.getValidPrevIndex(); } else { this.focusedIndex = this.getValidNextIndex(); } }, updateScrollValue: function updateScrollValue() { if (this.shouldScroll && this.$refs.childItem) { this.scrollAmount = this.getScrollAmountByOrientation(this.$refs.childItem[0], this.orientation) * this.focusedIndex; } }, resetFocus: function resetFocus(_ref) { var force = _ref.force; if (force || !this.isFocused) { this.focusedIndex = 0; this.scrollAmount = 0; } }, setExternalFocus: function setExternalFocus() { var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, index = _ref2.index, id = _ref2.id; if (id === this.id) { if (this.isEnabledIndex(index) && index >= 0 && index < this.items.length - 1) { this.focusedIndex = index; this.updateScrollValue(); } else { console.error("focus to the given index ".concat(index, " is not possible")); } } } }, updated: function updated() { this.handleFocusLost(); }, mounted: function mounted() { var _this = this, _enableNavigation; this.setInitialvalue(); var KEYS = this.getKeysByOrientation(this.orientation); enableNavigation((_enableNavigation = { id: "list-".concat(this.id) }, _defineProperty(_enableNavigation, KEYS.REVERSE, function () { if (_this.isPrevItemPresent()) { _this.updateFocus("reverse"); _this.updateScrollValue(); } }), _defineProperty(_enableNavigation, KEYS.FORWARD, function () { if (_this.isNextItemPresent()) { _this.updateFocus(); _this.updateScrollValue(); } }), _defineProperty(_enableNavigation, "preCondition", function preCondition() { return _this.isFocused && !_this.disabled; }), _enableNavigation)); focusHandler.on("RESET_FOCUS", this.resetFocus); focusHandler.on("SET_FOCUS", this.setExternalFocus); }, destroyed: function destroyed() { disableNavigation("list-".concat(this.id)); focusHandler.off("RESET_FOCUS", this.resetFocus); focusHandler.off("SET_FOCUS", this.setExternalFocus); } };/* script */ var __vue_script__$1 = script$1; /* template */ var __vue_render__$1 = function __vue_render__() { var _vm = this; var _h = _vm.$createElement; var _c = _vm._self._c || _h; return _c('div', { ref: "list", staticClass: "list", class: { focus: _vm.isFocused, vertical: _vm.orientation === 'VERTICAL' }, style: _vm.style }, _vm._l(_vm.items, function (item, index) { return _vm._ssrNode("<div class=\"child\" data-v-72ac378b>", "</div>", [_c(_vm.child, _vm._b({ tag: "component", class: { disabled: _vm.disabledIndex.includes(index) }, attrs: { "id": "child" + (item.id || index), "isFocused": _vm.isFocused && index === _vm.focusedIndex, "disabled": item.disabled || _vm.disabledIndex.includes(index) } }, 'component', item, false))], 1); }), 0); }; var __vue_staticRenderFns__$1 = []; /* style */ var __vue_inject_styles__$1 = function __vue_inject_styles__(inject) { if (!inject) return; inject("data-v-72ac378b_0", { source: ".list[data-v-72ac378b]{display:flex}.child[data-v-72ac378b]{display:flex}.vertical[data-v-72ac378b]{flex-direction:column}.disabled[data-v-72ac378b]{background:grey}", map: undefined, media: undefined }); }; /* scoped */ var __vue_scope_id__$1 = "data-v-72ac378b"; /* module identifier */ var __vue_module_identifier__$1 = "data-v-72ac378b"; /* functional template */ var __vue_is_functional_template__$1 = false; /* style inject shadow dom */ var __vue_component__$1 = /*#__PURE__*/normalizeComponent({ render: __vue_render__$1, staticRenderFns: __vue_staticRenderFns__$1 }, __vue_inject_styles__$1, __vue_script__$1, __vue_scope_id__$1, __vue_is_functional_template__$1, __vue_module_identifier__$1, false, undefined, createInjectorSSR, undefined);var components=/*#__PURE__*/Object.freeze({__proto__:null,FocusableList: __vue_component__$1,Grid: __vue_component__});var install = function installVueSpatialNavigation(Vue) { if (install.installed) return; install.installed = true; Object.entries(components).forEach(function (_ref) { var _ref2 = _slicedToArray(_ref, 2), componentName = _ref2[0], component = _ref2[1]; console.error('componentName', componentName); Vue.component(componentName, component); }); }; // Create module definition for Vue.use() var plugin = { install: install }; // To auto-install on non-es builds, when vue is found // eslint-disable-next-line no-redeclare /* global window, global */ { var GlobalVue = null; if (typeof window !== 'undefined') { GlobalVue = window.Vue; } else if (typeof global !== 'undefined') { GlobalVue = global.Vue; } if (GlobalVue) { GlobalVue.use(plugin); } } // Default export is library as a whole, registered via Vue.use() exports.FocusableList=__vue_component__$1;exports.Grid=__vue_component__;exports.default=plugin;