re-work the bookmarks table to incorporate the improved frequency input

This commit is contained in:
Jakob Ketterl 2021-03-27 23:08:43 +01:00
parent e1dd9d32f4
commit 29c0f7148a
2 changed files with 256 additions and 80 deletions

View File

@ -1,56 +1,218 @@
function Editor(table) {
this.table = table;
}
Editor.prototype.getInputHtml = function() {
return '<input>';
}
Editor.prototype.render = function(el) {
this.input = $(this.getInputHtml());
el.append(this.input);
this.setupEvents();
};
Editor.prototype.setupEvents = function() {
var me = this;
this.input.on('blur', function() { me.submit(); }).on('change', function() { me.submit(); }).on('keydown', function(e){
if (e.keyCode == 13) return me.submit();
if (e.keyCode == 27) return me.cancel();
});
};
Editor.prototype.submit = function() {
if (!this.onSubmit) return;
var submit = this.onSubmit;
delete this.onSubmit;
submit();
};
Editor.prototype.cancel = function() {
if (!this.onCancel) return;
var cancel = this.onCancel;
delete this.onCancel;
cancel();
};
Editor.prototype.focus = function() {
this.input.focus();
};
Editor.prototype.disable = function(flag) {
this.input.prop('disabled', flag);
};
Editor.prototype.setValue = function(value) {
this.input.val(value);
};
Editor.prototype.getValue = function() {
return this.input.val();
};
Editor.prototype.getHtml = function() {
return this.getValue();
};
function NameEditor(table) {
Editor.call(this, table);
}
NameEditor.prototype = new Editor();
NameEditor.prototype.getInputHtml = function() {
return '<input class="form-control form-control-sm" type="text">';
}
function FrequencyEditor(table) {
Editor.call(this, table);
this.suffixes = {
'K': 3,
'M': 6,
'G': 9,
'T': 12
};
}
FrequencyEditor.prototype = new Editor();
FrequencyEditor.prototype.getInputHtml = function() {
return '<div class="input-group input-group-sm exponential-input" name="frequency">' +
'<input class="form-control form-control-sm" type="number" step="1">' +
'<div class="input-group-append">' +
'<select class="input-group-text exponent">' +
'<option value="0">Hz</option>' +
$.map(this.suffixes, function(v, k) {
// fix lowercase "kHz"
if (k === "K") k = "k";
return '<option value="' + v + '">' + k + 'Hz</option>';
}).join('') +
'</select>' +
'</div>' +
'</div>';
};
FrequencyEditor.prototype.render = function(el) {
this.input = $(this.getInputHtml());
el.append(this.input);
this.freqInput = el.find('input');
this.expInput = el.find('select');
this.setupEvents();
};
FrequencyEditor.prototype.setupEvents = function() {
var me = this;
var inputs = [this.freqInput, this.expInput].map(function(i) { return i[0]; });
inputs.forEach(function(input) {
$(input).on('blur', function(e){
if (inputs.indexOf(e.relatedTarget) >= 0) {
return;
}
me.submit();
});
});
var me = this;
this.freqInput.on('keydown', function(e){
if (e.keyCode == 13) return me.submit();
if (e.keyCode == 27) return me.cancel();
var c = String.fromCharCode(e.which);
if (c in me.suffixes) {
me.expInput.val(me.suffixes[c]);
}
});
}
FrequencyEditor.prototype.getValue = function() {
var frequency = parseFloat(this.freqInput.val());
var exp = parseInt(this.expInput.val());
return Math.floor(frequency * 10 ** exp);
};
FrequencyEditor.prototype.setValue = function(value) {
var value = parseFloat(value);
var exp = 0;
if (!Number.isNaN(value)) {
exp = Math.floor(Math.log10(value) / 3) * 3;
}
this.freqInput.val(value / 10 ** exp);
this.expInput.val(exp);
};
FrequencyEditor.prototype.focus = function() {
this.freqInput.focus();
};
FrequencyEditor.prototype.getHtml = function() {
var value = this.getValue();
var exp = 0;
if (!Number.isNaN(value)) {
exp = Math.floor(Math.log10(value) / 3) * 3;
}
var frequency = value / 10 ** exp;
var expString = this.expInput.find('option[value=' + exp + ']').html();
return frequency + ' ' + expString;
};
function ModulationEditor(table) {
Editor.call(this, table);
this.modes = table.data('modes');
}
ModulationEditor.prototype = new Editor();
ModulationEditor.prototype.getInputHtml = function() {
return '<select class="form-control form-control-sm">' +
$.map(this.modes, function(name, modulation) {
return '<option value="' + modulation + '">' + name + '</option>';
}).join('') +
'</select>';
};
ModulationEditor.prototype.getHtml = function() {
var $option = this.input.find('option:selected')
return $option.html();
};
$.fn.bookmarktable = function() { $.fn.bookmarktable = function() {
var editors = {
name: NameEditor,
frequency: FrequencyEditor,
modulation: ModulationEditor
};
$.each(this, function(){ $.each(this, function(){
var $table = $(this).find('table'); var $table = $(this).find('table');
var inputs = $table.find('tr.inputs td').map(function(){
var candidates = $(this).find('input, select')
return candidates.length ? candidates.first() : false;
}).toArray();
$table.find('tr.inputs').remove();
var transformToHtml = function($cell) {
var $input = $cell.find('input, select');
var $option = $input.find('option:selected')
if ($option.length) {
$cell.html($option.html());
} else {
$cell.html($input.val());
}
};
$table.on('dblclick', 'td', function(e) { $table.on('dblclick', 'td', function(e) {
var $cell = $(e.target); var $cell = $(e.target);
var html = $cell.html(); var html = $cell.html();
var $row = $cell.parents('tr'); var $row = $cell.parents('tr');
var index = $row.find('td').index($cell); var name = $cell.data('editor');
var EditorCls = editors[name];
if (!EditorCls) return;
var $input = inputs[index]; var editor = new EditorCls($table);
if (!$input) return; editor.render($cell.html(''));
editor.setValue($cell.data('value'));
editor.focus();
$table.find('tr[data-id="new"]').remove(); editor.onSubmit = function() {
$input.val($cell.data('value') || html); editor.disable(true);
$input.prop('disabled', false);
$cell.html($input);
$input.focus();
var submit = function() {
$input.prop('disabled', true);
$.ajax(document.location.href + "/" + $row.data('id'), { $.ajax(document.location.href + "/" + $row.data('id'), {
data: JSON.stringify(Object.fromEntries([[$input.prop('name'), $input.val()]])), data: JSON.stringify(Object.fromEntries([[name, editor.getValue()]])),
contentType: 'application/json', contentType: 'application/json',
method: 'POST' method: 'POST'
}).then(function(){ }).then(function(){
transformToHtml($cell); $cell.data('value', editor.getValue());
$cell.html(editor.getHtml());
}); });
}; };
$input.on('blur', submit).on('change', submit).on('keyup', function(e){ editor.onCancel = function() {
if (e.keyCode == 13) return submit(); $cell.html(html);
if (e.keyCode == 27) { };
$cell.html(html);
}
});
}); });
$table.on('click', '.bookmark-delete', function(e) { $table.on('click', '.bookmark-delete', function(e) {
@ -70,22 +232,26 @@ $.fn.bookmarktable = function() {
if ($table.find('tr[data-id="new"]').length) return; if ($table.find('tr[data-id="new"]').length) return;
var row = $('<tr data-id="new">'); var row = $('<tr data-id="new">');
row.append(inputs.map(function(i){
var cell = $('<td>'); var inputs = Object.fromEntries(
if (i) { Object.entries(editors).map(function(e) {
i.prop('disabled', false); return [e[0], new e[1]($table)];
i.val(''); })
cell.html(i); );
} else {
cell.html( row.append($.map(inputs, function(editor, name){
'<div class="btn-group btn-group-sm">' + var cell = $('<td data-editor="' + name + '" class="' + name + '">');
'<button type="button" class="btn btn-primary bookmark-save">Save</button>' + editor.render(cell);
'<button type="button" class="btn btn-secondary bookmark-cancel">Cancel</button>' +
'</div>'
);
}
return cell; return cell;
})); }));
row.append($(
'<td>' +
'<div class="btn-group btn-group-sm">' +
'<button type="button" class="btn btn-primary bookmark-save">Save</button>' +
'<button type="button" class="btn btn-secondary bookmark-cancel">Cancel</button>' +
'</div>' +
'</td>'
));
row.on('click', '.bookmark-cancel', function() { row.on('click', '.bookmark-cancel', function() {
row.remove(); row.remove();
@ -93,10 +259,10 @@ $.fn.bookmarktable = function() {
row.on('click', '.bookmark-save', function() { row.on('click', '.bookmark-save', function() {
var data = Object.fromEntries( var data = Object.fromEntries(
row.find('input, select').toArray().map(function(input){ $.map(inputs, function(input, name){
var $input = $(input); input.disable(true);
$input.prop('disabled', true); // double wrapped because jQuery.map() flattens the result
return [$input.prop('name'), $input.val()] return [[name, input.getValue()]];
}) })
); );
@ -107,15 +273,20 @@ $.fn.bookmarktable = function() {
}).then(function(data){ }).then(function(data){
if ('bookmark_id' in data) { if ('bookmark_id' in data) {
row.attr('data-id', data['bookmark_id']); row.attr('data-id', data['bookmark_id']);
row.find('td').each(function(){ var tds = row.find('td');
var $cell = $(this);
var $group = $cell.find('.btn-group') Object.values(inputs).forEach(function(input, index) {
if ($group.length) { var td = $(tds[index]);
$group.remove; td.data('value', input.getValue());
$cell.html('<div class="btn btn-sm btn-danger bookmark-delete">delete</div>'); td.html(input.getHtml());
}
transformToHtml($cell);
}); });
var $cell = row.find('td').last();
var $group = $cell.find('.btn-group');
if ($group.length) {
$group.remove;
$cell.html('<div class="btn btn-sm btn-danger bookmark-delete">delete</div>');
}
} }
}); });

View File

@ -3,6 +3,7 @@ from owrx.controllers.admin import AuthorizationMixin
from owrx.bookmarks import Bookmark, Bookmarks from owrx.bookmarks import Bookmark, Bookmarks
from owrx.modes import Modes from owrx.modes import Modes
import json import json
import math
import logging import logging
@ -18,16 +19,8 @@ class BookmarksController(AuthorizationMixin, WebpageController):
def render_table(self): def render_table(self):
bookmarks = Bookmarks.getSharedInstance() bookmarks = Bookmarks.getSharedInstance()
def render_mode(m):
return """
<option value={mode}>{name}</option>
""".format(
mode=m.modulation,
name=m.name,
)
return """ return """
<table class="table"> <table class="table bookmarks" data-modes='{modes}'>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th class="frequency">Frequency</th> <th class="frequency">Frequency</th>
@ -35,25 +28,35 @@ class BookmarksController(AuthorizationMixin, WebpageController):
<th>Actions</th> <th>Actions</th>
</tr> </tr>
{bookmarks} {bookmarks}
<tr class="inputs" style="display:none;">
<td><input class="form-control form-control-sm" type="text" name="name"></td>
<td><input class="form-control form-control-sm" type="number" step="1" name="frequency"></td>
<td><select class="form-control form-control-sm" name="modulation">{options}</select></td>
<td></td>
</tr>
</table> </table>
""".format( """.format(
bookmarks="".join(self.render_bookmark(b) for b in bookmarks.getBookmarks()), bookmarks="".join(self.render_bookmark(b) for b in bookmarks.getBookmarks()),
options="".join(render_mode(m) for m in Modes.getAvailableModes()), modes=json.dumps({m.modulation: m.name for m in Modes.getAvailableModes()}),
) )
def render_bookmark(self, bookmark: Bookmark): def render_bookmark(self, bookmark: Bookmark):
def render_frequency(freq):
suffixes = {
0: "",
3: "k",
6: "M",
9: "G",
12: "T",
}
exp = int(math.log10(freq) / 3) * 3
num = freq
suffix = ""
if exp in suffixes:
num = freq / 10 ** exp
suffix = suffixes[exp]
return "{num:g} {suffix}Hz".format(num=num, suffix=suffix)
mode = Modes.findByModulation(bookmark.getModulation()) mode = Modes.findByModulation(bookmark.getModulation())
return """ return """
<tr data-id="{id}"> <tr data-id="{id}">
<td>{name}</td> <td data-editor="name" data-value="{name}">{name}</td>
<td class="frequency">{frequency}</td> <td data-editor="frequency" data-value="{frequency}" class="frequency">{rendered_frequency}</td>
<td data-value="{modulation}">{modulation_name}</td> <td data-editor="modulation" data-value="{modulation}">{modulation_name}</td>
<td> <td>
<button type="button" class="btn btn-sm btn-danger bookmark-delete">delete</button> <button type="button" class="btn btn-sm btn-danger bookmark-delete">delete</button>
</td> </td>
@ -61,7 +64,9 @@ class BookmarksController(AuthorizationMixin, WebpageController):
""".format( """.format(
id=id(bookmark), id=id(bookmark),
name=bookmark.getName(), name=bookmark.getName(),
# TODO render frequency in si units
frequency=bookmark.getFrequency(), frequency=bookmark.getFrequency(),
rendered_frequency=render_frequency(bookmark.getFrequency()),
modulation=bookmark.getModulation() if mode is None else mode.modulation, modulation=bookmark.getModulation() if mode is None else mode.modulation,
modulation_name=bookmark.getModulation() if mode is None else mode.name, modulation_name=bookmark.getModulation() if mode is None else mode.name,
) )