Commit d2214db7 authored by S Anand's avatar S Anand
Browse files

ENH: Editable FormHandler table. Fixes #35 @abinesh.lal

parent 1f6df7b4
Pipeline #47037 failed with stage
in 1 minute and 54 seconds
......@@ -277,14 +277,18 @@ In the above example, `data-page-size="10"` over-rides `pageSize: 20`.
The full list of options is below. Simple options can be specified as `data-` attributes as well.
- `src`: [FormHandler][formhandler] URL endpoint
- `namespace`: (Optional) If the URL has `?name:key=value`, the filter
`key=value` only applies to formhandlers with namespace as `name`.
Filters without a namespace like `?key=value` will apply to all formhandlers.
- `columns`: comma-separated column names to display, or a list of objects with these keys:
- `name`: column name. `"*"` is a special column placeholder for "all columns"
- `name`: column name. `"*"` is a special column placeholder for "all columns" (options given for `"*"` are applied for all columns)
- `title`: for header display. Defaults to the same value as `name`
- `type`: `text` (default) / `number` / `date`. Data type. Determines filters to be used
- `format`: string / function that returns formatted value.
- function(row, data) returns formatted value
- strings specify a numeral.js format if the value is a number (you must include numeral.js)
- strings specify a moment.js format if the value is a date (you must include moment.js)
- `editable`: `true` (default) / `false`. When `true`, edit and save buttons appears at end of each row.
- `template`: string template / function that renders the cell.
- function accepts an object with these keys:
- `value`: cell data value
......@@ -300,12 +304,15 @@ The full list of options is below. Simple options can be specified as `data-` at
- `link`: string / function that generates a link for this each cell.
- If no `link:` is specified, clicking on the cell filters by that cell.
- If `link:` is a string, opens a new window with the string URL interpolated as a lodash template with `row` as data.
Example: `"https://example.org/city/<%- city >"`
Example: `"https://example.org/city/<%- row.city >"`
- If `link:` is a function, opens a new window with the URL as `fn(row)`.
Example: `function(row) { return 'https://example.org/city/' + row.city }`
- `hideable`: `true` (default) / `false`. Show or hide `Hide` option in header dropdown
- `hide`: `true` / `false` (default). Hides the column
- `unique`: TODO: {dict of query parameter and display value} or [list of values] or function?
- `edit`: Shows the edit control. Can be `true` / `false` (default)
- `actions`: A list of objects. If `data-action='delete'` action name is delete, you need not add it to actions
- `actionName`: a function() that gets triggered on clicking the element with `data-action='actionName` attribute. The function has arguments ... //TODO
- `table`: Shows the table control. Can be:
- `true`: displays a table (default)
- `'grid'`: renders a grid instead of a table
......
......@@ -6,3 +6,5 @@ url:
handler: FormHandler
kwargs:
url: test/formhandler.csv
xsrf_cookies: false # TODO: enable this and test
id: ID
......@@ -24,7 +24,7 @@ export default [
},
{
input: "index-formhandler",
plugins: [htmlparts('src/formhandler.template.html'), uglify()],
plugins: [htmlparts('src/formhandler.template.html'), process.env.npm_lifecycle_event == 'dev' ? '' : uglify()],
output: { file: "dist/formhandler.min.js", format: "umd", name: "g1" }
},
{
......
......@@ -6,9 +6,11 @@ import { parse } from './url.js'
import { namespace } from './namespace_util.js'
// Render components in this order. The empty component is the root component.
var components = ['', 'table', 'page', 'size', 'count', 'export', 'filters', 'error', 'table_grid']
var components = ['', 'table', 'edit', 'add', 'page', 'size', 'count', 'export', 'filters', 'error', 'table_grid']
var default_options = {
table: true,
edit: false,
add: false,
page: true,
pageSize: 100,
size: true,
......@@ -72,6 +74,7 @@ export function formhandler(js_options) {
// Pre-process options
var options = $.extend({}, default_options, js_options, $this.data())
if (!options.columns)
options.columns = []
else if (typeof options.columns == 'string')
......@@ -89,12 +92,23 @@ export function formhandler(js_options) {
meta.rows = data.length
meta.columns = data.length ? _.map(data[0], function (val, col) { return { name: col } }) : []
// Assumption: Column name will not be '*'
if (options.columns.some(function (o) { return o['name'] === '*' }))
options.columns = _.map(meta.columns, function (col) {
// If any column name is '*', show all columns
var star_col = _.find(options.columns, function (o) { return o['name'] === '*' })
if (star_col) {
var action_header_cols = _.cloneDeep(meta.columns)
_.map(options.columns, function(option_col) {
var found = _.find(meta.columns, function (o) { return o['name'] === option_col.name })
if (!found && option_col.name !== '*')
action_header_cols.push(option_col)
})
action_header_cols = _.map(action_header_cols, function (col) {
var options_col = _.find(options.columns, function (o) { return o['name'] === col.name })
return options_col ? options_col : col
return options_col ? options_col : $.extend({}, star_col, col)
})
}
options.columns = action_header_cols ? action_header_cols : options.columns
// Render all components into respective targets
var template_data = {
......@@ -104,31 +118,19 @@ export function formhandler(js_options) {
options: options,
idcount: 0,
parse: parse,
col_defaults: col_defaults
col_defaults: col_defaults,
isEdit: false,
isAdd: false
}
// Store template_data in $this
$this.data('formhandler', template_data)
_.each(components, function (name) {
// Disable components if required. But root component '' is always displayed
if (name && !options[name])
return
var target
// The root '' component is rendered into $this.
if (!name)
target = $this
else {
// Rest are rendered into .<component-name> under $this
if (options[name] == 'grid') name = 'table_grid'
var selector = options[name + 'Target'] || '.' + name
target = $(selector, $this)
// But if they don't exist, treat the selector as a global selecctor
if (target.length == 0)
target = $(selector)
}
target.html(template[name](template_data))
compile_template(name, template_data, options, $this, template)
})
addHandler($this, template_data, options, template)
editHandler($this, template_data, options, template)
}
function render() {
......@@ -141,7 +143,6 @@ export function formhandler(js_options) {
_format: 'json',
_meta: 'y'
}, url_args)
// Show loader
$('.loader', $this).removeClass('d-none')
$.ajax(options.src, {
......@@ -149,7 +150,6 @@ export function formhandler(js_options) {
data: args,
traditional: true,
complete: function () {
// Hide loader
$('.loader', $this).addClass('d-none')
}
}).done(function (data, status, xhr) {
......@@ -173,29 +173,9 @@ export function formhandler(js_options) {
})
}
// Handle modal dialog
$this
.on('shown.bs.modal', '.formhandler-table-modal', function (e) {
var $el = $(e.relatedTarget)
var template_data = $this.data('formhandler')
var op = $el.data('op')
var col = $el.closest('[data-col]').data('col')
var val = ''
// If there is a value, show it, and allow user to remove the filter
if (template_data.args[col + op]) {
val = template_data.args[col + op].join(',')
$('.remove-action', this).attr('href', '?' + col + op + '=').show()
} else
$('.remove-action', this).hide()
$('input', this).val(val).attr('name', col + op).focus()
$('label', this).text($el.text())
})
.on('submit', 'form', function (e) {
e.preventDefault()
var filter = parse('?' + $(this).serialize()).searchKey
$(this).closest('.formhandler-table-modal').modal('hide')
window.location.hash = '#' + parse(location.hash.replace(/^#/, '')).update(filter)
})
modalHandler($this)
actionHandler($this, options, template)
// Re-render every time the URL changes
$(window).on('hashchange', render)
......@@ -205,3 +185,190 @@ export function formhandler(js_options) {
return this
}
function modalHandler($this) {
// Handle modal dialog
$this
.on('shown.bs.modal', '.formhandler-table-modal', function (e) {
var $el = $(e.relatedTarget)
var template_data = $this.data('formhandler')
var op = $el.data('op')
var col = $el.closest('[data-col]').data('col')
var val = ''
// If there is a value, show it, and allow user to remove the filter
if (template_data.args[col + op]) {
val = template_data.args[col + op].join(',')
$('.remove-action', this).attr('href', '?' + col + op + '=').show()
} else
$('.remove-action', this).hide()
$('input', this).val(val).attr('name', col + op).focus()
$('label', this).text($el.text())
})
.on('submit', 'form', function (e) {
e.preventDefault()
var filter = parse('?' + $(this).serialize()).searchKey
$(this).closest('.formhandler-table-modal').modal('hide')
window.location.hash = '#' + parse(location.hash.replace(/^#/, '')).update(filter)
})
}
function compile_template(name, data, options, $this, template) {
// Disable components if required. But root component '' is always displayed
if (name && !options[name])
return
var target
// The root '' component is rendered into $this.
if (!name)
target = $this
else {
if (options[name] == 'grid') name = 'table_grid'
// Rest are rendered into .<component-name> under $this
if (options[name] == 'grid') name = 'table_grid'
var selector = options[name + 'Target'] || '.' + name
target = $(selector, $this)
// But if they don't exist, treat the selector as a global selecctor
if (target.length == 0)
target = $(selector)
}
target.html(template[name](data))
}
function addHandler($this, template_data, options, template) {
$('.add button', $this)
.on('click', function () {
var add_btn = $('.add button', $this)
if (add_btn.html().toLowerCase() == 'save') {
add_btn.html('Add')
var columns_data = $('.new-row input[data-key]')
$('.loader', $this).removeClass('d-none')
var data = {}
$.each(columns_data, function (key, column) {
data[column.getAttribute('data-key')] = column.value
})
$.ajax(options.src, {
method: 'POST',
dataType: 'json',
data: data,
complete: options.add.addFunction ? options.add.addFunction() : function () {
// Hide loader
$('.loader', $this).addClass('d-none')
}
}).done(function (data, status, xhr) {
// TODO: show a bootstrap 4 success/failure message
$('.loader', $this).addClass('d-none')
}).fail(function (xhr, status, message) {
$this.html(template['error']({
message: message
}))
})
// update the current view with out re-render the table
var new_data = {}
if (template_data.data[0]) new_data = Object.assign({}, template_data.data[0])
for (var key in data) {
new_data[key] = data[key]
}
template_data.data.push(new_data)
compile_template('table', Object.assign({}, template_data, { isAdd: false }), options, $this, template)
} else if (add_btn.html().toLowerCase() == 'add') {
add_btn.html('Save')
// TODO: rerender each cell with <input value='' />
compile_template('table', Object.assign({}, template_data, { isAdd: true }), options, $this, template)
$('table tbody input', $this).on('click', function (event) {
// Attach a class to this row
$(this.parentElement.parentElement).addClass('dirty-row')
})
}
})
}
function editHandler($this, template_data, options, template) {
$('.edit button', $this)
.on('click', function () {
var edit_btn = $('.edit button', $this)
if (edit_btn.html().toLowerCase() == 'save') {
edit_btn.html('Edit') // TODO: remove hardcoding of name Edit
var dirty_rows = $('.dirty-row')
if (dirty_rows.length > 0)
$('.loader', $this).removeClass('d-none')
$.each(dirty_rows, function (key, dirty_row) {
var data = JSON.parse(dirty_row.getAttribute('data-val'))
var rowIndex = dirty_row.getAttribute('data-row')
for (var key in data) {
var dirty_td = $('td input[data-key=' + encodeURIComponent(key) + ']', $(dirty_row))
if (dirty_td.length > 0) {
data[key] = template_data['data'][rowIndex][key] = dirty_td.val()
}
}
$.ajax(options.src, {
method: 'PUT',
dataType: 'json',
data: data,
complete: options.edit.editFunction ? options.edit.editFunction() : function () {
$('.loader', $this).addClass('d-none')
}
}).done(function (data, status, xhr) {
// TODO: show a bootstrap 4 success/failure message
$('.loader', $this).addClass('d-none')
}).fail(function (xhr, status, message) {
$this.html(template['error']({
message: message
}))
})
})
compile_template('table', Object.assign({}, template_data, { isEdit: false }), options, $this, template)
} else if (edit_btn.html().toLowerCase() == 'edit') {
edit_btn.html('Save') // TODO: remove hardcoding of name Save
compile_template('table', Object.assign({}, template_data, { isEdit: true }), options, $this, template)
$('table tbody input', $this).on('click', function (event) {
// Attach a class to this row
$(this.parentElement.parentElement).addClass('dirty-row')
})
}
})
}
function actionHandler($this, options, template) {
var default_action_callback = {
'delete': function (row, rowNo) {
$('.loader', $this).removeClass('d-none')
$.ajax(options.src, {
method: 'DELETE',
dataType: 'json',
data: row
}).done(function (data, status, xhr) {
// TODO: show a bootstrap 4 success/failure message
$('.loader', $this).addClass('d-none')
$('.' + $this[0].className + ' tr[data-row="' + rowNo + '"]').hide()
}).fail(function (xhr, status, message) {
$this.html(template['error']({
message: message
}))
})
}
}
$this.on('click', '[data-action]', function () {
var action = $(this).data('action'),
row = $(this).closest('[data-val]').data('val'),
rowNo = $(this).closest('[data-row]').data('row')
if (options.actions && options.actions[action]) {
options.actions[action](row, rowNo)
} else {
default_action_callback[action](row, rowNo)
}
})
}
......@@ -16,6 +16,8 @@ Each template receives these variables:
<div class="formhandler">
<div class="formhandler-table-header d-flex justify-content-between mb-2">
<div class="d-flex flex-wrap">
<div class="edit mr-2"></div>
<div class="add mr-2"></div>
<div class="count mr-2"></div>
<div class="page mr-2"></div>
<div class="size mr-2"></div>
......@@ -25,7 +27,7 @@ Each template receives these variables:
<div class="export"></div>
</div>
</div>
<div class="<%- (options.table == 'grid') ? 'table_grid' : 'table' %>"></div>
<div class="<%- (options.table == 'grid') ? 'table_grid' : 'table table-responsive' %>"></div>
</div>
<div class="loader pos-cc d-none">
<div class="fa fa-spinner fa-spin fa-3x fa-fw"></div>
......@@ -60,11 +62,8 @@ Each template receives these variables:
cols = cols.filter(function(col) { return col.hide !== true})
var form_id = idcount
%>
<% if(options.rowTemplate) { %>
<% _.each(data, function(row) { %>
<%= typeof options.rowTemplate == 'function' ? options.rowTemplate(row, data) : options.rowTemplate %>
<% }) %>
<% } else {%>
<!-- TODO: Abinesh to not override header row in table -->
<table class="table table-sm table-striped">
<thead>
<% _.each(cols, function(colinfo) {
......@@ -100,35 +99,56 @@ Each template receives these variables:
<% }) %>
</thead>
<tbody>
<% _.each(data, function(row) { %>
<tr>
<% if (isAdd) { %>
<tr class="new-row">
<% _.each(cols, function(colinfo) { %>
<% var fmt = typeof(colinfo.format),
val = row[colinfo.name],
disp = fmt == "function" ?
colinfo.format(row, data) :
fmt === "string" && colinfo.type === "number" ?
numeral(val).format(colinfo.format) :
fmt === "string" && colinfo.type === "date" ?
moment(val).format(colinfo.format):
val,
col_link %>
<% if('link' in colinfo) col_link = typeof colinfo.link == 'function' ? colinfo.link(row) : _.template(colinfo.link)(row) %>
<% if(colinfo.template) { %>
<%= typeof colinfo.template == 'function' ? colinfo.template(val, disp, col_link, data) : colinfo.template %>
<% } else if (col_link) { %>
<td><a href="<%- col_link %>" target="_blank"><%= disp %></a></td>
<% } else { %>
<td><a class="urlfilter" href="?<%- colinfo.name %>=<%- val %>&amp;_offset=">
<%= disp %>
</a></td>
<% } %>
<% if (!colinfo.template) { %>
<td><input class="form-control form-control-sm" data-key="<%- colinfo.name %>" value=""/></td>
<% } else { %>
<td></td>
<% } %>
<% }) %>
</tr>
<% }) %>
<% } %>
<% if (options.rowTemplate) { %>
<% _.each(data, function(row) { %>
<%= typeof options.rowTemplate == 'function' ? options.rowTemplate({row: row, data: data, col: cols}) : _.template(options.rowTemplate)({row: row, data: data, cols: cols}) %>
<% }) %>
<% } else {%>
<% _.each(data, function(row, rowIndex) { %>
<tr data-val="<%- JSON.stringify(row) %>" data-row="<%- rowIndex %>">
<% _.each(cols, function(colinfo) { %>
<% var fmt = typeof(colinfo.format),
val = row[colinfo.name],
isEditable = colinfo.editable === undefined ? true : colinfo.editable,
disp = fmt == "function" ?
colinfo.format({row: row, data:data }) :
fmt === "string" && colinfo.type === "number" ?
numeral(val).format(colinfo.format) :
fmt === "string" && colinfo.type === "date" ?
moment(val).format(colinfo.format):
val,
col_link %>
<% if (!isEdit && 'link' in colinfo) var col_link = typeof colinfo.link == 'function' ? colinfo.link({row: row}) : _.template(colinfo.link)({row: row}) %>
<% if (colinfo.template) { %>
<%= typeof colinfo.template == 'function' ? colinfo.template({value: val, format: disp, link: col_link, data: data}) : _.template(colinfo.template)(({value: val, format: disp, link: col_link, data: data})) %>
<% } else if (col_link) { %>
<td><a href="<%- col_link %>" target="_blank"><%= disp %></a></td>
<% } else { %>
<% if (isEdit && isEditable) { %>
<td><input class="form-control form-control-sm" data-key="<%- colinfo.name %>" value="<%- val %>"/></td>
<% } else { %>
<td><a class="urlfilter" href="?<%- colinfo.name %>=<%- val %>&amp;_offset=">
<%= disp %>
</a></td>
<% } %>
<% } %>
<% }) %>
</tr>
<% }) %>
<% } %>
</tbody>
</table>
<% } %>
<!-- end -->
<!-- var template_page -->
......@@ -143,6 +163,7 @@ Each template receives these variables:
</li>
<% if (lo > 1) { %>
<li class="page-item">
<!-- TODO: Review Shouldn't this be _offset=0 ? -->
<a class="page-link" href="?_offset=">1</a>
</li>
<% if (lo > 2) { %>
......@@ -199,6 +220,18 @@ Each template receives these variables:
<% } %>
<!-- end -->
<!-- var template_edit -->
<button type="button" class="btn btn-primary mr-2 btn-sm edit-btn">
Edit
</button>
<!-- end -->
<!-- var template_add -->
<button type="button" class="btn btn-primary mr-2 btn-sm add-btn">
Add
</button>
<!-- end -->
<!-- var template_export -->
<div class="btn-group btn-group-sm" role="group">
<button id="formhandler-export-<%- idcount++ %>" type="button" class="btn btn-light btn-sm dropdown-toggle" data-toggle="dropdown"
......@@ -265,9 +298,9 @@ Each template receives these variables:
var form_id = idcount
var img = (options.icon) ? options.icon : 'http://icons.iconarchive.com/icons/mazenl77/NX11/256/Folder-Default-icon.png'
%>
<% if(options.rowTemplate) { %>
<% if (options.rowTemplate) { %>
<% _.each(data, function(row) { %>
<%= typeof options.rowTemplate == 'function' ? options.rowTemplate(row, data) : options.rowTemplate %>
<%= typeof options.rowTemplate == 'function' ? options.rowTemplate({row: row, data: data}) : _.template(options.rowTemplate)({row: row, data: data}) %>
<% }) %>
<% } else {%>
<div class="formhandler-grid row">
......
ID,c1,c2,c3,c4,c5,c6,c7,c8,Name,Continent,Symbols,Shapes,Stripes,Cross,Union-Flag,Text
AND,35,1,26,0,32,0,0,4,Andorra,Europe,,,Vertical,,,
ARE,24,0,0,25,0,0,25,24,United Arab Emirates,Asia,,,Horizontal,,,
AFG,28,1,0,33,0,0,33,3,Afghanistan,Asia,,,Vertical,,,Country
ATG,50,0,5,0,0,10,25,7,Antigua Barbuda,North America,,,,,,
ALB,87,0,0,0,0,0,12,0,Albania,Europe,Bird,,,,,
ARM,33,33,0,0,33,0,0,0,Armenia,Asia,,,Horizontal,,,
AGO,48,0,4,0,0,0,46,0,Angola,Africa,"Weapon, Agriculture",,Horizontal,,,
ARG,0,2,0,0,0,66,0,31,Argentina,South America,Sun,,Horizontal,,,
AUT,66,0,0,0,0,0,0,33,Austria,Europe,,,Horizontal,,,
AUS,9,0,0,0,79,0,0,11,Australia,Oceania,,Stars,,,Yes,
AZE,31,0,0,33,0,33,0,1,Azerbaijan,Asia,,"Stars, Crescent",Horizontal,,,
BIH,0,0,25,0,70,0,0,4,Bosnia Herzegovina,Europe,,,,,,
BRB,0,0,29,0,66,0,3,0,Barbados,North America,Emblem,,Vertical,,,
BGD,20,0,0,79,0,0,0,0,Bangladesh,Asia,Sun,Circle,,,,
BEL,33,0,33,0,0,0,33,0,Belgium,Europe,,,Vertical,,,
BFA,49,0,2,48,0,0,0,0,Burkina Faso,Africa,,,Horizontal,,,
BGR,33,0,0,33,0,0,0,33,Bulgaria,Europe,,,Horizontal,,,
BHR,67,0,0,0,0,0,0,32,Bahrain,Asia,,,,,,
BDI,30,0,0,34,0,0,0,34,Burundi,Africa,,Stars,Diagonal,Yes,,
BEN,30,0,29,40,0,0,0,0,Benin,Africa,,,Horizontal,,,
BRN,6,0,54,0,0,0,17,21,Brunei Darussalam,Asia,,,,,,
BOL,33,0,33,33,0,0,0,0,Bolivia,South America,,,Horizontal,,,
BRA,0,0,17,68,12,0,0,1,Brazil,South America,,"Stars, Circle",,,,Mottos
BHS,0,0,21,0,0,57,21,0,Bahamas,North America,,Triangle,Horizontal,,,
BTN,41,1,43,0,0,0,0,14,Bhutan,Asia,Animal,,,,,
BWA,0,0,0,0,0,74,16,8,Botswana,Africa,,,Horizontal,,,
BLR,64,0,0,29,0,0,0,6,Belarus,Europe,,,,,,
BLZ,17,0,0,5,54,0,1,21,Belize,North America,,,,,,
CAN,63,0,0,0,0,0,0,36,Canada,North America,Plant,,Vertical,,,
COD,20,0,12,0,0,67,0,0,Congo,Africa,,,,,,
CAF,16,0,21,20,19,0,0,20,Central African Rep,Africa,,,,,,
COG,33,0,33,33,0,0,0,0,Congo,Africa,,,,,,
CHE,77,0,0,0,0,0,0,22,Switzerland,Europe,,,,Yes,,
CIV,0,33,0,33,0,0,0,33,Cote d Ivoire,Africa,,,,,,
CHL,50,0,0,0,15,0,0,34,Chile,South America,,Stars,,,,
CMR,31,0,34,33,0,0,0,0,Cameroon,Africa,,Stars,Vertical,,,
CHN,97,0,2,0,0,0,0,0,China,Asia,,Stars,,,,
COL,25,0,50,0,24,0,0,0,Colombia,South America,,,Horizontal,,,
CRI,33,0,0,0,33,0,0,33,Costa Rica,North America,Landscape,,Horizontal,,,
CUB,20,0,0,0,48,0,0,30,Cuba,North America,,"Stars, Triangle",Horizontal,,,
CPV,8,0,1,0,74,0,0,16,Cabo Verde,Africa,,,,,,
CYP,0,6,0,2,0,0,0,90,Cyprus,Asia,Plant,,,,,
CZE,37,0,0,0,25,0,0,37,Czech Republic,Europe,,Triangle,Horizontal,,,
DEU,33,0,33,0,0,0,33,0,Germany,Europe,,,Horizontal,,,
DJI,0,2,0,35,0,35,0,28,Djibouti,Africa,,Triangle,Horizontal,,,
DNK,76,0,0,0,0,0,0,23,Denmark,Europe,,,,Yes,,
DMA,8,0,8,66,0,0,8,8,Dominica,North America,Bird,,,,,
DOM,33,0,0,2,33,0,0,30,Dominican,North America,,,,,,
DZA,6,0,0,46,0,0,0,47,Algeria,Africa,,Crescent,Vertical,,,
ECU,26,1,47,1,22,0,1,0,Ecuador,South America,Bird,,Horizontal,,,
EST,0,0,0,0,0,33,33,33,Estonia,Europe,,,Horizontal,,,
EGY,33,0,1,0,0,0,33,31,Egypt,Africa,Bird,,Horizontal,,,Country
SAH,17,0,0,29,0,0,29,22,Western Sahara,Africa,,,,,,
ERI,44,0,5,24,0,24,0,0,Eritrea,Africa,Plant,Triangle,Diagonal,,,
ESP,53,1,44,0,0,0,0,1,Spain,Europe,"Landscape, Crown",,Horizontal,,,Mottos
ETH,32,0,26,32,0,8,0,0,Ethiopia,Africa,,,Horizontal,,,
FIN,0,0,0,0,39,0,0,60,Finland,Europe,,,,Yes,,
FJI,14,0,0,0,7,66,0,11,Fiji,Oceania,Animal,,,,Yes,
FSM,0,0,0,0,0,97,0,2,Micronesia,Oceania,,Stars,,,,
FRA,33,0,0,0,33,0,0,33,France,Europe,,,Vertical,,,
GAB,0,0,33,33,0,33,0,0,Gabon,Africa,,,Horizontal,,,
GBR,37,0,0,0,28,0,0,33,United Kingdom,Europe,,,,Yes,,
GRD,40,0,31,28,0,0,0,0,Grenada,North America,Plant,,,,,
GEO,35,0,0,0,0,0,0,64,Georgia,Asia,,,,Yes,,
GHA,33,0,30,33,0,0,2,0,Ghana,Africa,,Stars,Horizontal,,,
GMB,33,0,0,33,22,0,0,11,Gambia,Africa,,,Horizontal,,,