diff --git a/client/dom/index.ts b/client/dom/index.ts index a3a496c769..2423a3f4ee 100644 --- a/client/dom/index.ts +++ b/client/dom/index.ts @@ -10,7 +10,7 @@ export * from './fillbar' export * from './genesearch' export * from './menu' export * from './numericAxis' -export * from './radiobutton' +export * from './radiobutton.ts' export * from './sandbox' export * from './sayerror' export * from './search' diff --git a/client/dom/radio2.js b/client/dom/radio2.js deleted file mode 100644 index 735572eaf6..0000000000 --- a/client/dom/radio2.js +++ /dev/null @@ -1,64 +0,0 @@ -import { select as d3select, selectAll as d3selectAll } from 'd3-selection' - -// TODO may replace with radiobutton.js -export function initRadioInputs(opts) { - const divs = opts.holder - .selectAll('div') - .data(opts.options, d => d.value) - .style('display', 'block') - - divs.exit().each(function (d) { - d3select(this).on('input', null).on('click', null).remove() - }) - - const labels = divs - .enter() - .append('div') - .attr('aria-label', d => d.title) - .style('display', 'block') - .style('padding', opts.styles && 'padding' in opts.styles ? opts.styles.padding : '5px') - .append('label') - - if (opts.styles) { - for (const key in opts.styles) { - labels.style(key, opts.styles[key]) - } - } - - const inputs = labels - .append('input') - .attr('type', 'radio') - .attr('name', opts.name) - .attr('value', d => d.value) - .style('vertical-align', 'top') - .style('margin-top', '2px') - .style('margin-right', 0) - .property('checked', opts.isCheckedFxn) - .on('mouseup', opts.listeners.input) - .on('keyup', opts.listeners.input) - - labels - .append('span') - .style('vertical-align', 'top') - .html(d => ' ' + d.label) - .on('mouseup', opts.listeners.input) - .on('keyup', opts.listeners.input) - - function isChecked(d) { - return d.value == radio.currValue - } - - const radio = { - main(currValue) { - radio.currValue = currValue - inputs.property('checked', isChecked) - }, - dom: { - divs: opts.holder.selectAll('div'), - labels: opts.holder.selectAll('label').select('span'), - inputs: labels.selectAll('input') - } - } - - return radio -} diff --git a/client/dom/radiobutton.js b/client/dom/radiobutton.js deleted file mode 100644 index da9f557790..0000000000 --- a/client/dom/radiobutton.js +++ /dev/null @@ -1,56 +0,0 @@ -/* makes radio buttons - -******* Required -.holder -.options[ {} ] - .label - .value - .checked - only set for at most one option -.callback - async - -******* Optional -.styles{} - css to be applied to each
of the options - e.g. { "padding":"5px", "display":"inline-block" } -.inputName - common Name of , use random number if not given -*/ - -let nameSuffix = 0 - -export function make_radios(arg) { - const { holder, options, callback, styles } = arg - const inputName = arg.inputName || 'pp-dom-radio-' + nameSuffix++ - - const divs = holder - .selectAll() - .data(options, d => d.value) - .enter() - .append('div') - .style('margin', '5px') - - if (styles) { - for (const k in styles) { - divs.style(k, styles[k]) - } - } - - const labels = divs.append('label') - - const inputs = labels - .append('input') - .attr('type', 'radio') - .attr('name', inputName) - .attr('value', d => d.value) - .on('input', async (event, d) => { - inputs.property('disabled', true) - await callback(d.value) - inputs.property('disabled', false) - }) - inputs.filter(d => d.checked).property('checked', true) - - labels.append('span').html(d => ' ' + d.label) - return { divs, labels, inputs } -} diff --git a/client/dom/radiobutton.ts b/client/dom/radiobutton.ts new file mode 100644 index 0000000000..f48c137c53 --- /dev/null +++ b/client/dom/radiobutton.ts @@ -0,0 +1,122 @@ +import { Elem, Input } from '../types/d3' + +/* makes radio buttons */ + +type RadioButtonOpts = { + /** Fires .oninput. Intended for general use. */ + callback?: (f?: any) => void + /** Required. */ + holder: any + /** common Name of , use random number if not given */ + inputName?: string | number + /** Mass ui specific logic. Optional callback methods for + * non oninput events. Intended to address needs for assistive techology + */ + listeners?: { + /** Fires on onmouseup and onkeyup for the button and text label + */ + input: () => void + } + /** arr of objs defining the radio buttons and properties */ + options: OptionEntry[] + /** css to be applied to each
of the options + * e.g. { "padding":"5px", "display":"inline-block" } + */ + styles?: { [index: string]: string | number } +} + +type OptionEntry = { + /** only set for only *one* option */ + checked?: boolean | number + /** Text shown in the span to the right of the radio button */ + label: string + /** Text shown in tooltip */ + title?: string + /** Should correspond to 'currValue' in callbacks */ + value: string | number +} + +type RadioApi = { + /** Divs containing the labels with the padding and display styles applied. */ + divs: Elem + /** Divs encapsulating the radio buttons and text. All styles provided in opts + * applied here. */ + labels: Elem + /** Radio buttons, corresponding to the .options[] opt. */ + inputs: Input + /** Trigger changing the checked button from the cooresponding value, + * independent of user and other callbacks. */ + main: (value: number) => void +} + +let nameSuffix = 0 + +export function make_radios(opts: RadioButtonOpts): RadioApi { + if (!opts.callback && !opts.listeners) throw `Missing event callback for radios [#dom/radiobutton.js]` + if (opts.callback && opts.listeners) + throw `Both callback() and .listeners defined [#dom/radiobutton.js]. Only supply one.` + const inputName = opts.inputName || 'pp-dom-radio-' + nameSuffix++ + + const divs = opts.holder + .selectAll('div') + .style('display', 'block') + .data(opts.options, (d: any) => d?.value) + + const labels = divs + .enter() + .append('div') + .attr('aria-label', (d: OptionEntry) => d.title) + .style('display', opts.styles && 'display' in opts.styles ? opts.styles.display : 'block') + .style('padding', opts.styles && 'padding' in opts.styles ? opts.styles.padding : '3px') + .append('label') + + if (opts.styles) { + for (const k in opts.styles) { + labels.style(k, opts.styles[k]) + } + } + + const inputs = labels + .append('input') + .attr('type', 'radio') + .attr('name', inputName) + .attr('value', (d: OptionEntry) => d.value) + .style('vertical-align', 'top') + .style('margin-top', '2px') + .style('margin-right', 0) + .property('checked', (d: OptionEntry) => d?.checked) + if (opts.callback) { + inputs.on('input', async (event: KeyboardEvent, d: OptionEntry) => { + //Disable the radio buttons while the callback is running + inputs.property('disabled', true) + if (!opts.callback) return //So eslint doesn't complain + await opts.callback(d.value) + radio.main(d.value) + //Re-enable the radio buttons after the callback finishes + inputs.property('disabled', false) + }) + } + + const radioText = labels + .append('span') + .style('vertical-align', 'top') + .html((d: OptionEntry) => ' ' + d.label) + + if (opts?.listeners?.input) { + //Mass UI specific logic for assistive technologies + inputs.on('mouseup', opts.listeners.input).on('keyup', opts.listeners.input) + radioText.on('mouseup', opts.listeners.input).on('keyup', opts.listeners.input) + } + + const radio = { + divs, + labels, + inputs, + main(currValue: string | number) { + radio['currValue'] = currValue + inputs.property('checked', (d: OptionEntry) => d.value == radio['currValue']) + } + } + + return radio +} diff --git a/client/dom/test/radiobutton.unit.spec.ts b/client/dom/test/radiobutton.unit.spec.ts new file mode 100644 index 0000000000..1b1b9da717 --- /dev/null +++ b/client/dom/test/radiobutton.unit.spec.ts @@ -0,0 +1,175 @@ +import tape from 'tape' +import * as d3s from 'd3-selection' +import { make_radios } from '#dom' + +/* Tests + - default radio button rendering + - Missing callbacks + - Duplicate callbacks +*/ + +/************** + helper functions +***************/ + +function getHolder() { + return d3s + .select('body') + .append('div') + .style('border', '1px solid #aaa') + .style('padding', '5px') + .style('margin', '5px') +} + +/************** + test sections +***************/ + +tape('\n', test => { + test.pass('-***- dom/radiobutton -***-') + test.end() +}) + +tape('default radio button rendering', test => { + test.timeoutAfter(100) + const holder = getHolder() as any + + const styles = { + padding: '10px', + display: 'inline-block', + color: 'red' + } + + const options = [ + { + checked: true, + label: 'Test button', + title: 'test', + value: 1 + }, + { + checked: false, + label: 'Test button 2', + title: 'test', + value: 2 + } + ] + + const testArgs = { + holder, + styles, + options, + callback: () => { + //Comment so eslint doesn't complain + } + } + + const { divs, labels, inputs, main } = make_radios(testArgs) + + /** Divs */ + const divData = divs + .enter() + .nodes() + .map((d: any) => d.__data__) + test.deepEqual(divData, options, 'Should create divs with the correct data') + + /** Labels */ + const renderedLabels = labels.nodes() + test.equal(renderedLabels.length, options.length, `Should create ${options.length} radio buttons`) + const labelStyles = renderedLabels[0].style + test.true( + labelStyles.display == styles.display && labelStyles.color == styles.color && labelStyles.padding == styles.padding, + 'Should render labels with opts.styles' + ) + + /** Inputs */ + const renderedInputs = inputs.nodes() + test.equal(renderedInputs[0].checked, true, `Should check the first button`) + + /** .main() */ + main(2) + test.equal(renderedInputs[1].checked, true, `Should check the second button when main is called with it's value`) + + if (test['_ok']) holder.remove() + test.end() +}) + +tape('Missing callbacks', test => { + test.timeoutAfter(100) + const holder = getHolder() as any + + const options = [ + { + checked: true, + label: 'Test button', + title: 'test', + value: 1 + }, + { + checked: false, + label: 'Test button 2', + title: 'test', + value: 2 + } + ] + + const testArgs = { + holder, + options + } + + const message = 'Should throw when no callback(s) provided' + try { + make_radios(testArgs) + test.fail(message) + } catch (e: any) { + test.pass(`${message}: ${e.message || e}`) + } + + if (test['_ok']) holder.remove() + test.end() +}) + +tape('Duplicate callbacks', test => { + test.timeoutAfter(100) + const holder = getHolder() as any + + const options = [ + { + checked: true, + label: 'Test button', + title: 'test', + value: 1 + }, + { + checked: false, + label: 'Test button 2', + title: 'test', + value: 2 + } + ] + + const testArgs = { + holder, + options, + callback: () => { + //Comment so eslint doesn't complain + }, + listeners: { + input: () => { + //Comment so eslint doesn't complain + } + } + } + + const message = 'Should throw when both callback() and listeners() provided' + try { + make_radios(testArgs) + test.fail(message) + } catch (e: any) { + test.pass(`${message}: ${e.message || e}`) + } + + if (test['_ok']) holder.remove() + test.end() +}) diff --git a/client/gdc/maf.js b/client/gdc/maf.js index 5952c02208..23764a87a3 100644 --- a/client/gdc/maf.js +++ b/client/gdc/maf.js @@ -1,7 +1,5 @@ import { dofetch3 } from '#common/dofetch' -import { sayerror } from '../dom/sayerror.ts' -import { renderTable } from '../dom/table.ts' -import { make_radios } from '#dom/radiobutton' +import { make_radios, renderTable, sayerror } from '#dom' import { fileSize } from '#shared/fileSize.js' import { Menu } from '#dom/menu' diff --git a/client/mds3/skewer.js b/client/mds3/skewer.js index ae0e2415e8..c6bc77540d 100644 --- a/client/mds3/skewer.js +++ b/client/mds3/skewer.js @@ -95,7 +95,6 @@ export function may_render_skewer(data, tk, block) { const currentMode = tk.skewer.viewModes.find(n => n.inuse) if (!currentMode) throw 'no mode!!' - if (data.skewer) { // register new mlst data // otherwise will not overwrite skewer.mlst diff --git a/client/plots/controls.config.js b/client/plots/controls.config.js index cad5e0ff74..179e59cf1e 100644 --- a/client/plots/controls.config.js +++ b/client/plots/controls.config.js @@ -1,9 +1,9 @@ import { getCompInit, multiInit } from '../rx' -import { initRadioInputs } from '../dom/radio2' +import { make_radios } from '#dom' import { termsettingInit } from '#termsetting' import { rgb } from 'd3-color' import { select } from 'd3-selection' -import { TermTypes } from '#shared/terms.js' +// import { TermTypes } from '#shared/terms.js' // unique element ID's are needed for to be used for assigning unique // radio button names by object instance @@ -418,10 +418,10 @@ function setRadioInput(opts) { } ] - const styles = opts.styles || {} + const styles = opts.styles || { display: 'inline-block' } for (const input of inputs) { - self.inputs[input.settingsKey] = initRadioInputs({ - name: getElemId(opts.instanceNum), + self.inputs[input.settingsKey] = make_radios({ + inputName: getElemId(opts.instanceNum), holder: opts.holder .append('td') .attr('colspan', opts.colspan || '') @@ -461,11 +461,11 @@ function setRadioInput(opts) { for (const settingsKey in self.inputs) { const radio = self.inputs[settingsKey] radio.main(plot.settings[opts.chartType][settingsKey]) - radio.dom.divs.style('display', d => + radio.divs.style('display', d => d.getDisplayStyle ? d.getDisplayStyle(plot) : opts.labelDisplay || 'inline-block' ) - //radio.dom.labels.style('display', d => opts.labelDisplay || 'span') - if (opts.setRadioLabel) radio.dom.labels.html(opts.setRadioLabel) + //radio.labels.style('display', d => opts.labelDisplay || 'span') + if (opts.setRadioLabel) radio.labels.html(opts.setRadioLabel) } } } diff --git a/client/plots/matrix/matrix.controls.js b/client/plots/matrix/matrix.controls.js index 13b5576500..1b155225b8 100644 --- a/client/plots/matrix/matrix.controls.js +++ b/client/plots/matrix/matrix.controls.js @@ -398,7 +398,7 @@ export class MatrixControls { { label: 'By Input Data Order', value: 'asListed' }, { label: `By ${l.sample} Count`, value: 'sampleCount' } ], - styles: { padding: 0, 'padding-right': '10px', margin: 0 } + styles: { padding: 0, 'padding-right': '10px', margin: 0, display: 'inline-block' } } ] }) diff --git a/client/src/block.tk.aicheck.js b/client/src/block.tk.aicheck.js index 8794a8749d..534ead4495 100644 --- a/client/src/block.tk.aicheck.js +++ b/client/src/block.tk.aicheck.js @@ -1,7 +1,7 @@ import { scaleLinear } from 'd3-scale' import { axisLeft, axisRight } from 'd3-axis' import * as client from './client' -import { make_radios } from '../dom/radiobutton' +import { make_radios } from '#dom' /* follows bigwig track, main & subpanel rendered separately @@ -129,26 +129,19 @@ export function loadTk(tk, block) { const imgh = tk.vafheight * 3 + tk.rowspace * 4 + tk.coverageheight * 2 tk.height_main = tk.toppad + imgh + tk.bottompad - tk.img - .attr('width', block.width) - .attr('height', imgh) - .attr('xlink:href', data.src) + tk.img.attr('width', block.width).attr('height', imgh).attr('xlink:href', data.src) if (data.coveragemax) { tk.coveragemax = data.coveragemax } if (!data.nodata) { - const scale = scaleLinear() - .domain([0, 1]) - .range([tk.vafheight, 0]) + const scale = scaleLinear().domain([0, 1]).range([tk.vafheight, 0]) let y = 0 client.axisstyle({ axis: tk.Tvafaxis.attr('transform', 'translate(0,' + y + ')').call( - axisLeft() - .scale(scale) - .tickValues([0, 1]) + axisLeft().scale(scale).tickValues([0, 1]) ), color: 'black', showline: true @@ -158,9 +151,7 @@ export function loadTk(tk, block) { y = tk.vafheight + tk.rowspace + tk.coverageheight + tk.rowspace client.axisstyle({ axis: tk.Nvafaxis.attr('transform', 'translate(0,' + y + ')').call( - axisLeft() - .scale(scale) - .tickValues([0, 1]) + axisLeft().scale(scale).tickValues([0, 1]) ), color: 'black', showline: true @@ -169,26 +160,18 @@ export function loadTk(tk, block) { y = 2 * (tk.vafheight + tk.rowspace + tk.coverageheight + tk.rowspace) client.axisstyle({ - axis: tk.aiaxis.attr('transform', 'translate(0,' + y + ')').call( - axisLeft() - .scale(scale) - .tickValues([0, 1]) - ), + axis: tk.aiaxis.attr('transform', 'translate(0,' + y + ')').call(axisLeft().scale(scale).tickValues([0, 1])), color: 'black', showline: true }) tk.label_ai.attr('y', y + tk.vafheight / 2) - const scale2 = scaleLinear() - .domain([0, tk.coveragemax]) - .range([tk.coverageheight, 0]) + const scale2 = scaleLinear().domain([0, tk.coveragemax]).range([tk.coverageheight, 0]) y = tk.vafheight + tk.rowspace client.axisstyle({ axis: tk.Tcovaxis.attr('transform', 'translate(0,' + y + ')').call( - axisRight() - .scale(scale2) - .tickValues([0, tk.coveragemax]) + axisRight().scale(scale2).tickValues([0, tk.coveragemax]) ), color: 'black', showline: true @@ -198,9 +181,7 @@ export function loadTk(tk, block) { y = 2 * (tk.vafheight + tk.rowspace) + tk.coverageheight + tk.rowspace client.axisstyle({ axis: tk.Ncovaxis.attr('transform', 'translate(0,' + y + ')').call( - axisRight() - .scale(scale2) - .tickValues([0, tk.coveragemax]) + axisRight().scale(scale2).tickValues([0, tk.coveragemax]) ), color: 'black', showline: true @@ -331,11 +312,7 @@ function configPanel(tk, block) { tk.gtotalcutoff = v loadTk(tk, block) }) - row - .append('div') - .style('font-size', '.7em') - .style('opacity', 0.5) - .text('Set to 0 to use all markers') + row.append('div').style('font-size', '.7em').style('opacity', 0.5).text('Set to 0 to use all markers') } // gmafrestrict { diff --git a/client/src/block.tk.bam.js b/client/src/block.tk.bam.js index e03f41e8a4..a9506a552c 100644 --- a/client/src/block.tk.bam.js +++ b/client/src/block.tk.bam.js @@ -2,13 +2,8 @@ import { select as d3select } from 'd3-selection' import { pointer } from 'd3-selection' import { axisRight, axisTop } from 'd3-axis' import { scaleLinear } from 'd3-scale' -import { axisstyle } from '#dom/axisstyle' -import { Menu } from '#dom/menu' -import { sayerror } from '../dom/sayerror' import { dofetch3 } from '#common/dofetch' -import { make_radios } from '#dom/radiobutton' -import { make_table_2col } from '#dom/table2col' -import { make_one_checkbox } from '#dom/checkbox' +import { Menu, axisstyle, sayerror, make_radios, make_table_2col, make_one_checkbox } from '#dom' import urlmap from '#common/urlmap' /* diff --git a/client/termsetting/handlers/condition.ts b/client/termsetting/handlers/condition.ts index 06f11c3ef7..5ce500f524 100755 --- a/client/termsetting/handlers/condition.ts +++ b/client/termsetting/handlers/condition.ts @@ -328,7 +328,7 @@ function showMenu_cutoff(self, div: any) { make_radios({ holder: holder.append('div'), options, - styles: { margin: '' }, + styles: { padding: '' }, callback: (v: 'age' | 'time') => (timeScaleChoice = v) }) }