├── src
├── declaration.d.ts
├── dbEditor.scss
├── dbEditor.html
└── dbEditor.ts
├── tsconfig.json
├── package.json
├── README.md
└── .gitignore
/src/declaration.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.html" {
2 | const content: string;
3 | export default content;
4 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "sourceMap": true,
5 | "strict": true,
6 | "noImplicitReturns": true,
7 | "noImplicitAny": true,
8 | "module": "es6",
9 | "moduleResolution": "node",
10 | "target": "es5",
11 | "lib": ["es2017", "dom"],
12 | "allowSyntheticDefaultImports": true,
13 | "typeRoots": ["./node_modules/@types", "./src/@types"],
14 | "downlevelIteration": true,
15 | "resolveJsonModule": true,
16 | "declaration": true
17 | },
18 | "include": [
19 | "./src/**/*"
20 | ]
21 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "db-editor",
3 | "version": "0.1.0",
4 | "description": "Database viewer / editor for various data types",
5 | "main": "dist/db-editor.js",
6 | "types": "dist/db-editor.d.ts",
7 | "author": {
8 | "name": "Pacharapol Withayasakpunt",
9 | "url": "http://patarapolw.github.io",
10 | "email": "patarapolw@gmail.com"
11 | },
12 | "license": "MIT",
13 | "dependencies": {
14 | "bootstrap": "^4.3.1",
15 | "flatpickr": "^4.5.7",
16 | "jquery": "^3.3.1",
17 | "popper.js": "^1.14.7",
18 | "simplemde": "^1.11.2",
19 | "tingle.js": "^0.14.0"
20 | },
21 | "devDependencies": {
22 | "@types/jquery": "^3.3.29",
23 | "@types/simplemde": "^1.11.7",
24 | "@types/tingle.js": "^0.13.0",
25 | "node-sass": "^4.11.0"
26 | },
27 | "scripts": {
28 | "build": "tsc",
29 | "build-css": "node-sass src/ -o dist/"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DB Editor
2 |
3 | Database editor without HandsOnTable
4 |
5 | ## Example usage
6 |
7 | ```typescript
8 | const editor = new DbEditor({
9 | el: document.getElementById("App")!,
10 | endpoint: "/api/editor/",
11 | convert: md2html,
12 | columns: [
13 | {name: "deck", width: 200, type: "one-line", required: true},
14 | {name: "front", width: 500, type: "markdown", required: true},
15 | {name: "back", width: 500, type: "markdown"},
16 | {name: "tag", width: 150, type: "list", newEntry: false},
17 | {name: "mnemonic", width: 300, type: "markdown", newEntry: false},
18 | {name: "srsLevel", width: 150, type: "number", label: "SRS Level", newEntry: false},
19 | {name: "nextReview", width: 200, type: "datetime", label: "Next Review", newEntry: false}
20 | ]
21 | });
22 | ```
23 |
24 | ```typescript
25 | const editor = new DbEditor({
26 | el: document.getElementById("App")!,
27 | endpoint: "/api/editor/",
28 | convert: md2html,
29 | readOnly: true,
30 | newEntry: false,
31 | columns: [
32 | {name: "deck", width: 200, type: "one-line", required: true},
33 | {name: "front", width: 600, type: "markdown", required: true},
34 | {name: "back", width: 600, type: "markdown"},
35 | {name: "tag", width: 150, type: "list", newEntry: false}
36 | ]
37 | });
38 | ```
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | *.lock
3 | .idea/
4 | logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 | lerna-debug.log*
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 | pids
12 | *.pid
13 | *.seed
14 | *.pid.lock
15 | lib-cov
16 | coverage
17 | .nyc_output
18 | .grunt
19 | bower_components
20 | .lock-wscript
21 | build/Release
22 | node_modules/
23 | jspm_packages/
24 | typings/
25 | .npm
26 | .eslintcache
27 | .node_repl_history
28 | *.tgz
29 | .yarn-integrity
30 | .env
31 | .env.test
32 | .cache
33 | .next
34 | .nuxt
35 | .vuepress/dist
36 | .serverless/
37 | .fusebox/
38 | .dynamodb/
39 | __pycache__/
40 | *.py[cod]
41 | *$py.class
42 | *.so
43 | .Python
44 | build/
45 | develop-eggs/
46 | dist/
47 | downloads/
48 | eggs/
49 | .eggs/
50 | lib/
51 | lib64/
52 | parts/
53 | sdist/
54 | var/
55 | wheels/
56 | pip-wheel-metadata/
57 | share/python-wheels/
58 | *.egg-info/
59 | .installed.cfg
60 | *.egg
61 | MANIFEST
62 | *.manifest
63 | *.spec
64 | pip-log.txt
65 | pip-delete-this-directory.txt
66 | htmlcov/
67 | .tox/
68 | .nox/
69 | .coverage
70 | .coverage.*
71 | nosetests.xml
72 | coverage.xml
73 | *.cover
74 | .hypothesis/
75 | .pytest_cache/
76 | *.mo
77 | *.pot
78 | local_settings.py
79 | db.sqlite3
80 | instance/
81 | .webassets-cache
82 | .scrapy
83 | docs/_build/
84 | target/
85 | .ipynb_checkpoints
86 | profile_default/
87 | ipython_config.py
88 | .python-version
89 | celerybeat-schedule
90 | *.sage.py
91 | .venv
92 | env/
93 | venv/
94 | ENV/
95 | env.bak/
96 | venv.bak/
97 | .spyderproject
98 | .spyproject
99 | .ropeproject
100 | /site
101 | .mypy_cache/
102 | .dmypy.json
103 | dmypy.json
104 | .pyre/
105 |
--------------------------------------------------------------------------------
/src/dbEditor.scss:
--------------------------------------------------------------------------------
1 | @import "~bootstrap/scss/bootstrap.scss";
2 | @import "~tingle.js/src/tingle";
3 | @import "~simplemde/dist/simplemde.min";
4 | @import "~flatpickr/dist/flatpickr";
5 |
6 | html, body {
7 | height: 100%;
8 | overflow: hidden;
9 | }
10 |
11 | .db-editor {
12 | height: 100%;
13 | overflow: hidden;
14 |
15 | .navbar {
16 | height: 60px;
17 | }
18 |
19 | nav {
20 | z-index: 999 !important;
21 | }
22 |
23 | .db-editor-table {
24 | height: calc(100% - 60px);
25 | overflow: scroll;
26 |
27 | thead, thead th {
28 | position: sticky;
29 | top: 0;
30 | z-index: 100;
31 | background-color: white;
32 | }
33 |
34 | td .cell-wrapper {
35 | max-width: 500px;
36 | max-height: 150px !important;
37 | overflow-y: scroll;
38 | }
39 | }
40 |
41 | .navbar-collapse {
42 | background: #f8f9fa;
43 | border: 1px gray !important;
44 | }
45 | }
46 |
47 |
48 | textarea {
49 | resize: none;
50 | }
51 |
52 | pre {
53 | white-space: pre-wrap;
54 | }
55 |
56 | .CodeMirror,
57 | .CodeMirror-fullscreen.CodeMirror,
58 | .editor-preview-side {
59 | max-height: 80% !important;
60 | height: 80% !important;
61 | }
62 |
63 | .db-editor-md-editor {
64 | height: 500px;
65 | overflow: scroll;
66 | }
67 |
68 | form .CodeMirror {
69 | min-height: 100px !important;
70 | height: 100px !important;
71 | }
72 |
73 | .clear {
74 | background: rgba(0, 0, 0, 0);
75 | border: 0px;
76 | }
77 |
78 | .db-editor-new-entry-editor textarea {
79 | height: 150px;
80 | overflow: scroll;
81 | }
82 |
83 | img {
84 | max-width: 500px;
85 | }
--------------------------------------------------------------------------------
/src/dbEditor.html:
--------------------------------------------------------------------------------
1 |
43 |
--------------------------------------------------------------------------------
/src/dbEditor.ts:
--------------------------------------------------------------------------------
1 | import $ from "jquery";
2 | import tingle from "tingle.js";
3 | import SimpleMDE from "simplemde";
4 | import "bootstrap";
5 | import flatpickr from "flatpickr";
6 | import dbEditorHtml from "./dbEditor.html";
7 | import "./dbEditor.scss";
8 |
9 | export interface IColumn {
10 | name: string;
11 | width: number;
12 | readOnly?: boolean;
13 | label?: string;
14 | type?: "one-line" | "multi-line" | "markdown" | "number" | "datetime" | "list";
15 | newEntry?: boolean;
16 | required?: boolean;
17 | requiredText?: string;
18 | convert?: (x: any) => string;
19 | parse?: (x: string) => any;
20 | constraint?: (x: any) => boolean;
21 | }
22 |
23 | export interface IDbEditorSettings {
24 | el: JQuery | HTMLElement;
25 | columns: IColumn[];
26 | endpoint: string;
27 | readOnly?: boolean;
28 | newEntry?: boolean;
29 | convert?: (x: any) => string;
30 | }
31 |
32 | interface IJqList {
33 | [key: string]: JQuery;
34 | }
35 |
36 | interface IMdeList {
37 | [key: string]: SimpleMDE;
38 | }
39 |
40 | interface IModalList {
41 | [key: string]: tingle.modal;
42 | }
43 |
44 | export class DbEditor {
45 | private settings: IDbEditorSettings;
46 | private page = {
47 | current: 0,
48 | count: 0,
49 | from: 0,
50 | to: 0,
51 | total: 0
52 | };
53 |
54 | private $el: IJqList = {};
55 | private mde: IMdeList = {};
56 | private modal: IModalList = {};
57 |
58 | constructor(settings: IDbEditorSettings) {
59 | this.settings = settings;
60 | this.$el.main = $(settings.el);
61 | this.$el.main.addClass("db-editor");
62 | this.$el.main.html(dbEditorHtml);
63 |
64 | this.$el.tbody = $("tbody", settings.el);
65 | this.$el.newEntryButton = $(".db-editor-new-entry-button", settings.el);
66 | this.$el.searchBar = $(".db-editor-search-bar", settings.el);
67 | this.$el.prev = $(".db-editor-prev", settings.el);
68 | this.$el.prevAll = $(".db-editor-prevAll", settings.el);
69 | this.$el.next = $(".db-editor-next", settings.el);
70 | this.$el.nextAll = $(".db-editor-next-all", settings.el);
71 | this.$el.numberCurrent = $(".db-editor-number-current", settings.el);
72 | this.$el.numberTotal = $(".db-editor-number-total", settings.el);
73 |
74 | if (typeof settings.newEntry === "boolean" && !settings.newEntry) {
75 | $(".db-editor-new-entry-button-nav", settings.el).hide();
76 | } else {
77 | this.$el.newEntry = $(`
78 | `);
81 |
82 | for (const col of this.settings.columns) {
83 | if (typeof col.newEntry === "boolean" && !col.newEntry) {
84 | continue;
85 | }
86 |
87 | switch (col.type) {
88 | case "one-line":
89 | case "number":
90 | case "list":
91 | case "datetime":
92 | this.$el[col.name] = $(`
93 | `);
100 | break;
101 | case "markdown":
102 | default:
103 | this.$el[col.name] = $(`
104 |
105 |
106 |
108 |
"`);
109 | }
110 |
111 | this.$el.newEntry.append(this.$el[col.name]);
112 | this.$el[col.name].data("col", col);
113 |
114 | if (col.required) {
115 | $("input, textarea", this.$el[col.name]).parent().append(`
116 |
117 | ${col.requiredText ? col.requiredText : `${toTitle(col.name)} is required.`}
118 |
`);
119 | }
120 | }
121 |
122 | this.modal.newEntry = new tingle.modal({
123 | footer: true,
124 | stickyFooter: false,
125 | onClose: () => {
126 | (this.$el.newEntry.get(0) as HTMLFormElement).reset();
127 | Object.values(this.mde).forEach((el) => el.value(""));
128 | }
129 | });
130 |
131 | this.$el.main.append(this.$el.newEntry);
132 | this.modal.newEntry.setContent(this.$el.newEntry.get(0));
133 | this.modal.newEntry.addFooterBtn("Save", "tingle-btn tingle-btn--primary", () => {
134 | for (const col of this.settings.columns) {
135 | if (col.type === "markdown") {
136 | this.$el[col.name].val(this.mde[col.name].value());
137 | }
138 | }
139 |
140 | for (const col of this.settings.columns) {
141 | if (col.required) {
142 | col.constraint = col.constraint ? col.constraint : (x: any) => !!x;
143 | if (!col.constraint!(this.$el[col.name].val())) {
144 | this.$el.newEntry.addClass("was-validated");
145 | return;
146 | }
147 | }
148 | }
149 |
150 | const entry = {} as any;
151 | for (const col of this.settings.columns) {
152 | entry[col.name] = this.$el[col.name].val();
153 | }
154 |
155 | this.addEntry(entry, true);
156 | this.modal.newEntry.close();
157 | });
158 | }
159 |
160 | if (!settings.readOnly) {
161 | this.$el.mdEditor = $(`
162 |
163 |
164 |
`);
165 | this.$el.main.append(this.$el.mdEditor);
166 |
167 | this.$el.listEditor = $(`
168 | `);
174 | this.$el.main.append(this.$el.listEditor);
175 |
176 | this.mde.mdEditor = new SimpleMDE({
177 | element: $("textarea", this.$el.mdEditor).get(0),
178 | spellChecker: false,
179 | previewRender: settings.convert
180 | });
181 |
182 | for (const col of this.settings.columns) {
183 | if (col.type === "markdown") {
184 | if (typeof col.newEntry === "boolean" && !col.newEntry) {
185 | continue;
186 | }
187 |
188 | col.convert = col.convert ? col.convert : this.settings.convert;
189 |
190 | this.mde[col.name] = new SimpleMDE({
191 | element: $("textarea", this.$el[col.name]).get(0),
192 | spellChecker: false,
193 | previewRender: col.convert
194 | });
195 | }
196 | }
197 |
198 | this.modal.mdEditor = new tingle.modal({
199 | footer: true,
200 | stickyFooter: false,
201 | onClose: () => this.mde.mdEditor.value("")
202 | });
203 | this.modal.mdEditor.setContent(this.$el.mdEditor.get(0));
204 | this.modal.mdEditor.addFooterBtn("Save", "tingle-btn tingle-btn--primary", () => {
205 | const val = this.mde.mdEditor.value();
206 | const $target = this.$el.mdEditor.data("$target");
207 | const convertFn = $target.data("col").convert;
208 |
209 | this.updateServer($target, val)
210 | .then(() => convertFn ? $target.html(convertFn(val)) : $target.text(val));
211 |
212 | this.modal.mdEditor.close();
213 | });
214 |
215 | this.modal.listEditor = new tingle.modal({
216 | footer: true,
217 | stickyFooter: false,
218 | onClose: () => $(".db-editor-list-entry").remove()
219 | });
220 | this.modal.listEditor.setContent(this.$el.listEditor.get(0));
221 | this.modal.listEditor.addFooterBtn("Save", "tingle-btn tingle-btn--primary", () => {
222 | const ls = $(".db-editor-list-entry input").toArray().map((el) => $(el).val()).filter((el) => el).sort();
223 | const $target = this.$el.mdEditor.data("$target");
224 |
225 | this.updateServer($target, ls)
226 | .then(() => $target.text(ls.join("\n")));
227 |
228 | this.modal.listEditor.close();
229 | });
230 | }
231 |
232 | this.$el.prevAll.on("click", () => {
233 | this.page.current = 1;
234 | this.fetchData();
235 | });
236 |
237 | this.$el.prev.on("click", () => {
238 | this.page.current--;
239 | this.fetchData();
240 | });
241 |
242 | this.$el.next.on("click", () => {
243 | this.page.current++;
244 | this.fetchData();
245 | });
246 |
247 | this.$el.nextAll.on("click", () => {
248 | this.page.current = this.page.count;
249 | this.fetchData();
250 | });
251 |
252 | this.$el.newEntryButton.on("click", () => {
253 | this.modal.newEntry.open();
254 | });
255 |
256 | if (!settings.readOnly) {
257 | this.$el.tbody.on("click", "td", (e) => {
258 | const $target = $(e.target).closest("td");
259 | const fieldName: string = $target.data("name");
260 | const fieldData = $target.data("data");
261 | const col: IColumn = $target.data("col");
262 |
263 | if (col.type === "datetime") {
264 | $target.find("input").get(0)._flatpickr.open();
265 | return;
266 | }
267 |
268 | if (col.type === "list") {
269 | if (fieldData) {
270 | fieldData.forEach((el: string) => this.addListEntry(el));
271 | }
272 | this.addListEntry();
273 |
274 | $(".db-entry-list-entry input", settings.el).each((i, el) => this.checkListInput(el as HTMLInputElement));
275 | this.$el.listEditor.data("$target", $target);
276 |
277 | this.modal.listEditor.open();
278 | return;
279 | }
280 |
281 | if (col.type === "markdown") {
282 | this.mde.mdEditor.value(fieldData);
283 | this.$el.mdEditor.data("$target", $target);
284 | this.modal.mdEditor.open();
285 | setTimeout(() => this.mde.mdEditor.codemirror.refresh(), 0);
286 | return;
287 | }
288 |
289 | const data = {
290 | offset: $target.offset(),
291 | height: e.target.clientHeight,
292 | width: e.target.clientWidth,
293 | fieldName,
294 | fieldData
295 | };
296 |
297 | const $input = $("