Compare commits
No commits in common. 'master' and 'v0.09-beta' have entirely different histories.
master
...
v0.09-beta
@ -1,19 +0,0 @@
|
||||
--- library.js.orig 2020-05-18 17:14:18.682328912 +0200
|
||||
+++ library.js 2020-05-18 17:14:48.366639562 +0200
|
||||
@@ -271,7 +271,7 @@
|
||||
// pid_t fork(void);
|
||||
// http://pubs.opengroup.org/onlinepubs/000095399/functions/fork.html
|
||||
// We don't support multiple processes.
|
||||
- setErrNo({{{ cDefine('EAGAIN') }}});
|
||||
+ setErrNo({{{ cDefine('ENOTSUP') }}});
|
||||
return -1;
|
||||
},
|
||||
vfork: 'fork',
|
||||
@@ -696,7 +696,7 @@
|
||||
// http://pubs.opengroup.org/onlinepubs/000095399/functions/system.html
|
||||
// Can't call external programs.
|
||||
if (!command) return 0; // no shell available
|
||||
- setErrNo({{{ cDefine('EAGAIN') }}});
|
||||
+ setErrNo({{{ cDefine('ENOTSUP') }}});
|
||||
return -1;
|
||||
},
|
||||
@ -1,5 +0,0 @@
|
||||
/database.db
|
||||
/web/webperl.js
|
||||
/web/emperl.*
|
||||
/gui_basic
|
||||
/gui_basic.exe
|
||||
@ -1,50 +0,0 @@
|
||||
|
||||
WebPerl Basic GUI Example
|
||||
=========================
|
||||
|
||||
This is a demo of a very basic GUI using WebPerl. It consists of a
|
||||
local web server, which includes code to access an SQLite database,
|
||||
and this web server also serves up WebPerl code to a browser, where
|
||||
the GUI is implemented as HTML with Perl.
|
||||
|
||||
To get this to work, you will need to copy the `webperl.js` and the
|
||||
three `emperl.*` files from the main `web` directory to the `web`
|
||||
subdirectory in this project.
|
||||
|
||||
Note that this should not be considered production-ready, as there
|
||||
are several key features missing, such as HTTPS or access control.
|
||||
|
||||
Also, a limitation is that the server does not know when the browser
|
||||
window is closed, so it must be stopped manually.
|
||||
|
||||
You can pack this application into a single executable using:
|
||||
|
||||
DOING_PAR_PACKER=1 pp -o gui_basic -z 9 -x -a gui_basic_app.psgi -a web gui_basic.pl
|
||||
|
||||
Note: I'm not yet sure why, but sometimes this fails with errors such
|
||||
as *"error extracting info from -c/-x file"*, in that case just try
|
||||
the above command again.
|
||||
|
||||
|
||||
Author, Copyright, and License
|
||||
==============================
|
||||
|
||||
**WebPerl - <http://webperl.zero-g.net>**
|
||||
|
||||
Copyright (c) 2019 Hauke Daempfling (haukex@zero-g.net)
|
||||
at the Leibniz Institute of Freshwater Ecology and Inland Fisheries (IGB),
|
||||
Berlin, Germany, <http://www.igb-berlin.de>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the same terms as Perl 5 itself: either the GNU General Public
|
||||
License as published by the Free Software Foundation (either version 1,
|
||||
or, at your option, any later version), or the "Artistic License" which
|
||||
comes with Perl 5.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
**WITHOUT ANY WARRANTY**; without even the implied warranty of
|
||||
**MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE**.
|
||||
See the licenses for details.
|
||||
|
||||
You should have received a copy of the licenses along with this program.
|
||||
If not, see <http://perldoc.perl.org/index-licence.html>.
|
||||
@ -1,50 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
use warnings;
|
||||
use 5.018;
|
||||
use FindBin;
|
||||
use File::Spec::Functions qw/catdir/;
|
||||
use Plack::Runner ();
|
||||
use Starman ();
|
||||
use Browser::Open qw/open_browser/;
|
||||
|
||||
# This just serves up gui_basic_app.psgi in the Starman web server.
|
||||
# You can also say "plackup gui_basic_app.psgi" instead.
|
||||
|
||||
BEGIN {
|
||||
my $dir = $ENV{PAR_TEMP} ? catdir($ENV{PAR_TEMP},'inc') : $FindBin::Bin;
|
||||
chdir $dir or die "chdir $dir: $!";
|
||||
}
|
||||
|
||||
my $SERV_PORT = 5000;
|
||||
my $THE_APP = 'gui_basic_app.psgi';
|
||||
|
||||
# AFAICT, both Plack::Runner->new(@args) and ->parse_options(@argv) set
|
||||
# options, and these options are shared between "Starman::Server"
|
||||
# (documented in "starman") and "Plack::Runner" (documented in "plackup").
|
||||
my @args = (
|
||||
server => 'Starman', loader => 'Delayed', env => 'development',
|
||||
version_cb => sub { print "Starman $Starman::VERSION\n" } );
|
||||
my @argv = ( '--listen', "localhost:$SERV_PORT", $THE_APP );
|
||||
my $runner = Plack::Runner->new(@args);
|
||||
$runner->parse_options(@argv);
|
||||
$runner->set_options(argv => \@argv);
|
||||
die "loader shouldn't be Restarter" if $runner->{loader} eq 'Restarter';
|
||||
|
||||
if ($ENV{DOING_PAR_PACKER}) {
|
||||
require Plack::Util;
|
||||
Plack::Util::load_psgi($THE_APP); # for dependency resolution
|
||||
# arrange to have the server shut down in a few moments
|
||||
my $procpid = $$;
|
||||
my $pid = fork();
|
||||
if (!defined $pid) { die "fork failed" }
|
||||
elsif ($pid==0) { sleep 5; kill 'INT', $procpid; exit; } # child
|
||||
print "====> Please wait a few seconds...\n";
|
||||
}
|
||||
else {
|
||||
# There's a small chance here that the browser could open before the server
|
||||
# starts up. In that case, a reload of the browser window is needed.
|
||||
print "Attempting to open in browser: http://localhost:$SERV_PORT/\n";
|
||||
open_browser("http://localhost:$SERV_PORT/");
|
||||
}
|
||||
|
||||
$runner->run;
|
||||
@ -1,67 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
use warnings;
|
||||
use 5.018;
|
||||
use Plack::MIME;
|
||||
use Plack::Builder qw/builder enable mount/;
|
||||
use Plack::Request ();
|
||||
use Plack::Response (); # declare compile-time dependency
|
||||
use Cpanel::JSON::XS qw/decode_json encode_json/;
|
||||
use DBI ();
|
||||
use DBD::SQLite (); # declare compile-time dependency
|
||||
use HTML::Tiny ();
|
||||
|
||||
# This is the server-side code.
|
||||
|
||||
# note we rely on gui_basic.pl to set the working directory correctly
|
||||
my $SERV_ROOT = 'web';
|
||||
my $DB_FILE = 'database.db';
|
||||
|
||||
my $dbh = DBI->connect("DBI:SQLite:dbname=$DB_FILE",
|
||||
undef, undef, { RaiseError=>1, AutoCommit=>1 });
|
||||
|
||||
$dbh->do(q{ CREATE TABLE IF NOT EXISTS FooBar (
|
||||
foo VARCHAR(255), bar VARCHAR(255) ) });
|
||||
|
||||
# This sends HTML to the browser, but we could also send JSON
|
||||
# and build the HTML table dynamically in the browser.
|
||||
my $app_select = sub {
|
||||
state $html = HTML::Tiny->new;
|
||||
state $sth_select = $dbh->prepare(q{ SELECT rowid,foo,bar FROM FooBar });
|
||||
$sth_select->execute;
|
||||
my $data = $sth_select->fetchall_arrayref;
|
||||
my $out = $html->table(
|
||||
[ \'tr',
|
||||
[ \'th', 'rowid', 'foo', 'bar' ],
|
||||
map { [ \'td', @$_ ] } @$data
|
||||
] );
|
||||
return [ 200, [ "Content-Type"=>"text/html" ], [ $out ] ];
|
||||
};
|
||||
|
||||
# This is an example of one way to communicate with JSON.
|
||||
my $app_insert = sub {
|
||||
my $req = Plack::Request->new(shift);
|
||||
state $sth_insert = $dbh->prepare(q{ INSERT INTO FooBar (foo,bar) VALUES (?,?) });
|
||||
my $rv = eval { # catch errors and return as 500 Server Error
|
||||
my $content = decode_json( $req->content );
|
||||
$sth_insert->execute($content->{foo}, $content->{bar});
|
||||
{ ok=>1 }; # return value from eval, sent to client as JSON
|
||||
}; my $e = $@||'unknown error';
|
||||
my $res = $req->new_response($rv ? 200 : 500);
|
||||
$res->content_type($rv ? 'application/json' : 'text/plain');
|
||||
$res->body($rv ? encode_json($rv) : 'Server Error: '.$e);
|
||||
return $res->finalize;
|
||||
};
|
||||
|
||||
Plack::MIME->add_type(".js" => "application/javascript");
|
||||
Plack::MIME->add_type(".data" => "application/octet-stream");
|
||||
Plack::MIME->add_type(".mem" => "application/octet-stream");
|
||||
Plack::MIME->add_type(".wasm" => "application/wasm");
|
||||
|
||||
builder {
|
||||
enable 'SimpleLogger';
|
||||
enable 'Static',
|
||||
path => sub { s#\A/\z#/index.html#; /\.(?:html?|js|css|data|mem|wasm|pl)\z/i },
|
||||
root => $SERV_ROOT;
|
||||
mount '/select' => $app_select;
|
||||
mount '/insert' => $app_insert;
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>WebPerl GUI Demo</title>
|
||||
<script src="webperl.js"></script>
|
||||
<script type="text/perl" src="web.pl"></script>
|
||||
</head>
|
||||
<body style="font-family:sans-serif;">
|
||||
<h1>WebPerl GUI Demo</h1>
|
||||
|
||||
<div id="datatable"><i>No data loaded yet...</i></div>
|
||||
<div><button id="reload_data">Reload Data</button></div>
|
||||
|
||||
<div style="margin-top:1em">
|
||||
<div>
|
||||
<label for="input_foo">foo</label>
|
||||
<input type="text" id="input_foo">
|
||||
</div>
|
||||
<div>
|
||||
<label for="input_bar">bar</label>
|
||||
<input type="text" id="input_bar">
|
||||
</div>
|
||||
<div>
|
||||
<button id="do_insert">Insert Data</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Powered by <a href="http://webperl.zero-g.net" target="_blank">WebPerl</a> (beta)</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -1,69 +0,0 @@
|
||||
#!perl
|
||||
use warnings;
|
||||
use 5.028;
|
||||
use WebPerl qw/js js_new sub1 encode_json/;
|
||||
|
||||
# This is the code that WebPerl runs in the browser. It is loaded by index.html.
|
||||
|
||||
sub do_xhr {
|
||||
my %args = @_;
|
||||
die "must specify a url" unless $args{url};
|
||||
$args{fail} ||= sub { js('window')->alert(shift) };
|
||||
my $xhr = js_new('XMLHttpRequest');
|
||||
$xhr->addEventListener("error", sub1 {
|
||||
$args{fail}->("XHR Error on $args{url}: ".(shift->{textContent}||"unknown"));
|
||||
return;
|
||||
});
|
||||
$xhr->addEventListener("load", sub1 {
|
||||
if ($xhr->{status}==200) {
|
||||
$args{done}->($xhr->{response}) if $args{done};
|
||||
}
|
||||
else {
|
||||
$args{fail}->("XHR Error on $args{url}: ".$xhr->{status}." ".$xhr->{statusText});
|
||||
}
|
||||
return;
|
||||
});
|
||||
$xhr->addEventListener("loadend", sub1 {
|
||||
$args{always}->() if $args{always};
|
||||
return;
|
||||
});
|
||||
# when given data, default to POST (JSON), otherwise GET
|
||||
if ($args{data}) {
|
||||
$xhr->open($args{method}||'POST', $args{url});
|
||||
$xhr->setRequestHeader('Content-Type', 'application/json');
|
||||
$xhr->send(encode_json($args{data}));
|
||||
}
|
||||
else {
|
||||
$xhr->open($args{method}||'GET', $args{url});
|
||||
$xhr->send();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
my $document = js('document');
|
||||
|
||||
my $btn_reload = $document->getElementById('reload_data');
|
||||
sub do_reload {
|
||||
state $dtbl = $document->getElementById('datatable');
|
||||
$btn_reload->{disabled} = 1;
|
||||
do_xhr(url => 'select',
|
||||
done => sub { $dtbl->{innerHTML} = shift; },
|
||||
always => sub { $btn_reload->{disabled} = 0; } );
|
||||
return;
|
||||
}
|
||||
$btn_reload->addEventListener("click", \&do_reload);
|
||||
|
||||
my $btn_insert = $document->getElementById('do_insert');
|
||||
sub do_insert {
|
||||
state $txt_foo = $document->getElementById('input_foo');
|
||||
state $txt_bar = $document->getElementById('input_bar');
|
||||
$btn_insert->{disabled} = 1;
|
||||
do_xhr(url => 'insert',
|
||||
data => { foo=>$txt_foo->{value}, bar=>$txt_bar->{value} },
|
||||
always => sub { $btn_insert->{disabled} = 0; do_reload; } );
|
||||
return;
|
||||
}
|
||||
$btn_insert->addEventListener("click", \&do_insert);
|
||||
|
||||
do_reload; # initial load
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
/public/webperl.js
|
||||
/public/emperl.*
|
||||
/gui_sweet
|
||||
/gui_sweet.exe
|
||||
@ -1,44 +0,0 @@
|
||||
|
||||
WebPerl Advanced GUI Example
|
||||
============================
|
||||
|
||||
Similar to the "WebPerl Basic GUI Example", this is a demo of a GUI
|
||||
using WebPerl, but using [Bootstrap](https://getbootstrap.com/)
|
||||
and [jQuery](https://jquery.com/) instead of plain JavaScript,
|
||||
and [Mojolicious](https://mojolicious.org/) instead of plain Plack.
|
||||
|
||||
To get this to work, you will need to copy the `webperl.js` and the
|
||||
three `emperl.*` files from the main `web` directory to the `public`
|
||||
subdirectory in this project.
|
||||
|
||||
Also, a limitation is that the server does not know when the browser
|
||||
window is closed, so it must be stopped manually.
|
||||
|
||||
You can pack this application into a single executable using `do_pp.pl`.
|
||||
Note: I'm not yet sure why, but sometimes this fails with errors such
|
||||
as *"error extracting info from -c/-x file"*, in that case just try
|
||||
the command again.
|
||||
|
||||
|
||||
Author, Copyright, and License
|
||||
==============================
|
||||
|
||||
**WebPerl - <http://webperl.zero-g.net>**
|
||||
|
||||
Copyright (c) 2019 Hauke Daempfling (haukex@zero-g.net)
|
||||
at the Leibniz Institute of Freshwater Ecology and Inland Fisheries (IGB),
|
||||
Berlin, Germany, <http://www.igb-berlin.de>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the same terms as Perl 5 itself: either the GNU General Public
|
||||
License as published by the Free Software Foundation (either version 1,
|
||||
or, at your option, any later version), or the "Artistic License" which
|
||||
comes with Perl 5.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
**WITHOUT ANY WARRANTY**; without even the implied warranty of
|
||||
**MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE**.
|
||||
See the licenses for details.
|
||||
|
||||
You should have received a copy of the licenses along with this program.
|
||||
If not, see <http://perldoc.perl.org/index-licence.html>.
|
||||
@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
use warnings;
|
||||
use strict;
|
||||
use File::Basename qw/fileparse/;
|
||||
use File::Spec::Functions qw/catfile/;
|
||||
use File::Temp qw/tempfile/;
|
||||
|
||||
# this attempts to locate Mojo's default server.crt/server.key files
|
||||
chomp( my $dir = `perldoc -l Mojo::IOLoop::Server` );
|
||||
die "perldoc -l failed, \$?=$?" if $? || !-e $dir;
|
||||
(undef, $dir) = fileparse($dir);
|
||||
|
||||
# set up a file for pp's -A switch
|
||||
my ($tfh, $tfn) = tempfile(UNLINK=>1);
|
||||
print {$tfh} catfile($dir,'resources','server.crt'),";server.crt\n";
|
||||
print {$tfh} catfile($dir,'resources','server.key'),";server.key\n";
|
||||
close $tfh;
|
||||
|
||||
my @args = (qw/ -a public -a templates -A /, $tfn);
|
||||
|
||||
local $ENV{DOING_PAR_PACKER}=1;
|
||||
system(qw/ pp -o gui_sweet -z 9 -x /,@args,'gui_sweet.pl')==0
|
||||
or die "pp failed, \$?=$?";
|
||||
@ -1,77 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
use Mojolicious::Lite;
|
||||
use Mojo::Util qw/md5_sum/;
|
||||
use FindBin;
|
||||
use File::Spec::Functions qw/catdir/;
|
||||
use Browser::Open qw/open_browser/;
|
||||
|
||||
# This is the server-side code.
|
||||
|
||||
my $SERV_PORT = 3000;
|
||||
|
||||
my ($SSLCERTS,$HOMEDIR);
|
||||
BEGIN {
|
||||
$HOMEDIR = $ENV{PAR_TEMP} ? catdir($ENV{PAR_TEMP},'inc') : $FindBin::Bin;
|
||||
chdir $HOMEDIR or die "chdir $HOMEDIR: $!";
|
||||
# do_pp.pl pulls the default Mojo SSL certs into the archive for us
|
||||
$SSLCERTS = $ENV{PAR_TEMP} ? '?cert=./server.crt&key=./server.key' : '';
|
||||
}
|
||||
|
||||
app->static->paths([catdir($HOMEDIR,'public')]);
|
||||
app->renderer->paths([catdir($HOMEDIR,'templates')]);
|
||||
app->secrets(['Hello, Perl World!']);
|
||||
app->types->type(js => "application/javascript");
|
||||
app->types->type(data => "application/octet-stream");
|
||||
app->types->type(mem => "application/octet-stream");
|
||||
app->types->type(wasm => "application/wasm");
|
||||
|
||||
# Authentication and browser-launching stuff (optional)
|
||||
my $TOKEN = md5_sum(rand(1e15).time);
|
||||
hook before_server_start => sub {
|
||||
my ($server, $app) = @_;
|
||||
my @urls = map {Mojo::URL->new($_)->query(token=>$TOKEN)} @{$server->listen};
|
||||
my $url = shift @urls or die "No urls?";
|
||||
if ($ENV{DOING_PAR_PACKER}) {
|
||||
# arrange to have the server shut down in a few moments
|
||||
my $procpid = $$;
|
||||
my $pid = fork();
|
||||
if (!defined $pid) { die "fork failed" }
|
||||
elsif ($pid==0) { sleep 5; kill 'USR1', $procpid; exit; } # child
|
||||
print "====> Please wait a few seconds...\n";
|
||||
$SIG{USR1} = sub { $server->stop; exit };
|
||||
}
|
||||
else {
|
||||
print "Attempting to open in browser: $url\n";
|
||||
open_browser($url);
|
||||
}
|
||||
};
|
||||
under sub {
|
||||
my $c = shift;
|
||||
return 1 if ($c->param('token')//'') eq $TOKEN;
|
||||
$c->render(text => 'Bad token!', status => 403);
|
||||
return undef;
|
||||
};
|
||||
|
||||
get '/' => sub { shift->render } => 'index';
|
||||
|
||||
post '/example' => sub {
|
||||
my $c = shift;
|
||||
my $data = $c->req->json;
|
||||
# can do anything here, this is just an example
|
||||
$data->{string} = reverse $data->{string};
|
||||
$c->render(json => $data);
|
||||
};
|
||||
|
||||
app->start('daemon', '-l', "https://localhost:$SERV_PORT$SSLCERTS");
|
||||
|
||||
__DATA__
|
||||
|
||||
@@ index.html.ep
|
||||
% layout 'main', title => 'WebPerl GUI Demo';
|
||||
<main role="main" class="container">
|
||||
<div>
|
||||
<h1>WebPerl Advanced GUI Demo</h1>
|
||||
<p class="lead">Hello, Perl World!</p>
|
||||
<div id="buttons"></div>
|
||||
</div>
|
||||
</main>
|
||||
@ -1,44 +0,0 @@
|
||||
#!perl
|
||||
use warnings;
|
||||
use 5.028;
|
||||
use WebPerl qw/js sub1 encode_json/;
|
||||
|
||||
# This is the code that WebPerl runs in the browser. It is loaded by index.html.
|
||||
|
||||
my $window = js('window');
|
||||
my $document = js('document');
|
||||
my $jq = js('jQuery');
|
||||
|
||||
sub do_ajax {
|
||||
my %args = @_;
|
||||
die "must specify a url" unless $args{url};
|
||||
$args{fail} ||= sub { $window->alert(shift) };
|
||||
$jq->ajax( $args{url}, {
|
||||
$args{data} # when given data, default to POST (JSON), otherwise GET
|
||||
? ( method=>$args{method}||'POST',
|
||||
data=>encode_json($args{data}) )
|
||||
: ( method=>$args{method}||'GET' ),
|
||||
} )->done( sub1 {
|
||||
$args{done}->(shift) if $args{done};
|
||||
} )->fail( sub1 {
|
||||
my ($jqXHR, $textStatus, $errorThrown) = @_;
|
||||
$args{fail}->("AJAX Failed! ($errorThrown)");
|
||||
} )->always( sub1 {
|
||||
$args{always}->() if $args{always};
|
||||
} );
|
||||
return;
|
||||
}
|
||||
|
||||
# slightly hacky way to get the access token, but it works fine
|
||||
my ($token) = $window->{location}{search}=~/\btoken=([a-fA-F0-9]+)\b/;
|
||||
|
||||
my $btn = $jq->('<button>', { text=>"Click me!" } );
|
||||
$btn->click(sub {
|
||||
$btn->prop('disabled',1);
|
||||
do_ajax( url=>"/example?token=$token",
|
||||
data => { string=>"rekcaH lreP rehtonA tsuJ" },
|
||||
done => sub { $window->alert("The server says: ".shift->{string}) },
|
||||
always => sub { $btn->prop('disabled',0); } );
|
||||
} );
|
||||
$btn->appendTo( $jq->('#buttons') );
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><%= title %></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
||||
<style>
|
||||
body { padding-top: 5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
||||
<a class="navbar-brand" href="#"><%= title %></a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="dropdown01" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdown01">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
<a class="dropdown-item" href="#">Something else here</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<%= content %>
|
||||
|
||||
<!-- Bootstrap wants its script tags at the end of the body element, so we'll put everything here: -->
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
|
||||
<script src="webperl.js"></script>
|
||||
<script type="text/perl" src="web.pl"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue