Browse Source

comboSearch

SunRiseGC 3 years ago
parent
commit
9064037cff
10 changed files with 266 additions and 48 deletions
  1. 4 2
      include/comboLookup.hrl
  2. 3 1
      include/n2o.hrl
  3. 20 0
      include/n2o_core.hrl
  4. 10 0
      include/nitro_pi.hrl
  5. 2 2
      lib/NITRO.ex
  6. 33 41
      lib/combo.ex
  7. 104 0
      lib/comboSearch.ex
  8. 1 1
      mix.exs
  9. 3 1
      src/elements/combo/element_comboLookup.erl
  10. 86 0
      src/nitro_pi.erl

+ 4 - 2
include/comboLookup.hrl

@@ -3,10 +3,12 @@
 
 -include_lib("nitro/include/nitro.hrl").
 
--record(comboKey,  { value=[], dom=[], feed=[], delegate=[]}).
+-record(comboKey,    { value=[], dom=[], feed=[], delegate=[]}).
 -record(comboKeyup,  { value=[], dom=[], feed=[], delegate=[]}).
--record(comboSelect,  { value=[], dom=[], feed=[], delegate=[] }).
+-record(comboSelect, { value=[], dom=[], feed=[], delegate=[] }).
 -record(comboNext,   { pos=[],  count=[], feed=[]}).
