node-mongo-admin
Version:
A simple web application to visualize mongo data inspired by PHPMyAdmin
1,034 lines (912 loc) • 27.5 kB
JavaScript
/**
* @file Manage the result display
*/
/* globals Panel, ObjectId, BinData, DBRef, MinKey, MaxKey, Long, json, explore, Menu, Export, Storage, Populate, Populated, Select, Plot, Simple, Clipboard*/
let Query = {}
/**
* @property {Select}
*/
Query.connectionsSelect = null
/**
* @property {Select}
*/
Query.collectionsSelect = null
/**
* Query.collections[connectionName] is a array of collection names
* @property {Object<Array<string>>}
*/
Query.collections = {}
/**
* Object paths that are expanded in the result table, displaying subdocs fields
* @property {Storage}
*/
Query.expandedPaths = new Storage('expand')
/**
* Store data of current result
* @property {Array<Object>}
*/
Query.result = null
/**
* Active connection
* @property {string}
* @readonly
*/
Query.connection = ''
/**
* Active collection
* @property {string}
* @readonly
*/
Query.collection = ''
/**
* @typedef {Object} Mode
* @property {string} name
* @property {function()} execute
* @property {function():Array} toSearchParts
* @property {function(...*)} executeFromSearchParts
* @property {function(string):string} toString
* @property {function()} [init]
* @property {boolean} [default=false]
* @property {function()} [onChangeCollection]
* @property {function(*,string,HTMLElement,Object)} [processCellMenu]
* @property {function(*,string,HTMLElement,Object)} [processGlobalCellMenu]
* @property {function(string,Object)} [processHeadMenu]
*/
/**
* Active mode
* @property {Mode}
*/
Query.mode = null
/**
* Registered modes
* @property {Array<Mode>}
*/
Query.modes = []
Query.specialTypes = [ObjectId, BinData, DBRef, MinKey, MaxKey, Long, Date, RegExp]
/**
* Selected rows
* @property {Array<HTMLElement>}
*/
Query.selection = []
/**
* Hidden columns
* @property {Storage}
*/
Query.hiddenPaths = new Storage('hide')
/**
* Prompt mode, if any. Either 'one' or 'many'
* @property {?string}
*/
Query.prompt = null
/**
* @property {?Timeout}
*/
Query.refreshInterval = null
window.addEventListener('load', () => {
Panel.request('collections', {}, result => {
Query.init(result.connections)
})
})
/**
* @param {Mode} mode
*/
Query.registerMode = function (mode) {
Query.modes.push(mode)
}
/**
* @param {Array<Object>} connections
* @param {string} connections.$.name
* @param {Array<string>} connections.$.collections
*/
Query.init = function (connections) {
let connectionNames = [],
lastConnection = window.localStorage.getItem('node-mongo-admin-connection'),
initialMode
Query.connectionsSelect = new Select('query-connections')
Query.collectionsSelect = new Select('query-collections')
// Setup modes
Query.modes.forEach(mode => {
let btEl = Panel.create('input[type=button]')
btEl.value = Panel.formatDocPath(mode.name)
btEl.id = 'bt-' + mode.name
btEl.className = 'header-btn'
btEl.onclick = Query.setMode.bind(Query, mode)
Panel.get('mode-buttons').appendChild(btEl)
if (mode.default) {
initialMode = mode
}
})
Query.setMode(initialMode)
connections.forEach(connection => {
Query.collections[connection.name] = connection.collections
connectionNames.push({
value: connection.name,
text: Panel.formatDocPath(connection.name)
})
})
Query.modes.forEach(mode => {
if (mode.init) {
mode.init()
}
})
Query.connectionsSelect.setOptions(connectionNames)
Query.connectionsSelect.onchange = Query.onChangeConnection
if (lastConnection) {
Query.connectionsSelect.value = lastConnection
}
Query.onChangeConnection()
Query.collectionsSelect.onchange = Query.onChangeCollection
Panel.get('query-form').onsubmit = Query.onFormSubmit
Panel.get('export').onclick = Query.export
Panel.get('query-copy').onclick = Query.copy
Panel.get('query-refresh').onchange = Query.onChangeRefresh
Panel.get('query-refresh-interval').onchange = Query.onChangeRefresh
Query.onChangeRefresh()
// eslint-disable-next-line no-new
new Clipboard('#query-copy', {
text: () => {
let coll = Query.collection,
prefix = 'db.'
if (/^[a-z_][a-z0-9_]*$/i.test(coll)) {
prefix += coll
} else {
prefix += 'getCollection(\'' + coll.replace(/'/g, '\\\'') + '\')'
}
return Query.mode.toString(prefix)
}
})
Panel.get('return-selected').onclick = Query.returnSelected
if (window.location.search) {
Query.executeFromSearch()
}
}
Query.onChangeConnection = function () {
let collection = Query.collectionsSelect.value,
collectionNames = [],
connection = Query.connectionsSelect.value,
collections = Query.collections[connection]
Query.connection = connection
collectionNames = collections.map(each => ({
value: each,
text: Panel.formatDocPath(each)
}))
Query.collectionsSelect.setOptions(collectionNames)
// Try to recover selected collection
if (collections.indexOf(collection) !== -1) {
Query.collectionsSelect.value = collection
} else {
Query.onChangeCollection()
}
// Save to keep on reload
window.localStorage.setItem('node-mongo-admin-connection', connection)
Query.updateTitle()
}
Query.onChangeCollection = function () {
Query.collection = Query.collectionsSelect.value
Query.updateTitle()
if (Query.mode.onChangeCollection) {
Query.mode.onChangeCollection()
}
}
/**
* Update the window title
*/
Query.updateTitle = function () {
document.title = Query.connection + '.' + Query.collection + ' - Node Mongo Admin'
}
/**
* Export the current result set
*/
Query.export = function () {
let title, url
title = Panel.formatDocPath(Query.mode.name) +
' query on ' +
Panel.formatDocPath(Query.connection + '.' + Query.collection)
url = Export.export(Query.result, title)
window.open(url)
}
/**
* @param {Mode} mode
*/
Query.setMode = function (mode) {
Query.mode = mode
Query.modes.forEach(each => {
Panel.get('bt-' + each.name).disabled = each === mode
Panel.get('query-' + each.name).style.display = each === mode ? '' : 'none'
})
}
/**
* Change the connection and collection select fields value
* @param {?string} connection
* @param {?string} collection
*/
Query.setCollection = function (connection, collection) {
if (connection && Query.connectionsSelect.value !== connection) {
Query.connectionsSelect.value = connection
Query.onChangeConnection()
}
if (collection && Query.collectionsSelect.value !== collection) {
Query.collectionsSelect.value = collection
Query.onChangeCollection()
}
}
/**
* @param {Event} [event]
* @param {boolean} [dontPushState] whether to push to browser history
*/
Query.onFormSubmit = function (event, dontPushState) {
if (event) {
event.preventDefault()
}
Query.mode.execute()
if (!dontPushState) {
// Update page URL
window.history.pushState(null, '', Query.toSearch())
}
}
/**
* Set current layout for a loading state
* @param {boolean} loading
*/
Query.setLoading = function (loading) {
if (loading) {
Panel.get('query-loading').style.display = ''
Panel.get('query-result').classList.add('loading')
} else {
Panel.get('query-loading').style.display = 'none'
Panel.get('query-result').classList.remove('loading')
Panel.get('query-form').scrollIntoView()
}
}
/**
* @param {Object[]} docs
* @param {number} [page=0]
* @param {boolean} [hasMore=false]
* @param {function(number)} [findPage=null]
*/
Query.showResult = function (docs, page, hasMore, findPage) {
let prevEl = Panel.get('query-prev'),
nextEl = Panel.get('query-next'),
pageEl = Panel.get('query-page'),
prevEl2 = Panel.get('query-prev2'),
nextEl2 = Panel.get('query-next2'),
pageEl2 = Panel.get('query-page2')
if (findPage) {
prevEl.className = prevEl2.className = !page ? 'prev-off' : 'prev-on'
prevEl.onmousedown = prevEl2.onmousedown = !page ? null : function (event) {
event.preventDefault()
findPage(page - 1)
}
nextEl.className = nextEl2.className = !hasMore ? 'next-off' : 'next-on'
nextEl.onmousedown = nextEl2.onmousedown = !hasMore ? null : function (event) {
event.preventDefault()
findPage(page + 1)
}
if (!docs.length) {
pageEl.textContent = pageEl2.textContent = 'No results'
} else if (docs.length === 1) {
pageEl.textContent = pageEl2.textContent = 'Page ' + (page + 1)
} else {
pageEl.textContent = pageEl2.textContent = docs.length +
' results on page ' + (page + 1)
}
Panel.get('query-controls').style.display = ''
Panel.get('query-controls2').style.display = docs.length > 10 ? '' : 'none'
} else {
Panel.get('query-controls').style.display =
Panel.get('query-controls2').style.display = 'none'
}
Panel.get('export').style.display = docs.length && !Query.prompt ? '' : 'none'
Panel.get('return-selected').style.display = docs.length && Query.prompt ? '' : 'none'
Panel.get('return-selected').disabled = true
Query.result = docs
Query.populateResultTable()
Populate.runAll(Query.connection, Query.collection)
Plot.updatePlot()
}
/**
* Populate result table with data from Query.result
* Paths in Query.expandedPaths are shown in the table
*/
Query.populateResultTable = function () {
let paths = {},
tree = [],
treeDepth = 1,
tableEl = Panel.get('query-result'),
rowEls = [],
docs = Query.result,
conn = Query.connection,
coll = Query.collection,
pathsToHide = Query.hiddenPaths.getArray(conn, coll),
pathsToExpand = Query.expandedPaths.getArray(conn, coll),
populatedPaths = Populate.getPaths(conn, coll),
pathNames, i, th
/**
* @param {Array} tree
* @param {string[]} path
* @param {number} depth
*/
let addToTree = function (tree, path, depth) {
let pathPart = path[depth],
i
treeDepth = Math.max(treeDepth, depth + 1)
if (depth === path.length - 1) {
tree.push(pathPart)
} else {
for (i = 0; i < tree.length; i++) {
if (tree[i].name === pathPart) {
return addToTree(tree[i].subpaths, path, depth + 1)
}
}
tree.push({
name: pathPart,
subpaths: []
})
addToTree(tree[i].subpaths, path, depth + 1)
}
}
/**
* Group by path
* @param {Object} subdoc
* @param {string} path
* @param {number} i
* @param {boolean} populated
* @param {*} original - original value, before population
*/
let addSubDoc = function (subdoc, path, i, populated, original) {
let key, subpath, value
for (key in subdoc) {
subpath = path ? path + '.' + key : key
value = subdoc[key]
if (pathsToHide.indexOf(subpath) !== -1) {
continue
}
if (value instanceof Populated) {
populated = true
original = value.original
value = value.display
}
if (value &&
typeof value === 'object' &&
!Array.isArray(value) &&
Query.specialTypes.indexOf(value.constructor) === -1 &&
(Object.keys(value).length === 1 ||
pathsToExpand.indexOf(subpath) !== -1) &&
!isGeoJSON(value)) {
addSubDoc(value, subpath, i, populated, original)
} else {
// Primitive value
if (!(subpath in paths)) {
// New result path
paths[subpath] = []
}
paths[subpath][i] = populated ? new Populated(original, value) : value
}
}
}
Panel.get('query-unhide-p').style.display = pathsToHide.length ? '' : 'none'
Panel.get('query-unhide').onclick = function () {
pathsToHide.clear()
Query.populateResultTable()
}
docs.forEach((doc, i) => {
addSubDoc(doc, '', i)
})
pathNames = Object.keys(paths).sort()
pathNames.forEach(path => {
addToTree(tree, path.split('.'), 0)
})
tableEl.innerHTML = ''
for (i = 0; i < treeDepth; i++) {
rowEls[i] = tableEl.insertRow(-1)
}
th = Panel.create('th', ' ')
th.rowSpan = treeDepth
rowEls[0].appendChild(th)
Query.selection = []
/**
* @param {(string|Object)} treeEl
* @param {number} depth
* @param {string} prefix
* @returns {number} number of child fields
*/
let createHeader = function (treeEl, depth, prefix) {
let cell = Panel.create('th'),
cols = 0,
path, newPath, leaf
if (typeof treeEl === 'string') {
path = treeEl
cell.rowSpan = treeDepth - depth
cell.classList.add('header-leaf')
cols = 1
leaf = true
} else {
path = treeEl.name
treeEl.subpaths.forEach(each => {
cols += createHeader(each, depth + 1, prefix + path + '.')
})
cell.colSpan = cols
leaf = false
}
rowEls[depth].className = 'header'
rowEls[depth].appendChild(cell)
newPath = prefix + path
cell.textContent = Panel.formatDocPath(path)
cell.oncontextmenu = function (event) {
let options = {}
if (!leaf || pathsToExpand.indexOf(newPath) !== -1) {
options['Collapse column'] = function () {
// Remove this path and subpaths from expand list
pathsToExpand.set(pathsToExpand.filter(each => each !== newPath && each.indexOf(newPath + '.') !== 0))
Query.populateResultTable()
}
}
options['Show field name'] = function () {
explore(newPath)
}
options['Hide this column'] = function () {
pathsToHide.pushAndSave(newPath)
Query.populateResultTable()
}
// Add custom buttons
if (Query.mode.processHeadMenu) {
Query.mode.processHeadMenu(newPath, options)
}
// Show it
event.preventDefault()
Menu.show(event, options)
}
cell.title = newPath
if (Populate.isPopulated(populatedPaths, newPath)) {
cell.classList.add('populated')
}
return cols
}
tree.forEach(each => {
createHeader(each, 0, '')
})
// Build the table
docs.forEach((doc, i) => {
let rowEl = tableEl.insertRow(-1),
eye = Panel.create('span.eye'),
map = Panel.create('span.map'),
firstCell = rowEl.insertCell(-1)
firstCell.appendChild(eye)
eye.onclick = function (event) {
explore(doc)
event.stopPropagation()
}
eye.title = 'Show raw document'
if (hasSomeGeoJSON(doc)) {
firstCell.appendChild(document.createTextNode(' '))
firstCell.appendChild(map)
map.onclick = function (event) {
showAllGeoJSON(doc)
event.stopPropagation()
}
map.title = 'Show GeoJSON data'
}
pathNames.forEach(path => {
let cell = rowEl.insertCell(-1),
value = paths[path][i]
Query.fillResultValue(cell, value, path, pathsToExpand.indexOf(path) === -1)
if (Populate.isPopulated(populatedPaths, path)) {
cell.classList.add('populated')
if (value !== undefined && !(value instanceof Populated)) {
cell.classList.add('populated-fail')
}
}
})
rowEl.onclick = Query.selectRow
rowEl.onmousedown = function (event) {
// Prevent ctrl+click selection
if (event.ctrlKey || event.shiftKey) {
event.preventDefault()
}
}
})
stickHeader()
}
/**
* Select the clicked row
* @param {Event} event
*/
Query.selectRow = function (event) {
let multi = event.shiftKey && Query.prompt !== 'one',
add = (event.ctrlKey || event.metaKey) && Query.prompt !== 'one',
row = event.currentTarget,
previous = Query.selection,
start, end
event.preventDefault()
if (!add) {
// Clear previous selection
Query.selection.forEach(el => {
el.classList.remove('selected')
})
Query.selection = []
if (previous.length === 1 && previous[0] === row) {
// Toggle effect
Panel.get('return-selected').disabled = true
return
}
}
if (!multi || !previous.length) {
// Select current
row.classList.add('selected')
Query.selection.push(row)
} else {
previous = previous[previous.length - 1]
if (row.compareDocumentPosition(previous) & Node.DOCUMENT_POSITION_FOLLOWING) {
start = row
end = previous.nextSibling
} else {
start = previous
end = row.nextSibling
}
do {
start.classList.add('selected')
Query.selection.push(start)
start = start.nextSibling
} while (start && start !== end)
}
Panel.get('return-selected').disabled = Query.selection.length === 0
}
/**
* Aux function for Query.showResult
* @param {DOMElement} cell
* @param {*} value
* @param {string} path
* @param {boolean} mayCollapse - whether the value may be expressed in short form
*/
Query.fillResultValue = function (cell, value, path, mayCollapse) {
let create = Panel.create,
localDate = Boolean(Storage.get('localDate')),
oidTimestamp = Boolean(Storage.get('oidTimestamp')),
hexBinary = Boolean(Storage.get('hexBinary')),
display = value instanceof Populated ? value.display : value
cell.dataset.path = path
if (display === undefined) {
cell.innerHTML = '-'
} else if (Array.isArray(display)) {
cell.appendChild(create('span.json-keyword', [
'Array[',
create('span.json-number', display.length),
']'
]))
cell.dataset.explore = true
} else if (typeof display === 'string') {
if (mayCollapse && display.length > 20) {
cell.innerHTML = json.stringify(display.substr(0, 17), true, false) + '…'
cell.dataset.collapsed = true
cell.dataset.explore = true
} else {
cell.innerHTML = json.stringify(display, true, false)
}
} else if (isGeoJSON(display)) {
cell.appendChild(create('span.json-keyword', ['GeoJSON{',
create('span.json-string', display.type),
'}'
]))
cell.dataset.isGeoJson = true
cell.dataset.explore = true
} else if (display && typeof display === 'object' && display.constructor === Object) {
cell.appendChild(create('span.json-keyword', [
'Object{',
create('span.json-number', Object.keys(display).length),
'}'
]))
cell.dataset.collapsed = true
cell.dataset.explore = true
} else {
cell.innerHTML = json.stringify(display, true, false, localDate, hexBinary, oidTimestamp)
}
// Add context menu
cell.oncontextmenu = Query.openMenu.bind(Query, value, path, cell)
}
/**
* Construct options for the context menu
* @param {*} value
* @param {string} path
* @param {HTMLElement} cell
* @param {MouseEvent} event
*/
Query.openMenu = function (value, path, cell, event) {
let options = {},
conn = Query.connection,
coll = Query.collection,
isPopulated = value instanceof Populated,
localDate = Boolean(Storage.getCached('localDate')),
oidTimestamp = Boolean(Storage.getCached('oidTimestamp')),
hexBinary = Boolean(Storage.getCached('hexBinary')),
display = isPopulated ? value.display : value
// Explore array/object
if (cell.dataset.explore) {
options['Show content'] = function () {
explore(display)
}
}
// Expand path (object or string)
if (cell.dataset.collapsed) {
options['Expand this column'] = function () {
Query.expandedPaths.getArray(conn, coll).pushAndSave(path)
Query.populateResultTable()
}
}
// Show GeoJSON in map
if (cell.dataset.isGeoJson) {
options['Show in map'] = function () {
open('http://geojson.io/#data=data:application/json,' +
encodeURIComponent(JSON.stringify(display)))
}
}
// Timestamp from object id
if (display instanceof ObjectId) {
options[oidTimestamp ? 'Show ObjectId' : 'Show timestamp'] = function () {
Storage.set('oidTimestamp', !oidTimestamp)
Query.populateResultTable()
}
if (path !== '_id' && !isPopulated) {
options['Populate with'] = Query.getMenuForId(display, path, (conn2, coll2) => {
let foreignPath = prompt('Path from ' + coll2 + ' to populate with')
if (foreignPath !== null) {
Populate.create(conn, coll, path, conn2, coll2, foreignPath)
Query.populateResultTable()
}
})
}
}
if (isPopulated) {
options.Unpopulate = function () {
Populate.remove(conn, coll, path)
}
}
// Date display format
if (display instanceof Date) {
options[localDate ? 'Show UTC date' : 'Show local date'] = function () {
Storage.set('localDate', !localDate)
Query.populateResultTable()
}
}
// Binary display format
if (display instanceof BinData) {
options[hexBinary ? 'Show in base64' : 'Show in hex'] = function () {
Storage.set('hexBinary', !hexBinary)
Query.populateResultTable()
}
}
// Add custom buttons
Query.modes.forEach(mode => {
if (mode.processGlobalCellMenu) {
mode.processGlobalCellMenu(value, path, cell, options)
}
})
if (Query.mode.processCellMenu) {
Query.mode.processCellMenu(value, path, cell, options)
}
// Show it
event.preventDefault()
Menu.show(event, options)
}
/**
* Construct the a context menu
* @param {ObjectId} value
* @param {string} path
* @param {function(string, string)} fn - the click function(connection, collection)
* @returns {Object<Function>}
*/
Query.getMenuForId = function (value, path, fn) {
let options = {},
pathParts = path.split('.')
Object.keys(Query.collections).forEach(conn => {
Query.collections[conn].forEach(coll => {
// For each collection, test if match with the path
let match = pathParts.some(part => {
if (part.substr(-2) === 'Id' || part.substr(-2) === 'ID') {
part = part.substr(0, part.length - 2)
}
return coll.toLowerCase().indexOf(part.toLowerCase()) !== -1
}),
fn2 = fn.bind(fn, conn, coll),
conn2
if (match) {
if (conn === Query.connection) {
options[Panel.formatDocPath(coll)] = fn2
} else {
// Submenu for other connection
// appending empty space is a hack to avoid name colission
conn2 = Panel.formatDocPath(conn) + '\u200b'
options[conn2] = options[conn2] || {}
options[conn2][Panel.formatDocPath(coll)] = fn2
}
}
})
})
return options
}
/**
* Convert the current query to a URL search component
*/
Query.toSearch = function () {
let parts = [Query.mode.name, Query.connection, Query.collection].concat(Query.mode.toSearchParts())
return '?' + parts.map(encodeURIComponent).join('&')
}
/**
* Do a find operation based on a search URL component (generated by Query.toSearch)
*/
Query.executeFromSearch = function () {
let search = window.location.search,
i
if (search[0] !== '?') {
return
}
let parts = search.substr(1).split('&').map(decodeURIComponent),
mode = parts[0],
connection = parts[1],
collection = parts[2]
if (mode === 'promptOne' || mode === 'promptMany') {
// Prepare simple mode and hide unwanted controls
Panel.get('mode-buttons').style.display = 'none'
Panel.get('plot').style.display = 'none'
Panel.get('return-selected').style.display = ''
Panel.get('return-selected').disabled = true
Query.prompt = mode === 'promptOne' ? 'one' : 'many'
Query.setMode(Simple)
Query.setCollection(connection, collection)
Simple.selectorInput.select()
return
}
for (i = 0; i < Query.modes.length; i++) {
if (Query.modes[i].name === mode) {
Query.setMode(Query.modes[i])
break
}
}
Query.setCollection(connection, collection)
Query.mode.executeFromSearchParts(...parts.slice(3))
}
/**
* Post a message with the selected row ids
*/
Query.returnSelected = function () {
let parentWindow = window.opener || window.parent
parentWindow.postMessage({
type: 'return-selected',
connection: Query.connection,
collection: Query.collection,
ids: Query.selection.map(row => {
let idTd = row.querySelector('td[data-path=_id]')
return idTd && idTd.textContent
}).filter(Boolean)
}, '*')
}
window.addEventListener('popstate', () => {
Query.executeFromSearch()
})
/*
* Make table header fixed after scroll limit is reached
*/
function stickHeader() {
let stickyHeader = Panel.get('sticky-table-header'),
rows = Panel.getAll('#query-result tr.header')
stickyHeader.innerHTML = ''
rows.forEach(row => {
let newRow = stickyHeader.insertRow(-1),
cells = [].slice.call(row.children)
cells.forEach(cell => {
// Fix size
cell.style.width = cell.offsetWidth + 'px'
cell.style.height = cell.offsetHeight + 'px'
let newCell = cell.cloneNode(true)
newCell.oncontextmenu = cell.oncontextmenu
newRow.appendChild(newCell)
})
})
}
/**
* Checks if a given value is a geojson
* @param {*} value
* @returns {boolean}
*/
function isGeoJSON(value) {
if (!value || typeof value !== 'object' || typeof value.type !== 'string') {
return false
}
if (['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon', 'GeometryCollection'].indexOf(value.type) !== -1 && Array.isArray(value.coordinates)) {
return true
} else if (value.type === 'GeometryCollection' && Array.isArray(value.geometries)) {
return true
} else if (value.type === 'Feature' && value.geometry) {
return true
} else if (value.type === 'FeatureCollection' && Array.isArray(value.features)) {
return true
}
return false
}
/**
* Checks if a given document has any field containing GeoJSON data
* @param {Object} doc
* @returns {boolean}
*/
function hasSomeGeoJSON(doc) {
if (isGeoJSON(doc)) {
return true
}
if (Array.isArray(doc)) {
return doc.some(hasSomeGeoJSON)
} else if (doc && typeof doc === 'object' && doc.constructor === Object) {
for (let key in doc) {
if (hasSomeGeoJSON(doc[key])) {
return true
}
}
}
return false
}
/**
* Open a map with all GeoJSON features in the document
* @param {Object} doc
*/
function showAllGeoJSON(doc) {
let features = []
collectGeoJSON(doc, '')
// Generate colors for each feature, based on its title
try {
let encoder = new TextEncoder
Promise.all(features.map(feature => {
// SHA1(feature.properties.title)
let bytes = encoder.encode(feature.properties.title)
return window.crypto.subtle.digest('SHA-1', bytes)
})).then(digests => {
digests.forEach((digest, i) => {
let feature = features[i],
bytes = new Uint8Array(digest),
r = '0' + bytes[0].toString(16),
g = '0' + bytes[1].toString(16),
b = '0' + bytes[2].toString(16),
color = '#' + r.slice(-2) + g.slice(-2) + b.slice(-2)
feature.properties['marker-color'] = color
})
openIt()
})
} catch (e) {
// Fallback without using colors
openIt()
}
function openIt() {
open('http://geojson.io/#data=data:application/json,' +
encodeURIComponent(JSON.stringify({
type: 'FeatureCollection',
features
})))
}
function collectGeoJSON(doc, path) {
if (isGeoJSON(doc)) {
return features.push({
type: 'Feature',
properties: {
title: path
},
geometry: doc
})
}
if (Array.isArray(doc)) {
for (let i = 0; i < doc.length; i++) {
collectGeoJSON(doc[i], path + (path ? '.' : '') + i)
}
} else if (doc && typeof doc === 'object' && doc.constructor === Object) {
for (let key in doc) {
collectGeoJSON(doc[key], path + (path ? '.' : '') + key)
}
}
}
}
Query.onChangeRefresh = function () {
clearInterval(Query.refreshInterval)
let checked = Panel.get('query-refresh').checked
Panel.get('query-refresh-options').style.display = checked ? '' : 'none'
if (checked) {
let time = Number(Panel.get('query-refresh-interval').value) * 1e3
Query.refreshInterval = setInterval(() => Query.onFormSubmit(null, true), time)
}
}