├── dist
├── .gitkeep
├── aframe-selectable-component.min.js
└── aframe-selectable-component.js
├── examples
├── main.js
├── index.html
└── basic
│ └── index.html
├── .gitignore
├── tests
├── index.test.js
├── karma.conf.js
├── __init.test.js
└── helpers.js
├── LICENSE
├── README.md
├── package.json
├── scripts
└── unboil.js
└── index.js
/dist/.gitkeep:
--------------------------------------------------------------------------------
1 | `npm run dist` to generate browser files.
2 |
--------------------------------------------------------------------------------
/examples/main.js:
--------------------------------------------------------------------------------
1 | require('aframe/src/index.js');
2 | require('../index.js');
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .sw[ponm]
2 | examples/build.js
3 | examples/node_modules/
4 | gh-pages
5 | node_modules/
6 | npm-debug.log
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | A-Frame Selected Component
4 |
5 |
22 |
23 |
24 | A-Frame Selected Component
25 | Basic
26 |
27 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/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/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(){}})}]);
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | /******/ ]);
--------------------------------------------------------------------------------