diff --git a/www/form/index.html b/www/form/index.html index aa5a39fc6..97b69f270 100644 --- a/www/form/index.html +++ b/www/form/index.html @@ -11,33 +11,59 @@ overflow: hidden; box-sizing: border-box; } + + form { + border: 3px solid black; + border-radius: 5px; + padding: 15px; + font-weight: bold !important; + font-size: 18px !important; + } + + input[type="text"], + input[type="password"], + input[type="number"], + input[type="range"], + select + { + margin-top: 5px; + margin-bottom: 5px; + width: 80%; + } + textarea { + width: 80%; + height: 40vh; + font-weight: bold; + font-size: 18px; + }
diff --git a/www/form/main.js b/www/form/main.js index ce756275e..102ed9bf9 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -1,80 +1,201 @@ +require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ '/api/config?cb=' + Math.random().toString(16).substring(2), - '/common/RealtimeTextarea.js', - '/common/messages.js', + '/common/realtime-input.js', '/common/crypto.js', '/common/TextPatcher.js', + 'json.sortify', + '/form/ula.js', + '/common/json-ot.js', '/bower_components/jquery/dist/jquery.min.js', '/customize/pad.js' -], function (Config, Realtime, Messages, Crypto, TextPatcher) { +], function (Config, Realtime, Crypto, TextPatcher, Sortify, Formula, JsonOT) { var $ = window.jQuery; - $(window).on('hashchange', function() { - window.location.reload(); - }); - if (window.location.href.indexOf('#') === -1) { - window.location.href = window.location.href + '#' + Crypto.genKey(); - return; + + var key; + var channel = ''; + var hash = false; + if (!/#/.test(window.location.href)) { + key = Crypto.genKey(); + } else { + hash = window.location.hash.slice(1); + channel = hash.slice(0,32); + key = hash.slice(32); } - var module = window.APP = {}; - var key = Crypto.parseKey(window.location.hash.substring(1)); + var module = window.APP = { + TextPatcher: TextPatcher, + Sortify: Sortify, + Formula: Formula, + }; var initializing = true; - /* elements that we need to listen to */ - /* - * text - * password - * radio - * checkbox - * number - * range - * select - * textarea - */ + var uid = module.uid = Formula.uid; + + var getInputType = Formula.getInputType; + var $elements = module.elements = $('input, select, textarea') + + var eventsByType = Formula.eventsByType; - var $textarea = $('textarea'); + var Map = module.Map = {}; + + var UI = module.UI = { + ids: [], + each: function (f) { + UI.ids.forEach(function (id, i, list) { + f(UI[id], i, list); + }); + } + }; + + var cursorTypes = ['textarea', 'password', 'text']; + + var canonicalize = function (text) { return text.replace(/\r\n/g, '\n'); }; + $elements.each(function (element) { + var $this = $(this); + + var id = uid(); + var type = getInputType($this); + + $this // give each element a uid + .data('rtform-uid', id) + // get its type + .data('rt-ui-type', type); + + UI.ids.push(id); + + var component = UI[id] = { + id: id, + $: $this, + element: element, + type: type, + preserveCursor: cursorTypes.indexOf(type) !== -1, + name: $this.prop('name'), + }; + + component.value = (function () { + var checker = ['radio', 'checkbox'].indexOf(type) !== -1; + + if (checker) { + return function (content) { + return typeof content !== 'undefined'? + $this.prop('checked', !!content): + $this.prop('checked'); + }; + } else { + return function (content) { + return typeof content !== 'undefined' ? + $this.val(content): + canonicalize($this.val()); + }; + } + }()); + + var update = component.update = function () { Map[id] = component.value(); }; + update(); + }); var config = module.config = { - websocketURL: Config.websocketURL + '_old', + initialState: Sortify(Map) || '{}', + websocketURL: Config.websocketURL, userName: Crypto.rand64(8), - channel: key.channel, - cryptKey: key.cryptKey + channel: channel, + cryptKey: key, + crypto: Crypto, + transformFunction: JsonOT.validate }; - var setEditable = function (bool) {/* allow editing */}; - var canonicalize = function (text) {/* canonicalize all the things */}; + var setEditable = module.setEditable = function (bool) { + /* (dis)allow editing */ + $elements.each(function () { + $(this).attr('disabled', !bool); + }); + }; setEditable(false); - var onInit = config.onInit = function (info) { }; + var onInit = config.onInit = function (info) { + var realtime = module.realtime = info.realtime; + window.location.hash = info.channel + key; - var onRemote = config.onRemote = function (info) { - if (initializing) { return; } - /* integrate remote changes */ + // create your patcher + module.patchText = TextPatcher.create({ + realtime: realtime, + logging: true, + }); }; var onLocal = config.onLocal = function () { if (initializing) { return; } /* serialize local changes */ + readValues(); + module.patchText(Sortify(Map)); }; - var onReady = config.onReady = function (info) { - var realtime = module.realtime = info.realtime; + var readValues = function () { + UI.each(function (ui, i, list) { + Map[ui.id] = ui.value(); + }); + }; - // create your patcher - module.patchText = TextPatcher.create({ - realtime: realtime + var updateValues = function () { + var userDoc = module.realtime.getUserDoc(); + var parsed = JSON.parse(userDoc); + + console.log(userDoc); + + UI.each(function (ui, i, list) { + var newval = parsed[ui.id]; + var oldval = ui.value(); + + if (newval === oldval) { return; } + + var op; + var element = ui.element; + if (ui.preserveCursor) { + op = TextPatcher.diff(oldval, newval); + var selects = ['selectionStart', 'selectionEnd'].map(function (attr) { + var before = element[attr]; + var after = TextPatcher.transformCursor(element[attr], op); + return after; + }); + } + + ui.value(newval); + ui.update(); + + if (op) { + console.log(selects); + element.selectionStart = selects[0]; + element.selectionEnd = selects[1]; + } }); + }; - // get ready + var onRemote = config.onRemote = function (info) { + if (initializing) { return; } + /* integrate remote changes */ + updateValues(); + }; + + var onReady = config.onReady = function (info) { + updateValues(); + console.log("READY"); setEditable(true); initializing = false; }; - var onAbort = config.onAbort = function (info) {}; + var onAbort = config.onAbort = function (info) { + window.alert("Network Connection Lost"); + }; var rt = Realtime.start(config); - // bind to events... + UI.each(function (ui, i, list) { + var type = ui.type; + var events = eventsByType[type]; + ui.$.on(events, onLocal); + }); + }); diff --git a/www/form/types.md b/www/form/types.md new file mode 100644 index 000000000..ab73d3bfa --- /dev/null +++ b/www/form/types.md @@ -0,0 +1,14 @@ + +```Javascript +/* elements that we need to listen to */ +/* + * text => $(text).val() + * password => $(password).val() + * radio => $(radio).prop('checked') + * checkbox => $(checkbox).prop('checked') + * number => $(number).val() // returns string, no default + * range => $(range).val() + * select => $(select).val() + * textarea => $(textarea).val() +*/ +``` diff --git a/www/form/ula.js b/www/form/ula.js new file mode 100644 index 000000000..4591bb5eb --- /dev/null +++ b/www/form/ula.js @@ -0,0 +1,24 @@ +define([], function () { + var ula = {}; + + var uid = ula.uid = (function () { + var i = 0; + var prefix = 'rt_'; + return function () { return prefix + i++; }; + }()); + + ula.getInputType = function ($el) { return $el[0].type; }; + + ula.eventsByType = { + text: 'change keyup', + password: 'change keyup', + radio: 'change click', + checkbox: 'change click', + number: 'change', + range: 'keyup change', + 'select-one': 'change', + textarea: 'change keyup', + }; + + return ula; +});