+-record(comboScroll, { dom=[], delegate=[] }).
+-record(comboInsert, { dom=[], rows=[], chunks=[], status=[], delegate=[], feed=[] }).
 -record(comboLookup, { ?ELEMENT_BASE(element_comboLookup),
     value=[],
     disabled=false,

+ 3 - 1
include/n2o.hrl

@@ -20,6 +20,8 @@
               table    :: atom(),
               sup      :: atom(),
               module   :: atom(),
+              timeout   = 5000 :: integer(),
+              restart   = transient :: atom(),
               state    :: term()  }).
 
 -record(cx, { handlers  = [] :: list({atom(),atom()}),
@@ -45,4 +47,4 @@
 
 -include_lib("nitro/include/proto.hrl").
 
--endif.
+-endif.

+ 20 - 0
include/n2o_core.hrl

@@ -0,0 +1,20 @@
+-ifndef(N2O_CORE).
+-define(N2O_CORE, true).
+
+-include("n2o.hrl").
+
+-record(bert,    { data     :: term() }).
+-record(json,    { data     :: term() }).
+-record(binary,  { data     :: term() }).
+-record(default, { data     :: term() }).
+-record(ok,      { data     :: term() }).
+-record(error,   { data     :: term() }).
+-record(reply,   { msg      :: n2o(), req :: term(), ctx :: cx() } ).
+-record(unknown, { msg      :: n2o(), req :: term(), ctx :: cx() } ).
+
+-type n2o()       :: #bert{} | #json{} | #binary{} | #default{}.
+-type cx()        :: #cx{}.
+-type formatter() :: binary | json | bert | text | default | atom().
+-type response()  :: { formatter(), binary() }.
+
+-endif.

+ 10 - 0
include/nitro_pi.hrl

@@ -0,0 +1,10 @@
+-ifndef(NITRO_PI).
+-define(NITRO_PI, true).
+
+-include("n2o_core.hrl").
+-include("n2o.hrl").
+
+-spec start(#pi{}) -> {pid(),term()} | #error{}.
+-spec stop(term(),atom()) -> #pi{} | #error{}.
+
+-endif.

+ 2 - 2
lib/NITRO.ex

@@ -1,8 +1,8 @@
 defmodule NITRO do
   require Record
 
-  files = ["calendar.hrl", "nitro.hrl", "comboLookup.hrl",
-           "comboLookupVec.hrl", "comboLookupEdit.hrl", "koatuuControl.hrl"]
+  files = ["calendar.hrl", "nitro.hrl", "comboLookup.hrl", "nitro_pi.hrl",
+           "comboLookupVec.hrl", "comboLookupEdit.hrl", "koatuuControl.hrl", "n2o.hrl"]
 
   hrl_files =
     Enum.filter(files, fn f ->

+ 33 - 41
lib/combo.ex

@@ -11,13 +11,22 @@ defmodule NITRO.Combo do
     dropDown(obj, dom, module, feed)
   end
 
-  def proto(NITRO.comboKey(value: :all) = msg), do: keyUp(msg)
-  def proto(NITRO.comboKey(delegate: []) = msg), do: keyUp(msg)
+  def proto(NITRO.comboKey(value: "")), do: []
+  def proto(NITRO.comboKey(delegate: []) = msg), do: NITRO.Combo.Search.keyUp(msg)
+  def proto(NITRO.comboScroll(delegate: []) = msg), do: NITRO.Combo.Search.comboScroll(msg)
+  def proto(NITRO.comboInsert(delegate: []) = msg), do: comboInsert(msg)
 
   def proto(NITRO.comboKey(delegate: module) = msg) do
     case has_function(module, :keyUp) do
       true -> module.keyUp(msg)
-      false -> keyUp(msg)
+      false -> NITRO.Combo.Search.keyUp(msg)
+    end
+  end
+
+  def proto(NITRO.comboScroll(delegate: module) = msg) do
+    case has_function(module, :comboScroll) do
+      true -> module.comboScroll(msg)
+      false -> NITRO.Combo.Search.comboScroll(msg)
     end
   end
 
@@ -28,44 +37,27 @@ defmodule NITRO.Combo do
     end
   end
 
-  def keyUp(NITRO.comboKey(value: :all, dom: field0, feed: feed, delegate: module)) do
-    field = :nitro.to_list(field0)
-    all = apply(:kvs,:all,[feed])
-    :nitro.clear(:nitro.atom([:comboContainer, field]))
-    dropDownList0(all, field, module, feed)
-  end
-
-  def keyUp(NITRO.comboKey(value: value0, dom: field0, feed: feed, delegate: module)) do
-    value = :string.lowercase(:unicode.characters_to_list(value0, :unicode))
-    field = :nitro.to_list(field0)
-    all = apply(:kvs,:all,[feed])
-    index = index(module)
-    :nitro.clear(:nitro.atom([:comboContainer, field]))
-    :nitro.wire("comboLookupChange('#{field0}');")
-
-    filtered =
-      Enum.filter(all, fn x ->
-        Enum.any?(index, fn i ->
-          fld0 =
-            if is_function(i) do
-              i.(x)
-            else
-              f0 = apply(:kvs,:field, [x, i])
-              tab = elem(x, 0)
-
-              if f0 == tab do
-                :nitro.to_list(f0)
-              else
-                f0
-              end
-            end
-
-          fld = to_list(fld0)
-          fld != elem(x, 0) and :string.rstr(:string.lowercase(:nitro.to_list(fld)), value) > 0
-        end)
-      end)
-
-    dropDownList0(filtered, field, module, feed)
+  def proto(NITRO.comboInsert(delegate: module) = msg) do
+    case has_function(module, :comboInsert) do
+      true -> module.comboInsert(msg)
+      false -> comboInsert(msg)
+    end
+  end
+
+  def comboInsert(NITRO.comboInsert(chunks: 0, dom: field, status: :finished)) do
+    NITRO.Combo.Search.stop(field)
+    :nitro.wire("activeCombo = undefined; currentItem = undefined;")
+    :nitro.hide(:nitro.atom([:comboContainer, :nitro.to_list(field)]))
+    :nitro.wire("comboOpenFormById('#{:nitro.atom([:nitro.to_list(field), 'form'])}');")
+  end
+
+  def comboInsert(NITRO.comboInsert(dom: field, rows: rows, delegate: module, feed: feed)) do
+    :lists.foreach(fn row ->
+      :nitro.insert_bottom(
+        :nitro.atom([:comboContainer, :nitro.to_list(field)]),
+        :nitro.render(dropDown0(row, :nitro.to_list(field), module, feed))
+      )
+    end, rows)
   end
 
   def dropDownList0(filtered, field, module, feed) do

+ 104 - 0
lib/comboSearch.ex

@@ -0,0 +1,104 @@
+defmodule NITRO.Combo.Search do
+  require NITRO
+  require Record
+
+  Record.defrecord(:state, pid: [], chunks: 0, value: [], reader: [], opts: [], feed: [])
+
+  def start(value, field, feed, opts) do
+    Supervisor.start_link([], strategy: :one_for_one, name: NITRO.Combo.Search)
+    state = state(pid: self(), value: value, reader: :erlang.apply(:kvs, :reader, [feed]), opts: opts, feed: feed)
+    stop(field)
+    pi =
+      NITRO.pi(
+        module: NITRO.Combo.Search,
+        table: :async,
+        sup: NITRO.Combo.Search,
+        state: state,
+        name: "#{field}",
+        timeout: :brutal_kill,
+        restart: :temporary
+      )
+    case :nitro_pi.start(pi) do
+      {pid,_} -> send(pid, {:filterComboValues, :init, value})
+      x -> x
+    end
+  end
+
+  def stop(field) do
+    case :nitro_pi.pid(:async, "#{field}") do
+      [] -> :ok
+      pid ->
+        :erlang.exit(pid, :kill)
+         try do :nitro_pi.stop(:async, "#{field}") catch _,_ -> :skip end
+    end
+  end
+
+  def comboScroll(NITRO.comboScroll(dom: field)) do
+    case :nitro_pi.pid(:async, "#{field}") do
+      [] -> []
+      pid -> send(pid, {:filterComboValues, :append, []})
+    end
+  end
+
+  def keyUp(NITRO.comboKey(value: value, dom: field, feed: feed, delegate: module)) do
+    opts = [index: NITRO.Combo.index(module), field: field, delegate: module]
+    comboContainer = :nitro.atom([:comboContainer, :nitro.to_list(field)])
+    :nitro.display(:nitro.atom([:comboContainer, field]), :block)
+    :nitro.clear(comboContainer)
+    :nitro.wire("comboCloseFormById('#{:nitro.atom([:nitro.to_list(field), 'form'])}');")
+    :nitro.wire("comboLookupChange('#{field}');")
+    :nitro.wire(NITRO.bind(target: :nitro.to_binary(comboContainer), type: :scroll, postback: onscroll(field, module)))
+    start(value, field, feed, opts)
+  end
+
+  def onscroll(field, delegate), do:
+    :erlang.iolist_to_binary([
+      "if (event.target && (event.target.scrollTop + event.target.offsetHeight + 10 >= event.target.scrollHeight)) {",
+        "ws.send(enc(tuple(atom('direct'),
+         tuple(atom('comboScroll'),",
+            "bin('#{field}'),",
+            "atom('#{delegate}'))",
+        ")));",
+      "}"
+    ])
+
+  def proc(:init, NITRO.pi() = pi), do: {:ok, pi}
+
+  def proc({:filterComboValues, cmd, value0}, NITRO.pi(state: state(chunks: chunks) = st) = pi) do
+    state(feed: feed, reader: r, value: prev, pid: pid, opts: opts) = st
+    m = Keyword.get(opts, :delegate, [])
+    field = Keyword.get(opts, :field, [])
+    value = case cmd do :append -> prev; _ -> :string.lowercase(:unicode.characters_to_list(value0, :unicode)) end
+    r1 = :erlang.apply(:kvs, :take, [:erlang.apply(:kvs, :setfield, [r, :args, 10])])
+    case :erlang.apply(:kvs, :field, [r1, :args]) do
+      [] ->
+        send(pid, {:direct, NITRO.comboInsert(dom: field, delegate: m, chunks: chunks, status: :finished)})
+        {:stop, :normal, NITRO.pi(pi, state: state(st, reader: :erlang.apply(:kvs, :setfield, [r1, :args, []])))}
+      rows ->
+        filtered =
+          case value do
+            'all' -> rows
+            _ ->
+              :lists.filter(fn x -> :lists.any(&filter(value, x, &1), Keyword.get(opts, :index, [])) end, rows)
+          end
+        newChunks = chunks + length(filtered)
+        send(pid, {:direct, NITRO.comboInsert(dom: field, delegate: m, chunks: newChunks, feed: feed, rows: filtered)})
+        chunks < 100 and
+          case :nitro_pi.pid(:async, "#{field}") do [] -> []; pid -> send(pid, {:filterComboValues, cmd, value0}) end
+        {:noreply, NITRO.pi(pi, state: state(st, value: value, chunks: newChunks, reader: :erlang.apply(:kvs, :setfield, [r1, :args, []])))}
+    end
+  end
+
+  def proc(_, pi), do: {:noreply, pi}
+
+  defp filter(val, obj, i) do
+    fld =
+      if is_function(i) do
+        i.(obj)
+      else
+        fldVal = :erlang.apply(:kvs, :field, [obj, i])
+        if fldVal == elem(obj, 0), do: :nitro.to_list(fldVal), else: fldVal
+      end |> NITRO.Combo.to_list()
+    fld != elem(obj, 0) and :string.rstr(:string.lowercase(:nitro.to_list(fld)), val) > 0
+  end
+end

+ 1 - 1
mix.exs

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

+ 3 - 1
src/elements/combo/element_comboLookup.erl

@@ -5,7 +5,9 @@
 
 proto(#comboKey{delegate=Module}=Msg)    -> Module:proto(Msg);
 proto(#comboKeyup{delegate=Module}=Msg)  -> Module:proto(Msg);
-proto(#comboSelect{delegate=Module}=Msg) -> Module:proto(Msg).
+proto(#comboSelect{delegate=Module}=Msg) -> Module:proto(Msg);
+proto(#comboScroll{delegate=Module}=Msg) -> Module:proto(Msg);
+proto(#comboInsert{delegate=Module}=Msg) -> Module:proto(Msg).
 
 render_element(#comboLookup{id=Id, style=Style, value = Val, bind = Object,
   feed = Feed, disabled = Disabled, delegate = Module, class = Class} = Data) ->

+ 86 - 0
src/nitro_pi.erl

@@ -0,0 +1,86 @@
+-module(nitro_pi).
+-description('NITRO Process Instance'). % gen_server replacement
+-include_lib("nitro/include/nitro_pi.hrl").
+-include_lib("nitro/include/n2o.hrl").
+-behaviour(gen_server).
+-export([start_link/1]).
+-export([init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2,code_change/3]).
+-export([start/1,stop/2,send/2,send/3,cast/2,cast/3,pid/2,restart/2]).
+
+start(#pi{table=Tab,name=Name,module=Module,sup=Sup,timeout=Timeout,restart=Restart} = Async) ->
+    ChildSpec = {{Tab,Name},{?MODULE,start_link,[Async]},Restart,Timeout,worker,[Module]},
+    case supervisor:start_child(Sup,ChildSpec) of
+               {ok,Pid} -> {Pid,Async#pi.name};
+             {ok,Pid,_} -> {Pid,Async#pi.name};
+         {error,Reason} -> {error,Reason} end.
+
+stop(Tab,Name) ->
+    case nitro_pi:pid(Tab,Name) of
+        Pid when is_pid(Pid) ->
+                #pi{sup=Sup} = Async = send(Pid,{get}),
+                [ supervisor:F(Sup,{Tab,Name})
+                  || F <- [ terminate_child , delete_child ] ],
+                cache(Tab,{Tab,Name},undefined),
+                Async;
+        Data -> {error,{not_pid,Data}} end.
+
+send(Pid,Message) when is_pid(Pid) -> gen_server:call(Pid,Message).
+send(Tab,Name,Message) -> gen_server:call(nitro_pi:pid(Tab,Name),Message).
+
+cast(Pid,Message) when is_pid(Pid) -> gen_server:cast(Pid,Message).
+cast(Tab,Name,Message) -> gen_server:cast(nitro_pi:pid(Tab,Name),Message).
+
+cache(Tab, Key) ->
+    Res = ets:lookup(Tab,Key),
+    Val = case Res of [] -> []; [Value] -> Value; Values -> Values end,
+    case Val of [] -> [];
+                {_,{infinity,X}} -> X;
+                {_,{Expire,X}} -> case Expire < calendar:local_time() of
+                                  true ->  ets:delete(Tab,Key), [];
+                                  false -> X end end.
+cache(Tab, Key, undefined)   -> ets:delete(Tab,Key);
+cache(Tab, Key, Value)       -> cache(Tab, Key, Value, infinity).
+cache(Tab, Key, Value, Till) -> ets:insert(Tab,{Key,{Till,Value}}), Value.
+
+pid(Tab,Name) -> cache(Tab,{Tab,Name}).
+
+restart(Tab,Name) ->
+    case stop(Tab,Name)  of
+          #pi{}=Async -> start(Async);
+                     Error -> Error end.
+
+handle(Mod,Message,Async) ->
+  case Mod:proc(Message,Async) of
+                {ok,S} -> {ok,S};
+              {ok,S,T} -> {ok,S,T};
+          {stop,X,Y,S} -> {stop,X,Y,S};
+            {stop,X,S} -> {stop,X,S};
+              {stop,S} -> {stop,S};
+         {reply,X,S,T} -> {reply,X,S,T};
+           {reply,X,S} -> {reply,X,S};
+         {noreply,X,S} -> {noreply,X,S};
+           {noreply,S} -> {noreply,S};
+                 {_,S} -> {noreply,S};
+                     S -> {noreply,S} end.
+
+start_link (Parameters)    -> gen_server:start_link(?MODULE, Parameters, []).
+code_change(_,State,_)     -> {ok, State}.
+handle_call({get},_,Async) -> {reply,Async,Async};
+handle_call(_,_,#pi{module=undefined}) -> {noreply,[]};
+handle_call(Message,_,#pi{module=Mod}=Async) -> handle(Mod,Message,Async).
+handle_cast(_,  #pi{module=undefined}) -> {noreply,[]};
+handle_cast(Message,  #pi{module=Mod}=Async) -> handle(Mod,Message,Async).
+handle_info(timeout,  #pi{module=undefined}) -> {noreply,[]};
+handle_info(timeout,  #pi{module=Mod}=Async) -> handle(Mod,timeout,Async);
+handle_info(_,  #pi{module=undefined}) -> {noreply,[]};
+handle_info(Message,  #pi{module=Mod}=Async) -> handle(Mod,Message,Async);
+handle_info(_,  _) -> {noreply,[]}.
+
+init(#pi{module=Mod,table=Tab,name=Name}=Handler) ->
+    cache(Tab,{Tab,Name},self(),infinity),
+    Mod:proc(init,Handler).
+
+terminate(_Reason, #pi{name=Name,sup=Sup,table=Tab}) ->
+    spawn(fun() -> supervisor:delete_child(Sup,{Tab,Name}) end),
+    catch cache(Tab,{Tab,Name},undefined), ok.
+