diff --git a/README.md b/README.md index 67fe8be8..807fb3b0 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,9 @@ git clone https://github.com/plepe/openstreetbrowser-categories-main.git node_mo You are welcome to send pull requests via Github! ### Category definition -There are currently two types of categories: `index` (for sub categories) and `overpass` (for OpenStreetMap data, loaded via an Overpass API request). Each of them is defined via a JSON structure. They can be combined into a single file. +There are currently two types of categories: `index` (for sub categories) and `overpass` (for OpenStreetMap data, loaded via an Overpass API request). Each of them is defined via a JSON (old) or YAML (recommended) structure. They can be combined into a single file. + +Check out the [tutorial](./doc/Tutorial.md)! #### Category 'index' File: dir.json @@ -55,6 +57,16 @@ File: dir.json } ``` +or File: dir.yaml +```yaml +type: index +subCategories: + - id: foo + - id: bar + type: overpass + query: node[amenity=bar] +``` + This will define a category with the id 'dir' (from the file name) with two sub-categories: 'foo' (which will be loaded from the file `foo.json`) and 'bar' (which is defined inline as category of type 'overpass' and will show all nodes with the tag 'amenity' set to value 'bar' - see below for more details). #### Category 'overpass' diff --git a/ajax.php b/ajax.php index 0d50087e..f64e080b 100644 --- a/ajax.php +++ b/ajax.php @@ -17,9 +17,6 @@ function error($msg) { Header("Content-Type: application/json; charset=UTF-8"); $postdata = file_get_contents("php://input"); -if ($postdata) { - $postdata = json_decode($postdata, true); -} $fun = "ajax_{$_REQUEST['__func']}"; $return = $fun($_REQUEST, $postdata); diff --git a/conf.php-dist b/conf.php-dist index c9547d8d..29d0f399 100644 --- a/conf.php-dist +++ b/conf.php-dist @@ -95,6 +95,14 @@ $config['baseMaps'] = array( ), ); +// customCategory needs a database +$db_conf = [ + //'dsn' => 'mysql:host=localhost;dbname=openstreetbrowser', + 'dsn' => 'sqlite:data/db.sqlite', + 'username' => 'USERNAME', + 'password' => 'PASSWORD', +]; + // List of available user interface languages $languages = array( "en", // English diff --git a/customCategory.php b/customCategory.php new file mode 100644 index 00000000..03f1db50 --- /dev/null +++ b/customCategory.php @@ -0,0 +1,37 @@ + + + + + +list($_REQUEST); + + Header("Content-Type: application/json; charset=utf-8"); + print json_readable_encode($result); +} + +if (isset($_REQUEST['id'])) { + $category = $customCategoryRepository->getCategory($_REQUEST['id']); + if ($category) { + $customCategoryRepository->recordAccess($_REQUEST['id']); + } + + Header("Content-Type: application/yaml; charset=utf-8"); + Header("Content-Disposition: inline; filename=\"{$_REQUEST['id']}.yaml\""); + print $category; +} + +if (isset($_REQUEST['action']) && $_REQUEST['action'] === 'save') { + $content = file_get_contents("php://input"); + + $id = $customCategoryRepository->saveCategory($content); + $customCategoryRepository->recordAccess($id); + + Header("Content-Type: text/plain; charset=utf-8"); + print $id; +} diff --git a/doc/CategoryAsYAML.md b/doc/Tutorial.md similarity index 97% rename from doc/CategoryAsYAML.md rename to doc/Tutorial.md index 5e6596d1..e9f1b94a 100644 --- a/doc/CategoryAsYAML.md +++ b/doc/Tutorial.md @@ -128,6 +128,11 @@ feature: # each value individually. They are joined as enumeration. details: | {{ tagTransList('cuisine', tags.cuisine) }} + # Body is shown in the popup and the details in the sidebar. An easy way to + # show all tags is using the TwigJS 'yaml' filter, which produces YAML. + # Alternatively, you could use 'json_pp' (JSON pretty print). + body: | +
{{ tags|yaml }}filter: cuisine: name: "{{ keyTrans('cuisine') }}" diff --git a/doc/TwigJS.md b/doc/TwigJS.md index 0e7e9982..8d52d56e 100644 --- a/doc/TwigJS.md +++ b/doc/TwigJS.md @@ -78,6 +78,9 @@ Extra filters: * filter `debug`: print the value (and further arguments) to the javascript console (via `console.log()`) * filter `wikipediaAbstract`: shows the abstract of a Wikipedia article in the selected data language (or, if not available, the language which was used in input, resp. 'en' for Wikidata input). Input is either 'language:article' (e.g. 'en:Douglas Adams') or a wikidata id (e.g. 'Q42'). * filter `wikidataEntity`: returns the wikidata entity in structured form (or `null` if the entity is not cached or `false` if it does not exist). Example: https://www.wikidata.org/wiki/Special:EntityData/Q42.json +* filter `json_pp`: JSON pretty print the object. As parameter to the filter, the following options can be passed: + * `indent`: indentation (default: 2) +* filter `yaml`: YAML pretty print the object. As options the filter, all options to [yaml.dump of js-yaml](https://github.com/nodeca/js-yaml#dump-object---options-) can be used. Notes: * Variables will automatically be HTML escaped, unless the filter raw is used, e.g.: `{{ tags.name|raw }}` diff --git a/init.sql b/init.sql new file mode 100644 index 00000000..5731376c --- /dev/null +++ b/init.sql @@ -0,0 +1,12 @@ +create table customCategory ( + id char(32) not null, + content mediumtext not null, + created datetime not null default CURRENT_TIMESTAMP, + primary key(id) +); + +create table customCategoryAccess ( + id char(32) not null, + ts datetime not null default CURRENT_TIMESTAMP, + foreign key(id) references customCategory(id) on delete cascade +); diff --git a/lang/en.json b/lang/en.json index a02708b6..5012e210 100644 --- a/lang/en.json +++ b/lang/en.json @@ -6,8 +6,18 @@ "cancel": "Cancel", "categories": "Categories", "category-info-tooltip": "Info & Map key", + "close": "Close", "closed": "closed", "default": "default", + "download": "Download", + "apply-keep": "Apply & keep editing", + "apply-close": "Apply & close", + "tip-tutorial": "Check out the [Tutorial]", + "customCategory:header": "Custom categories", + "customCategory:clone": "Clone as custom category", + "customCategory:create": "Create custom category", + "customCategory:list": "List popular custom categories", + "copied-clipboard": "Copied to clipboard", "edit": "edit", "editor:id": "iD (in-browser editor)", "editor:remote": "Remote Control (JOSM or Merkaator)", diff --git a/modulekit.php b/modulekit.php index 6bbce73a..6b3cf35f 100644 --- a/modulekit.php +++ b/modulekit.php @@ -12,6 +12,7 @@ $depend = array( $include = array( 'php' => array( 'src/defaults.php', + 'src/database.php', 'src/options.php', 'src/language.php', 'src/ip-location.php', @@ -22,6 +23,8 @@ $include = array( 'src/RepositoryDir.php', 'src/RepositoryGit.php', 'src/repositories.php', + 'src/repositoriesGitea.php', + 'src/customCategory.php', ), 'css' => array( 'style.css', diff --git a/package-lock.json b/package-lock.json index d5b06e8f..018ffe57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4819,7 +4819,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "requires": { "argparse": "^2.0.1" }, @@ -4827,8 +4826,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" } } }, @@ -7737,6 +7735,11 @@ "is-typed-array": "^1.1.6" } }, + "window-modal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/window-modal/-/window-modal-1.0.5.tgz", + "integrity": "sha512-DbpgUeFeYLUoq/ZLCR2IESFxcf5koyKdtRgncumPCu9l/iu7lzGLOJon/XJZ/zRA1XEZv5Ga/rtOpldqF4bEjg==" + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/package.json b/package.json index df68799e..a19a8344 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "formatcoords": "^1.1.3", "i18next-client": "^1.11.4", "ip-location": "^1.0.1", + "js-yaml": "^4.1.0", "json-multiline-strings": "^0.1.0", "leaflet": "^1.7.1", "leaflet-geosearch": "^3.6.1", diff --git a/repo.php b/repo.php index 38f1b82f..c58ab485 100644 --- a/repo.php +++ b/repo.php @@ -26,6 +26,7 @@ if (!isset($_REQUEST['repo'])) { if (isset($repoData['categoryUrl'])) { $info['categoryUrl'] = $repoData['categoryUrl']; } + $info['group'] = $repoData['group'] ?? 'default'; print json_encode($info, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_FORCE_OBJECT); } @@ -59,6 +60,25 @@ if ($branchId) { } } +if (array_key_exists('file', $_REQUEST)) { + $file = $repo->file_get_contents($_REQUEST['file']); + + if ($file === false) { + Header("HTTP/1.1 403 Forbidden"); + print "Access denied."; + } + else if ($file === null) { + Header("HTTP/1.1 404 File not found"); + print "File not found."; + } + else { + Header("Content-Type: text/plain; charset=utf-8"); + print $file; + } + + exit(0); +} + $cacheDir = null; $ts = $repo->timestamp($path); if (isset($config['cache'])) { diff --git a/src/Browser.js b/src/Browser.js new file mode 100644 index 00000000..d372426e --- /dev/null +++ b/src/Browser.js @@ -0,0 +1,53 @@ +const EventEmitter = require('events') +const queryString = require('query-string') + +const domSort = require('./domSort') + +module.exports = class Browser extends EventEmitter { + constructor (id, dom) { + super() + + this.id = id + this.dom = dom + this.history = [] + } + + buildPage (parameters) { + this.clear() + + hooks.call('browser-' + this.id, this, parameters) + this.emit('buildPage', parameters) + this.parameters = parameters + + domSort(this.dom) + } + + clear () { + while (this.dom.lastChild) { + this.dom.removeChild(this.dom.lastChild) + } + } + + catchLinks () { + const links = this.dom.getElementsByTagName('a') + Array.from(links).forEach(link => { + const href = link.getAttribute('href') + + if (href && href.substr(0, this.id.length + 2) === '#' + this.id + '?') { + link.onclick = () => { + this.history.push(this.parameters) + + const parameters = queryString.parse(href.substr(this.id.length + 2)) + this.buildPage(parameters) + + return false + } + } + }) + } + + close () { + this.clear() + this.emit('close') + } +} diff --git a/src/OpenStreetBrowserLoader.js b/src/OpenStreetBrowserLoader.js index e8659ed9..11c4ef45 100644 --- a/src/OpenStreetBrowserLoader.js +++ b/src/OpenStreetBrowserLoader.js @@ -2,268 +2,282 @@ var OverpassLayer = require('overpass-layer') const Repository = require('./Repository') -function OpenStreetBrowserLoader () { - this.types = {} - this.categories = {} - this.repoCache = {} - this.repositories = {} - this.templates = {} - this._loadClash = {} // if a category is being loaded multiple times, collect callbacks -} - -OpenStreetBrowserLoader.prototype.setMap = function (map) { - this.map = map -} - -/** - * @param string id ID of the category - * @param [object] options Options. - * @waram {boolean} [options.force=false] Whether repository should be reload or not. - * @param function callback Callback which will be called with (err, category) - */ -OpenStreetBrowserLoader.prototype.getCategory = function (id, options, callback) { - if (typeof options === 'function') { - callback = options - options = {} +class OpenStreetBrowserLoader { + constructor () { + this.types = {} + this.categories = {} + this.repositories = {} + this.templates = {} + this._loadClash = {} // if a category is being loaded multiple times, collect callbacks } - var ids = this.getFullId(id, options) - if (ids === null) { - return callback(new Error('invalid id'), null) + setMap (map) { + this.map = map } - if (options.force) { - delete this.categories[ids.fullId] - } + /** + * @param string id ID of the category + * @param [object] options Options. + * @waram {boolean} [options.force=false] Whether repository should be reload or not. + * @param function callback Callback which will be called with (err, category) + */ + getCategory (id, options, callback) { + if (typeof options === 'function') { + callback = options + options = {} + } - if (ids.fullId in this.categories) { - return callback(null, this.categories[ids.fullId]) - } + var ids = this.getFullId(id, options) + if (ids === null) { + return callback(new Error('invalid id'), null) + } - var opt = JSON.parse(JSON.stringify(options)) - opt.categoryId = ids.entityId - opt.repositoryId = ids.repositoryId + if (options.force) { + delete this.categories[ids.fullId] + } - this.getRepo(ids.repositoryId, opt, function (err, repoData) { - // maybe loaded in the meantime? if (ids.fullId in this.categories) { return callback(null, this.categories[ids.fullId]) } - if (err) { - return callback(err, null) - } + var opt = JSON.parse(JSON.stringify(options)) + opt.categoryId = ids.entityId + opt.repositoryId = ids.repositoryId - if (!(ids.entityId in repoData.categories)) { - return callback(new Error('category "' + ids.entityId + '" not defined'), null) - } + this.getRepository(ids.repositoryId, opt, (err, repository) => { + // maybe loaded in the meantime? + if (ids.fullId in this.categories) { + return callback(null, this.categories[ids.fullId]) + } - this.getCategoryFromData(ids.id, opt, repoData.categories[ids.entityId], function (err, category) { - if (category) { - category.setMap(this.map) + if (err) { + return callback(err, null) } - callback(err, category) - }.bind(this)) - }.bind(this)) -} + repository.getCategory(ids.entityId, opt, (err, data) => { + // maybe loaded in the meantime? + if (ids.fullId in this.categories) { + return callback(null, this.categories[ids.fullId]) + } -/** - * @param string repo ID of the repository - * @parapm [object] options Options. - * @waram {boolean} [options.force=false] Whether repository should be reload or not. - * @param function callback Callback which will be called with (err, repoData) - */ -OpenStreetBrowserLoader.prototype.getRepo = function (repo, options, callback) { - if (options.force) { - delete this.repoCache[repo] - } + if (err) { return callback(err) } - if (repo in this.repoCache) { - return callback.apply(this, this.repoCache[repo]) - } + this.getCategoryFromData(ids.id, opt, data, (err, category) =>{ + if (category) { + category.setMap(this.map) + } - if (repo in this._loadClash) { - this._loadClash[repo].push(callback) - return + callback(err, category) + }) + }) + }) } - this._loadClash[repo] = [ callback ] - - function reqListener (req) { - if (req.status !== 200) { - console.log('http error when loading repository', req) - this.repoCache[repo] = [ req.statusText, null ] - } else { - try { - let repoData = JSON.parse(req.responseText) - this.repositories[repo] = new Repository(repo, repoData) - this.repoCache[repo] = [ null, repoData ] - } catch (err) { - console.log('couldn\'t parse repository', req.responseText) - this.repoCache[repo] = [ 'couldn\t parse repository', null ] + /** + * @param string repo ID of the repository + * @param [object] options Options. + * @param {boolean} [options.force=false] Whether repository should be reloaded or not. + * @param function callback Callback which will be called with (err, repository) + */ + getRepository (id, options, callback) { + if (id in this.repositories) { + const repository = this.repositories[id] + + if (repository.loadCallbacks) { + return repository.loadCallbacks.push((err) => callback(err, repository)) } - } - var todo = this._loadClash[repo] - delete this._loadClash[repo] + if (options.force) { + repository.clearCache() + return repository.load((err) => { + if (err) { return callback(err) } - todo.forEach(function (callback) { - callback.apply(this, this.repoCache[repo]) - }.bind(this)) - } + options.force = false + callback(repository.err, repository) + }) + } - var param = [] - if (repo) { - param.push('repo=' + encodeURIComponent(repo)) - } - param.push('lang=' + encodeURIComponent(ui_lang)) - param.push(config.categoriesRev) - param = param.length ? '?' + param.join('&') : '' - - var req = new XMLHttpRequest() - req.addEventListener('load', reqListener.bind(this, req)) - req.open('GET', 'repo.php' + param) - req.send() -} + return callback(repository.err, repository) + } -OpenStreetBrowserLoader.prototype.getRepository = function (id, options, callback) { - if (id in this.repositories) { - return callback(null, this.repositories[id]) + this.repositories[id] = new Repository(id) + this.repositories[id].load((err) => callback(err, this.repositories[id])) } - this.getRepo(id, options, (err, repoData) => { - if (err) { - return callback(err) + /** + * @param [object] options Options. + * @param {boolean} [options.force=false] Whether repository should be reloaded or not. + * @param function callback Callback which will be called with (err, list) + */ + getRepositoryList (options, callback) { + if (this.list) { + return callback(null, this.list) } - callback(null, this.repositories[id]) - }) -} + if (this.repositoryListCallbacks) { + return this.repositoryListCallbacks.push(callback) + } -/** - * @param string id ID of the template - * @parapm [object] options Options. - * @waram {boolean} [options.force=false] Whether repository should be reload or not. - * @param function callback Callback which will be called with (err, template) - */ -OpenStreetBrowserLoader.prototype.getTemplate = function (id, options, callback) { - if (typeof options === 'function') { - callback = options - options = {} + this.repositoryListCallbacks = [callback] + + var param = [] + param.push('lang=' + encodeURIComponent(ui_lang)) + param.push(config.categoriesRev) + param = param.length ? '?' + param.join('&') : '' + + fetch('repo.php' + param) + .then(res => res.json()) + .then(data => { + this.list = data + + global.setTimeout(() => { + const cbs = this.repositoryListCallbacks + this.repositoryListCallbacks = null + cbs.forEach(cb => cb(null, this.list)) + }, 0) + }) + .catch(err => { + global.setTimeout(() => { + const cbs = this.repositoryListCallbacks + this.repositoryListCallbacks = null + cbs.forEach(cb => cb(err)) + }, 0) + }) } - var ids = this.getFullId(id, options) - - if (options.force) { - delete this.templates[ids.fullId] - } + /** + * @param string id ID of the template + * @parapm [object] options Options. + * @waram {boolean} [options.force=false] Whether repository should be reload or not. + * @param function callback Callback which will be called with (err, template) + */ + getTemplate (id, options, callback) { + if (typeof options === 'function') { + callback = options + options = {} + } - if (ids.fullId in this.templates) { - return callback(null, this.templates[ids.fullId]) - } + var ids = this.getFullId(id, options) - var opt = JSON.parse(JSON.stringify(options)) - opt.templateId = ids.entityId - opt.repositoryId = ids.repositoryId + if (options.force) { + delete this.templates[ids.fullId] + } - this.getRepo(ids.repositoryId, opt, function (err, repoData) { - // maybe loaded in the meantime? if (ids.fullId in this.templates) { return callback(null, this.templates[ids.fullId]) } - if (err) { - return callback(err, null) - } + var opt = JSON.parse(JSON.stringify(options)) + opt.templateId = ids.entityId + opt.repositoryId = ids.repositoryId - if (!repoData.templates || !(ids.entityId in repoData.templates)) { - return callback(new Error('template not defined'), null) - } + this.getRepository(ids.repositoryId, opt, (err, repository) => { + // maybe loaded in the meantime? + if (ids.fullId in this.templates) { + return callback(null, this.templates[ids.fullId]) + } - this.templates[ids.fullId] = OverpassLayer.twig.twig({ data: repoData.templates[ids.entityId], autoescape: true }) + if (err) { + return callback(err, null) + } - callback(null, this.templates[ids.fullId]) - }.bind(this)) -} + repository.getTemplate(ids.entityId, opt, (err, data) => { + // maybe loaded in the meantime? + if (ids.fullId in this.templates) { + return callback(null, this.templates[ids.fullId]) + } -OpenStreetBrowserLoader.prototype.getCategoryFromData = function (id, options, data, callback) { - var ids = this.getFullId(id, options) + if (err) { return callback(err) } - if (ids.fullId in this.categories) { - return callback(null, this.categories[ids.fullId]) - } + this.templates[ids.fullId] = OverpassLayer.twig.twig({ data, autoescape: true }) - if (!data.type) { - return callback(new Error('no type defined'), null) + callback(null, this.templates[ids.fullId]) + }) + }) } - if (!(data.type in this.types)) { - return callback(new Error('unknown type'), null) - } + getCategoryFromData (id, options, data, callback) { + var ids = this.getFullId(id, options) - let repository = this.repositories[ids.repositoryId] + if (ids.fullId in this.categories) { + return callback(null, this.categories[ids.fullId]) + } - var opt = JSON.parse(JSON.stringify(options)) - opt.id = ids.id - var layer = new this.types[data.type](opt, data, repository) + if (!data.type) { + data.type = 'overpass' + } - layer.setMap(this.map) + if (!(data.type in this.types)) { + return callback(new Error('unknown type'), null) + } - this.categories[ids.fullId] = layer + let repository = this.repositories[ids.repositoryId] - if ('load' in layer) { - layer.load(function (err) { - callback(err, layer) - }) - } else { - callback(null, layer) - } -} + var opt = JSON.parse(JSON.stringify(options)) + opt.id = ids.id + var layer = new this.types[data.type](opt, data, repository) -OpenStreetBrowserLoader.prototype.getFullId = function (id, options) { - var result = {} + layer.setMap(this.map) - if (!id) { - return null - } + this.categories[ids.fullId] = layer - let m = id.match(/^(.*)\/([^/]*)/) - if (m) { - result.id = id - result.repositoryId = m[1] - result.entityId = m[2] - } else if (options.repositoryId && options.repositoryId !== 'default') { - result.repositoryId = options.repositoryId - result.entityId = id - result.id = result.repositoryId + '/' + id - } else { - result.id = id - result.repositoryId = 'default' - result.entityId = id + if ('load' in layer) { + layer.load(function (err) { + callback(err, layer) + }) + } else { + callback(null, layer) + } } - result.sublayerId = null - m = result.entityId.split(/:/) - if (m.length > 1) { - result.sublayerId = m[0] - result.entityId = m[1] - } + getFullId (id, options) { + var result = {} + + if (!id) { + return null + } - result.fullId = result.repositoryId + '/' + (result.sublayerId ? result.sublayerId + ':' : '') + result.entityId + let m = id.match(/^(.*)\/([^/]*)/) + if (m) { + result.id = id + result.repositoryId = m[1] + result.entityId = m[2] + } else if (options.repositoryId && options.repositoryId !== 'default') { + result.repositoryId = options.repositoryId + result.entityId = id + result.id = result.repositoryId + '/' + id + } else { + result.id = id + result.repositoryId = 'default' + result.entityId = id + } - return result -} + result.sublayerId = null + m = result.entityId.split(/:/) + if (m.length > 1) { + result.sublayerId = m[0] + result.entityId = m[1] + } + + result.fullId = result.repositoryId + '/' + (result.sublayerId ? result.sublayerId + ':' : '') + result.entityId + + return result + } -OpenStreetBrowserLoader.prototype.forget = function (id) { - var ids = this.getFullId(id, options) + forget (id) { + var ids = this.getFullId(id, options) - this.categories[ids.fullId].remove() - delete this.categories[ids.fullId] + this.categories[ids.fullId].remove() + delete this.categories[ids.fullId] + } + + registerType (type, classObject) { + this.types[type] = classObject + } } -OpenStreetBrowserLoader.prototype.registerType = function (type, classObject) { - this.types[type] = classObject +OpenStreetBrowserLoader.prototype.registerRepository = function (id, repository) { + this.repositories[id] = repository } module.exports = new OpenStreetBrowserLoader() diff --git a/src/Repository.js b/src/Repository.js index c5e7de30..9f951665 100644 --- a/src/Repository.js +++ b/src/Repository.js @@ -1,8 +1,90 @@ module.exports = class Repository { constructor (id, data) { this.id = id - this.data = data + this.isLoaded = false - this.lang = this.data.lang || {} + if (data) { + this.data = data + this.lang = this.data.lang || {} + this.loadCallbacks = null + } + } + + file_get_contents (fileName, options, callback) { + let param = [] + param.push('repo=' + encodeURIComponent(this.id)) + param.push('file=' + encodeURIComponent(fileName)) + param.push(config.categoriesRev) + param = param.length ? '?' + param.join('&') : '' + + fetch('repo.php' + param) + .then(res => res.text()) + .then(data => { + global.setTimeout(() => { + callback(null, data) + }, 0) + }) + .catch(err => { + global.setTimeout(() => { + callback(err) + }, 0) + }) + } + + load (callback) { + if (this.loadCallbacks) { + return this.loadCallbacks.push(callback) + } + + this.loadCallbacks = [callback] + + var param = [] + + param.push('repo=' + encodeURIComponent(this.id)) + param.push('lang=' + encodeURIComponent(ui_lang)) + param.push(config.categoriesRev) + param = param.length ? '?' + param.join('&') : '' + + fetch('repo.php' + param) + .then(res => res.json()) + .then(data => { + this.data = data + this.lang = this.data.lang || {} + this.err = null + + global.setTimeout(() => { + const cbs = this.loadCallbacks + this.loadCallbacks = null + cbs.forEach(cb => cb(null)) + }, 0) + }) + .catch(err => { + this.err = err + global.setTimeout(() => { + const cbs = this.loadCallbacks + this.loadCallbacks = null + cbs.forEach(cb => cb(err)) + }, 0) + }) + } + + clearCache () { + this.data = null + } + + getCategory (id, options, callback) { + if (!(id in this.data.categories)) { + return callback(new Error('Repository ' + this.id + ': Category "' + id + '" not defined'), null) + } + + callback(null, this.data.categories[id]) + } + + getTemplate (id, options, callback) { + if (!(id in this.data.templates)) { + return callback(new Error('Repository ' + this.id + ': Template "' + id + '" not defined'), null) + } + + callback(null, this.data.templates[id]) } } diff --git a/src/RepositoryDir.php b/src/RepositoryDir.php index 07e33667..769c59b8 100644 --- a/src/RepositoryDir.php +++ b/src/RepositoryDir.php @@ -34,6 +34,7 @@ class RepositoryDir extends RepositoryBase { if (preg_match("/^([0-9a-zA-Z_\-]+)\.json$/", $f, $m) && $f !== 'package.json') { $d1 = json_decode(file_get_contents("{$this->path}/{$f}"), true); $d1['format'] = 'json'; + $d1['fileName'] = $f; if (!$this->isCategory($d1)) { continue; @@ -45,6 +46,7 @@ class RepositoryDir extends RepositoryBase { if (preg_match("/^([0-9a-zA-Z_\-]+)\.yaml$/", $f, $m)) { $d1 = yaml_parse(file_get_contents("{$this->path}/{$f}")); $d1['format'] = 'yaml'; + $d1['fileName'] = $f; if (!$this->isCategory($d1)) { continue; @@ -80,13 +82,17 @@ class RepositoryDir extends RepositoryBase { function file_get_contents ($file) { if (substr($file, 0, 1) === '.' || preg_match("/\/\./", $file)) { - return null; + return false; } if (!$this->access($file)) { return false; } + if (!file_exists("{$this->path}/{$file}")) { + return null; + } + return file_get_contents("{$this->path}/{$file}"); } diff --git a/src/RepositoryGit.php b/src/RepositoryGit.php index 6a95c89e..536e9072 100644 --- a/src/RepositoryGit.php +++ b/src/RepositoryGit.php @@ -72,6 +72,7 @@ class RepositoryGit extends RepositoryBase { $d1 = json_decode(shell_exec("cd " . escapeShellArg($this->path) . "; git show {$this->branchEsc}:" . escapeShellArg($f)), true); $d1['format'] = 'json'; + $d1['fileName'] = $f; if (!$this->isCategory($d1)) { continue; @@ -86,6 +87,7 @@ class RepositoryGit extends RepositoryBase { $d1 = yaml_parse(shell_exec("cd " . escapeShellArg($this->path) . "; git show {$this->branchEsc}:" . escapeShellArg($f))); $d1['format'] = 'yaml'; + $d1['fileName'] = $f; if (!$this->isCategory($d1)) { continue; diff --git a/src/Window.js b/src/Window.js new file mode 100644 index 00000000..c4203411 --- /dev/null +++ b/src/Window.js @@ -0,0 +1,97 @@ +const EventEmitter = require('events') + +module.exports = class Window extends EventEmitter { + constructor (options) { + super() + + this.visible = false + this.dom = document.createElement('div') + this.dom.className = 'Window' + + this.header = document.createElement('div') + this.header.className = 'header' + this.header.innerHTML = options.title + this.dom.appendChild(this.header) + + this.closeBtn = document.createElement('div') + this.closeBtn.className = 'closeBtn' + this.closeBtn.title = lang('close') + this.closeBtn.onclick = (e) => { + this.close() + e.stopImmediatePropagation() + } + this.header.appendChild(this.closeBtn) + + this.content = document.createElement('div') + this.content.className = 'content' + this.dom.appendChild(this.content) + + dragElement(this.dom) + + this.dom.onclick = () => { + if (!this.visible) { return } + + const activeEl = document.activeElement + + if (document.body.lastElementChild !== this.dom) { + document.body.appendChild(this.dom) + activeEl.focus() + } + } + } + + show () { + this.visible = true + document.body.appendChild(this.dom) + this.emit('show') + } + + close () { + this.visible = false + document.body.removeChild(this.dom) + this.emit('close') + } +} + +// copied from https://www.w3schools.com/HOWTO/howto_js_draggable.asp +// Make the DIV element draggable: +function dragElement(elmnt) { + var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; + if (elmnt.firstChild) { + // if present, the header is where you move the DIV from: + elmnt.firstChild.onmousedown = dragMouseDown; + } else { + // otherwise, move the DIV from anywhere inside the DIV: + elmnt.onmousedown = dragMouseDown; + } + + function dragMouseDown(e) { + e = e || window.event; + e.preventDefault(); + // get the mouse cursor position at startup: + pos3 = e.clientX; + pos4 = e.clientY; + document.onmouseup = closeDragElement; + // call a function whenever the cursor moves: + document.onmousemove = elementDrag; + } + + function elementDrag(e) { + e = e || window.event; + e.preventDefault(); + // calculate the new cursor position: + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + // set the element's new position: + elmnt.style.top = (elmnt.offsetTop - pos2) + "px"; + elmnt.style.left = (elmnt.offsetLeft - pos1) + "px"; + } + + function closeDragElement() { + // stop moving when mouse button is released: + document.onmouseup = null; + document.onmousemove = null; + } +} diff --git a/src/addCategories.js b/src/addCategories.js index 1944f987..e832476a 100644 --- a/src/addCategories.js +++ b/src/addCategories.js @@ -4,31 +4,110 @@ require('./addCategories.css') const tabs = require('modulekit-tabs') const weightSort = require('weight-sort') +const state = require('./state') const OpenStreetBrowserLoader = require('./OpenStreetBrowserLoader') let tab -function addCategoriesShow (repo, options={}) { - let content = tab.content - let repoId - let branchId +function addCategoriesList (content, browser, options = {}) { + content.innerHTML = ' ' + lang('loading') - if (repo) { - [ repoId, branchId ] = repo.split(/~/) - } + OpenStreetBrowserLoader.getRepositoryList(options, function (err, repoData) { + if (err) { + return global.alert(err) + } + + var categoryUrl = null + if (repoData.categoryUrl) { + categoryUrl = OverpassLayer.twig.twig({ data: repoData.categoryUrl, autoescape: true }) + } + + var list = {} + + if (typeof repositoriesGitea === 'object' && repositoriesGitea.url) { + let a = document.createElement('a') + a.href = repositoriesGitea.url + a.target = '_blank' + a.innerHTML = lang('more_categories_gitea') + content.appendChild(a) + } + + list = weightSort(repoData, { + key: 'timestamp', + reverse: true + }) + + let menu = document.createElement('ul') + menu.className = 'menu' + content.appendChild(menu) + + let header = document.createElement('h3') + header.innerHTML = lang('repositories') + ':' + content.appendChild(header) + + while (content.lastChild) { + content.removeChild(content.lastChild) + } + + var ul = document.createElement('ul') + + for (var id in list) { + var data = list[id] + + var repositoryUrl = null + if (data.repositoryUrl) { + repositoryUrl = OverpassLayer.twig.twig({ data: data.repositoryUrl, autoescape: true }) + } + + var li = document.createElement('li') + + let a = document.createElement('a') + a.href = '#more-categories?id=' + id + + li.appendChild(a) + a.appendChild(document.createTextNode('name' in data ? lang(data.name) : id)) + + var editLink = null + if (repositoryUrl) { + editLink = document.createElement('a') + editLink.href = repositoryUrl.render({ repositoryId: id }) + } + if (editLink) { + editLink.className = 'source-code' + editLink.title = 'Show source code' + editLink.target = '_blank' + editLink.innerHTML = '' + li.appendChild(document.createTextNode(' ')) + li.appendChild(editLink) + } + + ul.appendChild(li) + } + + content.appendChild(ul) + browser.catchLinks() + }) +} + +function addCategoriesShow (repo, browser, options={}) { + const content = browser.dom + + let [ repoId, branchId ] = repo.split(/~/) if (!branchId) { branchId = 'master' } - content.innerHTML = '