janusweb 1.7.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy.yml +71 -0
- package/Dockerfile +14 -0
- package/README.md +210 -7
- package/janusxr.com/.args +2 -0
- package/janusxr.com/.init.lua +30 -0
- package/media/assets/webui/apps/comms/voip.js +27 -25
- package/media/assets/webui/apps/editor/editor.js +3 -0
- package/media/assets/webui/apps/navigation/navigation.js +1 -1
- package/media/assets/webui/apps/xrfragment/level0-sidecarfile.js +308 -0
- package/media/assets/webui/apps/xrfragment/level2-hyperlink.js +265 -0
- package/media/assets/webui/apps/xrfragment/level7-engine-prefixes.export.js +93 -0
- package/media/assets/webui/apps/xrfragment/level7-engine-prefixes.import.js +229 -0
- package/media/assets/webui/apps/xrfragment/patch/metaquest-fix-flickering.js +15 -0
- package/media/assets/webui/apps/xrfragment/xrfragment.json +14 -0
- package/media/assets/webui/default.json +1 -0
- package/media/images/portal.svg +130 -0
- package/media/index.html +48 -1
- package/package.json +1 -1
- package/scripts/client.js +24 -0
- package/scripts/config.js +12 -1
- package/scripts/janusbase.js +1 -0
- package/scripts/janusparagraph.js +169 -22
- package/scripts/janusxrplayer.js +4 -6
- package/scripts/room.js +59 -36
- package/scripts/translators/paragraph/html-xml-rss.js +47 -0
- package/scripts/translators/peertube.js +89 -0
- package/scripts/translators/xrfragments.js +5 -5
- package/tests/room/paragraph.xml +47 -0
- package/utils/build.sh +1 -0
- package/utils/dev-link.sh +75 -0
- package/utils/release.binary.sh +36 -0
- package/utils/release.docker.sh +11 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: Build and Deploy to Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
# Monitor all branches so we can catch '#deploy' in commit messages
|
|
6
|
+
branches: ["**"]
|
|
7
|
+
tags:
|
|
8
|
+
- "v*.*.*"
|
|
9
|
+
|
|
10
|
+
permissions: write-all
|
|
11
|
+
|
|
12
|
+
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
|
13
|
+
concurrency:
|
|
14
|
+
group: "pages"
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build-and-deploy:
|
|
19
|
+
# Run only on the master branch OR if the commit message contains '#deploy'
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- name: Checkout code
|
|
24
|
+
uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- name: Setup Node.js
|
|
27
|
+
uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version: '20' # Change to your preferred version
|
|
30
|
+
cache: 'npm' # Only works if you have a package-lock.json
|
|
31
|
+
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
# Only runs if package.json exists; avoids errors if it doesn't
|
|
34
|
+
run: |
|
|
35
|
+
if [ -f package.json ]; then
|
|
36
|
+
npm install
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
- name: Run build script
|
|
40
|
+
run: |
|
|
41
|
+
chmod +x ./utils/build.sh
|
|
42
|
+
./utils/build.sh
|
|
43
|
+
./utils/release.binary.sh
|
|
44
|
+
|
|
45
|
+
- name: Create Release and Upload Assets
|
|
46
|
+
uses: softprops/action-gh-release@v3
|
|
47
|
+
with:
|
|
48
|
+
files: |
|
|
49
|
+
build/janusweb-*.tar.gz
|
|
50
|
+
build/*/janusxr.com
|
|
51
|
+
#env: # only needed when creating a release in other repo
|
|
52
|
+
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
53
|
+
|
|
54
|
+
- name: Organize build artifacts
|
|
55
|
+
# Creates a clean 'www' folder and copies your build/sub/folders into it
|
|
56
|
+
run: |
|
|
57
|
+
mkdir -p www
|
|
58
|
+
cp -r build/*/* www/ 2>/dev/null || cp -r build/* www/
|
|
59
|
+
|
|
60
|
+
- name: Setup Pages
|
|
61
|
+
uses: actions/configure-pages@v4
|
|
62
|
+
|
|
63
|
+
- name: Upload artifact
|
|
64
|
+
uses: actions/upload-pages-artifact@v3
|
|
65
|
+
with:
|
|
66
|
+
path: 'www'
|
|
67
|
+
|
|
68
|
+
- name: Deploy to GitHub Pages
|
|
69
|
+
id: deployment
|
|
70
|
+
uses: actions/deploy-pages@v4
|
|
71
|
+
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
FROM busybox:latest
|
|
2
|
+
|
|
3
|
+
ARG JANUSXR_VERSION="1.5.56"
|
|
4
|
+
|
|
5
|
+
WORKDIR /www
|
|
6
|
+
COPY build/$JANUSXR_VERSION/janusxr.com .
|
|
7
|
+
|
|
8
|
+
# assimilate the crossplatform binary into host architecture
|
|
9
|
+
# otherwise docker will complain with 'Exec format error'
|
|
10
|
+
RUN wget https://cosmo.zip/pub/cosmos/bin/assimilate && \
|
|
11
|
+
chmod +x assimilate && \
|
|
12
|
+
./assimilate ./janusxr.com
|
|
13
|
+
|
|
14
|
+
CMD ["/www/janusxr.com"]
|
package/README.md
CHANGED
|
@@ -70,10 +70,188 @@ You can even put your mark-up onto sites like PasteBin or PiratePad. Then just
|
|
|
70
70
|
in our viewer by entering the URL into the navigation bar, and you can link directly to it, share
|
|
71
71
|
on social media, or embed our viewer directly into other webpages, blog posts, or articles.
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
### Single-binary server+client
|
|
74
|
+
|
|
75
|
+
Use our all-in-one **janusxr.com** multi-platform binary file:
|
|
76
|
+
Run a **local-first** viewer locally on your desktop, raspberry pi (ARM64) or server.<br>
|
|
77
|
+
|
|
78
|
+
| executable | platforms |
|
|
79
|
+
|-|-|
|
|
80
|
+
| [janusxr.com](https://jbaicoianu.github.io/janusweb/janusxr.com) | <img src="https://i.imgur.com/v7cYVq1.png"/> |
|
|
81
|
+
|
|
82
|
+
Run `janusxr.com` from anywhere, or put it into a folder with your 3D models or JanusXR rooms:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
$ chmod +x janusxr.com # only for non-windows users
|
|
86
|
+
|
|
87
|
+
$ ./janusxr.com
|
|
88
|
+
I2026-01-19T15:41:10 (srvr) listen http://127.0.0.1:8080
|
|
89
|
+
I2026-01-19T15:41:10 (srvr) listen http://192.168.1.168:8080
|
|
90
|
+
Launching browser..
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A browser will popup, and connect to `https://janusxr.org` via Janusweb.<br>
|
|
94
|
+
Webbrowsers on the same network can access your node `https://192.168.1.168:8080`
|
|
95
|
+
|
|
96
|
+
> Optionally, you can expose this janusweb-node to the internet. Check [here](https://redbean.dev) for more commandline options.
|
|
97
|
+
</details>
|
|
98
|
+
|
|
99
|
+
<details>
|
|
100
|
+
<summary><b>Usecase:</b> 3D file viewer</summary>
|
|
101
|
+
<br>
|
|
102
|
+
Make sure `janusxr.com` is in a folder with your 3D models
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
$ ls
|
|
106
|
+
janusxr.com
|
|
107
|
+
world1.glb
|
|
108
|
+
world2.glb
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Then make sure you rename `janusxr.com` to whatever you want to set as homepage:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
$ mv janusxr.com world1.com
|
|
115
|
+
$ ./world1.com
|
|
116
|
+
I2026-01-19T15:41:10 (srvr) listen http://127.0.0.1:8080
|
|
117
|
+
I2026-01-19T15:41:10 (srvr) listen http://192.168.1.168:8080
|
|
118
|
+
✅ detected /world1.glb (setting as spatial home)
|
|
119
|
+
Launching browser..
|
|
120
|
+
```
|
|
121
|
+
</details>
|
|
122
|
+
|
|
123
|
+
<details>
|
|
124
|
+
<summary><b>Usecase:</b> all-in-one JanusXR runtime</summary>
|
|
125
|
+
<br>
|
|
126
|
+
You can bundle all your XR experiences with the server/client binary.<br>
|
|
127
|
+
Just Make sure `janusxr.com` is in a folder with your JanusXR rooms or 3D models
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
$ ls
|
|
131
|
+
janusxr.com
|
|
132
|
+
world1.html
|
|
133
|
+
world2.html
|
|
134
|
+
foo.glb
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Then make sure you rename `janusxr.com` to whatever you want to set as homepage:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
$ zip -r janusxr.com * # add janus files to janusxr.com
|
|
141
|
+
$ mv janusxr.com world1.com # hint homepage room (world1.html) sidecarfile
|
|
142
|
+
$ ./world1.com
|
|
143
|
+
✅ detected /world1.html (setting as spatial home)
|
|
144
|
+
I2026-01-19T15:41:10 (srvr) listen http://127.0.0.1:8080
|
|
145
|
+
I2026-01-19T15:41:10 (srvr) listen http://192.168.1.168:8080
|
|
146
|
+
Launching browser..
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Profit! You can now send `world1.com` to a friend, and his browser will launch straight into world1.html
|
|
150
|
+
|
|
151
|
+
> NOTE: you can also point directly to a 3D model (`foo.com` would load `foo.glb`) or PDF-file etc!
|
|
152
|
+
</details>
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
<details>
|
|
156
|
+
<summary><b>Usecase:</b> run as a docker</summary>
|
|
157
|
+
<br>
|
|
158
|
+
You can run our docker-image as following:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
$ docker run -p 8080:8080 codeberg.org/coderofsalvation/janusxr:1.5.56
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Profit! You can now point your browser to `http://localhost:8080` or configure your reverseproxy for SSL certs/domain etc.<br>
|
|
165
|
+
Or optionally, serve your own XR experiences with it:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
$ ls
|
|
169
|
+
myworld1.xml myworld2.html myworld3.glb
|
|
170
|
+
$
|
|
171
|
+
$ docker run -p 8081:8080 -v $(pwd):/data codeberg.org/coderofsalvation/janusxr:1.5.56 /www/janusxr.com -D /data
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Profit! You can now use URLs like `http://localhost:8080/#janus.url=https://localhost:8080/myworld1.xml` e.g.<br>
|
|
175
|
+
Btw. you can build the docker image yourself too:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
$ utils/release.binary.sh # build the janusxr.com binary
|
|
179
|
+
$ docker build -t janusxr.com .
|
|
180
|
+
$ docker run -p 8080:8080 janusxr
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
</summary>
|
|
184
|
+
</details>
|
|
185
|
+
|
|
186
|
+
<details>
|
|
187
|
+
<summary><b>Usecase:</b> serve all-in-one-binary in different docker/OCI container</summary>
|
|
188
|
+
<br>
|
|
189
|
+
|
|
190
|
+
Make sure to run you're in a folder with your JanusXR rooms or 3D models, and `janusxr.com`
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
$ ls
|
|
194
|
+
janusxr.com
|
|
195
|
+
|
|
196
|
+
$ unzip janusxr.com # extract the viewer files
|
|
197
|
+
|
|
198
|
+
$ ls
|
|
199
|
+
janusxr.com
|
|
200
|
+
world1.html
|
|
201
|
+
world2.html
|
|
202
|
+
foo.glb
|
|
203
|
+
|
|
204
|
+
$ docker run -d -v ./:/www -p 8080:8080 --name janusweb busybox:latest /bin/httpd -f -h /www -p 8080
|
|
205
|
+
e7928798379e872983b7ec9b89237ebc
|
|
206
|
+
|
|
207
|
+
$ docker ps
|
|
208
|
+
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
|
209
|
+
6711ed9739f3 docker.io/library/busybox:latest /bin/httpd -f -h ... 16 seconds ago Up 16 seconds 0.0.0.0:8080->8080/tcp janusweb
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Profit! You can now point your browser to `http://127.0.0.1:8080` or configure your reverseproxy for SSL certs/domain etc.
|
|
213
|
+
|
|
214
|
+
</details>
|
|
215
|
+
|
|
216
|
+
<details>
|
|
217
|
+
<summary><b>Usecase:</b> deploy janusweb as github/gitlab/codeforge webpage</summary>
|
|
218
|
+
|
|
219
|
+
<br>
|
|
220
|
+
*TODO* provide action-files
|
|
221
|
+
</details>
|
|
222
|
+
|
|
223
|
+
<details>
|
|
224
|
+
<summary><b>Usecase:</b> get the latest build files</summary>
|
|
225
|
+
|
|
226
|
+
<br>
|
|
227
|
+
Sometimes, developers just want the latest janusweb **files**.<br>
|
|
228
|
+
Good news, the executable is also a zip-file.<br>
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
$ ls
|
|
232
|
+
janusxr.com
|
|
233
|
+
$ unzip janusxr.com
|
|
234
|
+
$ ls
|
|
235
|
+
index.html
|
|
236
|
+
janusweb.js
|
|
237
|
+
(...)
|
|
238
|
+
|
|
239
|
+
$ janusxr.com
|
|
240
|
+
I2026-01-19T15:41:10 (srvr) listen http://127.0.0.1:8080
|
|
241
|
+
I2026-01-19T15:41:10 (srvr) listen http://192.168.1.168:8080
|
|
242
|
+
Launching browser..
|
|
243
|
+
|
|
244
|
+
( the webserver will prioritize the unzipped files )
|
|
245
|
+
|
|
246
|
+
$ zip janusxr.com index.html # you can even update the binary
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
> Profit!
|
|
250
|
+
|
|
251
|
+
</details>
|
|
74
252
|
|
|
75
253
|
### Pull our scripts into your page
|
|
76
|
-
Using the above method, all of your links would go through
|
|
254
|
+
Using the above method, all of your links would go through community CORS-proxies. If you'd prefer to
|
|
77
255
|
link to your own servers, you can pull our JS into your page and use JanusWeb as a scriptable
|
|
78
256
|
client via its API. This looks something like this:
|
|
79
257
|
|
|
@@ -97,13 +275,38 @@ client via its API. This looks something like this:
|
|
|
97
275
|
```
|
|
98
276
|
|
|
99
277
|
The `elation.janusweb.init()` function can take a number of arguments, and returns a promise which
|
|
100
|
-
receives an instance of the client. This client reference can be controlled via its API
|
|
101
|
-
|
|
278
|
+
receives an instance of the client. This client reference can be controlled via its API:
|
|
279
|
+
|
|
280
|
+
```html
|
|
281
|
+
<html>
|
|
282
|
+
<head>
|
|
283
|
+
<title>My JanusVR Room</title>
|
|
284
|
+
</head>
|
|
285
|
+
<body>
|
|
286
|
+
<script src="https://web.janusvr.com/janusweb.js"></script>
|
|
287
|
+
<script>
|
|
288
|
+
elation.janusweb.init({url: document.location.href })
|
|
289
|
+
.then( (client)=> console.log("rendering this page's virtual twin") )
|
|
290
|
+
</script>
|
|
291
|
+
|
|
292
|
+
<!--
|
|
293
|
+
<fireboxroom>
|
|
294
|
+
<room use_local_asset="room1">
|
|
295
|
+
<object id="cube" pos="0 1 5" />
|
|
296
|
+
<text col="1 0 0" pos="0 2 4">my first room</text>
|
|
297
|
+
</room>
|
|
298
|
+
</fireboxroom>
|
|
299
|
+
-->
|
|
300
|
+
|
|
301
|
+
</body>
|
|
302
|
+
</html>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
> **NOTE**: you can also update the `janusxr.com` binary to your likings: (`unzip janusxr.com index.html && vi index.html && zip janusxr.com index.html` see 'get the latest build files' above)
|
|
102
306
|
|
|
103
|
-
See
|
|
307
|
+
See the sections on **Arguments** and **Scripting** below.
|
|
104
308
|
|
|
105
|
-
|
|
106
|
-
(TODO - we will start shipping zip builds of JanusWeb once we release v1.0)
|
|
309
|
+
> See also **Using a specific version of JanusWeb** below.
|
|
107
310
|
|
|
108
311
|
### Install from NPM
|
|
109
312
|
(TODO - we will start shipping official NPM packages of JanusWeb once we release v1.0)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
HidePath("/usr/share/zoneinfo/")
|
|
2
|
+
HidePath("/usr/share/ssl/")
|
|
3
|
+
homepage = false
|
|
4
|
+
homepath = false
|
|
5
|
+
home = arg[-1]:gsub(".*/","")
|
|
6
|
+
:gsub(".com","")
|
|
7
|
+
ext = { "txt", "xml", "html", "pdf", "mp3", "yaml", "json", "xlsx", "png", "jpg", "glb", "gltf", "dae", "obj"}
|
|
8
|
+
for k,v in pairs(ext) do
|
|
9
|
+
file = string.format("%s.%s", home, v)
|
|
10
|
+
if( path.exists( file ) ) then homepath = "/" .. file; end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
if( homepath ) then
|
|
14
|
+
print("✅ detected " .. homepath .. " (setting as spatial home)")
|
|
15
|
+
homepage = Slurp("/zip/index.html"):gsub("<janus%-viewer .-</janus%-viewer>","<janus-viewer src=\"" .. homepath .. "\"></janus-viewer>")
|
|
16
|
+
|
|
17
|
+
function OnHttpRequest()
|
|
18
|
+
path = GetPath()
|
|
19
|
+
if( homepage and path == "/" ) then
|
|
20
|
+
SetStatus(200)
|
|
21
|
+
SetHeader("Content-Type", "text/html; charset=utf-8")
|
|
22
|
+
SetHeader("Access-Control-Allow-Origin", "*")
|
|
23
|
+
Write(homepage)
|
|
24
|
+
else
|
|
25
|
+
Route()
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
LaunchBrowser()
|
|
30
|
+
|
|
@@ -681,35 +681,37 @@ console.log('[voip-remoteuser] got a remote voip user', data);
|
|
|
681
681
|
this.audio = audio;
|
|
682
682
|
this.appendChild(audio);
|
|
683
683
|
|
|
684
|
-
|
|
685
|
-
.
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
684
|
+
room.getAudioNodes().then(() => {
|
|
685
|
+
audio.play()
|
|
686
|
+
.then(() => {
|
|
687
|
+
console.log('[voip-remoteuser] remote user audio playing', audio, this);
|
|
688
|
+
this.hasaudio = true;
|
|
689
|
+
});
|
|
689
690
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
691
|
+
let listener = player.engine.systems.sound.getRealListener();
|
|
692
|
+
let context = listener.context;
|
|
693
|
+
let analyser = context.createAnalyser();
|
|
694
|
+
let source = context.createMediaStreamSource(audio.srcObject);
|
|
695
|
+
source.connect(analyser);
|
|
695
696
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
697
|
+
analyser.fftSize = 32;
|
|
698
|
+
this.analyser = analyser;
|
|
699
|
+
this.audiobuffer = new Uint8Array(analyser.frequencyBinCount);
|
|
699
700
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
this.updateVolumeInterval = setInterval(() => {
|
|
705
|
-
analyser.getByteFrequencyData(this.audiobuffer);
|
|
706
|
-
let sum = 0;
|
|
707
|
-
for (let i = 0; i < this.audiobuffer.length; i++) {
|
|
708
|
-
sum += this.audiobuffer[i];
|
|
701
|
+
if (this.updateVolumeInterval) {
|
|
702
|
+
clearInterval(this.updateVolumeInterval);
|
|
703
|
+
this.updateVolumeInterval = false;
|
|
709
704
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
705
|
+
this.updateVolumeInterval = setInterval(() => {
|
|
706
|
+
analyser.getByteFrequencyData(this.audiobuffer);
|
|
707
|
+
let sum = 0;
|
|
708
|
+
for (let i = 0; i < this.audiobuffer.length; i++) {
|
|
709
|
+
sum += this.audiobuffer[i];
|
|
710
|
+
}
|
|
711
|
+
let avg = sum / this.audiobuffer.length;
|
|
712
|
+
this.updateVolume(avg / 100);
|
|
713
|
+
}, 20);
|
|
714
|
+
});
|
|
713
715
|
}
|
|
714
716
|
|
|
715
717
|
let remoteuser = janus.network.remoteplayers[this.id];
|
|
@@ -325,7 +325,10 @@ console.log('set translation snap', ev.data, ev);
|
|
|
325
325
|
room._target.objects['3d'].traverse(n => {
|
|
326
326
|
userDatas.set(n, n.userData);
|
|
327
327
|
n.userData = {};
|
|
328
|
+
elation.events.fire({element: this, type: 'webui_editor_export_node', data:{n,userData: userDatas.get(n)} })
|
|
328
329
|
});
|
|
330
|
+
elation.events.fire({element: this, type: 'webui_editor_export', data:{ scene: room._target.objects['3d']} })
|
|
331
|
+
|
|
329
332
|
exporter.parse(room._target.objects['3d'], (data) => {
|
|
330
333
|
// Restore userData objects
|
|
331
334
|
room._target.objects['3d'].traverse(n => {
|
|
@@ -391,7 +391,7 @@ elation.elements.define('janus.ui.urlbar', class extends elation.elements.ui.pan
|
|
|
391
391
|
handleAccept(ev) {
|
|
392
392
|
var url = this.input.value;
|
|
393
393
|
if (url.length > 0) {
|
|
394
|
-
if (url.indexOf(':') == -1) {
|
|
394
|
+
if (url.indexOf(':') == -1 && !url.match(/^\.\//) ) {
|
|
395
395
|
url = 'http://' + url;
|
|
396
396
|
}
|
|
397
397
|
if (ev.altKey) {
|