From 363c4e0b14adb5fa27f7ef274c7fe20c5ae7c10e Mon Sep 17 00:00:00 2001 From: electron128 Date: Sat, 2 Jan 2016 15:52:17 +0100 Subject: [PATCH] started mithril.js based webui --- libresapi/src/webui-src/.gitignore | 2 + libresapi/src/webui-src/app/_chat.sass | 71 + libresapi/src/webui-src/app/_reset.scss | 48 + libresapi/src/webui-src/app/assets/index.html | 21 + libresapi/src/webui-src/app/chat.js | 27 + libresapi/src/webui-src/app/main.sass | 9 + libresapi/src/webui-src/app/mithril.js | 1159 +++++++++++++++++ libresapi/src/webui-src/brunch-config.js | 12 + libresapi/src/webui-src/package.json | 11 + 9 files changed, 1360 insertions(+) create mode 100644 libresapi/src/webui-src/.gitignore create mode 100644 libresapi/src/webui-src/app/_chat.sass create mode 100644 libresapi/src/webui-src/app/_reset.scss create mode 100644 libresapi/src/webui-src/app/assets/index.html create mode 100644 libresapi/src/webui-src/app/chat.js create mode 100644 libresapi/src/webui-src/app/main.sass create mode 100644 libresapi/src/webui-src/app/mithril.js create mode 100644 libresapi/src/webui-src/brunch-config.js create mode 100644 libresapi/src/webui-src/package.json diff --git a/libresapi/src/webui-src/.gitignore b/libresapi/src/webui-src/.gitignore new file mode 100644 index 000000000..cff629a7a --- /dev/null +++ b/libresapi/src/webui-src/.gitignore @@ -0,0 +1,2 @@ +node_modules/* +public/* diff --git a/libresapi/src/webui-src/app/_chat.sass b/libresapi/src/webui-src/app/_chat.sass new file mode 100644 index 000000000..c0c46007a --- /dev/null +++ b/libresapi/src/webui-src/app/_chat.sass @@ -0,0 +1,71 @@ +.chat + $color: lightblue + $header_height: 50px + $left_width: 200px + $right_width: 200px + $input_height: 100px + padding: 15px + &.container + height: 100% + padding: 0px + position: relative + &.header + position: absolute + top: 0px + left: 0px + right: 0px + height: $header_height + background-color: $color + border-bottom: solid 1px gray + box-sizing: border-box + &.left + position: absolute + top: $header_height + bottom: 0px + left: 0px + width: $left_width + border-right: solid 1px gray + box-sizing: border-box + background-color: lightgray + &.right + position: absolute + top: $header_height + right: 0px + bottom: 0px + width: $right_width + box-sizing: border-box + border-left: solid 1px gray + &.middle + //background-color: blue + position: absolute + top: $header_height + left: $left_width + right: $right_width + box-sizing: border-box + padding-top: 0px + padding-left: 0px + padding-right: 0px + + &.msg + padding: 0px + $author_width: 100px + &.container + position: relative + border-bottom: solid 1px lightgray + padding: 10px + //background-color: lime + &.from + position: absolute + width: $author_width + top: 10px + left: 0px + color: gray + text-align: right + &.when + float: right + color: lightgray + margin-bottom: 10px + &.text + padding-left: $author_width + top: 0px + left: $author_width \ No newline at end of file diff --git a/libresapi/src/webui-src/app/_reset.scss b/libresapi/src/webui-src/app/_reset.scss new file mode 100644 index 000000000..af944401f --- /dev/null +++ b/libresapi/src/webui-src/app/_reset.scss @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/libresapi/src/webui-src/app/assets/index.html b/libresapi/src/webui-src/app/assets/index.html new file mode 100644 index 000000000..b914d9932 --- /dev/null +++ b/libresapi/src/webui-src/app/assets/index.html @@ -0,0 +1,21 @@ + + + + + rswebui5 + + + +
if app does not load, enable JavaScript!
+ + + + diff --git a/libresapi/src/webui-src/app/chat.js b/libresapi/src/webui-src/app/chat.js new file mode 100644 index 000000000..f91ab536c --- /dev/null +++ b/libresapi/src/webui-src/app/chat.js @@ -0,0 +1,27 @@ +"use strict"; + +var m = require("mithril"); + +function msg(from, when, text){ + return m(".chat.msg.container",[ + m(".chat.msg.from", from), + m(".chat.msg.when", when), + m(".chat.msg.text", text), + ]); +} + +module.exports = { + view: function(){ + return m(".chat.container", [ + m(".chat.header", "headerbar"), + m(".chat.left", "left"), + m(".chat.right", "right"), + m(".chat.middle", [ + msg("Andi", "now", "Hallo"), + msg("Test", "now", "Hallo back"), + msg("Somebody", "now", "Hallo back, sfjhfu dsjkchsd wehfskf sdjksdf sjdnfkjsf sdjkfhjksdf jksdfjksdnf sjdefhsjkn cesjdfhsjk fskldcjhsklc ksdj"), + ]), + m(".chat.clear", ""), + ]); + } +} \ No newline at end of file diff --git a/libresapi/src/webui-src/app/main.sass b/libresapi/src/webui-src/app/main.sass new file mode 100644 index 000000000..1a7df2b49 --- /dev/null +++ b/libresapi/src/webui-src/app/main.sass @@ -0,0 +1,9 @@ +@import "reset" + +html, body, #main + height: 100% + +body + font-family: "Sans-serif" + +@import "chat" \ No newline at end of file diff --git a/libresapi/src/webui-src/app/mithril.js b/libresapi/src/webui-src/app/mithril.js new file mode 100644 index 000000000..318573d8c --- /dev/null +++ b/libresapi/src/webui-src/app/mithril.js @@ -0,0 +1,1159 @@ +var m = (function app(window, undefined) { + var OBJECT = "[object Object]", ARRAY = "[object Array]", STRING = "[object String]", FUNCTION = "function"; + var type = {}.toString; + var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/; + var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/; + var noop = function() {} + + // caching commonly used variables + var $document, $location, $requestAnimationFrame, $cancelAnimationFrame; + + // self invoking function needed because of the way mocks work + function initialize(window){ + $document = window.document; + $location = window.location; + $cancelAnimationFrame = window.cancelAnimationFrame || window.clearTimeout; + $requestAnimationFrame = window.requestAnimationFrame || window.setTimeout; + } + + initialize(window); + + + /** + * @typedef {String} Tag + * A string that looks like -> div.classname#id[param=one][param2=two] + * Which describes a DOM node + */ + + /** + * + * @param {Tag} The DOM node tag + * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs + * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, or splat (optional) + * + */ + function m() { + var args = [].slice.call(arguments); + var hasAttrs = args[1] != null && type.call(args[1]) === OBJECT && !("tag" in args[1] || "view" in args[1]) && !("subtree" in args[1]); + var attrs = hasAttrs ? args[1] : {}; + var classAttrName = "class" in attrs ? "class" : "className"; + var cell = {tag: "div", attrs: {}}; + var match, classes = []; + if (type.call(args[0]) != STRING) throw new Error("selector in m(selector, attrs, children) should be a string") + while (match = parser.exec(args[0])) { + if (match[1] === "" && match[2]) cell.tag = match[2]; + else if (match[1] === "#") cell.attrs.id = match[2]; + else if (match[1] === ".") classes.push(match[2]); + else if (match[3][0] === "[") { + var pair = attrParser.exec(match[3]); + cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true) + } + } + + var children = hasAttrs ? args.slice(2) : args.slice(1); + if (children.length === 1 && type.call(children[0]) === ARRAY) { + cell.children = children[0] + } + else { + cell.children = children + } + + for (var attrName in attrs) { + if (attrs.hasOwnProperty(attrName)) { + if (attrName === classAttrName && attrs[attrName] != null && attrs[attrName] !== "") { + classes.push(attrs[attrName]) + cell.attrs[attrName] = "" //create key in correct iteration order + } + else cell.attrs[attrName] = attrs[attrName] + } + } + if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" "); + + return cell + } + function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) { + //`build` is a recursive function that manages creation/diffing/removal of DOM elements based on comparison between `data` and `cached` + //the diff algorithm can be summarized as this: + //1 - compare `data` and `cached` + //2 - if they are different, copy `data` to `cached` and update the DOM based on what the difference is + //3 - recursively apply this algorithm for every array and for the children of every virtual element + + //the `cached` data structure is essentially the same as the previous redraw's `data` data structure, with a few additions: + //- `cached` always has a property called `nodes`, which is a list of DOM elements that correspond to the data represented by the respective virtual element + //- in order to support attaching `nodes` as a property of `cached`, `cached` is *always* a non-primitive object, i.e. if the data was a string, then cached is a String instance. If data was `null` or `undefined`, cached is `new String("")` + //- `cached also has a `configContext` property, which is the state storage object exposed by config(element, isInitialized, context) + //- when `cached` is an Object, it represents a virtual element; when it's an Array, it represents a list of elements; when it's a String, Number or Boolean, it represents a text node + + //`parentElement` is a DOM element used for W3C DOM API calls + //`parentTag` is only used for handling a corner case for textarea values + //`parentCache` is used to remove nodes in some multi-node cases + //`parentIndex` and `index` are used to figure out the offset of nodes. They're artifacts from before arrays started being flattened and are likely refactorable + //`data` and `cached` are, respectively, the new and old nodes being diffed + //`shouldReattach` is a flag indicating whether a parent node was recreated (if so, and if this node is reused, then this node must reattach itself to the new parent) + //`editable` is a flag that indicates whether an ancestor is contenteditable + //`namespace` indicates the closest HTML namespace as it cascades down from an ancestor + //`configs` is a list of config functions to run after the topmost `build` call finishes running + + //there's logic that relies on the assumption that null and undefined data are equivalent to empty strings + //- this prevents lifecycle surprises from procedural helpers that mix implicit and explicit return statements (e.g. function foo() {if (cond) return m("div")} + //- it simplifies diffing code + //data.toString() might throw or return null if data is the return value of Console.log in Firefox (behavior depends on version) + try {if (data == null || data.toString() == null) data = "";} catch (e) {data = ""} + if (data.subtree === "retain") return cached; + var cachedType = type.call(cached), dataType = type.call(data); + if (cached == null || cachedType !== dataType) { + if (cached != null) { + if (parentCache && parentCache.nodes) { + var offset = index - parentIndex; + var end = offset + (dataType === ARRAY ? data : cached.nodes).length; + clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end)) + } + else if (cached.nodes) clear(cached.nodes, cached) + } + cached = new data.constructor; + if (cached.tag) cached = {}; //if constructor creates a virtual dom element, use a blank object as the base cached node instead of copying the virtual el (#277) + cached.nodes = [] + } + + if (dataType === ARRAY) { + //recursively flatten array + for (var i = 0, len = data.length; i < len; i++) { + if (type.call(data[i]) === ARRAY) { + data = data.concat.apply([], data); + i-- //check current index again and flatten until there are no more nested arrays at that index + len = data.length + } + } + + var nodes = [], intact = cached.length === data.length, subArrayCount = 0; + + //keys algorithm: sort elements without recreating them if keys are present + //1) create a map of all existing keys, and mark all for deletion + //2) add new keys to map and mark them for addition + //3) if key exists in new list, change action from deletion to a move + //4) for each key, handle its corresponding action as marked in previous steps + var DELETION = 1, INSERTION = 2 , MOVE = 3; + var existing = {}, shouldMaintainIdentities = false; + for (var i = 0; i < cached.length; i++) { + if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) { + shouldMaintainIdentities = true; + existing[cached[i].attrs.key] = {action: DELETION, index: i} + } + } + + var guid = 0 + for (var i = 0, len = data.length; i < len; i++) { + if (data[i] && data[i].attrs && data[i].attrs.key != null) { + for (var j = 0, len = data.length; j < len; j++) { + if (data[j] && data[j].attrs && data[j].attrs.key == null) data[j].attrs.key = "__mithril__" + guid++ + } + break + } + } + + if (shouldMaintainIdentities) { + var keysDiffer = false + if (data.length != cached.length) keysDiffer = true + else for (var i = 0, cachedCell, dataCell; cachedCell = cached[i], dataCell = data[i]; i++) { + if (cachedCell.attrs && dataCell.attrs && cachedCell.attrs.key != dataCell.attrs.key) { + keysDiffer = true + break + } + } + + if (keysDiffer) { + for (var i = 0, len = data.length; i < len; i++) { + if (data[i] && data[i].attrs) { + if (data[i].attrs.key != null) { + var key = data[i].attrs.key; + if (!existing[key]) existing[key] = {action: INSERTION, index: i}; + else existing[key] = { + action: MOVE, + index: i, + from: existing[key].index, + element: cached.nodes[existing[key].index] || $document.createElement("div") + } + } + } + } + var actions = [] + for (var prop in existing) actions.push(existing[prop]) + var changes = actions.sort(sortChanges); + var newCached = new Array(cached.length) + newCached.nodes = cached.nodes.slice() + + for (var i = 0, change; change = changes[i]; i++) { + if (change.action === DELETION) { + clear(cached[change.index].nodes, cached[change.index]); + newCached.splice(change.index, 1) + } + if (change.action === INSERTION) { + var dummy = $document.createElement("div"); + dummy.key = data[change.index].attrs.key; + parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null); + newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]}) + newCached.nodes[change.index] = dummy + } + + if (change.action === MOVE) { + if (parentElement.childNodes[change.index] !== change.element && change.element !== null) { + parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null) + } + newCached[change.index] = cached[change.from] + newCached.nodes[change.index] = change.element + } + } + cached = newCached; + } + } + //end key algorithm + + for (var i = 0, cacheCount = 0, len = data.length; i < len; i++) { + //diff each item in the array + var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs); + if (item === undefined) continue; + if (!item.nodes.intact) intact = false; + if (item.$trusted) { + //fix offset of next element if item was a trusted string w/ more than one html element + //the first clause in the regexp matches elements + //the second clause (after the pipe) matches text nodes + subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || [0]).length + } + else subArrayCount += type.call(item) === ARRAY ? item.length : 1; + cached[cacheCount++] = item + } + if (!intact) { + //diff the array itself + + //update the list of DOM nodes by collecting the nodes from each item + for (var i = 0, len = data.length; i < len; i++) { + if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes) + } + //remove items from the end of the array if the new array is shorter than the old one + //if errors ever happen here, the issue is most likely a bug in the construction of the `cached` data structure somewhere earlier in the program + for (var i = 0, node; node = cached.nodes[i]; i++) { + if (node.parentNode != null && nodes.indexOf(node) < 0) clear([node], [cached[i]]) + } + if (data.length < cached.length) cached.length = data.length; + cached.nodes = nodes + } + } + else if (data != null && dataType === OBJECT) { + var views = [], controllers = [] + while (data.view) { + var view = data.view.$original || data.view + var controllerIndex = m.redraw.strategy() == "diff" && cached.views ? cached.views.indexOf(view) : -1 + var controller = controllerIndex > -1 ? cached.controllers[controllerIndex] : new (data.controller || noop) + var key = data && data.attrs && data.attrs.key + data = pendingRequests == 0 || (cached && cached.controllers && cached.controllers.indexOf(controller) > -1) ? data.view(controller) : {tag: "placeholder"} + if (data.subtree === "retain") return cached; + if (key) { + if (!data.attrs) data.attrs = {} + data.attrs.key = key + } + if (controller.onunload) unloaders.push({controller: controller, handler: controller.onunload}) + views.push(view) + controllers.push(controller) + } + if (!data.tag && controllers.length) throw new Error("Component template must return a virtual element, not an array, string, etc.") + if (!data.attrs) data.attrs = {}; + if (!cached.attrs) cached.attrs = {}; + + var dataAttrKeys = Object.keys(data.attrs) + var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0) + //if an element is different enough from the one in cache, recreate it + if (data.tag != cached.tag || dataAttrKeys.sort().join() != Object.keys(cached.attrs).sort().join() || data.attrs.id != cached.attrs.id || data.attrs.key != cached.attrs.key || (m.redraw.strategy() == "all" && (!cached.configContext || cached.configContext.retain !== true)) || (m.redraw.strategy() == "diff" && cached.configContext && cached.configContext.retain === false)) { + if (cached.nodes.length) clear(cached.nodes); + if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload() + if (cached.controllers) { + for (var i = 0, controller; controller = cached.controllers[i]; i++) { + if (typeof controller.onunload === FUNCTION) controller.onunload({preventDefault: noop}) + } + } + } + if (type.call(data.tag) != STRING) return; + + var node, isNew = cached.nodes.length === 0; + if (data.attrs.xmlns) namespace = data.attrs.xmlns; + else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg"; + else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML"; + + if (isNew) { + if (data.attrs.is) node = namespace === undefined ? $document.createElement(data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag, data.attrs.is); + else node = namespace === undefined ? $document.createElement(data.tag) : $document.createElementNS(namespace, data.tag); + cached = { + tag: data.tag, + //set attributes first, then create children + attrs: hasKeys ? setAttributes(node, data.tag, data.attrs, {}, namespace) : data.attrs, + children: data.children != null && data.children.length > 0 ? + build(node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs) : + data.children, + nodes: [node] + }; + if (controllers.length) { + cached.views = views + cached.controllers = controllers + for (var i = 0, controller; controller = controllers[i]; i++) { + if (controller.onunload && controller.onunload.$old) controller.onunload = controller.onunload.$old + if (pendingRequests && controller.onunload) { + var onunload = controller.onunload + controller.onunload = noop + controller.onunload.$old = onunload + } + } + } + + if (cached.children && !cached.children.nodes) cached.children.nodes = []; + //edge case: setting value on