diff --git a/www/slide/index.html b/www/slide/index.html
new file mode 100644
index 000000000..1e66e73fc
--- /dev/null
+++ b/www/slide/index.html
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/www/slide/main.js b/www/slide/main.js
new file mode 100644
index 000000000..9b364ca9c
--- /dev/null
+++ b/www/slide/main.js
@@ -0,0 +1,118 @@
+define([
+ '/api/config?cb=' + Math.random().toString(16).substring(2),
+ '/bower_components/chainpad-netflux/chainpad-netflux.js',
+ '/bower_components/chainpad-crypto/crypto.js',
+ '/bower_components/textpatcher/TextPatcher.amd.js',
+ '/common/cryptpad-common.js',
+ '/slide/slide.js',
+ '/bower_components/jquery/dist/jquery.min.js',
+ '/customize/pad.js'
+], function (Config, Realtime, Crypto, TextPatcher, Cryptpad, Slide) {
+ var $ = window.jQuery;
+
+ /*
+ TODO
+ * patch in changes using DiffDOM
+ * predraw some things in case they use external assets
+ * strip out script tags?
+ * better CSS
+ * use codemirror instead of a text editor
+ * add ability to link to a rendered slide
+ * ui hint for escaping presentation mode
+ */
+
+ var secret = Cryptpad.getSecrets();
+
+ var module = window.APP = {
+ TextPatcher: TextPatcher,
+ Slide: Slide,
+ };
+
+ var userName = module.userName = Crypto.rand64(8);
+
+ var initializing = true;
+ var $textarea = $('textarea');
+
+ var $modal = $('#modal');
+ var $content = $('#content');
+ Slide.setModal($modal, $content);
+
+ var $present = $('#present').click(function () {
+ Slide.show(true, $textarea.val());
+ Cryptpad.log("Hit ESC to exit presentation mode");
+ });
+
+ var config = module.config = {
+ initialState: '',
+ websocketURL: Config.websocketURL,
+ channel: secret.channel,
+ crypto: Crypto.createEncryptor(secret.key),
+ };
+
+ var setEditable = function (bool) { $textarea.attr('disabled', !bool); };
+ var canonicalize = function (text) { return text.replace(/\r\n/g, '\n'); };
+
+ setEditable(false);
+
+ var onInit = config.onInit = function (info) {
+ window.location.hash = info.channel + secret.key;
+ $(window).on('hashchange', function() {
+ window.location.reload();
+ });
+ };
+
+ var onRemote = config.onRemote = function (info) {
+ if (initializing) { return; }
+ var userDoc = module.realtime.getUserDoc();
+ var content = canonicalize($textarea.val());
+
+ var op = TextPatcher.diff(content, userDoc);
+ var elem = $textarea[0];
+
+ var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
+ return TextPatcher.transformCursor(elem[attr], op);
+ });
+
+ $textarea.val(userDoc);
+ elem.selectionStart = selects[0];
+ elem.selectionEnd = selects[1];
+
+ Slide.update(content);
+ };
+
+ var onLocal = config.onLocal = function () {
+ if (initializing) { return; }
+ var content = canonicalize($textarea.val());
+ module.patchText(content);
+ Slide.update(content);
+ };
+
+ var onReady = config.onReady = function (info) {
+ var realtime = module.realtime = info.realtime;
+ module.patchText = TextPatcher.create({
+ realtime: realtime
+ });
+
+ var content = canonicalize(realtime.getUserDoc());
+ $textarea.val(content);
+
+ Slide.update(content);
+
+ setEditable(true);
+ initializing = false;
+ };
+
+ var onAbort = config.onAbort = function (info) {
+ $textarea.attr('disabled', true);
+ window.alert("Server Connection Lost");
+ };
+
+ var rt = Realtime.start(config);
+
+ ['cut', 'paste', 'change', 'keyup', 'keydown', 'select', 'textInput']
+ .forEach(function (evt) {
+ $textarea.on(evt, onLocal);
+ });
+
+
+});
diff --git a/www/slide/slide.js b/www/slide/slide.js
new file mode 100644
index 000000000..3e0227250
--- /dev/null
+++ b/www/slide/slide.js
@@ -0,0 +1,77 @@
+define([
+ '/bower_components/marked/marked.min.js',
+ '/bower_components/jquery/dist/jquery.min.js',
+],function (Marked) {
+ var $ = window.jQuery;
+
+ var truthy = function (x) { return x; };
+
+ var Slide = {
+ index: 0,
+ lastIndex: 0,
+ content: [],
+ };
+ var $modal;
+ var $content;
+ Slide.setModal = function ($m, $c) {
+ $modal = Slide.$modal = $m;
+ $content = Slide.$content = $c;
+ };
+ var draw = Slide.draw = function (i) {
+ console.log("Trying to draw slide #%s", i);
+ if (typeof(Slide.content[i]) !== 'string') { return; }
+
+ var c = Slide.content[i];
+ console.log(c);
+ $content.html(Marked(c));
+ };
+
+ var show = Slide.show = function (bool, content) {
+ Slide.shown = bool;
+ if (bool) {
+ Slide.update(content);
+ Slide.draw(Slide.index);
+ $modal.addClass('shown');
+ return;
+ }
+ $modal.removeClass('shown');
+ };
+
+ var update = Slide.update = function (content) {
+ if (!Slide.shown) { return; }
+ console.log(content);
+ Slide.content = content.split(/\n\-\-\-\n/).filter(truthy);
+ draw(Slide.index);
+ };
+
+ var left = Slide.left = function () {
+ console.log('left');
+ var i = Slide.index = Math.max(0, Slide.index - 1);
+ Slide.draw(i);
+ };
+
+ var right = Slide.right = function () {
+ console.log('right');
+ var i = Slide.index = Math.min(Slide.content.length, Slide.index + 1);
+ Slide.draw(i);
+ };
+
+ $(document).on('keyup', function (e) {
+ if (!Slide.shown) { return; }
+ switch(e.which) {
+ case 37:
+ Slide.left();
+ break;
+ case 39: // right
+ Slide.right();
+ break;
+ case 27: // esc
+ show(false);
+ break;
+ default:
+ console.log(e.which);
+ }
+ });
+
+ return Slide;
+});