├── .gitignore ├── LICENSE ├── README.md ├── dist ├── .gitkeep ├── aframe-selectable-component.js └── aframe-selectable-component.min.js ├── examples ├── basic │ └── index.html ├── index.html └── main.js ├── index.js ├── package.json ├── scripts └── unboil.js └── tests ├── __init.test.js ├── helpers.js ├── index.test.js └── karma.conf.js /.gitignore: -------------------------------------------------------------------------------- 1 | .sw[ponm] 2 | examples/build.js 3 | examples/node_modules/ 4 | gh-pages 5 | node_modules/ 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kevin Ngo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Selectable component 2 | 3 | Usage: 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | 21 | Now everything inside the first entity will be selectable. Focus the ring on it, and click, to put a bounding box around the selected entity. Fires a `selected` event on the element with the selectable attribute. 22 | 23 | document.querySelector('a-entity[selectable]').addEventListener('selected', (e) => { 24 | console.log(e.selected); 25 | }); 26 | 27 | Only one thing at a time can be selected, but I'm open to pull requests to allow multi-selection. 28 | 29 | Click an element again to toggle selection. 30 | 31 | Click anywhere else in the scene to remove the selection. -------------------------------------------------------------------------------- /dist/.gitkeep: -------------------------------------------------------------------------------- 1 | `npm run dist` to generate browser files. 2 | -------------------------------------------------------------------------------- /dist/aframe-selectable-component.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) 10 | /******/ return installedModules[moduleId].exports; 11 | 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ exports: {}, 15 | /******/ id: moduleId, 16 | /******/ loaded: false 17 | /******/ }; 18 | 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | 22 | /******/ // Flag the module as loaded 23 | /******/ module.loaded = true; 24 | 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | 29 | 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | 36 | /******/ // __webpack_public_path__ 37 | /******/ __webpack_require__.p = ""; 38 | 39 | /******/ // Load entry module and return exports 40 | /******/ return __webpack_require__(0); 41 | /******/ }) 42 | /************************************************************************/ 43 | /******/ ([ 44 | /* 0 */ 45 | /***/ function(module, exports) { 46 | 47 | /* globals AFRAME, Event, THREE */ 48 | 49 | if (typeof AFRAME === 'undefined') { 50 | throw new Error('Component attempted to register before AFRAME was available.'); 51 | } 52 | 53 | /** 54 | * Selected component for A-Frame. 55 | */ 56 | AFRAME.registerComponent('selectable', { 57 | schema: { }, 58 | 59 | /** 60 | * Called once when component is attached. Generally for initial setup. 61 | */ 62 | init: function () { 63 | this.selected = null; 64 | }, 65 | 66 | getScene: function () { 67 | var result = this.el; 68 | 69 | while (result.parentNode && result.nodeName !== 'a-scene') { 70 | result = result.parentNode; 71 | } 72 | 73 | return result; 74 | }, 75 | 76 | /** 77 | * Called when component is attached and when component data changes. 78 | * Generally modifies the entity based on the data. 79 | */ 80 | update: function (oldData) { 81 | var self = this; 82 | 83 | var preventDefault = false; 84 | 85 | this.getScene().addEventListener('click', function (e) { 86 | if (preventDefault) { 87 | return; 88 | } 89 | 90 | self.select(null); 91 | }); 92 | 93 | this.el.addEventListener('click', function (e) { 94 | if (e.target === self.el) { 95 | return; 96 | } 97 | 98 | self.select(e.target); 99 | 100 | preventDefault = true; 101 | 102 | // fixme: gross 103 | setTimeout(function () { 104 | preventDefault = false; 105 | }, 5); 106 | }); 107 | }, 108 | 109 | select: function (entity) { 110 | var obj = this.el.object3D; 111 | 112 | this.selected = entity; 113 | 114 | var event = new Event('selected'); 115 | event.selected = this.selected; 116 | this.el.dispatchEvent(event); 117 | 118 | if (this.bbox) { 119 | obj.remove(this.bbox); 120 | delete this.bbox; 121 | } 122 | 123 | if (this.selected) { 124 | this.bbox = new THREE.BoundingBoxHelper(this.selected.object3D, '#ff7700'); 125 | this.bbox.update(); 126 | obj.add(this.bbox); 127 | } 128 | }, 129 | 130 | /** 131 | * Called when a component is removed (e.g., via removeAttribute). 132 | * Generally undoes all modifications to the entity. 133 | */ 134 | remove: function () { 135 | if (this.bbox) { 136 | this.el.object3D.remove(this.bbox); 137 | } 138 | 139 | // Unassign 140 | this.selected = null; 141 | this.bbox = null; 142 | }, 143 | 144 | /** 145 | * Called on each scene tick. 146 | */ 147 | tick: function (t) { 148 | if (this.bbox) { 149 | this.bbox.update(); 150 | } 151 | }, 152 | 153 | /** 154 | * Called when entity pauses. 155 | * Use to stop or remove any dynamic or background behavior such as events. 156 | */ 157 | pause: function () { }, 158 | 159 | /** 160 | * Called when entity resumes. 161 | * Use to continue or add any dynamic or background behavior such as events. 162 | */ 163 | play: function () { } 164 | }); 165 | 166 | 167 | /***/ } 168 | /******/ ]); -------------------------------------------------------------------------------- /dist/aframe-selectable-component.min.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(i){if(n[i])return n[i].exports;var o=n[i]={exports:{},id:i,loaded:!1};return e[i].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t){if("undefined"==typeof AFRAME)throw new Error("Component attempted to register before AFRAME was available.");AFRAME.registerComponent("selectable",{schema:{},init:function(){this.selected=null},getScene:function(){for(var e=this.el;e.parentNode&&"a-scene"!==e.nodeName;)e=e.parentNode;return e},update:function(e){var t=this,n=!1;this.getScene().addEventListener("click",function(e){n||t.select(null)}),this.el.addEventListener("click",function(e){e.target!==t.el&&(t.select(e.target),n=!0,setTimeout(function(){n=!1},5))})},select:function(e){var t=this.el.object3D;this.selected=e;var n=new Event("selected");n.selected=this.selected,this.el.dispatchEvent(n),this.bbox&&(t.remove(this.bbox),delete this.bbox),this.selected&&(this.bbox=new THREE.BoundingBoxHelper(this.selected.object3D,"#ff7700"),this.bbox.update(),t.add(this.bbox))},remove:function(){this.bbox&&this.el.object3D.remove(this.bbox),this.selected=null,this.bbox=null},tick:function(e){this.bbox&&this.bbox.update()},pause:function(){},play:function(){}})}]); -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A-Frame Selectable Component - Basic 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A-Frame Selected Component 4 | 5 | 22 | 23 | 24 |

A-Frame Selected Component

25 | Basic 26 | 27 |
28 |
29 | Fork me on GitHub 30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/main.js: -------------------------------------------------------------------------------- 1 | require('aframe/src/index.js'); 2 | require('../index.js'); 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* globals AFRAME, Event, THREE */ 2 | 3 | if (typeof AFRAME === 'undefined') { 4 | throw new Error('Component attempted to register before AFRAME was available.'); 5 | } 6 | 7 | /** 8 | * Selected component for A-Frame. 9 | */ 10 | AFRAME.registerComponent('selectable', { 11 | schema: { }, 12 | 13 | /** 14 | * Called once when component is attached. Generally for initial setup. 15 | */ 16 | init: function () { 17 | this.selected = null; 18 | }, 19 | 20 | /** 21 | * Called when component is attached and when component data changes. 22 | * Generally modifies the entity based on the data. 23 | */ 24 | update: function (oldData) { 25 | var self = this; 26 | 27 | this.el.addEventListener('click', function (e) { 28 | if (e.target === self.el) { 29 | self.select(null); 30 | return; 31 | } 32 | 33 | self.select(e.target); 34 | }); 35 | }, 36 | 37 | select: function (entity) { 38 | this.selected = entity; 39 | 40 | var event = new Event('selected'); 41 | event.selected = this.selected; 42 | this.el.dispatchEvent(event); 43 | 44 | var obj = this.el.object3D; 45 | 46 | if (this.bbox) { 47 | obj.remove(this.bbox); 48 | delete this.bbox; 49 | } 50 | 51 | if (this.selected) { 52 | this.bbox = new THREE.BoundingBoxHelper(this.selected.object3D, '#ff7700'); 53 | this.bbox.update(); 54 | obj.add(this.bbox); 55 | } 56 | }, 57 | 58 | /** 59 | * Called when a component is removed (e.g., via removeAttribute). 60 | * Generally undoes all modifications to the entity. 61 | */ 62 | remove: function () { 63 | if (this.bbox) { 64 | this.el.object3D.remove(this.bbox); 65 | } 66 | 67 | // Unassign 68 | this.selected = null; 69 | this.bbox = null; 70 | }, 71 | 72 | /** 73 | * Called on each scene tick. 74 | */ 75 | tick: function (t) { 76 | if (this.bbox) { 77 | this.bbox.update(); 78 | } 79 | }, 80 | 81 | /** 82 | * Called when entity pauses. 83 | * Use to stop or remove any dynamic or background behavior such as events. 84 | */ 85 | pause: function () { }, 86 | 87 | /** 88 | * Called when entity resumes. 89 | * Use to continue or add any dynamic or background behavior such as events. 90 | */ 91 | play: function () { } 92 | }); 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aframe-selectable-component", 3 | "version": "1.4.4", 4 | "description": "Selected component for A-Frame VR.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "browserify examples/main.js -o examples/build.js", 8 | "dev": "budo examples/main.js:build.js --dir examples --port 8000 --live --open", 9 | "dist": "webpack index.js dist/aframe-selectable-component.js && webpack -p index.js dist/aframe-selectable-component.min.js", 10 | "postpublish": "npm run dist", 11 | "preghpages": "npm run build && rm -rf gh-pages && cp -r examples gh-pages", 12 | "ghpages": "npm run preghpages && ghpages -p gh-pages", 13 | "test": "karma start ./tests/karma.conf.js", 14 | "unboil": "node scripts/unboil.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/scenevr/selectable-component.git" 19 | }, 20 | "keywords": [ 21 | "aframe", 22 | "aframe-component", 23 | "layout", 24 | "aframe-vr", 25 | "vr", 26 | "aframe-layout", 27 | "mozvr", 28 | "webvr" 29 | ], 30 | "author": "Ben Nolan ", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/scenevr/selectable-component/issues" 34 | }, 35 | "homepage": "https://github.com/scenevr/selectable-component#readme", 36 | "devDependencies": { 37 | "aframe": "aframevr/aframe#dev", 38 | "browserify": "^12.0.1", 39 | "browserify-css": "^0.8.3", 40 | "budo": "^7.1.0", 41 | "chai": "^3.4.1", 42 | "chai-shallow-deep-equal": "^1.3.0", 43 | "ghpages": "0.0.3", 44 | "inquirer": "^0.12.0", 45 | "karma": "^0.13.15", 46 | "karma-browserify": "^4.4.2", 47 | "karma-chai-shallow-deep-equal": "0.0.4", 48 | "karma-firefox-launcher": "^0.1.7", 49 | "karma-mocha": "^0.2.1", 50 | "karma-mocha-reporter": "^1.1.3", 51 | "karma-sinon-chai": "^1.1.0", 52 | "mocha": "^2.3.4", 53 | "shelljs": "^0.6.0", 54 | "webpack": "^1.12.9" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /scripts/unboil.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | var exec = require('child_process').exec; 3 | var inquirer = require('inquirer'); 4 | 5 | var q1 = { 6 | name: 'shortname', 7 | message: 'What is your component\'s short-name? (e.g., `rick-roll` for aframe-rick-roll-component, ``)', 8 | type: 'input' 9 | }; 10 | 11 | var q2 = { 12 | name: 'longname', 13 | message: 'What is your component\'s long-name? (e.g., `Rick Roll` for A-Frame Rick Roll Component)', 14 | type: 'input' 15 | }; 16 | 17 | var q3 = { 18 | name: 'repo', 19 | message: 'Where is your component on Github? (e.g., yourusername/aframe-rick-roll-component)', 20 | type: 'input' 21 | }; 22 | 23 | var q4 = { 24 | name: 'author', 25 | message: 'Who are you? (e.g., Jane John )', 26 | type: 'input' 27 | }; 28 | 29 | inquirer.prompt([q1, q2, q3, q4], function (ans) { 30 | ls(['index.js', 'package.json', 'README.md']).forEach(function(file) { 31 | sed('-i', 'aframe-example-component', 'aframe-' + ans.shortname + '-component', file); 32 | sed('-i', 'Example Component', ans.longname + ' Component', file); 33 | sed('-i', 'Example component', ans.longname + ' component', file); 34 | sed('-i', "'example'", "'" + ans.shortname + "'", file); 35 | }); 36 | 37 | ls('README.md').forEach(function (file) { 38 | sed('-i', 'example component', ans.longname + ' component', file); 39 | sed('-i', 'example=', ans.shortname + '=', file); 40 | }); 41 | 42 | find('examples').filter(function (file) { return file.match(/\.html/); }).forEach(function (file) { 43 | sed('-i', 'Example Component', ans.longname + ' Component', file); 44 | }); 45 | 46 | ls(['package.json', 'README.md']).forEach(function (file) { 47 | sed('-i', 'aframe-example-component', 'aframe-' + ans.shortname + '-component', file); 48 | sed('-i', 'ngokevin/aframe-component-boilerplate', ans.repo, file); 49 | sed('-i', 'Kevin Ngo ', ans.author, file); 50 | }); 51 | }); 52 | 53 | exec("sed '1,/--trim--/d' README.md | tee README.md"); 54 | -------------------------------------------------------------------------------- /tests/__init.test.js: -------------------------------------------------------------------------------- 1 | /* global sinon, setup, teardown */ 2 | 3 | /** 4 | * __init.test.js is run before every test case. 5 | */ 6 | window.debug = true; 7 | 8 | var AScene = require('aframe-core').AScene; 9 | 10 | beforeEach(function () { 11 | this.sinon = sinon.sandbox.create(); 12 | // Stub to not create a WebGL context since Travis CI runs headless. 13 | this.sinon.stub(AScene.prototype, 'attachedCallback'); 14 | }); 15 | 16 | afterEach(function () { 17 | // Clean up any attached elements. 18 | ['canvas', 'a-assets', 'a-scene'].forEach(function (tagName) { 19 | var els = document.querySelectorAll(tagName); 20 | for (var i = 0; i < els.length; i++) { 21 | els[i].parentNode.removeChild(els[i]); 22 | } 23 | }); 24 | AScene.scene = null; 25 | 26 | this.sinon.restore(); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | /* global suite */ 2 | 3 | /** 4 | * Helper method to create a scene, create an entity, add entity to scene, 5 | * add scene to document. 6 | * 7 | * @returns {object} An `` element. 8 | */ 9 | module.exports.entityFactory = function () { 10 | var scene = document.createElement('a-scene'); 11 | var entity = document.createElement('a-entity'); 12 | scene.appendChild(entity); 13 | document.body.appendChild(scene); 14 | return entity; 15 | }; 16 | 17 | /** 18 | * Creates and attaches a mixin element (and an `` element if necessary). 19 | * 20 | * @param {string} id - ID of mixin. 21 | * @param {object} obj - Map of component names to attribute values. 22 | * @returns {object} An attached `` element. 23 | */ 24 | module.exports.mixinFactory = function (id, obj) { 25 | var mixinEl = document.createElement('a-mixin'); 26 | mixinEl.setAttribute('id', id); 27 | Object.keys(obj).forEach(function (componentName) { 28 | mixinEl.setAttribute(componentName, obj[componentName]); 29 | }); 30 | 31 | var assetsEl = document.querySelector('a-assets'); 32 | if (!assetsEl) { 33 | assetsEl = document.createElement('a-assets'); 34 | document.body.appendChild(assetsEl); 35 | } 36 | assetsEl.appendChild(mixinEl); 37 | 38 | return mixinEl; 39 | }; 40 | 41 | /** 42 | * Test that is only run locally and is skipped on CI. 43 | */ 44 | module.exports.getSkipCISuite = function () { 45 | if (window.__env__.TEST_ENV === 'ci') { 46 | return suite.skip; 47 | } else { 48 | return suite; 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | var Aframe = require('aframe-core'); 2 | var example = require('../index.js').component; 3 | var entityFactory = require('./helpers').entityFactory; 4 | 5 | Aframe.registerComponent('example', example); 6 | 7 | describe('example', function () { 8 | beforeEach(function (done) { 9 | this.el = entityFactory(); 10 | this.el.addEventListener('loaded', function () { 11 | done(); 12 | }); 13 | }); 14 | 15 | describe('example property', function () { 16 | it('is good', function () { 17 | assert.equal(1, 1); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function (config) { 3 | config.set({ 4 | basePath: '../', 5 | browserify: { 6 | paths: ['./'] 7 | }, 8 | browsers: ['firefox_latest'], 9 | customLaunchers: { 10 | firefox_latest: { 11 | base: 'FirefoxNightly', 12 | prefs: { /* empty */ } 13 | } 14 | }, 15 | client: { 16 | captureConsole: true, 17 | mocha: {ui: 'bdd'} 18 | }, 19 | envPreprocessor: [ 20 | 'TEST_ENV' 21 | ], 22 | files: [ 23 | 'tests/**/*.test.js', 24 | ], 25 | frameworks: ['mocha', 'sinon-chai', 'chai-shallow-deep-equal', 'browserify'], 26 | preprocessors: { 27 | 'tests/**/*.js': ['browserify'] 28 | }, 29 | reporters: ['mocha'] 30 | }); 31 | }; 32 | --------------------------------------------------------------------------------