diff --git a/CHANGELOG.md b/CHANGELOG.md index 333b42d5..ec6e31c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,11 @@ - Added `Emitter` as a base class for `Widget`/`App`, `PageRenderer` and `ViewRenderer` classes - Removed `Widget#definePage()` method, use `Widget#page.define()` instead - Extracted query and view editors from `report` page to a separate module, as `Widget#view.QueryEditor` and `Widget#view.ViewEditor` classes - - Added `content` option for `auto-link` view config + - Added `content` option in `auto-link` view config + - Changed `source` view: + - Removed `refs` preprocessing logic, now it takes array of `{ type: "error" | "ignore" | "link", range: [number, number], href?: string }` objects + - Disabled syntax highlighting when source size over 100k to avoid page freezing + - Added a pilot implementation of view presets. Preset's API available via `Widget#preset` and very common with page and view renderers. Preset can be used in views as preset name with `preset/` prefix (i.e. `{ view: 'preset/name', ... }`) ## 1.0.0-beta.7 (23-01-2019) diff --git a/client/core/dict.js b/client/core/dict.js new file mode 100644 index 00000000..df052027 --- /dev/null +++ b/client/core/dict.js @@ -0,0 +1,31 @@ +/* eslint-env browser */ + +import Emitter from './emitter.js'; + +const entries = new WeakMap(); + +export default class Dictionary extends Emitter { + constructor() { + super(); + + entries.set(this, Object.create(null)); + } + + define(key, value) { + entries.get(this)[key] = value; + + this.emit('define', key, value); + } + + isDefined(key) { + return key in entries.get(this); + } + + get(key) { + return entries.get(this)[key]; + } + + get names() { + return Object.keys(entries.get(this)).sort(); + } +} diff --git a/client/core/page.js b/client/core/page.js index 55609009..7eb48892 100644 --- a/client/core/page.js +++ b/client/core/page.js @@ -1,8 +1,7 @@ /* eslint-env browser */ -import Emitter from './emitter.js'; +import Dict from './dict.js'; -const pages = new WeakMap(); const BUILDIN_NOT_FOUND = { name: 'not-found', render: (el, { name }) => { @@ -11,40 +10,26 @@ const BUILDIN_NOT_FOUND = { } }; -export default class PageRenderer extends Emitter { +export default class PageRenderer extends Dict { constructor(view) { super(); this.view = view; this.lastPage = null; - pages.set(this, Object.create(null)); + this.lastPageId = null; } define(name, render, options) { - pages.get(this)[name] = Object.freeze({ + super.define(name, Object.freeze({ name, render: typeof render === 'function' - ? render + ? render.bind(this.view) : (el, data, context) => this.view.render(el, render, data, context), options: Object.freeze(Object.assign({}, options)) - }); - - this.emit('define', name); - } - - isDefined(name) { - return name in pages.get(this); - } - - get(name) { - return pages.get(this)[name]; - } - - get names() { - return Object.keys(pages.get(this)).sort(); + })); } - render(oldPageEl, name, data, context) { + render(prevPageEl, name, data, context) { const renderStartTime = Date.now(); let page = this.get(name); let rendered; @@ -58,12 +43,12 @@ export default class PageRenderer extends Emitter { const pageChanged = this.lastPage !== name; const pageRef = context && context.id; const pageRefChanged = this.lastPageId !== pageRef; - const newPageEl = reuseEl && !pageChanged ? oldPageEl : document.createElement('article'); - const parentEl = oldPageEl.parentNode; + const newPageEl = reuseEl && !pageChanged ? prevPageEl : document.createElement('article'); + const parentEl = prevPageEl.parentNode; this.lastPage = name; this.lastPageId = pageRef; - newPageEl.id = oldPageEl.id; + newPageEl.id = prevPageEl.id; newPageEl.classList.add('page', 'page-' + name); if (pageChanged && typeof init === 'function') { @@ -78,8 +63,8 @@ export default class PageRenderer extends Emitter { console.error(e); } - if (newPageEl !== oldPageEl) { - parentEl.replaceChild(newPageEl, oldPageEl); + if (newPageEl !== prevPageEl) { + parentEl.replaceChild(newPageEl, prevPageEl); } if (pageChanged || pageRefChanged || !keepScrollOffset) { diff --git a/client/core/preset.js b/client/core/preset.js new file mode 100644 index 00000000..262fba4f --- /dev/null +++ b/client/core/preset.js @@ -0,0 +1,39 @@ +/* eslint-env browser */ + +import Dict from './dict.js'; + +export default class PresetRenderer extends Dict { + constructor(view) { + super(); + + this.view = view; + } + + define(name, config) { + // FIXME: add check that config is serializable object + config = JSON.parse(JSON.stringify(config)); + + super.define(name, Object.freeze({ + name, + render: (el, _, data, context) => this.view.render(el, config, data, context), + config + })); + } + + render(container, name, data, context) { + let preset = this.get(name); + + if (!preset) { + const errorMsg = 'Preset `' + name + '` is not found'; + console.error(errorMsg, name); + + const el = container.appendChild(document.createElement('div')); + el.style.cssText = 'color:#a00;border:1px dashed #a00;font-size:12px;padding:4px'; + el.innerText = errorMsg; + + return Promise.resolve(); + } + + return preset.render(container, null, data, context); + } +} diff --git a/client/core/view.js b/client/core/view.js index 3d5bc819..387ef458 100644 --- a/client/core/view.js +++ b/client/core/view.js @@ -1,8 +1,7 @@ /* eslint-env browser */ -import Emitter from './emitter.js'; +import Dict from './dict.js'; -const views = new WeakMap(); const STUB_OBJECT = Object.freeze({}); const BUILDIN_FALLBACK = { name: 'fallback', @@ -36,36 +35,21 @@ function renderDom(renderer, placeholder, config, data, context) { }); } -export default class ViewRenderer extends Emitter { +export default class ViewRenderer extends Dict { constructor(host) { super(); this.host = host; - views.set(this, Object.create(null)); } - define(name, customRender, options) { - views.get(this)[name] = Object.freeze({ + define(name, render, options) { + super.define(name, Object.freeze({ name, - render: typeof customRender === 'function' - ? customRender.bind(this) - : (el, config, data, context) => this.render(el, customRender, data, context), + render: typeof render === 'function' + ? render.bind(this) + : (el, _, data, context) => this.render(el, render, data, context), options: Object.freeze(Object.assign({}, options)) - }); - - this.emit('define', name); - } - - isDefined(name) { - return name in views.get(this); - } - - get(name) { - return views.get(this)[name]; - } - - get names() { - return Object.keys(views.get(this)).sort(); + })); } render(container, config, data, context) { @@ -96,9 +80,31 @@ export default class ViewRenderer extends Emitter { }; } - let renderer = typeof config.view === 'function' - ? { render: config.view, name: false, options: STUB_OBJECT } - : this.get(config.view); + let renderer = null; + + switch (typeof config.view) { + case 'function': + renderer = { render: config.view, name: false, options: STUB_OBJECT }; + break; + + case 'string': + if (config.view.startsWith('preset/')) { + const presetName = config.view.substr(7); + + if (this.host.preset.isDefined(presetName)) { + renderer = { + render: this.host.preset.get(presetName).render, + name: false, + options: { tag: false } + }; + } else { + return this.host.preset.render(container, presetName, data, context); + } + } else { + renderer = this.get(config.view); + } + break; + } if (!renderer) { const errorMsg = typeof config.view === 'string' diff --git a/client/pages/report.js b/client/pages/report.js index efbc485e..6ae0735b 100644 --- a/client/pages/report.js +++ b/client/pages/report.js @@ -7,8 +7,19 @@ import copyText from '../core/utils/copy-text.js'; const defaultViewSource = '{\n view: \'struct\',\n expanded: 1\n}'; const defaultViewPresets = [ - { name: 'Table', content: '{\n view: \'table\'\n}' }, - { name: 'Autolink list', content: '{\n view: \'ol\',\n item: \'auto-link\'\n}' } + { + name: 'Table', + content: JSON.stringify({ + view: 'table' + }, null, 4) + }, + { + name: 'Auto-link list', + content: JSON.stringify({ + view: 'ol', + item: 'auto-link' + }, null, 4) + } ]; function valueDescriptor(value) { @@ -114,6 +125,15 @@ export default function(discovery) { }, replace); } + function createPresetTab(name, content) { + return createElement('div', { + class: 'tab', + onclick: () => updateParams({ + view: content // JSON.stringify(content, null, 4) + }) + }, name || 'Untitled preset'); + } + // // Header // @@ -128,7 +148,7 @@ export default function(discovery) { placeholder: 'Untitled report', oninput: (e) => updateParams({ title: e.target.value - }), + }, true), onkeypress: (e) => { if (event.charCode === 13 || event.keyCode === 13) { e.target.blur(); @@ -241,6 +261,7 @@ export default function(discovery) { // let viewSetupEl; let availableViewListEl; + // let availablePresetListEl; let viewModeTabsEls; let viewLiveEditEl; const viewEditor = new discovery.view.ViewEditor(discovery).on('change', value => @@ -256,13 +277,8 @@ export default function(discovery) { }, true) }, viewMode) )), - createElement('div', 'tabs presets', viewPresets.map(({ name, content }) => - createElement('div', { - class: 'tab', - onclick: () => updateParams({ - view: content - }) - }, name || 'Untitled preset') + /* availablePresetListEl = */createElement('div', 'tabs presets', viewPresets.map(({ name, content }) => + createPresetTab(name, content) )), viewSetupEl = createElement('div', { class: 'view-editor', @@ -309,6 +325,12 @@ export default function(discovery) { updateAvailableViewList(); discovery.view.on('define', updateAvailableViewList); + // sync view list + // const updateAvailablePresetList = (name, preset) => + // availablePresetListEl.appendChild(createPresetTab(name, preset.config)); + + // discovery.preset.on('define', updateAvailablePresetList); + // // Report form & content // diff --git a/client/widget/index.js b/client/widget/index.js index 94713213..44b9222f 100644 --- a/client/widget/index.js +++ b/client/widget/index.js @@ -2,6 +2,7 @@ import Emitter from '../core/emitter.js'; import ViewRenderer from '../core/view.js'; +import PresetRenderer from '../core/preset.js'; import PageRenderer from '../core/page.js'; import * as views from '../views/index.js'; import * as pages from '../pages/index.js'; @@ -115,6 +116,7 @@ export default class Widget extends Emitter { this.options = options || {}; this.view = new ViewRenderer(this); + this.preset = new PresetRenderer(this.view); this.page = new PageRenderer(this.view); this.page.on('define', name => this.addValueLinkResolver(extractValueLinkResolver(this, name))); this.entityResolvers = [];