5HT 5 years ago
parent
commit
eed570f67a

+ 7 - 0
include/koatuuControl.hrl

@@ -0,0 +1,7 @@
+-ifndef(KOATUU_NX_HRL).
+-define(KOATUU_NX_HRL, true).
+
+-include_lib("nitro/include/nitro.hrl").
+-record(koatuu, {?ELEMENT_BASE(element_koatuu)}).
+
+-endif.

+ 54 - 0
include/n2o.hrl

@@ -0,0 +1,54 @@
+-ifndef(N2O_HRL).
+-define(N2O_HRL, true).
+
+-define(FORMAT(F), case F of F when is_binary(F) -> binary_to_list(F);
+                             F when is_atom(F) -> atom_to_list(F);
+                             F when is_list(F) -> F end).
+
+-ifdef(OTP_RELEASE).
+-include_lib("kernel/include/logger.hrl").
+-else.
+-define(LOG_INFO(F), io:format(?FORMAT(F)) end).
+-define(LOG_INFO(F,X), io:format(?FORMAT(F),X)).
+-define(LOG_ERROR(F), io:format("{~p,~p}: ~p~n", [?MODULE,?LINE,F])).
+-define(LOG_ERROR(F,X), io:format(?FORMAT(F),X)).
+-endif.
+
+-define(LOG_EXCEPTION(E,R,S), ?LOG_ERROR(#{exception => E, reason => R, stack => S})).
+
+-record(pi, { name     :: term(),
+              table    :: atom(),
+              sup      :: atom(),
+              module   :: atom(),
+              state    :: term()  }).
+
+-record(cx, { handlers  = [] :: list({atom(),atom()}),
+              actions   = [] :: list(tuple()),
+              req       = [] :: [] | term(),
+              module    = [] :: [] | atom() | list(),
+              lang      = [] :: [] | atom(),
+              path      = [] :: [] | binary(),
+              session   = [] :: [] | binary(),
+              token     = [] :: [] | binary(),
+              formatter = bert :: bert | json | atom(),
+              params    = [] :: [] | list(tuple()) | binary() | list(),
+              node      = [] :: [] | atom() | list(),
+              client_pid= [] :: [] | term(),
+              state     = [] :: [] | term(),
+              from      = [] :: [] | binary(),
+              vsn       = [] :: [] | binary() }).
+
+-define(CTX(ClientId), n2o:cache(ClientId)).
+-define(REQ(ClientId), (n2o:cache(ClientId))#cx.req).
+
+% Nitrogen Protocol
+
+-record(client,  { data=[] }).
+-record(server,  { data=[] }).
+-record(init,    { token=[] }).
+-record(pickle,  { source=[], pickled=[], args=[] }).
+-record(flush,   { data=[] }).
+-record(direct,  { data=[] }).
+-record(ev,      { module=[], msg=[], trigger=[], name=[] }).
+
+-endif.

+ 7 - 0
include/sortable_item.hrl

@@ -0,0 +1,7 @@
+-ifndef(SORTABLE_ITEM_NX_HRL).
+-define(SORTABLE_ITEM_NX_HRL, true).
+
+-include_lib("nitro/include/nitro.hrl").
+-record(sortable_item, {?ELEMENT_BASE(element_sortable_item), list_id, value, closeable}).
+
+-endif.

+ 7 - 0
include/sortable_list.hrl

@@ -0,0 +1,7 @@
+-ifndef(SORTABLE_LIST_NX_HRL).
+-define(SORTABLE_LIST_NX_HRL, true).
+
+-include_lib("nitro/include/nitro.hrl").
+-record(sortable_list, {?ELEMENT_BASE(element_sortable_list), values, closeable}).
+
+-endif.

+ 1 - 1
mix.exs

@@ -4,7 +4,7 @@ defmodule NITRO.Mixfile do
   def project do
     [
       app: :nitro,
-      version: "4.12.3",
+      version: "4.12.4",
       description: "NITRO Nitrogen Web Framework",
       package: package(),
       deps: deps()

+ 99 - 0
priv/css/sortable.css

@@ -0,0 +1,99 @@
+
+body {
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-tap-highlight-color: transparent;
+}
+
+* {
+  box-sizing: border-box;
+}
+
+.title {
+  font-weight: 500;
+  text-align: center;
+  margin-bottom: 20px;
+  font-size: 20px;
+  color: #0E254E;
+}
+
+.list {
+  margin: 0 auto;
+  width: 100%;
+  max-width: 380px;
+  user-select: none;
+}
+.list__item {
+  transition: box-shadow 200ms ease-out, opacity 200ms ease-out;
+  border-radius: 6px;
+  background: #fff;
+  box-shadow: 0 0 12px rgba(0, 0, 0, 0.05);
+  display: flex;
+}
+.list__item:not(:last-child) {
+  margin-bottom: 7px;
+}
+.list__item.is-dragging {
+  box-shadow: 0 0 24px rgba(0, 0, 0, 0.1);
+  opacity: 0.8;
+}
+.list__item-content {
+  width: calc(100% - 40px - 40px);
+  padding: 10px 15px;
+}
+.list__item-title, .list__item-description {
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  overflow: hidden;
+}
+.list__item-title {
+  font-size: 15px;
+  color: #0E254E;
+}
+.list__item-description {
+  font-size: 13px;
+  color: #66748E;
+}
+.list__item-handle {
+  position: relative;
+  width: 40px;
+  cursor: pointer;
+}
+.list__item-handle:before, .list__item-handle:after {
+  content: "";
+  position: absolute;
+  left: 15px;
+  right: 15px;
+  top: 50%;
+  height: 1px;
+  background: #c2cada;
+}
+.list__item-handle:before {
+  transform: translateY(-4px);
+}
+.list__item-handle:after {
+  transform: translateY(4px);
+}
+
+.list__item-close {
+  position: relative;
+  width: 40px;
+  cursor: pointer;
+}
+.list__item-close:after {
+  content: "X";
+  position: absolute;
+  left: 15px;
+  right: 15px;
+  top: 30%;
+  height: 0px;
+  background: #c2cada;
+}
+.list__item-close:after {
+  transform: translateY(0px);
+}
+
+.list__item-close:hover {
+  background-color: #f44336;
+  color: white;
+}

+ 61 - 0
priv/js/nitro.js

@@ -0,0 +1,61 @@
+// Nitrogen Compatibility Layer
+
+function direct(term) { ws.send(enc(tuple(atom('direct'),term))); }
+function validateSources() { return true; }
+function querySourceRaw(Id) {
+    var val, el = document.getElementById(Id);
+    if (!el) {
+       val = qs('input[name='+Id+']:checked'); val = val ? val.value : "";
+    } else switch (el.tagName) {
+        case 'FIELDSET':
+            val = qs('[id="'+Id+'"]:checked'); val = val ? val.value : ""; break;
+        case 'INPUT':
+            switch (el.getAttribute("type")) {
+                case 'radio': case 'checkbox': val = qs('input[name='+Id+']:checked'); val = val ? val.value : ""; break;
+                case 'date': val = Date.parse(el.value);  val = val && new Date(val) || ""; break;
+                case 'calendar': val = pickers[el.id]._d || ""; break;
+                default: var edit = el.contentEditable;
+                    if (edit && edit === 'true') val = el.innerHTML;
+                    else val = el.value;
+            }
+            break;
+        default:
+            if (el.getAttribute('data-vector-input')) {
+                res = getSortableValues('#' + el.children[1].id);
+                console.log('data-vector-input:res =', res);
+                return res;
+            } else {
+                var edit = el.contentEditable;
+                if (edit && edit === 'true') {
+                    val = el.innerHTML;
+                } else {
+                    val = el.value;
+                    switch (val) {
+                        case "true": val = new Boolean(true); break;
+                        case "false": val = new Boolean(false); break;
+                    }
+                }
+            }
+    }
+    console.log("querySourceRaw:val:", val)
+    return val;
+}
+
+function querySource(Id) {
+    var qs = querySourceRaw(Id);
+    if (qs instanceof Date) {
+        return tuple(number(qs.getFullYear()),
+                     number(qs.getMonth() + 1),
+                     number(qs.getDate())); }
+    else if (qs instanceof Boolean) {
+        return atom(qs.valueOf()); }
+    else if (qs instanceof Array) {
+
+        return list.apply(null, qs.map(bin)); }
+    else { return bin(qs); }
+}
+
+(function () {
+    window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
+        window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
+})();

+ 212 - 0
priv/js/sortable.js

@@ -0,0 +1,212 @@
+"use strict";
+
+function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
+
+function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+window.addEventListener('touchmove', function () {});
+
+var Sortable =
+/*#__PURE__*/
+function () {
+  function Sortable(list, options) {
+    _classCallCheck(this, Sortable);
+    this.list = typeof list === 'string' ? document.querySelector(list) : list;
+    this.items = Array.from(this.list.children);
+    this.animation = false;
+    this.options = Object.assign({
+      animationSpeed: 200,
+      animationEasing: 'ease-out'
+    }, options || {});
+    this.dragStart = this.dragStart.bind(this);
+    this.dragMove = this.dragMove.bind(this);
+    this.dragEnd = this.dragEnd.bind(this);
+    this.list.addEventListener('touchstart', this.dragStart, false);
+    this.list.addEventListener('mousedown', this.dragStart, false);
+  }
+
+  _createClass(Sortable, [{
+    key: "dragStart",
+    value: function dragStart(e) {
+      var _this = this;
+
+      if (this.animation) return;
+      if (e.type === 'mousedown' && e.which !== 1) return;
+      if (e.type === 'touchstart' && e.touches.length > 1) return;
+      this.handle = null;
+      this.close = null;
+      var el = e.target;
+
+      while (el) {
+        if (el.hasAttribute('data-sortable-handle')) this.handle = el;
+        if (el.hasAttribute('data-sortable-close')) this.close = el;
+        if (el.hasAttribute('data-sortable-item')) this.item = el;
+        if (el.hasAttribute('data-sortable-list')) break;
+        el = el.parentElement;
+      }
+
+      if (!this.handle) return;
+      this.list.style.position = 'relative';
+      this.list.style.height = this.list.offsetHeight + 'px';
+      this.item.classList.add('is-dragging');
+      this.itemHeight = this.items[1].offsetTop;
+      this.listHeight = this.list.offsetHeight;
+      this.startTouchY = this.getDragY(e);
+      this.startTop = this.item.offsetTop;
+      var offsetsTop = this.items.map(function (item) {
+        return item.offsetTop;
+      });
+      this.items.forEach(function (item, index) {
+        item.style.position = 'absolute';
+        item.style.top = 0;
+        item.style.left = 0;
+        item.style.width = '100%';
+        item.style.transform = "translateY(".concat(offsetsTop[index], "px)");
+        item.style.zIndex = item == _this.item ? 2 : 1;
+      });
+      setTimeout(function () {
+        _this.items.forEach(function (item) {
+          if (_this.item == item) return;
+          item.style.transition = "transform ".concat(_this.options.animationSpeed, "ms ").concat(_this.options.animationEasing);
+        });
+      });
+      this.positions = this.items.map(function (item, index) {
+        return index;
+      });
+      this.position = Math.round(this.startTop / this.listHeight * this.items.length);
+      this.touch = e.type == 'touchstart';
+      window.addEventListener(this.touch ? 'touchmove' : 'mousemove', this.dragMove, {
+        passive: false
+      });
+      window.addEventListener(this.touch ? 'touchend' : 'mouseup', this.dragEnd, false);
+    }
+  }, {
+    key: "dragMove",
+    value: function dragMove(e) {
+      var _this2 = this;
+
+      if (this.animation) return;
+      var top = this.startTop + this.getDragY(e) - this.startTouchY;
+      var newPosition = Math.round(top / this.listHeight * this.items.length);
+      this.item.style.transform = "translateY(".concat(top, "px)");
+      this.positions.forEach(function (index) {
+        if (index == _this2.position || index != newPosition) return;
+
+        _this2.swapElements(_this2.positions, _this2.position, index);
+
+        _this2.position = index;
+      });
+      this.items.forEach(function (item, index) {
+        if (item == _this2.item) return;
+        item.style.transform = "translateY(".concat(_this2.positions.indexOf(index) * _this2.itemHeight, "px)");
+      });
+      e.preventDefault();
+    }
+  }, {
+    key: "dragEnd",
+    value: function dragEnd(e) {
+      var _this3 = this;
+
+      this.animation = true;
+      this.item.style.transition = "all ".concat(this.options.animationSpeed, "ms ").concat(this.options.animationEasing);
+      this.item.style.transform = "translateY(".concat(this.position * this.itemHeight, "px)");
+      this.item.classList.remove('is-dragging');
+      setTimeout(function () {
+        _this3.list.style.position = '';
+        _this3.list.style.height = '';
+
+        _this3.items.forEach(function (item) {
+          item.style.top = '';
+          item.style.left = '';
+          item.style.right = '';
+          item.style.position = '';
+          item.style.transform = '';
+          item.style.transition = '';
+          item.style.width = '';
+          item.style.zIndex = '';
+        });
+
+        _this3.positions.map(function (i) {
+          return _this3.list.appendChild(_this3.items[i]);
+        });
+
+        _this3.items = Array.from(_this3.list.children);
+        _this3.animation = false;
+      }, this.options.animationSpeed);
+      window.removeEventListener(this.touch ? 'touchmove' : 'mousemove', this.dragMove, {
+        passive: false
+      });
+      window.removeEventListener(this.touch ? 'touchend' : 'mouseup', this.dragEnd, false);
+    }
+  }, {
+    key: "swapElements",
+    value: function swapElements(array, a, b) {
+      var temp = array[a];
+      array[a] = array[b];
+      array[b] = temp;
+    }
+  }, {
+    key: "getDragY",
+    value: function getDragY(e) {
+      return e.touches ? (e.touches[0] || e.changedTouches[0]).pageY : e.pageY;
+    }
+  }, {
+    key: "removeItem",
+    value: function removeItem(item) {
+      this.items.splice(this.items.indexOf(item),1);
+      item.remove();
+    }
+  }, {
+    key: "addItemFrom",
+    value: function addItemFrom(input) {
+      var value = querySourceRaw(input);
+      var template = document.createElement('template');
+      template.innerHTML =
+      '<div class="list__item" data-sortable-item="data-sortable-item" style="">'+
+        '<div class="list__item-close" onclick="removeSortableItem(\'#' + this.list.id + '\', this.parentNode);" data-sortable-close="data-sortable-close"></div>'+
+        '<div class="list__item-content">'+
+          '<div class="list__item-title">' + value + '</div>'+
+        '</div>'+
+        '<div class="list__item-handle" data-sortable-handle="data-sortable-handle"></div>'+
+      '</div>'
+      var new_item = template.content.firstChild;
+      this.list.appendChild(new_item);
+      this.items.push(new_item);
+    }
+  }, {
+    key: "getValues",
+    value: function getValues() {
+      return Array.from(this.items.map(function(item) { return item.children[1].firstChild.innerHTML; }));
+    }
+  }]);
+
+  return Sortable;
+}();
+
+var SortableMap = new Map([]);
+
+function createSortable(list) {
+  SortableMap.set(list, new Sortable(list));
+}
+
+function removeSortableItem(list, item) {
+  SortableMap.get(list).removeItem(item);
+}
+
+function addSortableItemFrom(list, input) {
+  SortableMap.get(list).addItemFrom(input);
+}
+
+function getSortableValues(list) {
+  console.log('getSortableValues',list)
+  let sortable = SortableMap.get(list)
+  if(sortable) {
+    return sortable.getValues();
+  } else {
+    return Array.from([]);
+  }
+}

+ 0 - 0
src/elements/input/element_calendar.erl → src/elements/combo/element_calendar.erl


+ 0 - 0
src/elements/input/element_comboLookup.erl → src/elements/combo/element_comboLookup.erl


+ 18 - 0
src/elements/combo/element_koatuu.erl

@@ -0,0 +1,18 @@
+-module(element_koatuu).
+-include_lib("nitro/include/comboLookup.hrl").
+-include_lib("nitro/include/koatuuControl.hrl").
+-include_lib("nitro/include/nitro.hrl").
+-include_lib("nitro/include/event.hrl").
+-export([render_element/1]).
+
+render_element(#koatuu{id=Id, style=Style, postback = Postback,delegate = Module} = Data) ->
+  Options = [ #option{ value = <<"Хмельницька"/utf8>>,
+                       body = <<"Хмельницька"/utf8>>,
+                      selected = true}], % 25 regions from const feed
+  nitro:render(
+    #panel{id=form:atom([koatuu, Id]),
+           body=[ #select{ id=form:atom([koatuu_select, Id]), postback=Postback,
+                           body=Options},
+
+                  #comboLookup{ } ]}).
+

+ 38 - 0
src/elements/combo/element_sortable_item.erl

@@ -0,0 +1,38 @@
+-module(element_sortable_item).
+-include_lib("nitro/include/sortable_item.hrl").
+-include_lib("nitro/include/nitro.hrl").
+-include_lib("nitro/include/event.hrl").
+-export([render_element/1]).
+
+render_element(#sortable_item{list_id = ListId, value = Value, closeable = Close}) ->
+  nitro:render(#panel{
+                class = <<"list__item">>,
+                data_fields = [{<<"data-sortable-item">>,<<"data-sortable-item">>}],
+                body = [
+                  case Close of
+                    true -> 
+                      #panel{
+                        class = <<"list__item-close">>,
+                        onclick = nitro:jse("removeSortableItem('#" ++ ListId ++ "', this.parentNode);"),
+                        data_fields = [{<<"data-sortable-close">>,<<"data-sortable-close">>}]
+                      };
+                    _ ->
+                      #panel{
+                        style = <<"position:relative;width:40px">>
+                      }
+                    end,
+                  #panel{
+                    class = <<"list__item-content">>,
+                    body = [
+                      #panel{
+                        class = <<"list__item-title">>,
+                        body = Value
+                      }
+                    ]
+                  },
+                  #panel{
+                    class = <<"list__item-handle">>,
+                    data_fields = [{<<"data-sortable-handle">>,<<"data-sortable-handle">>}]
+                  }
+                ]
+              }).

