ransack_search_element 0.1.0.pre.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dd93e037848a8f700171aec719988067edbf515bbe1e512e4d27e66296d6f8b6
4
+ data.tar.gz: b6ef1f37b77de1ce0e78a07d2b16171c135a484596118c1c34aa611e75698d80
5
+ SHA512:
6
+ metadata.gz: 9caa070a79c203d9f6b69a6ebdf4c9a9f8cef79d33549cf25325ba9895d8ac3b954efb875f227cb96a05483ca89fedde1396e38feb618d0221a9cad927633731
7
+ data.tar.gz: 1dc72e8b176fae901a389a77eb2b5fd1d4712e1f7c8a16cfd1a4d1d05bb8d1381b59a290fa05958ac1f4a51634cbc769ad0a099ed905bdb8d8a0a9d7b88f1088
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 Arthur Dzieniszewski
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # RansackSearchElement
2
+
3
+ Generic [Ransack](https://github.com/activerecord-hackery/ransack) backed element
4
+ to give a dynamic frontend for advanced searching and filtering, such as for
5
+ exploratory joins. Still early in development (refactoring?), so contributions
6
+ welcome. This is originally based on a set of React and Bootstrap elements.
7
+
8
+ ## Todo
9
+
10
+ - [x] Write wrapper Rails tags
11
+ - [ ] Rip out React and replace with Web Components
12
+ - [ ] Rip out Bootstrap and replace with leaner default styles (possibly
13
+ customizable with sass?)
14
+ - [ ] Decouple javascript further
15
+ - [ ] Write adapters to interface with other search backends? (big maybe)
16
+
17
+ ## Usage
18
+ Use `ransack_search_tag` in your views, and supply it with more options
19
+ as of yet to be defined.
20
+
21
+ ## Installation
22
+
23
+ ```ruby
24
+ gem 'ransack_search_element'
25
+ ```
26
+
27
+ And then execute:
28
+ ```bash
29
+ $ bundle
30
+ ```
31
+
32
+ Or install it yourself as:
33
+ ```bash
34
+ $ gem install ransack_search_element
35
+ ```
36
+
37
+ You will also need to install the accompanying npm package,
38
+ `ransack-search-element`:
39
+
40
+ ```
41
+ yarn add ransack-search-element
42
+ ```
43
+
44
+ ## Contributing
45
+ Yeah just send it.
46
+
47
+ ![Alt](https://i.imgur.com/JNGUWm2.gif?noredirect)
48
+
49
+ ## License
50
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
9
+
10
+ require "rake/testtask"
11
+
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'test'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = false
16
+ end
17
+
18
+ task default: :test
@@ -0,0 +1,3 @@
1
+ //= link_directory ../stylesheets/ransack_search_element .css
2
+
3
+ console.log('engine here')
@@ -0,0 +1,19 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
16
+
17
+ html {
18
+ background-color: red;
19
+ }
@@ -0,0 +1,4 @@
1
+ module RansackSearchElement
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ module RansackSearchElement
2
+ module ApplicationHelper
3
+ def ransack_search_tag(columns, queries)
4
+ tag.div is: "ransack-search", data: {
5
+ columns: columns,
6
+ queries: queries,
7
+ }.to_json
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,347 @@
1
+ import React, { useState } from "react";
2
+
3
+ import FilterRow from "./FilterRow";
4
+
5
+ const options = [
6
+ ["Contains", "cont"],
7
+ ["Equals", "eq"],
8
+ ["Less than", "lt"],
9
+ ["Greater than", "gt"],
10
+ ["Less than/equal", "lteq"],
11
+ ["Greater than/equal", "gteq"],
12
+ ];
13
+
14
+ const options_values = options.map((v) => v[1]);
15
+
16
+ /**
17
+ * Recursively iterate through relations with matching prefixes.
18
+ * Attempts to get the accompanying AttributeInfo given a full query.
19
+ * @param full_query
20
+ * @param ass
21
+ */
22
+ const get_association = (
23
+ full_query,
24
+ ass
25
+ ): {
26
+ [association: string]: AttributeInfo;
27
+ } => {
28
+ if (ass instanceof Array) {
29
+ return ass.find((dict) => {
30
+ return full_query.includes(Object.keys(dict)[0]);
31
+ });
32
+ }
33
+
34
+ const key = Object.keys(ass).find((k) => full_query.includes(k));
35
+
36
+ if (typeof key === "undefined") return null;
37
+
38
+ const remainder = full_query.slice(full_query.indexOf(key) + key.length + 1);
39
+
40
+ return get_association(remainder, ass[key]);
41
+ };
42
+
43
+ export type AttributeInfo = {
44
+ type: string;
45
+ label: string;
46
+ ass?: string;
47
+ };
48
+
49
+ export type FilterDropdownProps = {
50
+ columns: {
51
+ root: {
52
+ [key: string]: AttributeInfo;
53
+ };
54
+ associations: {
55
+ [key: string]: {
56
+ [key: string]: AttributeInfo;
57
+ }[];
58
+ }[];
59
+ };
60
+ queries: {
61
+ [key: string]: string;
62
+ };
63
+ base: string;
64
+ };
65
+
66
+ /**
67
+ * This component handles the generation of filters for searching a model.
68
+ *
69
+ * @param props
70
+ * @returns {JSX.Element}
71
+ */
72
+ export default (props: FilterDropdownProps): JSX.Element => {
73
+ const { columns, queries } = props;
74
+
75
+ const { root: colNames, associations } = columns;
76
+
77
+ const possible_values = Object.keys(colNames).concat(
78
+ associations.map((v) => Object.keys(v)[0]).flat()
79
+ );
80
+
81
+ const [filters, setFilters] = useState<{
82
+ [key: string]: {
83
+ label: string;
84
+ query: string;
85
+ type: string;
86
+ value: string | number;
87
+ ass?: string;
88
+ }[];
89
+ }>(
90
+ /**
91
+ * Here we initialize parameters from the query string,
92
+ * and do basic validation for options and attributes.
93
+ */
94
+ (() => {
95
+ let s = queries || {};
96
+
97
+ let f = {};
98
+
99
+ if (Object.entries(s)?.length > 0) {
100
+ for (const [k, v] of Object.entries(s)) {
101
+ // sorting parameter
102
+ if (k === "s") continue;
103
+
104
+ const query = options_values
105
+ .filter((v) => k.includes(v))
106
+ .reduce((x, y) => (x.length > y.length ? x : y), "");
107
+
108
+ const attribute = possible_values
109
+ .filter((v) => k.includes(v))
110
+ .reduce((x, y) => (x.length > y.length ? x : y), "");
111
+
112
+ const asses = get_association(
113
+ k,
114
+ associations.reduce((acc, cur) => ({ ...acc, ...cur }), {})
115
+ );
116
+
117
+ if (asses) {
118
+ const full_ass_key = Object.keys(asses)[0];
119
+ const full_ass_values = Object.values(asses)[0];
120
+
121
+ const t = {};
122
+ t["query"] = query;
123
+ t["value"] = v;
124
+ t["type"] = full_ass_values?.type;
125
+ t["label"] = full_ass_values?.label;
126
+ t["ass"] = attribute;
127
+
128
+ if (query.length > 0 && attribute.length > 0) {
129
+ if (f[`${attribute}_${full_ass_key}`] instanceof Array) {
130
+ f[`${attribute}_${full_ass_key}`].push(t);
131
+ } else {
132
+ f[`${attribute}_${full_ass_key}`] = [];
133
+ f[`${attribute}_${full_ass_key}`].push(t);
134
+ }
135
+ }
136
+ } else {
137
+ const t = {};
138
+ t["query"] = query;
139
+ t["value"] = v;
140
+ t["type"] = colNames[attribute]?.type;
141
+ t["label"] = colNames[attribute]?.label;
142
+
143
+ if (query.length > 0 && attribute.length > 0) {
144
+ if (f[`${attribute}`] instanceof Array) {
145
+ f[`${attribute}`].push(t);
146
+ } else {
147
+ f[`${attribute}`] = [];
148
+ f[`${attribute}`].push(t);
149
+ }
150
+ }
151
+ }
152
+ }
153
+ } else {
154
+ return {};
155
+ }
156
+
157
+ return f;
158
+ })()
159
+ );
160
+
161
+ /**
162
+ * The constructed query string to replace the current location when
163
+ * the user "filters". Consists of compounding attribute value with its
164
+ * comparison in the case of a real attribute for the model, or by compounding
165
+ * the association entity's name, with the attribute, and its comparison in
166
+ * the case of an association.
167
+ */
168
+ const searchValue =
169
+ window.location.pathname +
170
+ "?" +
171
+ Object.entries(filters)
172
+ .map(([k, v]) => {
173
+ return v
174
+ .map((x) => {
175
+ if (x["ass"]) {
176
+ return (
177
+ `q[${x["ass"] + "_" + k + "_" + x["query"]}]` +
178
+ "=" +
179
+ (x["value"] || "")
180
+ );
181
+ } else {
182
+ return `q[${k + "_" + x["query"]}]` + "=" + (x["value"] || "");
183
+ }
184
+ })
185
+ .join("&");
186
+ })
187
+ .join("&");
188
+
189
+ return (
190
+ <div className="card shadow-sm mb-2" style={{ minHeight: "150px" }}>
191
+ <div className="card-body">
192
+ <div className="card-title">
193
+ <h4>Filters</h4>
194
+ </div>
195
+ {Object.keys(filters).map((v) => {
196
+ return filters[v].map((x, i) => (
197
+ <FilterRow
198
+ key={x}
199
+ idx={i}
200
+ attribute={v}
201
+ filters={filters}
202
+ setFilters={setFilters}
203
+ options={options}
204
+ />
205
+ ));
206
+ })}
207
+ </div>
208
+ <div className="card-footer d-flex align-items-center">
209
+ <div className="d-flex">
210
+ <div className="me-1">
211
+ <a role="button" className="btn btn-primary" href={searchValue}>
212
+ Filter
213
+ </a>
214
+ </div>
215
+ <div className="dropdown me-1">
216
+ <button
217
+ className="btn btn-secondary dropdown-toggle"
218
+ type="button"
219
+ id="dropdownMenuButton"
220
+ data-bs-toggle="dropdown"
221
+ aria-haspopup="true"
222
+ aria-expanded="false"
223
+ >
224
+ Add Filter
225
+ </button>
226
+ <ul className="dropdown-menu" aria-labelledby="dropdownMenuButton">
227
+ {Object.keys(colNames).map((v) => (
228
+ <li key={v}>
229
+ <a
230
+ href=""
231
+ className="dropdown-item"
232
+ onClick={(e) => {
233
+ e.preventDefault();
234
+ let copy = { ...filters };
235
+ const t = {
236
+ query: "",
237
+ type: "",
238
+ label: "",
239
+ value: "",
240
+ };
241
+ t["query"] = "eq";
242
+ t["type"] = colNames[v]?.type || associations[v]?.type;
243
+ t["label"] = colNames[v]?.label || associations[v]?.label;
244
+
245
+ if (copy[v] instanceof Array) {
246
+ copy[v].push(t);
247
+ } else {
248
+ copy[v] = [];
249
+ copy[v].push(t);
250
+ }
251
+
252
+ setFilters(copy);
253
+ }}
254
+ >
255
+ {colNames[v]?.label || associations[v]?.label}
256
+ </a>
257
+ </li>
258
+ ))}
259
+ {associations?.length > 0 ? (
260
+ <li>
261
+ <hr className="dropdown-divider" />
262
+ </li>
263
+ ) : null}
264
+ {associations?.map((x, i) => {
265
+ const association = Object.keys(associations[i])[0];
266
+ const attributes = associations[i][association];
267
+
268
+ return (
269
+ <div className="dropdown dropend">
270
+ <a
271
+ className="dropdown-item dropdown-toggle"
272
+ href="#"
273
+ id="dropdown-layouts"
274
+ data-bs-toggle="dropdown"
275
+ aria-haspopup="true"
276
+ aria-expanded="false"
277
+ >
278
+ {association}
279
+ </a>
280
+ <div
281
+ className="dropdown-menu"
282
+ aria-labelledby="dropdown-layouts"
283
+ >
284
+ {attributes.map((v) => {
285
+ const attr = Object.keys(v)[0];
286
+ const values = Object.values(v)[0];
287
+
288
+ return (
289
+ <li key={attr}>
290
+ <a
291
+ href=""
292
+ className="dropdown-item"
293
+ onClick={(e) => {
294
+ e.preventDefault();
295
+ let copy = { ...filters };
296
+
297
+ const t = {
298
+ query: "",
299
+ type: "",
300
+ label: "",
301
+ value: "",
302
+ };
303
+ t["query"] = "eq";
304
+ t["type"] = values.type;
305
+ t["label"] = values.label;
306
+ t["ass"] = values.ass;
307
+
308
+ if (copy[attr] instanceof Array) {
309
+ copy[attr].push(t);
310
+ } else {
311
+ copy[attr] = [];
312
+ copy[attr].push(t);
313
+ }
314
+
315
+ setFilters(copy);
316
+ }}
317
+ >
318
+ {values.label}
319
+ </a>
320
+ </li>
321
+ );
322
+ })}
323
+ </div>
324
+ </div>
325
+ );
326
+ })}
327
+ </ul>
328
+ </div>
329
+ </div>
330
+ <div>
331
+ <a href={window.location.pathname}>
332
+ <button className="btn btn-danger">Reset</button>
333
+ </a>
334
+ </div>
335
+ </div>
336
+ </div>
337
+ );
338
+ };
339
+
340
+ /*
341
+
342
+
343
+ */
344
+
345
+ /*
346
+
347
+ */
@@ -0,0 +1,115 @@
1
+ import React from "react";
2
+ import FormInput from "./FormInput";
3
+
4
+ function getOptions(type) {
5
+ switch (type) {
6
+ case "datetime":
7
+ return [
8
+ ["Equals", "eq"],
9
+ ["Before", "lt"],
10
+ ["After", "gt"],
11
+ ["Before/on", "lteq"],
12
+ ["After/on", "gteq"],
13
+ ];
14
+ case "text":
15
+ return [
16
+ ["Contains", "cont"],
17
+ ["Equals", "eq"],
18
+ ];
19
+ default:
20
+ return [
21
+ ["Contains", "cont"],
22
+ ["Equals", "eq"],
23
+ ["Less than", "lt"],
24
+ ["Greater than", "gt"],
25
+ ["Less than/equal", "lteq"],
26
+ ["Greater than/equal", "gteq"],
27
+ ];
28
+ }
29
+ }
30
+
31
+ export default (props) => {
32
+ const { filters, setFilters, attribute, idx } = props;
33
+
34
+ const { query, value, type, label } = filters[attribute][idx];
35
+
36
+ const options = getOptions(type);
37
+
38
+ return (
39
+ <div
40
+ className="d-flex align-items-center filter-row w-100 mb-3"
41
+ style={{ maxWidth: "650px" }}
42
+ key={attribute}
43
+ >
44
+ <a
45
+ className="border rounded-circle me-4"
46
+ style={{
47
+ height: "35px",
48
+ width: "35px",
49
+ display: "flex",
50
+ justifyContent: "center",
51
+ alignItems: "center",
52
+ }}
53
+ href=""
54
+ onClick={(e) => {
55
+ e.preventDefault();
56
+ let copy = { ...filters };
57
+ delete copy[attribute][idx];
58
+
59
+ setFilters(copy);
60
+ }}
61
+ >
62
+ <i className="fas fa-times" />
63
+ </a>
64
+ <div
65
+ className="d-flex justify-content-start align-items-center me-3 mb-0"
66
+ style={{ minWidth: "100px" }}
67
+ >
68
+ <b>{label}</b>
69
+ </div>
70
+ <div className="w-auto me-3 mb-0">
71
+ <select
72
+ defaultValue={filters[attribute][idx]["query"]}
73
+ className="form-select rounded-pill"
74
+ style={{ minWidth: "100px" }}
75
+ onInput={(e) => {
76
+ const copy = { ...filters };
77
+ copy[attribute][idx] = copy[attribute][idx]
78
+ ? copy[attribute][idx]
79
+ : {};
80
+ copy[attribute][idx]["query"] = e.target.value;
81
+ setFilters(copy);
82
+ }}
83
+ >
84
+ {options.map(([k, val]) => (
85
+ <option
86
+ key={`${k}${val}`}
87
+ defaultValue={`${filters[attribute][idx]["query"] === val}`}
88
+ value={val}
89
+ >
90
+ {k}
91
+ </option>
92
+ ))}
93
+ </select>
94
+ </div>
95
+
96
+ <div className="d-flex mb-0" style={{ minWidth: "100px" }}>
97
+ <FormInput
98
+ idx={idx}
99
+ name={attribute}
100
+ query={filters[attribute][idx]["query"]}
101
+ colType={filters[attribute][idx]["type"]}
102
+ className="form-control me-2"
103
+ filters={filters}
104
+ setFilters={setFilters}
105
+ onInput={(e) => {
106
+ let copy = { ...filters };
107
+ copy[attribute][idx]["value"] = e.target.value;
108
+ setFilters(copy);
109
+ }}
110
+ value={filters[attribute][idx]["value"]}
111
+ />
112
+ </div>
113
+ </div>
114
+ );
115
+ };
@@ -0,0 +1,98 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from "react";
2
+
3
+ import DatePicker from "react-datepicker";
4
+ import { convertToDate } from "../utilities/dateConverter";
5
+
6
+ /**
7
+ * Tries to handle the inputs generically.
8
+ * Unfortunately, lot of timezone stuff because we need to shift to and from the local time zone.
9
+ * @param props
10
+ * @returns {JSX.Element}
11
+ */
12
+ export default (props) => {
13
+ const {
14
+ className,
15
+ style,
16
+ colType,
17
+ value,
18
+ onInput,
19
+ filters,
20
+ setFilters,
21
+ name,
22
+ query,
23
+ idx,
24
+ } = props;
25
+
26
+ const type = useMemo(() => {
27
+ if (colType === "datetime") return "datetime-local";
28
+ if (colType === "integer") return "number";
29
+ if (colType === "text") return "text";
30
+ }, []);
31
+
32
+ const ref = useRef();
33
+
34
+ const [date, setDate] = useState(
35
+ (() => {
36
+ const d = new Date();
37
+ d.setHours(d.getHours() - d.getTimezoneOffset() / 60);
38
+ return d;
39
+ })()
40
+ );
41
+
42
+ useEffect(() => {
43
+ if (colType === "datetime" && value) {
44
+ let copy = { ...filters };
45
+ const d = new Date(value);
46
+ d.setHours(d.getHours() + d.getTimezoneOffset() / 60);
47
+ copy[name][idx]["value"] = convertToDate(d);
48
+ setFilters(copy);
49
+ const adjusted_time = new Date(value);
50
+ adjusted_time.setHours(
51
+ adjusted_time.getHours() + adjusted_time.getTimezoneOffset() / 60
52
+ );
53
+ setDate(adjusted_time);
54
+ } else if (colType === "datetime") {
55
+ let copy = { ...filters };
56
+ copy[name][idx]["value"] = convertToDate(date);
57
+ setFilters(copy);
58
+ const adjusted_time = new Date(date);
59
+ adjusted_time.setHours(
60
+ adjusted_time.getHours() + adjusted_time.getTimezoneOffset() / 60
61
+ );
62
+ setDate(adjusted_time);
63
+ }
64
+ }, []);
65
+
66
+ switch (type) {
67
+ case "datetime-local":
68
+ return (
69
+ <DatePicker
70
+ ref={ref}
71
+ className={`${className} filter-input`}
72
+ selected={date}
73
+ onChange={(e: Date) => {
74
+ let copy = { ...filters };
75
+ copy[name][idx]["value"] = convertToDate(e);
76
+ setFilters(copy);
77
+ setDate(e);
78
+ }}
79
+ />
80
+ );
81
+ break;
82
+
83
+ default:
84
+ return (
85
+ <input
86
+ value={value}
87
+ onInput={(e) => {
88
+ e.preventDefault();
89
+ onInput(e);
90
+ }}
91
+ style={style}
92
+ className={className}
93
+ type={type}
94
+ />
95
+ );
96
+ break;
97
+ }
98
+ };
@@ -0,0 +1,44 @@
1
+ import React, { ReactComponentElement } from "react";
2
+ import FilterDropdown, {
3
+ FilterDropdownProps,
4
+ } from "./components/FilterDropdown";
5
+ import ReactDOM from "react-dom";
6
+ import registerNestedDropdown from "./utilities/registerNestedDropdown";
7
+
8
+ /**
9
+ * Class to handle wrapping of the
10
+ *
11
+ * TODO: remove dependency on React and Bootstrap
12
+ * NOTE: possibly remove dependency on React by just wrapping around a form
13
+ * with a Stimulus controller. Refactor to expose ERB partials.
14
+ */
15
+ class RansackSearch extends HTMLDivElement {
16
+ constructor() {
17
+ super();
18
+ }
19
+
20
+ connectedCallback() {
21
+ const data: FilterDropdownProps = {
22
+ columns: {
23
+ root: {},
24
+ associations: [],
25
+ },
26
+ queries: [],
27
+ ...(JSON.parse(this.getAttribute("data")) || {}),
28
+ };
29
+
30
+ ReactDOM.render(
31
+ [
32
+ React.createElement(FilterDropdown, {
33
+ ...data,
34
+ }),
35
+ ],
36
+ this,
37
+ () => {
38
+ registerNestedDropdown(this);
39
+ }
40
+ );
41
+ }
42
+ }
43
+
44
+ customElements.define("ransack-search", RansackSearch, { extends: "div" });
@@ -0,0 +1,5 @@
1
+ export function convertToDate(date) {
2
+ const offset = date.getTimezoneOffset();
3
+ const new_date = new Date(date.getTime() - offset * 60 * 1000);
4
+ return new_date.toISOString().split("T")[0];
5
+ }
@@ -0,0 +1,67 @@
1
+ import * as bootstrap from "bootstrap";
2
+
3
+ export default function (root: Document | HTMLElement) {
4
+ (function ($bs) {
5
+ const CLASS_NAME = "has-child-dropdown-show";
6
+ $bs.Dropdown.prototype.toggle = (function (_orginal) {
7
+ return function () {
8
+ root.querySelectorAll("." + CLASS_NAME).forEach(function (e) {
9
+ e.classList.remove(CLASS_NAME);
10
+ });
11
+ let dd = this._element
12
+ .closest(".dropdown")
13
+ .parentNode.closest(".dropdown");
14
+ for (; dd && dd !== root; dd = dd.parentNode.closest(".dropdown")) {
15
+ dd.classList.add(CLASS_NAME);
16
+ }
17
+ return _orginal.call(this);
18
+ };
19
+ })($bs.Dropdown.prototype.toggle);
20
+
21
+ root.querySelectorAll(".dropdown").forEach(function (dd) {
22
+ dd.addEventListener("hide.bs.dropdown", function (e) {
23
+ if (this.classList.contains(CLASS_NAME)) {
24
+ this.classList.remove(CLASS_NAME);
25
+ e.preventDefault();
26
+ }
27
+ if (
28
+ e.clickEvent &&
29
+ e.clickEvent
30
+ .composedPath()
31
+ .some(
32
+ (el) => el.classList && el.classList.contains("dropdown-toggle")
33
+ )
34
+ ) {
35
+ e.preventDefault();
36
+ }
37
+ e.stopPropagation(); // do not need pop in multi level mode
38
+ });
39
+ });
40
+
41
+ // for hover
42
+ function getDropdown(element) {
43
+ return $bs.Dropdown.getInstance(element) || new $bs.Dropdown(element);
44
+ }
45
+
46
+ root
47
+ .querySelectorAll(".dropdown-hover, .dropdown-hover-all .dropdown")
48
+ .forEach(function (dd) {
49
+ dd.addEventListener("mouseenter", function (e) {
50
+ let toggle = e.target.querySelector(
51
+ ':scope>[data-bs-toggle="dropdown"]'
52
+ );
53
+ if (!toggle.classList.contains("show")) {
54
+ getDropdown(toggle).toggle();
55
+ }
56
+ });
57
+ dd.addEventListener("mouseleave", function (e) {
58
+ let toggle = e.target.querySelector(
59
+ ':scope>[data-bs-toggle="dropdown"]'
60
+ );
61
+ if (toggle.classList.contains("show")) {
62
+ getDropdown(toggle).toggle();
63
+ }
64
+ });
65
+ });
66
+ })(bootstrap);
67
+ }
@@ -0,0 +1,4 @@
1
+ module RansackSearchElement
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module RansackSearchElement
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module RansackSearchElement
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Ransack search element</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "ransack_search_element/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ RansackSearchElement::Engine.routes.draw do
2
+ end
@@ -0,0 +1,5 @@
1
+ require "ransack_search_element/version"
2
+ require "ransack_search_element/engine"
3
+
4
+ module RansackSearchElement
5
+ end
@@ -0,0 +1,21 @@
1
+ module RansackSearchElement
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace RansackSearchElement
4
+
5
+ config.autoload_once_paths = %W(
6
+ #{root}/app/channels
7
+ #{root}/app/controllers
8
+ #{root}/app/controllers/concerns
9
+ #{root}/app/helpers
10
+ #{root}/app/models
11
+ #{root}/app/models/concerns
12
+ #{root}/app/jobs
13
+ )
14
+
15
+ initializer "ransack_search_element.helpers", before: :load_config_initializers do
16
+ ActiveSupport.on_load(:action_controller_base) do
17
+ helper RansackSearchElement::Engine.helpers
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module RansackSearchElement
2
+ VERSION = '0.1.0-alpha'
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :ransack_search_element do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ransack_search_element
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre.alpha
5
+ platform: ruby
6
+ authors:
7
+ - Arthur Dzieniszewski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-08-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 6.1.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 6.1.4
27
+ description: Generic, advanced search element for Ransack.
28
+ email:
29
+ - arthurdzieniszewski@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - MIT-LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - app/assets/config/ransack_search_element_manifest.js
38
+ - app/assets/stylesheets/ransack_search_element/application.scss
39
+ - app/controllers/ransack_search_element/application_controller.rb
40
+ - app/helpers/ransack_search_element/application_helper.rb
41
+ - app/javascript/ransack-search-element/components/FilterDropdown.tsx
42
+ - app/javascript/ransack-search-element/components/FilterRow.tsx
43
+ - app/javascript/ransack-search-element/components/FormInput.tsx
44
+ - app/javascript/ransack-search-element/index.ts
45
+ - app/javascript/ransack-search-element/utilities/dateConverter.ts
46
+ - app/javascript/ransack-search-element/utilities/registerNestedDropdown.ts
47
+ - app/jobs/ransack_search_element/application_job.rb
48
+ - app/mailers/ransack_search_element/application_mailer.rb
49
+ - app/models/ransack_search_element/application_record.rb
50
+ - app/views/layouts/ransack_search_element/application.html.erb
51
+ - config/routes.rb
52
+ - lib/ransack_search_element.rb
53
+ - lib/ransack_search_element/engine.rb
54
+ - lib/ransack_search_element/version.rb
55
+ - lib/tasks/ransack_search_element_tasks.rake
56
+ homepage: https://github.com/adzienis/ransack-search-element
57
+ licenses:
58
+ - MIT
59
+ metadata:
60
+ allowed_push_host: https://rubygems.org/
61
+ homepage_uri: https://github.com/adzienis/ransack-search-element
62
+ source_code_uri: https://github.com/adzienis/ransack-search-element
63
+ changelog_uri: https://github.com/adzienis/ransack-search-element/CHANGELOG.md
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">"
76
+ - !ruby/object:Gem::Version
77
+ version: 1.3.1
78
+ requirements: []
79
+ rubygems_version: 3.2.22
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Generic, advanced search element for Ransack.
83
+ test_files: []