+ 14 - 0
src/elements/combo/element_sortable_list.erl

@@ -0,0 +1,14 @@
+-module(element_sortable_list).
+-include_lib("nitro/include/sortable_list.hrl").
+-include_lib("nitro/include/sortable_item.hrl").
+-include_lib("nitro/include/nitro.hrl").
+-include_lib("nitro/include/event.hrl").
+-export([render_element/1]).
+
+render_element(#sortable_list{id=Id, values = Values, closeable = Close}) ->
+  nitro:wire("createSortable('#" ++ Id ++ "');"),
+  nitro:render(
+    #panel{
+      id = Id,
+      data_fields = [{<<"data-sortable-list">>, <<"data-sortable-list">>}],
+      body = [#sortable_item{list_id = Id, value = Val, closeable = Close} || Val <- Values]}).

+ 1 - 1
src/nitro.app.src

@@ -1,6 +1,6 @@
 {application, nitro, [
     {description,  "NITRO Nitrogen Web Framework"},
-    {vsn,          "4.12.3"},
+    {vsn,          "4.12.4"},
     {applications, [kernel, stdlib]},
     {modules, []},
     {registered,   []},

+ 104 - 0
src/nitro_n2o.erl

@@ -0,0 +1,104 @@
+-module(nitro_n2o).
+-description('N2O Nitrogen Web Framework Protocol').
+-include_lib("nitro/include/n2o.hrl").
+-export([info/3,render_actions/1,io/1,io/2,event/1]).
+
+% Nitrogen pickle handler
+
+info({text,<<"N2O,",Auth/binary>>}, Req, State) ->
+    info(#init{token=Auth},Req,State);
+
+info(#init{token=Auth}, Req, State) ->
+    {'Token', Token} = n2o_session:authenticate([], Auth),
+    Sid = case n2o:depickle(Token) of {{S,_},_} -> S; X -> X end,
+    New = State#cx{session = Sid, token = Auth},
+    put(context,New),
+    {reply,{bert,case io(init, State) of
+                      {io,_,{stack,_}} = Io -> Io;
+                      {io,Code,_} -> {io,Code,{'Token',Token}} end},
+            Req,New};
+
+info(#client{data=Message}, Req, State) ->
+    nitro:actions([]),
+    {reply,{bert,io(#client{data=Message},State)},Req,State};
+
+info(#pickle{}=Event, Req, State) ->
+    nitro:actions([]),
+    {reply,{bert,html_events(Event,State)},Req,State};
+
+info(#flush{data=Actions}, Req, State) ->
+    nitro:actions(Actions),
+    {reply,{bert,io(<<>>)},Req,State};
+
+info(#direct{data=Message}, Req, State) ->
+    nitro:actions([]),
+    {reply,{bert,case io(Message, State) of
+                      {io,_,{stack,_}} = Io -> Io;
+                      {io,Code,Res} -> {io,Code,{direct,Res}} end},
+            Req,State};
+
+info(Message,Req,State) -> {unknown,Message,Req,State}.
+
+% double render: actions could generate actions
+
+render_actions(Actions) ->
+    nitro:actions([]),
+    First  = nitro:render(Actions),
+    Second = nitro:render(nitro:actions()),
+    nitro:actions([]),
+    nitro:to_binary([First,Second]).
+
+% n2o events
+
+html_events(#pickle{source=Source,pickled=Pickled,args=Linked}, State=#cx{token = Token}) ->
+    Ev  = n2o:depickle(Pickled),
+    L   = n2o_session:prolongate(),
+    Res = case Ev of
+          #ev{} when L =:= false -> render_ev(Ev,Source,Linked,State), <<>>;
+          #ev{} -> render_ev(Ev,Source,Linked,State), n2o_session:authenticate([], Token);
+          _CustomEnvelop -> %?LOG_ERROR("EV expected: ~p~n",[CustomEnvelop]),
+                           {error,"EV expected"} end,
+    io(Res).
+
+% calling user code in exception-safe manner
+
+-ifdef(OTP_RELEASE).
+
+render_ev(#ev{module=M,name=F,msg=P,trigger=T},_Source,Linked,State) ->
+    try case F of
+         api_event -> M:F(P,Linked,State);
+             event -> [erlang:put(K,V) || {K,V} <- Linked], M:F(P);
+                 _ -> M:F(P,T,State) end
+    catch E:R:S -> ?LOG_EXCEPTION(E,R,S), {stack,S} end.
+
+io(Event, #cx{module=Module}) ->
+    try X = Module:event(Event), {io,render_actions(nitro:actions()),X}
+    catch E:R:S -> ?LOG_EXCEPTION(E,R,S), {io,[],{stack,S}} end.
+
+io(Data) ->
+    try {io,render_actions(nitro:actions()),Data}
+    catch E:R:S -> ?LOG_EXCEPTION(E,R,S), {io,[],{stack,S}} end.
+
+-else.
+
+render_ev(#ev{module=M,name=F,msg=P,trigger=T},_Source,Linked,State) ->
+    try case F of
+         api_event -> M:F(P,Linked,State);
+             event -> [erlang:put(K,V) || {K,V} <- Linked], M:F(P);
+                 _ -> M:F(P,T,State) end
+    catch E:R -> S = erlang:get_stacktrace(), ?LOG_EXCEPTION(E,R,S), {stack,S} end.
+
+io(Event, #cx{module=Module}) ->
+    try X = Module:event(Event), {io,render_actions(nitro:actions()),X}
+    catch E:R -> S = erlang:get_stacktrace(), ?LOG_EXCEPTION(E,R,S), {io,<<>>,{stack,S}} end.
+
+io(Data) ->
+    try {io,render_actions(nitro:actions()),Data}
+    catch E:R -> S = erlang:get_stacktrace(), ?LOG_EXCEPTION(E,R,S), {io,<<>>,{stack,S}} end.
+
+-endif.
+
+% event Nitrogen Web Framework protocol
+
+event(_) -> [].
+