diff --git a/ivette/.eslintignore b/ivette/.eslintignore index a5bbd22266ef469c4109f7160c1a950e81386b5a..ba02ae7397f98663969ed6a85953c7f336d21644 100644 --- a/ivette/.eslintignore +++ b/ivette/.eslintignore @@ -6,5 +6,5 @@ dist coverage # don't lint tsc output, if any lib -# don't lint dome, for the moment -src/dome \ No newline at end of file +# don't lint the generated API +api diff --git a/ivette/.eslintrc.js b/ivette/.eslintrc.js index 3743b5c868ed1be148cfdacef9520626e1f84149..3e4065c048a1906f5bb0b4f990ee3f72b52ff56c 100644 --- a/ivette/.eslintrc.js +++ b/ivette/.eslintrc.js @@ -15,8 +15,15 @@ module.exports = { parserOptions: { project: './tsconfig.json', }, + settings: { + // Electron is in devDependencies because of its special build system + "import/core-modules": [ "electron" ] + }, rules: { + // Do not enforce a displayName "react/display-name": "off", + // Do not enforce component methods order + "react/sort-comp": "off", // Be more strict on usage of useMemo and useRef "react-hooks/exhaustive-deps": "error", // Allow type any, even if it should be avoided @@ -51,6 +58,8 @@ module.exports = { "padded-blocks": "off", // Allow braces on their own line "@typescript-eslint/brace-style": "off", + // Already has built-in compiler checks in TSC for that + "@typescript-eslint/no-unused-vars": "off", // Allow range conditions such as 0 <= x && x < 10 "yoda": [2, "never", { "onlyEquality": true }], // Allow single command on new line after 'if' statement @@ -77,6 +86,23 @@ module.exports = { "react/destructuring-assignment": "off", // Allow console errors and warnings "no-console": ["error", { allow: ["warn", "error"] }], + // Disable accessibility rules + "jsx-a11y/label-has-associated-control": "off", + "jsx-a11y/click-events-have-key-events": "off", + "jsx-a11y/no-static-element-interactions": "off", + "jsx-a11y/no-noninteractive-element-interactions": "off", + "jsx-a11y/no-autofocus": "off", + // Completely broken rule + "react/prop-types": "off", + // Enable ++ and -- + "no-plusplus": "off", + // Enable nested ternary operations + "no-nested-ternary": "off", + // Checked by TSC compiler + "default-case": "off", + "consistent-return": "off", + // Allow modify properties of object passed in parameter + "no-param-reassign": "error", //[ "error", { "props": false } ], // Disallow the use of var in favor of let and const "no-var": "error", // Do not favor default import diff --git a/ivette/.gitignore b/ivette/.gitignore index 6cdb63fc2a1604ecfbbec90bf1de3799108da7d0..e776663c6d3cf6a1573ce6fd0d6ad83e4cfa63e0 100644 --- a/ivette/.gitignore +++ b/ivette/.gitignore @@ -5,6 +5,7 @@ .ivette .dome-*.stamp .dome-*.back +.eslint-cache node_modules yarn-error.log /bin diff --git a/ivette/Makefile b/ivette/Makefile index a9ec5f1eaebf64972c184c45a04a6ca087c11d26..6effb1ee1b2c0b918e8d54fce26e96a678fb6746 100644 --- a/ivette/Makefile +++ b/ivette/Makefile @@ -8,28 +8,24 @@ DOME_API=./src/frama-c COPYRIGHT=CEA LIST / LSL # -------------------------------------------------------------------------- -.PHONY: all app dev doc serve dist typecheck lint tsc +.PHONY: all app dev doc serve dist lint fixlint -all: typecheck lint app +all: lint app app: dome-app dev: dome-dev dist: dome-dist -typecheck: dome-pkg dome-templ - @echo "[Ivette] running ts typechecker" - yarn run typecheck - lint: dome-pkg dome-templ - @echo "[Ivette] running ts linter" + @echo "[Ivette] running typechecker & linter" + yarn run typecheck yarn run lint fixlint: dome-pkg dome-templ - @echo "[Ivette] running ts linter (with fix)" + @echo "[Ivette] running typechecker & linter (fix mode)" + yarn run typecheck yarn run lint --fix -tsc: typecheck fixlint - # -------------------------------------------------------------------------- # --- Frama-C API # -------------------------------------------------------------------------- diff --git a/ivette/api/plugins/eva/general/index.ts b/ivette/api/plugins/eva/general/index.ts index 9e02bf5bcd03023ecf42eb76d21bbb007a679d38..519329eeb53dfa7fda48ac9c286506bd2da3440c 100644 --- a/ivette/api/plugins/eva/general/index.ts +++ b/ivette/api/plugins/eva/general/index.ts @@ -31,13 +31,12 @@ const getCallers_internal: Server.GetRequest< kind: Server.RqKind.GET, name: 'plugins.eva.general.getCallers', input: Json.jKey<'#fct'>('#fct'), - output: Json.jList(Json.jTry( - Json.jPair( - Json.jFail(Json.jKey<'#fct'>('#fct'), - '#fct expected'), - Json.jFail(Json.jKey<'#stmt'>('#stmt'), - '#stmt expected'), - ))), + output: Json.jList( + Json.jTry( + Json.jPair( + Json.jFail(Json.jKey<'#fct'>('#fct'),'#fct expected'), + Json.jFail(Json.jKey<'#stmt'>('#stmt'),'#stmt expected'), + ))), }; /** Get the list of call site of a function */ export const getCallers: Server.GetRequest< diff --git a/ivette/api/server_tsc.ml b/ivette/api/server_tsc.ml index 67920d57a0efe34b280bade299d33f559dd29af4..4e11f8b611762b57c27393e014da65f20edd760a 100644 --- a/ivette/api/server_tsc.ml +++ b/ivette/api/server_tsc.ml @@ -77,7 +77,7 @@ let makeJtype ?self ~names = | Jtag a -> Format.fprintf fmt "\"%s\"" a | Jkey kd -> Format.fprintf fmt "Json.key<'#%s'>" kd | Jindex kd -> Format.fprintf fmt "Json.index<'#%s'>" kd - | Jdict(kd,js) -> Format.fprintf fmt "Json.Dict<'#%s',%a>" kd pp js + | Jdict js -> Format.fprintf fmt "@[<hov 2>Json.dict<@,%a>@]" pp js | Jdata id | Jenum id -> pp_ident fmt id | Joption js -> Format.fprintf fmt "%a |@ undefined" pp js | Jtuple js -> @@ -174,10 +174,10 @@ let rec makeDecoder ~safe ?self ~names fmt js = | Jenum id -> jsafe ~safe (Pkg.name_of_ident id) (jenum names) fmt id | Jself -> jcall names fmt (Pkg.Derived.decode ~safe (getSelf self)) | Joption js -> makeLoose fmt js - | Jdict(kd,js) -> - Format.fprintf fmt "@[<hov 2>Json.jDictionary('#%s',@,%a)@]" kd makeLoose js + | Jdict js -> + Format.fprintf fmt "@[<hov 2>Json.jDict(@,%a)@]" makeLoose js | Jlist js -> - Format.fprintf fmt "@[<hov 2>Json.jList(%a)@]" makeLoose js + Format.fprintf fmt "@[<hov 2>Json.jList(@,%a)@]" makeLoose js | Jarray js -> if safe then Format.fprintf fmt "@[<hov 2>Json.jArray(%a)@]" makeSafe js @@ -250,11 +250,11 @@ let makeOrder ~self ~names fmt js = List.iter (fun (fd,js) -> Format.fprintf fmt "@ @[<hov 2>%s: %a,@]" fd pp js) jfs ; Format.fprintf fmt "@]@ })@]" ; - | Jdict(kd,js) -> + | Jdict js -> let jtype fmt js = makeJtype ~names fmt js in Format.fprintf fmt - "@[<hov 2>Compare.dictionary<@,Json.dict<'#%s'@,%a>>(@,%a)@]" - kd jtype js pp js + "@[<hov 2>Compare.dictionary<@,Json.dict<%a>>(@,%a)@]" + jtype js pp js | Jany | Junion _ | Jtag _ -> Format.fprintf fmt "Compare.structural" in pp fmt js diff --git a/ivette/src/dome/src/misc/utils.ts b/ivette/src/dome/src/misc/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..9dbd931ae2cc4673f797078f4654ca50d9ad2201 --- /dev/null +++ b/ivette/src/dome/src/misc/utils.ts @@ -0,0 +1,90 @@ +// -------------------------------------------------------------------------- +// --- Utilities +// -------------------------------------------------------------------------- + +/** + @packageDocumentation + @module dome/misc/utils + */ + +import type { CSSProperties } from 'react'; + +type falsy = undefined | boolean | null | ''; + +export type ClassSpec = string | falsy | { [cname: string]: true | falsy }; + +/** + Utility function to merge various HTML class properties + into a `className` property. + Class specifications can be made of: + - a string, interpreted as a CSS class specification + - an object, with keys corresponding to CSS class associated + to true of falsy value. + - any falsy value, which is discarded + + Example of usage: + + * ```ts + * const className = classes( + * 'my-base-class', + * condition && 'my-class-when-condition', + * { + * 'my-class-1': cond-1, + * 'my-class-2': cond-2, + * 'my-class-3': cond-3, + * } + * ); + * ``` + + */ +export function classes( + ...args: ClassSpec[] +): string { + const buffer: string[] = []; + args.forEach((cla) => { + if (cla) { + if (typeof (cla) === 'string' && cla !== '') buffer.push(cla); + else if (typeof (cla) === 'object') { + const cs = Object.keys(cla); + cs.forEach((c) => { if (cla[c]) buffer.push(c); }); + } + } + }); + return buffer.join(' '); +} + +export type StyleSpec = falsy | CSSProperties; + +/** + Utility function to merge various CSS style properties + into a single CSS style object. + + Each style specification can be made of a CSS object or (discarded) + falsy values. + Example of usage: + + * ```ts + * const sty = styles( + * { ... }, + * cond-1 && { ... }, + * cond-2 && { ... }, + * ); + * ``` + +*/ + +export function styles( + ...args: StyleSpec[] +): CSSProperties | undefined { + let empty = true; + let buffer = {}; + args.forEach((sty) => { + if (sty && typeof (sty) === 'object') { + empty = false; + buffer = { ...buffer, ...sty }; + } + }); + return (empty ? undefined : buffer); +} + +// -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/controls/buttons.tsx b/ivette/src/dome/src/renderer/controls/buttons.tsx index 0789962f9c941cebc455ec5c53ea11d0b2cce94d..f3f2c0a38ce7e4039e988a00d6aa2f87d0c41d12 100644 --- a/ivette/src/dome/src/renderer/controls/buttons.tsx +++ b/ivette/src/dome/src/renderer/controls/buttons.tsx @@ -8,18 +8,21 @@ */ import React from 'react'; +import { classes } from 'dome/misc/utils'; import { Icon } from './icons'; import { LabelProps } from './labels'; import './style.css'; -const DISABLED = ({ disabled = false, enabled = true }) => !!disabled || !enabled; - interface EVENT { stopPropagation: () => void; } +const DISABLED = ({ disabled = false, enabled = true }) => ( + !!disabled || !enabled +); + const TRIGGER = (onClick?: () => void) => (evt?: EVENT) => { - evt && evt.stopPropagation(); + evt?.stopPropagation(); if (onClick) onClick(); }; @@ -27,13 +30,15 @@ const TRIGGER = (onClick?: () => void) => (evt?: EVENT) => { // --- LCD // -------------------------------------------------------------------------- -const LCDCLASS = 'dome-xButton dome-xBoxButton dome-text-code dome-xButton-lcd '; - /** Button-like label. */ export function LCD(props: LabelProps) { + const className = classes( + 'dome-xButton dome-xBoxButton dome-text-code dome-xButton-lcd ', + props.className, + ); return ( <label - className={LCDCLASS + (props.className || '')} + className={className} title={props.title} style={props.style} > @@ -42,7 +47,7 @@ export function LCD(props: LabelProps) { {props.children} </label> ); -}; +} // -------------------------------------------------------------------------- // --- Led @@ -72,11 +77,15 @@ export interface LEDprops { } export const LED = (props: LEDprops) => { - const classes = 'dome-xButton-led dome-xButton-led-' - + (props.status || 'inactive') - + (props.blink ? ' dome-xButton-blink' : '') - + (props.className ? ' ' + props.className : ''); - return (<div className={classes} title={props.title} style={props.style} />); + const className = classes( + 'dome-xButton-led', + `dome-xButton-led-${props.status || 'inactive'}`, + props.blink && 'dome-xButton-blink', + props.className, + ); + return ( + <div className={className} title={props.title} style={props.style} /> + ); }; // -------------------------------------------------------------------------- @@ -89,19 +98,26 @@ const HIDDEN: React.CSSProperties = { visibility: 'hidden' }; interface LABELprops { disabled: boolean; label: string; -}; +} const LABEL = ({ disabled, label }: LABELprops) => ( - <div className="dome-xButton-label" > - <div className="dome-xButton-label dome-control-enabled" - style={disabled ? HIDDEN : VISIBLE} >{label}</div> - <div className="dome-xButton-label dome-control-disabled" - style={disabled ? VISIBLE : HIDDEN}>{label}</div> + <div className="dome-xButton-label"> + <div + className="dome-xButton-label dome-control-enabled" + style={disabled ? HIDDEN : VISIBLE} + >{label} + </div> + <div + className="dome-xButton-label dome-control-disabled" + style={disabled ? VISIBLE : HIDDEN} + >{label} + </div> </div> ); export type ButtonKind = - undefined | 'default' | 'active' | 'primary' | 'warning' | 'positive' | 'negative'; + undefined | 'default' | + 'active' | 'primary' | 'warning' | 'positive' | 'negative'; export interface ButtonProps { /** Text of the label. Prepend to other children elements. */ @@ -151,18 +167,23 @@ export interface ButtonProps { /** Standard button. */ export function Button(props: ButtonProps) { const disabled = props.onClick ? DISABLED(props) : true; - const { focusable = false, kind = 'default', + const { + focusable = false, kind = 'default', visible = true, display = true, blink = false, - selected, icon, label, className = '' } = props; - const theClass = 'dome-xButton dome-xBoxButton dome-xButton-' - + (selected ? 'selected' : kind) - + (!blink ? '' : ' dome-xButton-blink') - + (visible ? '' : ' dome-control-hidden') - + (display ? '' : ' dome-control-erased') - + (className ? ' ' + className : ''); + selected, icon, label, className = '', + } = props; + const theClass = classes( + 'dome-xButton dome-xBoxButton', + `dome-xButton-${selected ? 'selected' : kind}`, + blink && 'dome-xButton-blink', + !visible && 'dome-control-hidden', + !display && 'dome-control-erased', + className, + ); const nofocus = focusable ? undefined : true; return ( - <button type='button' + <button + type="button" className={theClass} disabled={disabled} onClick={TRIGGER(props.onClick)} @@ -175,7 +196,7 @@ export function Button(props: ButtonProps) { {label && <LABEL disabled={disabled} label={label} />} </button> ); -}; +} // -------------------------------------------------------------------------- // --- Icon Button @@ -184,18 +205,23 @@ export function Button(props: ButtonProps) { /** Circled Icon Button. The label property is ignored. */ export const CircButton = (props: ButtonProps) => { const disabled = props.onClick ? DISABLED(props) : true; - const { focusable = false, kind = 'default', + const { + focusable = false, kind = 'default', visible = true, display = true, - selected, icon, blink, className = '' } = props; - const theClass = 'dome-xButton dome-xCircButton dome-xButton-' - + (selected ? 'selected' : kind) - + (!blink ? '' : ' dome-xButton-blink') - + (visible ? '' : ' dome-control-hidden') - + (display ? '' : ' dome-control-erased') - + (className ? ' ' + className : ''); + selected, icon, blink, className = '', + } = props; + const theClass = classes( + 'dome-xButton dome-xCircButton', + `dome-xButton-${selected ? 'selected' : kind}`, + blink && 'dome-xButton-blink', + !visible && 'dome-control-hidden', + !display && 'dome-control-erased', + className, + ); const nofocus = focusable ? undefined : true; return ( - <button type='button' + <button + type="button" className={theClass} disabled={disabled} onClick={TRIGGER(props.onClick)} @@ -259,15 +285,17 @@ export function IconButton(props: IconButtonProps) { const { icon, title, className, visible = true, display = true, selected, - kind = 'default' + kind = 'default', } = props; if (!icon) return null; - const theClass = 'dome-xIconButton' - + ' dome-xIconButton-' + (selected ? 'selected' : kind) - + (disabled ? ' dome-control-disabled' : ' dome-control-enabled') - + (visible ? '' : ' dome-control-hidden') - + (display ? '' : ' dome-control-erased') - + (className ? ' ' + className : ''); + const theClass = classes( + 'dome-xIconButton', + `dome-xIconButton-${selected ? 'selected' : kind}`, + (disabled ? 'dome-control-disabled' : 'dome-control-enabled'), + !visible && 'dome-control-hidden', + !display && 'dome-control-erased', + className, + ); return ( <Icon id={icon} @@ -279,7 +307,7 @@ export function IconButton(props: IconButtonProps) { onClick={TRIGGER(disabled ? undefined : props.onClick)} /> ); -}; +} // -------------------------------------------------------------------------- // --- CheckBox @@ -318,11 +346,14 @@ export const Checkbox = (props: CheckProps) => { <label title={props.title} style={props.style} - className={baseClass + labelClass} > - <input type="checkbox" + className={baseClass + labelClass} + > + <input + type="checkbox" disabled={disabled} checked={value} - onChange={callback} /> + onChange={callback} + /> {props.label} </label> ); @@ -334,9 +365,11 @@ export const Switch = (props: CheckProps) => { const disabled = onChange ? DISABLED(props) : true; const iconId = props.value ? 'SWITCH.ON' : 'SWITCH.OFF'; const onClick = onChange && (() => onChange(!value)); - const className = 'dome-xSwitch ' - + (disabled ? 'dome-control-disabled' : 'dome-control-enabled') - + (props.className ? ' ' + props.className : ''); + const className = classes( + 'dome-xSwitch', + (disabled ? 'dome-control-disabled' : 'dome-control-enabled'), + props.className, + ); return ( <label title={props.title} @@ -387,13 +420,18 @@ export function Radio<A>(props: RadioProps<A>) { <label title={props.title} style={props.style} - className={baseClass + labelClass} > - <input type="radio" - disabled={disabled} checked={checked} onChange={onChange} /> + className={baseClass + labelClass} + > + <input + type="radio" + disabled={disabled} + checked={checked} + onChange={onChange} + /> {props.label} </label> ); -}; +} // -------------------------------------------------------------------------- // --- Radio Group @@ -410,15 +448,15 @@ export interface RadioGroupProps<A> { onChange?: (newValue: A) => void; /** Default selected value. */ className?: string; - /** Additional style for the `<dov/>` container of Raiods */ + /** Additional style for the `< dov /> ` container of Raiods */ style?: React.CSSProperties; /** [[Radio]] Buttons. */ children: any; -}; +} /** - Selector of Radio Buttons. - Childrens of the `RadioGroup` shall be [[Radio]] buttons. + Selector of Radio Buttons. Childrens of the `RadioGroup` shall be [[Radio]] + buttons. The selected value of the group is broadcasted to the radio buttons. Their callbacks are activated _before_ the radio group one, if any. @@ -428,26 +466,34 @@ export interface RadioGroupProps<A> { group is enabled, the `disabled` property of each radio button is taken into account. - The radio buttons inside a group are laidout in a vertical box with the additional - styling properties. - */ + The radio buttons inside a group are laidout in a vertical box with the + additional styling properties. +*/ export function RadioGroup<A>(props: RadioGroupProps<A>) { - const { className = '', style, value: selection, onChange: onGroupSelect } = props; + const { + className = '', + style, + value: selection, + onChange: onGroupSelect, + } = props; const disabledGroup = onGroupSelect ? DISABLED(props) : true; const makeRadio = (elt: any) => { const radioProps = elt.props as RadioProps<A>; const disabled = disabledGroup || DISABLED(radioProps); const { onSelection: onRadioSelect } = radioProps; const onSelection = (v: A) => { - onRadioSelect && onRadioSelect(v); - onGroupSelect && onGroupSelect(v); + if (onRadioSelect) onRadioSelect(v); + if (onGroupSelect) onGroupSelect(v); }; return React.cloneElement(elt, { - disabled, enabled: !disabled, selection, onSelection + disabled, + enabled: !disabled, + selection, + onSelection, }); }; return ( - <div className={'dome-xRadio-group ' + className} style={style}> + <div className={`dome - xRadio - group ${className} `} style={style}> {React.Children.map(props.children, makeRadio)} </div> ); @@ -474,7 +520,7 @@ export interface SelectProps { onChange?: (newValue?: string) => void; /** Default selected value. */ className?: string; - /** Additional style for the `<dov/>` container of Raiods */ + /** Additional style for the `< dov /> ` container of Raiods */ style?: React.CSSProperties; /** Shall be [[Item]] elements. */ children: any; @@ -483,34 +529,37 @@ export interface SelectProps { /** Menu Button. - The different options shall be specified with HTML `<option/>` and `<optgroup />` elements. + The different options shall be specified with HTML + `< option/>` and `<optgroup/>` elements. Options and group shall be specified as follows: - <optgroup label='…'>…</optgroup> - <option value='…' disabled=… >…</option> + * <optgroup label='…'>…</optgroup> + * <option value='…' disabled=… >…</option> - **Warning:** most non-positionning CSS properties might not work on the`<select>` element due - to the native rendering used by Chrome. - You might use`-webkit-appearance: none` to cancel this behavior, you will have to restyle the + **Warning:** most non-positionning CSS properties might not + work on the`<select>` element due to the native rendering used + by Chrome. + You might use `-webkit-appearance: none` to cancel this behavior, + you will have to restyle the component entirely, which is quite ugly by default. */ export function Select(props: SelectProps) { const { onChange, className = '', placeholder } = props; const disabled = onChange ? DISABLED(props) : true; const callback = (evt: React.ChangeEvent<HTMLSelectElement>) => { - onChange && onChange(evt.target.value); + if (onChange) onChange(evt.target.value); }; return ( <select id={props.id} disabled={disabled} - className={'dome-xSelect ' + className} + className={`dome - xSelect ${className} `} style={props.style} title={props.title} value={props.value} onChange={callback} > - {placeholder && <option value=''>— {placeholder} —</option>} + {placeholder && <option value="">— {placeholder} —</option>} {props.children} </select> ); @@ -533,7 +582,7 @@ export interface FieldProps { disabled?: boolean; /** Default fo `false`. */ autoFocus?: boolean; - /** Currently selected value (updated on `ENTER` key)*/ + /** Currently selected value (updated on `ENTER` key) */ value?: string; /** Callback on `ENTER` key. */ onChange?: (newValue: string) => void; @@ -541,7 +590,7 @@ export interface FieldProps { onEdited?: (tmpValue: string) => void; /** Default selected value. */ className?: string; - /** Additional style for the `<dov/>` container of Raiods */ + /** Additional style for the `< dov /> ` container of Raiods */ style?: React.CSSProperties; } @@ -554,31 +603,36 @@ export const Field = (props: FieldProps) => { const disabled = onChange ? DISABLED(props) : true; const theValue = current ?? value; const ONCHANGE = (evt: React.ChangeEvent<HTMLInputElement>) => { - let text = evt.target.value || ''; + const text = evt.target.value || ''; setCurrent(text); - onEdited && onEdited(text); + if (onEdited) onEdited(text); }; const ONKEYPRESS = (evt: React.KeyboardEvent) => { switch (evt.key) { case 'Enter': setCurrent(undefined); - onChange && current && onChange(current); + if (onChange && current) onChange(current); break; case 'Escape': setCurrent(undefined); break; - }; + default: + break; + } }; return ( - <input id={props.id} type='text' + <input + id={props.id} + type="text" autoFocus={!disabled && props.autoFocus} value={theValue} - className={'dome-xField ' + className} + className={`dome - xField ${className} `} style={props.style} disabled={disabled} placeholder={props.placeholder} onKeyPress={ONKEYPRESS} - onChange={ONCHANGE} /> + onChange={ONCHANGE} + /> ); }; diff --git a/ivette/src/dome/src/renderer/controls/icons.tsx b/ivette/src/dome/src/renderer/controls/icons.tsx index a15c2642911594d421d221b8355de6def6d2aa82..d6547bf5a8d89165ae31e9d4d1a9154aa4ffb33a 100644 --- a/ivette/src/dome/src/renderer/controls/icons.tsx +++ b/ivette/src/dome/src/renderer/controls/icons.tsx @@ -12,11 +12,12 @@ import _ from 'lodash'; import React from 'react'; +import { classes } from 'dome/misc/utils'; import Gallery from './gallery.json'; import './style.css'; -/*@ internal */ -const Icons: { [id: string]: { viewBox?: string, path: string } } = Gallery; +/* @ internal */ +const Icons: { [id: string]: { viewBox?: string; path: string } } = Gallery; // -------------------------------------------------------------------------- // --- Raw SVG element @@ -48,9 +49,9 @@ export function SVG(props: SVGprops): null | JSX.Element { const { title, size = 12, - offset = _.floor(- size * 0.125), + offset = _.floor(-size * 0.125), } = props; - const { path, viewBox = "0 0 24 24" } = icon; + const { path, viewBox = '0 0 24 24' } = icon; return ( <svg height={size} @@ -77,7 +78,7 @@ export interface IconProps extends SVGprops { /** Fill style property. */ fill?: string; /** Click callback. */ - onClick?: (event?: React.MouseEvent) => void; + onClick?: (event?: React.MouseEvent<HTMLDivElement>) => void; } /** @@ -85,8 +86,11 @@ export interface IconProps extends SVGprops { Consult the [Icon Gallery](../guides/icons.md.html) for default icons. */ export function Icon(props: IconProps) { - const { id, title, onClick, fill, size, className = '', offset, style } = props; - const divClass = 'dome-xIcon ' + className; + const { + id, title, onClick, fill, + size, className = '', offset, style, + } = props; + const divClass = classes('dome-xIcon', className); const divStyle = fill ? { fill, ...style } : style; return ( <div @@ -130,13 +134,16 @@ export function Badge(props: BadgeProps) { } else { const style = (typeof (value) === 'number' && value < 10) || - (typeof (value) === 'string' && value.length == 1) ? + (typeof (value) === 'string' && value.length === 1) ? { paddingLeft: 2, paddingRight: 2 } : {}; - content = <label style={style} className='dome-text-label'>{value}</label>; + content = <label style={style} className="dome-text-label">{value}</label>; } return ( - <div className="dome-xBadge" - title={title} onClick={onClick}> + <div + className="dome-xBadge" + title={title} + onClick={onClick} + > {content} </div> ); @@ -169,10 +176,11 @@ export function register(icon: CustomIcon) { See [[register]] to add custom icons to the gallery. */ export function forEach(fn: (ico: CustomIcon) => void) { - for (let id in Icons) { + const ids = Object.keys(Icons); + ids.forEach((id) => { const jsicon = Icons[id]; fn({ id, ...jsicon }); - } + }); } // -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/controls/labels.tsx b/ivette/src/dome/src/renderer/controls/labels.tsx index 43273c5ee1505b8cf6aec5548c6157b452315a17..28c1b23777a292e970418364efb5d4e4c4afd22d 100644 --- a/ivette/src/dome/src/renderer/controls/labels.tsx +++ b/ivette/src/dome/src/renderer/controls/labels.tsx @@ -8,6 +8,7 @@ */ import React from 'react'; +import { classes } from 'dome/misc/utils'; import { Icon } from './icons'; import './style.css'; @@ -34,14 +35,17 @@ export interface LabelProps { const makeLabel = (className: string, props: LabelProps) => { const { display = true } = props; - const allClasses = - className + - (display ? ' ' : ' dome-control-erased ') + - (props.className || ''); + const allClasses = classes( + className, + !display && 'dome-control-erased', + props.className, + ); return ( - <label className={allClasses} + <label + className={allClasses} title={props.title} - style={props.style} > + style={props.style} + > {props.icon && <Icon title={props.title} id={props.icon} />} {props.label} {props.children} @@ -53,11 +57,11 @@ const makeLabel = (className: string, props: LabelProps) => { // --- CSS Classes // -------------------------------------------------------------------------- -const LABEL = "dome-xLabel dome-text-label"; -const TITLE = "dome-xLabel dome-text-title"; -const DESCR = "dome-xLabel dome-text-descr"; -const TDATA = "dome-xLabel dome-text-data"; -const TCODE = "dome-xLabel dome-text-code"; +const LABEL = 'dome-xLabel dome-text-label'; +const TITLE = 'dome-xLabel dome-text-title'; +const DESCR = 'dome-xLabel dome-text-descr'; +const TDATA = 'dome-xLabel dome-text-data'; +const TCODE = 'dome-xLabel dome-text-code'; // -------------------------------------------------------------------------- // --- Components diff --git a/ivette/src/dome/src/renderer/data/compare.ts b/ivette/src/dome/src/renderer/data/compare.ts index b9bcc989b71af9d42a5f7ab74406649a466ad81d..337be8717b306847b32aa58963bf784c3e2d0fcf 100644 --- a/ivette/src/dome/src/renderer/data/compare.ts +++ b/ivette/src/dome/src/renderer/data/compare.ts @@ -13,7 +13,7 @@ import FastCompare from 'react-fast-compare'; /** Interface for comparison functions. These function shall fullfill the following contract: - - `compare(x,y) == 0` shall be an equivalence relation + - `compare(x,y) === 0` shall be an equivalence relation (reflexive, symmetric, transitive) - `compare(x,y) <= 0` shall be a complete order (reflexive, antisymetric, transitive) @@ -38,7 +38,10 @@ export type bignum = bigint | number; /** Detect Non-NaN numbers and big-ints. */ export function isBigNum(x: any): x is bignum { - return typeof (x) === 'bigint' || (typeof (x) === 'number' && !Number.isNaN(x)); + return ( + (typeof (x) === 'bigint') || + (typeof (x) === 'number' && !Number.isNaN(x)) + ); } /** @internal */ @@ -88,17 +91,18 @@ export function number(x: number, y: number) { */ export function alpha(x: string, y: string) { const cmp = primitive(x.toLowerCase(), y.toLowerCase()); - return cmp != 0 ? cmp : primitive(x, y); + return cmp !== 0 ? cmp : primitive(x, y); } /** Combine comparison orders in sequence. */ export function sequence<A>(...orders: (Order<A> | undefined)[]): Order<A> { return (x: A, y: A) => { if (x === y) return 0; - for (const order of orders) { + for (let k = 0; k < orders.length; k++) { + const order = orders[k]; if (order) { const cmp = order(x, y); - if (cmp != 0) return cmp; + if (cmp !== 0) return cmp; } } return 0; @@ -108,9 +112,9 @@ export function sequence<A>(...orders: (Order<A> | undefined)[]): Order<A> { /** Compare optional values. Undefined values come first. */ export function option<A>(order: Order<A>): Order<undefined | A> { return (x?: A, y?: A) => { - if (x == undefined && y == undefined) return 0; - if (x == undefined) return -1; - if (y == undefined) return 1; + if (x === undefined && y === undefined) return 0; + if (x === undefined) return -1; + if (y === undefined) return 1; return order(x, y); }; } @@ -118,9 +122,9 @@ export function option<A>(order: Order<A>): Order<undefined | A> { /** Compare optional values. Undefined values come last. */ export function defined<A>(order: Order<A>): Order<undefined | A> { return (x?: A, y?: A) => { - if (x == undefined && y == undefined) return 0; - if (x == undefined) return 1; - if (y == undefined) return -1; + if (x === undefined && y === undefined) return 0; + if (x === undefined) return 1; + if (y === undefined) return -1; return order(x, y); }; } @@ -134,7 +138,7 @@ export function array<A>(order: Order<A>): Order<A[]> { const m = p < q ? p : q; for (let k = 0; k < m; k++) { const cmp = order(x[k], y[k]); - if (cmp != 0) return cmp; + if (cmp !== 0) return cmp; } return p - q; }; @@ -143,11 +147,13 @@ export function array<A>(order: Order<A>): Order<A[]> { /** Order by dictionary order. Can be used directly with an enum type declaration. */ -export function byEnum<A extends string>(d: { [key: string]: A }): Order<A> { +export function byEnum<A extends string>( + d: { [key: string]: A }, +): Order<A> { const ranks: { [index: string]: number } = {}; const values = Object.keys(d); const wildcard = values.length; - values.forEach((C, k) => ranks[C] = k); + values.forEach((C, k) => { ranks[C] = k; }); return (x: A, y: A) => { if (x === y) return 0; const rx = ranks[x] ?? wildcard; @@ -157,20 +163,21 @@ export function byEnum<A extends string>(d: { [key: string]: A }): Order<A> { } /** Order string enumeration constants. - `byRank(v1,...,vN)` will order constant following the order of arguments. - Non-listed constants appear at the end, or at the rank specified by `'*'`. */ + `byRank(v1,...,vN)` will order constant following the + order of arguments. + Non-listed constants appear at the end, or at the rank + specified by `'*'`. */ export function byRank(...args: string[]): Order<string> { const ranks: { [index: string]: number } = {}; - args.forEach((C, k) => ranks[C] = k); + args.forEach((C, k) => { ranks[C] = k; }); const wildcard = ranks['*'] ?? ranks.length; return (x: string, y: string) => { if (x === y) return 0; const rx = ranks[x] ?? wildcard; const ry = ranks[y] ?? wildcard; - if (rx == wildcard && ry == wildcard) + if (rx === wildcard && ry === wildcard) return primitive(x, y); - else - return rx - ry; + return rx - ry; }; } @@ -196,7 +203,7 @@ export function getKeys<T>(a: T): (keyof T)[] { */ export type ByFields<A> = { [P in keyof A]?: Order<A[P]>; -} +}; /** Maps each field of `A` to some comparison of the associated type. @@ -205,36 +212,39 @@ export type ByFields<A> = { */ export type ByAllFields<A> = { [P in keyof A]: Order<A[P]>; -} +}; -/** Object comparison by (some) fields. +/** + Object comparison by (some) fields. - Compare objects field by field, using the comparison orders provided by the - `order` argument. Order of field comparison is taken from the `order` - argument, not from the compared values. + Compare objects field by field, using the comparison orders provided by the + `order` argument. Order of field comparison is taken from the `order` + argument, not from the compared values. - You may not compare _all_ fields of the compared values. For optional - fields, you shall provide a comparison function compatible with type - `undefined`. + You may not compare _all_ fields of the compared values. For optional + fields, you shall provide a comparison function compatible with type + `undefined`. - It might be difficult for Typescript to typecheck `byFields(…)` expressions - when dealing with optional types. In such cases, you shall use `byFields<A>(…)` - and explicitly mention the type of compared values. + It might be difficult for Typescript to typecheck `byFields(…)` expressions + when dealing with optional types. In such cases, you shall use + `byFields<A>(…)` and explicitly mention the type of compared values. - Example: + Example: - type foo = { id: number, name?: string, descr?: string } - const compare = fields<foo>({ id: number, name: option(alpha) }); + * type foo = { id: number, name?: string, descr?: string } + * const compare = fields<foo>({ id: number, name: option(alpha) }); */ export function byFields<A>(order: ByFields<A>): Order<A> { return (x: A, y: A) => { if (x === y) return 0; - for (const fd of getKeys(order)) { + const fds = getKeys(order); + for (let k = 0; k < fds.length; k++) { + const fd = fds[k]; const byFd = order[fd]; if (byFd !== undefined) { const cmp = byFd(x[fd], y[fd]); - if (cmp != 0) return cmp; + if (cmp !== 0) return cmp; } } return 0; @@ -248,10 +258,12 @@ export function byFields<A>(order: ByFields<A>): Order<A> { export function byAllFields<A>(order: ByAllFields<A>): Order<A> { return (x: A, y: A) => { if (x === y) return 0; - for (const fd of getKeys<ByFields<A>>(order)) { + const fds = getKeys<ByFields<A>>(order); + for (let k = 0; k < fds.length; k++) { + const fd = fds[k]; const byFd = order[fd]; const cmp = byFd(x[fd], y[fd]); - if (cmp != 0) return cmp; + if (cmp !== 0) return cmp; } return 0; }; @@ -273,13 +285,14 @@ export function dictionary<A>(order: Order<A>): Order<dict<A>> { const p = fs.length; const q = gs.length; for (let i = 0, j = 0; i < p && j < q;) { - let a = undefined, b = undefined; + let a; + let b; const f = fs[i]; const g = gs[j]; if (f <= g) { a = dx[f]; i++; } if (g <= f) { b = dy[g]; j++; } const cmp = phi(a, b); - if (cmp != 0) return cmp; + if (cmp !== 0) return cmp; } return p - q; }; @@ -292,7 +305,7 @@ export function pair<A, B>(ordA: Order<A>, ordB: Order<B>): Order<[A, B]> { const [x1, y1] = u; const [x2, y2] = v; const cmp = ordA(x1, x2); - return cmp != 0 ? cmp : ordB(y1, y2); + return cmp !== 0 ? cmp : ordB(y1, y2); }; } @@ -307,9 +320,9 @@ export function triple<A, B, C>( const [x1, y1, z1] = u; const [x2, y2, z2] = v; const cmp1 = ordA(x1, x2); - if (cmp1 != 0) return cmp1; + if (cmp1 !== 0) return cmp1; const cmp2 = ordB(y1, y2); - if (cmp2 != 0) return cmp2; + if (cmp2 !== 0) return cmp2; return ordC(z1, z2); }; } @@ -326,11 +339,11 @@ export function tuple4<A, B, C, D>( const [x1, y1, z1, t1] = u; const [x2, y2, z2, t2] = v; const cmp1 = ordA(x1, x2); - if (cmp1 != 0) return cmp1; + if (cmp1 !== 0) return cmp1; const cmp2 = ordB(y1, y2); - if (cmp2 != 0) return cmp2; + if (cmp2 !== 0) return cmp2; const cmp3 = ordC(z1, z2); - if (cmp3 != 0) return cmp3; + if (cmp3 !== 0) return cmp3; return ordD(t1, t2); }; } @@ -348,13 +361,13 @@ export function tuple5<A, B, C, D, E>( const [x1, y1, z1, t1, w1] = u; const [x2, y2, z2, t2, w2] = v; const cmp1 = ordA(x1, x2); - if (cmp1 != 0) return cmp1; + if (cmp1 !== 0) return cmp1; const cmp2 = ordB(y1, y2); - if (cmp2 != 0) return cmp2; + if (cmp2 !== 0) return cmp2; const cmp3 = ordC(z1, z2); - if (cmp3 != 0) return cmp3; + if (cmp3 !== 0) return cmp3; const cmp4 = ordD(t1, t2); - if (cmp4 != 0) return cmp4; + if (cmp4 !== 0) return cmp4; return ordE(w1, w2); }; } @@ -364,11 +377,16 @@ export function tuple5<A, B, C, D, E>( // -------------------------------------------------------------------------- /** @internal */ -enum RANK { UNDEFINED, BOOLEAN, SYMBOL, NAN, BIGNUM, STRING, ARRAY, OBJECT, FUNCTION }; +enum RANK { + UNDEFINED, + BOOLEAN, SYMBOL, NAN, BIGNUM, + STRING, + ARRAY, OBJECT, FUNCTION +} /** @internal */ function rank(x: any): RANK { - let t = typeof x; + const t = typeof x; switch (t) { case 'undefined': return RANK.UNDEFINED; case 'boolean': return RANK.BOOLEAN; @@ -378,14 +396,16 @@ function rank(x: any): RANK { case 'bigint': return RANK.BIGNUM; case 'string': return RANK.STRING; - case 'object': return Array.isArray(x) ? RANK.ARRAY : RANK.OBJECT; case 'function': return RANK.FUNCTION; + case 'object': + return Array.isArray(x) ? RANK.ARRAY : RANK.OBJECT; } } /** Universal structural comparison. - Values are ordered by _rank_, each being associated with some type of values: + Values are ordered by _rank_, each being + associated with some type of values: 1. undefined values; 2. booleans; 3. symbols; @@ -398,10 +418,9 @@ function rank(x: any): RANK { For values of same primitive type, primitive ordering is performed. For array values, lexicographic ordering is performed. - - For object values, lexicographic ordering is performed over their properties: - properties are ordered by name, and recursive structural ordering is performed - on property values. + For object values, lexicographic ordering is performed over their + properties: they are ordered by name, and recursive structural + ordering is performed on property values. All functions are compared equal. */ @@ -418,17 +437,18 @@ export function structural(x: any, y: any): number { const p = fs.length; const q = gs.length; for (let i = 0, j = 0; i < p && j < q;) { - let a = undefined, b = undefined; + let a; + let b; const f = fs[i]; const g = gs[j]; if (f <= g) { a = x[f]; i++; } if (g <= f) { b = y[g]; j++; } const cmp = structural(a, b); - if (cmp != 0) return cmp; + if (cmp !== 0) return cmp; } return p - q; } return rank(x) - rank(y); -}; +} // -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/data/json.ts b/ivette/src/dome/src/renderer/data/json.ts index e34c8b6aa9f1e5e1255309262ac168956b820c0a..2dfd4a7b52b6c5ad984f6ad6d41ae84fd294a98d 100644 --- a/ivette/src/dome/src/renderer/data/json.ts +++ b/ivette/src/dome/src/renderer/data/json.ts @@ -11,7 +11,7 @@ import { DEVEL } from 'dome/system'; export type json = - undefined | null | number | string | json[] | { [key: string]: json } + undefined | null | number | string | json[] | { [key: string]: json }; /** Parse without _revivals_. @@ -71,14 +71,14 @@ export interface Encoder<D> { } /** Can be used for most encoders. */ -export function identity<A>(v: A): A { return v; }; +export function identity<A>(v: A): A { return v; } // -------------------------------------------------------------------------- // --- Primitives // -------------------------------------------------------------------------- /** Always returns `undefined` on any input. */ -export const jNull: Safe<undefined> = (_: json) => undefined; +export const jNull: Safe<undefined> = () => undefined; /** Identity. */ export const jAny: Safe<json> = (js: json) => js; @@ -124,7 +124,7 @@ export const jString: Loose<string> = (js: json) => ( might be more efficient. */ export function jTag<A>(tg: A): Loose<A> { - return (js: json) => Object.is(js, tg) ? tg : undefined; + return (js: json) => (Object.is(js, tg) ? tg : undefined); } /** @@ -132,7 +132,7 @@ export function jTag<A>(tg: A): Loose<A> { Can be used directly for enum types, eg. `jEnum(myEnumType)`. */ export function jEnum<A>(d: { [tag: string]: A }): Loose<A> { - return (v: json) => typeof v === 'string' ? d[v] : undefined; + return (v: json) => (typeof v === 'string' ? d[v] : undefined); } /** @@ -141,8 +141,8 @@ export function jEnum<A>(d: { [tag: string]: A }): Loose<A> { type `A`. However, it will not protected you from missings constants in `A`. */ export function jTags<A>(...values: ((string | number) & A)[]): Loose<A> { - var m = new Map<string | number, A>(); - values.forEach(v => m.set(v, v)); + const m = new Map<string | number, A>(); + values.forEach((v) => m.set(v, v)); return (v: json) => (typeof v === 'string' ? m.get(v) : undefined); } @@ -155,16 +155,20 @@ export function jDefault<A>( fn: Loose<A>, defaultValue: A, ): Safe<A> { - return (js: json) => - js === undefined ? defaultValue : (fn(js) ?? defaultValue); + return (js: json) => ( + js === undefined ? defaultValue : (fn(js) ?? defaultValue) + ); } /** - Force returning `undefined` or a default value for `undefined` _or_ `null` JSON input. + Force returning `undefined` or a default value for + `undefined` _or_ `null` JSON input. Typically useful to leverage an existing `Safe<A>` decoder. */ export function jOption<A>(fn: Safe<A>, defaultValue?: A): Loose<A> { - return (js: json) => (js === undefined || js === null ? defaultValue : fn(js)); + return (js: json) => ( + js === undefined || js === null ? defaultValue : fn(js) + ); } /** @@ -175,7 +179,7 @@ export function jFail<A>(fn: Loose<A>, error: string | Error): Safe<A> { return (js: json) => { const d = fn(js); if (d !== undefined) return d; - throw error; + throw (typeof (error) === 'string' ? new Error(error) : error); }; } @@ -216,10 +220,11 @@ export function jMap<A>(fn: Loose<A>): Safe<Map<string, A>> { return (js: json) => { const m = new Map<string, A>(); if (js !== null && typeof js === 'object' && !Array.isArray(js)) { - for (let k of Object.keys(js)) { + const keys = Object.keys(js); + keys.forEach((k) => { const v = fn(js[k]); if (v !== undefined) m.set(k, v); - } + }); } return m; }; @@ -229,7 +234,7 @@ export function jMap<A>(fn: Loose<A>): Safe<Map<string, A>> { Converts dictionaries to maps. */ export function eMap<A>(fn: Encoder<A>): Encoder<Map<string, undefined | A>> { - return m => { + return (m) => { const js: json = {}; m.forEach((v, k) => { if (v !== undefined) { @@ -248,7 +253,7 @@ export function eMap<A>(fn: Encoder<A>): Encoder<Map<string, undefined | A>> { to discard undefined elements, or use a true _safe_ decoder. */ export function jArray<A>(fn: Safe<A>): Safe<A[]> { - return (js: json) => Array.isArray(js) ? js.map(fn) : []; + return (js: json) => (Array.isArray(js) ? js.map(fn) : []); } /** @@ -259,7 +264,7 @@ export function jArray<A>(fn: Safe<A>): Safe<A[]> { export function jList<A>(fn: Loose<A>): Safe<A[]> { return (js: json) => { const buffer: A[] = []; - if (Array.isArray(js)) js.forEach(vj => { + if (Array.isArray(js)) js.forEach((vj) => { const d = fn(vj); if (d !== undefined) buffer.push(d); }); @@ -271,9 +276,9 @@ export function jList<A>(fn: Loose<A>): Safe<A[]> { Exports all non-undefined elements. */ export function eList<A>(fn: Encoder<A>): Encoder<(A | undefined)[]> { - return m => { + return (m) => { const js: json[] = []; - m.forEach(v => { + m.forEach((v) => { if (v !== undefined) { const u = fn(v); if (u !== undefined) js.push(u); @@ -288,10 +293,10 @@ export function jPair<A, B>( fa: Safe<A>, fb: Safe<B>, ): Loose<[A, B]> { - return (js: json) => Array.isArray(js) ? [ + return (js: json) => (Array.isArray(js) ? [ fa(js[0]), fb(js[1]), - ] : undefined; + ] : undefined); } /** Similar to [[jPair]]. */ @@ -300,11 +305,11 @@ export function jTriple<A, B, C>( fb: Safe<B>, fc: Safe<C>, ): Loose<[A, B, C]> { - return (js: json) => Array.isArray(js) ? [ + return (js: json) => (Array.isArray(js) ? [ fa(js[0]), fb(js[1]), fc(js[2]), - ] : undefined; + ] : undefined); } /** Similar to [[jPair]]. */ @@ -314,12 +319,12 @@ export function jTuple4<A, B, C, D>( fc: Safe<C>, fd: Safe<D>, ): Loose<[A, B, C, D]> { - return (js: json) => Array.isArray(js) ? [ + return (js: json) => (Array.isArray(js) ? [ fa(js[0]), fb(js[1]), fc(js[2]), fd(js[3]), - ] : undefined; + ] : undefined); } /** Similar to [[jPair]]. */ @@ -330,13 +335,13 @@ export function jTuple5<A, B, C, D, E>( fd: Safe<D>, fe: Safe<E>, ): Loose<[A, B, C, D, E]> { - return (js: json) => Array.isArray(js) ? [ + return (js: json) => (Array.isArray(js) ? [ fa(js[0]), fb(js[1]), fc(js[2]), fd(js[3]), fe(js[4]), - ] : undefined; + ] : undefined); } /** @@ -345,7 +350,7 @@ export function jTuple5<A, B, C, D, E>( */ export type Props<A> = { [P in keyof A]: Safe<A[P]>; -} +}; /** Decode an object given the decoders of its fields. @@ -355,7 +360,8 @@ export function jObject<A>(fp: Props<A>): Loose<A> { return (js: json) => { if (js !== null && typeof js === 'object' && !Array.isArray(js)) { const buffer = {} as A; - for (var k of Object.keys(fp)) { + const keys = Object.keys(fp); + keys.forEach((k) => { const fn = fp[k as keyof A]; if (fn !== undefined) { const fj = js[k]; @@ -364,7 +370,7 @@ export function jObject<A>(fp: Props<A>): Loose<A> { if (fv !== undefined) buffer[k as keyof A] = fv; } } - } + }); return buffer; } return undefined; @@ -376,8 +382,8 @@ export function jObject<A>(fp: Props<A>): Loose<A> { */ export function jUnion<A>(...cases: Loose<A>[]): Loose<A> { return (js: json) => { - for (var fn of cases) { - const fv = fn(js); + for (let i = 0; i < cases.length; i++) { + const fv = cases[i](js); if (fv !== undefined) return fv; } return undefined; @@ -389,7 +395,7 @@ export function jUnion<A>(...cases: Loose<A>[]): Loose<A> { */ export type EProps<A> = { [P in keyof A]?: Encoder<A[P]>; -} +}; /** Encode an object given the provided encoders by fields. @@ -399,7 +405,8 @@ export type EProps<A> = { export function eObject<A>(fp: EProps<A>): Encoder<A> { return (m: A) => { const js: json = {}; - for (var k of Object.keys(fp)) { + const keys = Object.keys(fp); + keys.forEach((k) => { const fn = fp[k as keyof A]; if (fn !== undefined) { const fv = m[k as keyof A]; @@ -408,9 +415,9 @@ export function eObject<A>(fp: EProps<A>): Encoder<A> { if (r !== undefined) js[k] = r; } } - } + }); return js; - } + }; } // Intentionnaly internal and only declared @@ -423,56 +430,43 @@ export function forge<K, A>(_tag: K, data: A): phantom<K, A> { return data as any; } -/** String key with kind. Can be used as a `string` but shall be created with [forge]. */ +/** String key with kind. + Can be used as a `string` but shall be created with [forge]. */ export type key<K> = phantom<K, string>; -/** Number index with kind. Can be used as a `number` but shall be created with [forge]. */ +/** Number index with kind. + Can be used as a `number` but shall be created with [forge]. */ export type index<K> = phantom<K, number>; /** Decoder for `key<K>` strings. */ export function jKey<K>(kd: K): Loose<key<K>> { - return (js: json) => typeof js === 'string' ? forge(kd, js) : undefined; + return (js: json) => (typeof js === 'string' ? forge(kd, js) : undefined); } /** Decoder for `index<K>` numbers. */ export function jIndex<K>(kd: K): Loose<index<K>> { - return (js: json) => typeof js === 'number' ? forge(kd, js) : undefined; -} - -/** Dictionaries with « typed » keys. */ -export type dict<K, A> = phantom<K, { [key: string]: A }> - -/** Lookup into dictionary. - Better than a direct access to `d[k]` for undefined values. */ -export function lookup<K, A>(d: dict<K, A>, k: key<K>): A | undefined { - return d[k]; -} - -/** Empty dictionary. */ -export function empty<K, A>(kd: K): dict<K, A> { - return forge(kd, {} as any); + return (js: json) => (typeof js === 'number' ? forge(kd, js) : undefined); } -/** Dictionary extension. */ -export function index<K, A>(d: dict<K, A>, key: key<K>, value: A) { - d[key] = value; -} +/** Dictionaries. */ +export type dict<A> = { [key: string]: A }; /** Decode a JSON dictionary, discarding all inconsistent entries. If the JSON contains no valid entry, still returns `{}`. */ -export function jDictionary<K, A>(kd: K, fn: Loose<A>): Safe<dict<K, A>> { +export function jDict<A>(fn: Loose<A>): Safe<dict<A>> { return (js: json) => { - const buffer: dict<K, A> = empty(kd); + const buffer: dict<A> = {}; if (js !== null && typeof js === 'object' && !Array.isArray(js)) { - for (var key of Object.keys(js)) { + const keys = Object.keys(js); + keys.forEach((key) => { const fd = js[key]; if (fd !== undefined) { const fv = fn(fd); - if (fv !== undefined) index(buffer, forge(kd, key), fv); + if (fv !== undefined) buffer[key] = fv; } - } + }); } return buffer; }; @@ -482,16 +476,17 @@ export function jDictionary<K, A>(kd: K, fn: Loose<A>): Safe<dict<K, A>> { Encode a dictionary into JSON, discarding all inconsistent entries. If the dictionary contains no valid entry, still returns `{}`. */ -export function eDictionary<K, A>(fn: Encoder<A>): Encoder<dict<K, A>> { - return (d: dict<K, A>) => { +export function eDict<A>(fn: Encoder<A>): Encoder<dict<A>> { + return (d: dict<A>) => { const js: json = {}; - for (var k of Object.keys(d)) { + const keys = Object.keys(d); + keys.forEach((k) => { const fv = d[k]; if (fv !== undefined) { const fr = fn(fv); if (fr !== undefined) js[k] = fr; } - } + }); return js; }; } diff --git a/ivette/src/dome/src/renderer/data/states.ts b/ivette/src/dome/src/renderer/data/states.ts index c1fbb827435c3b1f721268d059e9a4640d7aeb0b..ce7704696991024b8dc1ecb49baaa7825a6c3466 100644 --- a/ivette/src/dome/src/renderer/data/states.ts +++ b/ivette/src/dome/src/renderer/data/states.ts @@ -11,9 +11,6 @@ import React from 'react'; import Emitter from 'events'; import isEqual from 'react-fast-compare'; -import { DEVEL } from 'dome/misc/system'; -import * as Dome from 'dome'; -import * as JSON from './json'; const UPDATE = 'dome.states.update'; @@ -25,7 +22,7 @@ export class State<A> { constructor(initValue: A) { this.value = initValue; - this.emitter = new Emitter; + this.emitter = new Emitter(); this.getValue = this.getValue.bind(this); this.setValue = this.setValue.bind(this); } @@ -59,222 +56,8 @@ export function useState<A>(s: State<A>): [A, (update: A) => void] { React.useEffect(() => { s.on(setCurrent); return () => s.off(setCurrent); - }); + }, [s]); return [current, s.setValue]; -}; - -// -------------------------------------------------------------------------- -// --- Settings -// -------------------------------------------------------------------------- - -/** - Generic interface to Window and Global Settings. - To be used with [[useSettings]] with instances of its derived classes, - typically [[WindowSettings]] and [[GlobalSettings]]. You should never have - to implement a Settings class on your own. - - All setting values are identified with - an untyped `dataKey: string`, that can be dynamically modified - for each component. Hence, two components might share both datakeys - and settings. - - When several components share the same setting `dataKey` the behavior will be - different depending on the situation: - - for Window Settings, each component in each window retains its own - setting value, although the last modified value from _any_ of them will be - saved and used for any further initial value; - - for Global Settings, all components synchronize to the last modified value - from any component of any window. - - Type safety is ensured by safe JSON encoders and decoders, however, they - might fail at runtime, causing settings value to be initialized to their - fallback and not to be saved nor synchronized. - This is not harmful but annoying. - - To mitigate this effect, each instance of a Settings class has its - own, private, unique symbol that we call its « role ». A given `dataKey` - shall always be used with the same « role » otherwise it is discarded, - and an error message is logged when in DEVEL mode. - */ -export abstract class Settings<A> { - - private static keyRoles = new Map<string, symbol>(); - - private readonly role: symbol; - protected readonly decoder: JSON.Safe<A>; - protected readonly encoder: JSON.Encoder<A>; - - /** - Encoders shall be protected against exception. - Use [[dome/data/json.jTry]] and [[dome/data/json.jCatch]] in case of uncertainty. - Decoders are automatically protected internally to the Settings class. - @param role Debugging name of instance roles (each instance has its unique - role, though) - @param decoder JSON decoder for the setting values - @param encoder JSON encoder for the setting values - @param fallback If provided, used to automatically protect your encoders - against exceptions. - */ - constructor( - role: string, - decoder: JSON.Safe<A>, - encoder: JSON.Encoder<A>, - fallback?: A, - ) { - this.role = Symbol(role); - this.encoder = encoder; - this.decoder = - fallback !== undefined ? JSON.jCatch(decoder, fallback) : decoder; - } - - /** - Returns identity if the data key is only - used with the same setting instance. - Otherwise, returns `undefined`. - */ - validateKey(dataKey?: string): string | undefined { - if (dataKey === undefined) return undefined; - const rq = this.role; - const rk = Settings.keyRoles.get(dataKey); - if (rk === undefined) { - Settings.keyRoles.set(dataKey, rq); - } else { - if (rk !== rq) { - if (DEVEL) console.error( - `[Dome.settings] Key ${dataKey} used with incompatible roles`, rk, rq, - ); - return undefined; - } - } - return dataKey; - } - - /** @internal */ - abstract loadData(key: string): JSON.json; - - /** @internal */ - abstract saveData(key: string, data: JSON.json): void; - - /** @internal */ - abstract event: string; - - /** Returns the current setting value for the provided data key. You shall - only use validated keys otherwise you might fallback to default values. */ - loadValue(dataKey?: string) { - return this.decoder(dataKey ? this.loadData(dataKey) : undefined) - } - - /** Push the new setting value for the provided data key. - You shall only use validated keys otherwise further loads - might fail and fallback to defaults. */ - saveValue(dataKey: string, value: A) { - try { this.saveData(dataKey, this.encoder(value)); } - catch (err) { - if (DEVEL) console.error( - '[Dome.settings] Error while encoding value', - dataKey, value, err, - ); - } - } - -} - -/** - Generic React Hook to be used with any kind of [[Settings]]. - You may share `dataKey` between components, or change it dynamically. - However, a given data key shall always be used for the same Setting instance. - See [[Settings]] documentation for details. - @param S The instance settings to be used. - @param dataKey Identifies which value in the settings to be used. - */ -export function useSettings<A>( - S: Settings<A>, - dataKey?: string, -): [A, (update: A) => void] { - - const theKey = React.useMemo(() => S.validateKey(dataKey), [S, dataKey]); - const [value, setValue] = React.useState<A>(() => S.loadValue(theKey)); - - React.useEffect(() => { - if (theKey) { - const callback = () => setValue(S.loadValue(theKey)); - Dome.on(S.event, callback); - return () => Dome.off(S.event, callback); - } - return undefined; - }); - - const updateValue = React.useCallback((update: A) => { - if (!isEqual(value, update)) { - setValue(update); - if (theKey) S.saveValue(theKey, update); - } - }, [S, theKey]); - - return [value, updateValue]; - -} - -/** Window Settings for non-JSON data. - In most situations, you can use [[WindowSettings]] instead. - You can use a [[dome/data/json.Loose]] decoder for optional values. */ -export class WindowSettingsData<A> extends Settings<A> { - - constructor( - role: string, - decoder: JSON.Safe<A>, - encoder: JSON.Encoder<A>, - fallback?: A, - ) { - super(role, decoder, encoder, fallback); - } - - event = 'dome.defaults'; - loadData(key: string) { return Dome.getWindowSetting(key) as JSON.json; } - saveData(key: string, data: JSON.json) { Dome.setWindowSetting(key, data); } - -} - -/** Global Settings for non-JSON data. - In most situations, you can use [[WindowSettings]] instead. - You can use a [[dome/data/json.Loose]] decoder for optional values. */ -export class GlobalSettingsData<A> extends Settings<A> { - - constructor( - role: string, - decoder: JSON.Safe<A>, - encoder: JSON.Encoder<A>, - fallback?: A, - ) { - super(role, decoder, encoder, fallback); - } - - event = 'dome.settings'; - loadData(key: string) { return Dome.getGlobalSetting(key) as JSON.json; } - saveData(key: string, data: JSON.json) { Dome.setGlobalSetting(key, data); } - -} - -/** Window Settings. - For non-JSON data, use [[WindowSettingsData]] instead. - You can use a [[dome/data/json.Loose]] decoder for optional values. */ -export class WindowSettings<A extends JSON.json> extends WindowSettingsData<A> { - - constructor(role: string, decoder: JSON.Safe<A>, fallback?: A) { - super(role, decoder, JSON.identity, fallback); - } - -} - -/** Global Settings. - For non-JSON data, use [[WindowSettingsData]] instead. - You can use a [[dome/data/json.Loose]] decoder for optional values. */ -export class GlobalSettings<A extends JSON.json> extends GlobalSettingsData<A> { - - constructor(role: string, decoder: JSON.Safe<A>, fallback?: A) { - super(role, decoder, JSON.identity, fallback); - } - } // -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/dialogs.js b/ivette/src/dome/src/renderer/dialogs.js deleted file mode 100644 index 1532f867e03d3d77374951c3e39839fb4c39f349..0000000000000000000000000000000000000000 --- a/ivette/src/dome/src/renderer/dialogs.js +++ /dev/null @@ -1,307 +0,0 @@ -/** - @packageDocumentation - @module dome/dialogs - @description - Various kind of (modal) dialogs attached to the main application window. - */ - -import filepath from 'path' ; -import { remote } from 'electron' ; -import * as System from 'dome/system' ; - -// -------------------------------------------------------------------------- -// --- Message Box -// -------------------------------------------------------------------------- - -const defaultItems = [ - { value:undefined }, - { value:true, label:'Ok' } -]; - -const valueLabel = (v) => { - switch(v) { - case undefined: return 'Cancel' ; - case true: return 'Ok' ; - case false: return 'No' ; - default: return ''+v ; - } -}; - -const itemLabel = ({value,label}) => label || valueLabel(value) ; -const isDefault = ({value,label}) => value===true || label==='Ok' || label==='Yes' ; -const isCancel = ({value,label}) => !value || label==='Cancel' || label==='No' ; - -/** - @summary Show a configurable message box. - @parameter {object} options - configuration (see above) - @return {Promise} the selected option (see above) - @description -The available fields and options for configuring the dialog are: - -| Options | Type | Description | -|:--------|:----:|:------------| -| `kind` | `'none','info','error','warning'` | Icon of the message box | -| `title` | `string` (_opt._) | Heading of message box | -| `message` | `string` | Message text | -| `buttons` | `button[]` (_opt._) | Dialog buttons | -| `defaultValue` | (any) (_opt._) | Value of the default button | -| `cancelValue` | (any) (_opt._) | Value of the cancel key | - -The dialog buttons are specified by objects with the following fields: - -| Button Field | Type | Description | -|:-------------|:----:|:------------| -| `label` | `string` | Button label | -| `value` | (any) | Button identifier (items only) | - -The returned promise object is never rejected, and is asynchronously -resolved into: -- the cancel value if the cancel key is pressed, -- the default value if the enter key is pressed, -- or the value of the clicked button otherwised. - -The default buttons are `"Ok"` and `"Cancel"` associated to values `true` and -`undefined`, which are also associated to the enter and cancel keys. -Unless specified, the default value is associated with the first button -with 'true' value or 'Ok' or 'Yes' label, -and the cancel value is the first button with a falsy value or 'Cancel' -or 'No' label. -*/ -export function showMessageBox( options ) -{ - const { - kind, - title, - message, - defaultValue, - cancelValue, - buttons = defaultItems - } = options ; - - const labels = buttons.map(itemLabel); - let defaultId = - defaultValue === undefined - ? buttons.findIndex(isDefault) - : buttons.findIndex((a) => a.value === defaultValue); - let cancelId = - cancelValue === undefined - ? buttons.findIndex(isCancel) - : buttons.findIndex((a) => a.value === cancelValue); - - if (cancelId === defaultId) cancelId = -1; - - return remote.dialog.showMessageBox( - remote.getCurrentWindow(), - { - type:kind, - message: message && title, - detail: message || title, - defaultId, cancelId, buttons: labels - } - ).then((result) => { - let itemIndex = result ? result.response : -1 ; - return itemIndex ? buttons[itemIndex].value : cancelValue ; - }); -} - -// -------------------------------------------------------------------------- -// --- openFile dialog -// -------------------------------------------------------------------------- - -const defaultPath = (path) => filepath.extname(path) ? filepath.dirname(path) : path ; - -/** - @summary Show a dialog for opening file. - @parameter {object} options - configuration (see above) - @return {Promise} the selected file (see above) - @description -The available fields and options for configuring the dialog are: - -| Options | Type | Description | -|:--------|:----:|:------------| -| `message` | `string` (_opt._) | Prompt message | -| `label` | `string` (_opt._) | Open button label | -| `path` | `filepath` (_opt._) | Initially selected path | -| `hidden` | `boolean` (_opt._) | Show hidden files (not by default) | -| `filters` | `filter[]` (_opt._) | File filters (all files by default) | - -The file filters are object with the following fields: - -| Filter Field | Type | Description | -|:-------------|:----:|:------------| -| `name` | `string` | Filter name | -| `extensions` | `string[]` | Allowed file extensions, _without_ dots («.») | - -A file filter with `extensions:["*"]` would accept any file extension. - -The returned promise object will be asynchronously: -- either _resolved_ with `undefined` if no file has been selected, -- or _resolved_ with the selected path - -The promise is never rejected. - -*/ -export function showOpenFile( options ) -{ - const { message, label, path, hidden, filters } = options ; - const properties = [ 'openFile' ]; - if (hidden) properties.push('showHiddenFiles'); - - return remote.dialog.showOpenDialog( - remote.getCurrentWindow(), - { - message, buttonLabel: label, - defaultPath: path && defaultPath(path), - properties, filters - } - ).then(result => { - if (!result.canceled && result.filePaths && result.filePaths.length > 0) - return result.filePaths[0] ; - else - return undefined ; - }); -} - -/** - @summary Show a dialog for opening file. - @parameter {object} options - configuration (see above) - @return {Promise} the selected file(s) (see above) - @description -The available fields and options for configuring the dialog are: - -| Options | Type | Description | -|:--------|:----:|:------------| -| `message` | `string` (_opt._) | Prompt message | -| `label` | `string` (_opt._) | Open button label | -| `path` | `filepath` (_opt._) | Initially selected path | -| `hidden` | `boolean` (_opt._) | Show hidden files (not by default) | -| `filters` | `filter[]` (_opt._) | File filters (all files by default) | - -The file filters are object with the following fields: - -| Filter Field | Type | Description | -|:-------------|:----:|:------------| -| `name` | `string` | Filter name | -| `extensions` | `string[]` | Allowed file extensions, _without_ dots («.») | - -A file filter with `extensions:["*"]` would accept any file extension. - -The returned promise object will be asynchronously: -- either _resolved_ with `undefined` if no file has been selected, -- or _resolved_ with the selected paths - -The promise is never rejected. - -*/ -export function showOpenFiles( options ) -{ - const { message, label, path, hidden, filters } = options ; - const properties = [ 'openFile', 'multiSelections' ]; - if (hidden) properties.push('showHiddenFiles'); - - return remote.dialog.showOpenDialog( - remote.getCurrentWindow(), - { - message, buttonLabel: label, - defaultPath: path && defaultPath(path), - properties, filters - } - ).then(result => { - if (!result.canceled && result.filePaths && result.filePaths.length > 0) - return result.filePaths ; - else - return undefined ; - }); -} - - -// -------------------------------------------------------------------------- -// --- saveFile dialog -// -------------------------------------------------------------------------- - -/** - @summary Show a dialog for saving file. - @parameter {object} options - configuration (see above) - @return {Promise} the selected path (see above) - @description -The available fields and options for configuring the dialog are: - -| Options | Type | Description | -|:--------|:----:|:------------| -| `message` | `string` (_opt._) | Prompt message | -| `label` | `string` (_opt._) | Save button label | -| `path` | `filepath` (_opt._) | Initially selected path | -| `filters` | `filter[]` (_opt._) | Cf. `openFileDialog` | - -The returned promise object will be asynchronously: -- either _resolved_ with `undefined` if no file has been selected, -- or _resolved_ with the selected (single) path. - -The promise is never rejected. - -*/ -export function showSaveFile( options ) -{ - const { message, label, path, filters } = options ; - - return remote.dialog.showSaveDialog( - remote.getCurrentWindow(), - { - message, buttonLabel: label, - defaultPath: path, - filters - } - ); -} - -// -------------------------------------------------------------------------- -// --- openDir dialog -// -------------------------------------------------------------------------- - -/** - @summary Show a dialog for selecting directories. - @parameter {object} options - configuration (see above) - @return {Promise} the selected directories (see above) - @description -The available fields and options for configuring the dialog are: - -| Options | Type | Description | -|:--------|:----:|:------------| -| `message` | `string` (_opt._) | Prompt message | -| `label` | `string` (_opt._) | Open button label | -| `path` | `filepath` (_opt._) | Initially selected path | -| `hidden` | `boolean` (_opt._) | Show hidden files (not by default) | - -The returned promise object will be asynchronously: -- either _resolved_ with `undefined` if no file has been selected, -- or _resolved_ with the selected path - -*/ -export function showOpenDir( options ) -{ - const { message, label, path, hidden } = options ; - const properties = [ 'openDirectory' ]; - if (hidden) properties.push('showHiddenFiles'); - - switch(System.platform) { - case 'macos': properties.push( 'createDirectory' ); break; - case 'windows': properties.push( 'promptToCreate' ); break; - } - - return remote.dialog.showOpenDialog( - remote.getCurrentWindow(), - { - message, - buttonLabel: label, - defaultPath: path, - properties - } - ).then(result => { - if (!result.canceled && result.filePaths && result.filePaths.length > 0) - return result.filePaths[0] ; - else - return undefined ; - }); -} - -// -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/dialogs.tsx b/ivette/src/dome/src/renderer/dialogs.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e5bc3fa9f1d88b946358fdaff1577acb0c937a67 --- /dev/null +++ b/ivette/src/dome/src/renderer/dialogs.tsx @@ -0,0 +1,288 @@ +/** + Various kind of (modal) dialogs attached to the main application window. + @packageDocumentation + @module dome/dialogs + */ + +import filepath from 'path'; +import { remote } from 'electron'; +import * as System from 'dome/system'; + +// -------------------------------------------------------------------------- +// --- Message Box +// -------------------------------------------------------------------------- + +export interface DialogButton<A> { + label?: string; + value?: A; +} + +const defaultItems: DialogButton<boolean>[] = [ + { value: undefined }, + { value: true, label: 'Ok' }, +]; + +const valueLabel = (v: any) => { + switch (v) { + case undefined: return 'Cancel'; + case true: return 'Ok'; + case false: return 'No'; + default: return `${v}`; + } +}; + +const itemLabel = ({ value, label }: DialogButton<any>) => ( + (label || valueLabel(value)) +); + +const isDefault = ({ value, label }: DialogButton<any>) => ( + (value === true || label === 'Ok' || label === 'Yes') +); + +const isCancel = ({ value, label }: DialogButton<any>) => ( + (!value || label === 'Cancel' || label === 'No') +); + +export type MessageKind = 'none' | 'info' | 'error' | 'warning'; + +export interface MessageProps<A> { + /** Dialog window icon (default is `'none'`. */ + kind?: MessageKind; + /** Message text (short sentence). */ + message: string; + /** Message details (short sentence). */ + details?: string; + /** Message buttons. */ + buttons?: DialogButton<A>[]; + /** Default button's value. */ + defaultValue?: A; + /** Cancel value. */ + cancelValue?: A; +} + +/** + Show a configurable message box. + + The returned promise object is never rejected, and is asynchronously + resolved into: + - the cancel value if the cancel key is pressed, + - the default value if the enter key is pressed, + - or the value of the clicked button otherwised. + + The default buttons are `"Ok"` and `"Cancel"` associated to values `true` and + `undefined`, which are also associated to the enter and cancel keys. + Unless specified, the default value is associated with the first button + with 'true' value or 'Ok' or 'Yes' label, + and the cancel value is the first button with a falsy value or 'Cancel' + or 'No' label. + */ +export async function showMessageBox<A>( + props: MessageProps<A>, +): Promise<A | boolean | undefined> { + const { + kind, + message, + details, + defaultValue, + cancelValue, + buttons = (defaultItems as DialogButton<A | boolean>[]), + } = props; + + const labels = buttons.map(itemLabel); + const defaultId = + defaultValue === undefined + ? buttons.findIndex(isDefault) + : buttons.findIndex((a) => a.value === defaultValue); + let cancelId = + cancelValue === undefined + ? buttons.findIndex(isCancel) + : buttons.findIndex((a) => a.value === cancelValue); + + if (cancelId === defaultId) cancelId = -1; + + return remote.dialog.showMessageBox( + remote.getCurrentWindow(), + { + type: kind, + message, + detail: details, + defaultId, + cancelId, + buttons: labels, + }, + ).then((result) => { + const itemIndex = result ? result.response : -1; + return itemIndex ? buttons[itemIndex].value : cancelValue; + }); +} + +// -------------------------------------------------------------------------- +// --- File Dialogs +// -------------------------------------------------------------------------- + +const defaultPath = + (path: string) => (filepath.extname(path) ? filepath.dirname(path) : path); + +export interface FileFilter { + /** Filter name. */ + name: string; + /** + Allowed extensions, _without_ dot. + Use `['*']` to accept all files. + */ + extensions: string[]; +} + +export interface FileDialogProps { + /** Prompt message. */ + message?: string; + /** Open button label (default is « Open »). */ + label?: string; + /** Initially selected path. */ + path?: string; +} + +export interface SaveFileProps extends FileDialogProps { + /** File filters (default to all). */ + filters?: FileFilter[]; +} + +export interface OpenFileProps extends SaveFileProps { + /** Show hidden files (default is `false`). */ + hidden?: boolean; +} + +export interface OpenDirProps extends FileDialogProps { + /** Show hidden directories (default is `false`). */ + hidden?: boolean; +} + +// -------------------------------------------------------------------------- +// --- openFile dialog +// -------------------------------------------------------------------------- + +/** + Show a dialog for opening file. + A file filter with `extensions:["*"]` would accept any file extension. + + The returned promise object will be asynchronously: + - either _resolved_ with `undefined` if no file has been selected, + - or _resolved_ with the selected path + + The promise is never rejected. + */ +export async function showOpenFile( + props: OpenFileProps, +): Promise<string | undefined> { + const { message, label, path, hidden = false, filters } = props; + return remote.dialog.showOpenDialog( + remote.getCurrentWindow(), + { + message, + buttonLabel: label, + defaultPath: path && defaultPath(path), + properties: (hidden ? ['openFile', 'showHiddenFiles'] : ['openFile']), + filters, + }, + ).then((result) => { + if (!result.canceled && result.filePaths && result.filePaths.length > 0) + return result.filePaths[0]; + return undefined; + }); +} + +/** + Show a dialog for opening files multiple files. +*/ +export async function showOpenFiles( + props: OpenFileProps, +): Promise<string[] | undefined> { + const { message, label, path, hidden, filters } = props; + + return remote.dialog.showOpenDialog( + remote.getCurrentWindow(), + { + message, + buttonLabel: label, + defaultPath: path && defaultPath(path), + properties: ( + hidden + ? ['openFile', 'multiSelections', 'showHiddenFiles'] + : ['openFile', 'multiSelections'] + ), + filters, + }, + ).then((result) => { + if (!result.canceled && result.filePaths && result.filePaths.length > 0) + return result.filePaths; + return undefined; + }); +} + +// -------------------------------------------------------------------------- +// --- saveFile dialog +// -------------------------------------------------------------------------- + +/** + Show a dialog for saving file. + + The returned promise object will be asynchronously: + - either _resolved_ with `undefined` when canceled, + - or _resolved_ with the selected (single) path. + + The promise is never rejected. +*/ +export async function showSaveFile( + props: SaveFileProps, +): Promise<string | undefined> { + const { message, label, path, filters } = props; + return remote.dialog.showSaveDialog( + remote.getCurrentWindow(), + { + message, + buttonLabel: label, + defaultPath: path, + filters, + }, + ).then(({ canceled, filePath }) => (canceled ? undefined : filePath)); +} + +// -------------------------------------------------------------------------- +// --- openDir dialog +// -------------------------------------------------------------------------- + +type openDirProperty = + 'openDirectory' | 'showHiddenFiles' | 'createDirectory' | 'promptToCreate'; + +/** + Show a dialog for selecting directories. + */ +export async function showOpenDir( + props: OpenDirProps, +): Promise<string | undefined> { + const { message, label, path, hidden } = props; + const properties: openDirProperty[] = ['openDirectory']; + if (hidden) properties.push('showHiddenFiles'); + + switch (System.platform) { + case 'macos': properties.push('createDirectory'); break; + case 'windows': properties.push('promptToCreate'); break; + default: break; + } + + return remote.dialog.showOpenDialog( + remote.getCurrentWindow(), + { + message, + buttonLabel: label, + defaultPath: path, + properties, + }, + ).then((result) => { + if (!result.canceled && result.filePaths && result.filePaths.length > 0) + return result.filePaths[0]; + return undefined; + }); +} + +// -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/errors.js b/ivette/src/dome/src/renderer/errors.js deleted file mode 100644 index ae951aa7ee0fe87c9be98dd687e9d0e769b63fa8..0000000000000000000000000000000000000000 --- a/ivette/src/dome/src/renderer/errors.js +++ /dev/null @@ -1,78 +0,0 @@ -// -------------------------------------------------------------------------- -// --- Managing Errors -// -------------------------------------------------------------------------- - -/** - @packageDocumentation - @module dome/errors -*/ - -import React from 'react' ; -import { Label } from 'dome/controls/labels' ; -import { Button } from 'dome/controls/buttons' ; - -// -------------------------------------------------------------------------- -// --- Error Boundaries -// -------------------------------------------------------------------------- - -/** - @summary React Error Boundaries. - @property {string} [label] - Default error box label - @property {function} [onError] - Alternative renderer - @description - Install an error boundary. In case of error, the default - rendering is a warning button that output on console the - catched error. - - An alternative rendering can be supplied - with `onError:(error,info) => React.Element`. - - */ -export class Catch extends React.Component -{ - - constructor(props) { - super(props); - this.state = { }; - this.logerr = this.logerr.bind(this); - this.reload = this.reload.bind(this); - } - - dumpError(error,info) { - } - - componentDidCatch(error, info) { - this.setState({ error, info }); - } - - logerr() { - const { error, info } = this.state ; - console.error('[dome] Catched error:',error,info); - } - - reload() { - this.setState({ error: undefined, info: undefined }); - } - - render() { - const { error, info } = this.state ; - if (error) { - const { onError, label='Error' } = this.props ; - if (typeof(onError)==='function') - return onError(error,info,this.reload); - else - return ( - <div> - <Button icon='WARNING' kind='warning' - title={error} - onClick={this.logerr} /> - <Button icon='RELOAD' onClick={this.reload} /> - <Label>{label}</Label> - </div> - ); - } - return this.props.children || null ; - } -} - -// -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/errors.tsx b/ivette/src/dome/src/renderer/errors.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b8cfc300071c79342ab28a8912b3296e5b3c1f11 --- /dev/null +++ b/ivette/src/dome/src/renderer/errors.tsx @@ -0,0 +1,86 @@ +// -------------------------------------------------------------------------- +// --- Managing Errors +// -------------------------------------------------------------------------- + +/** + @packageDocumentation + @module dome/errors +*/ + +import React from 'react'; +import { Label } from 'dome/controls/labels'; +import { Button } from 'dome/controls/buttons'; + +// -------------------------------------------------------------------------- +// --- Error Boundaries +// -------------------------------------------------------------------------- + +/** + Alternative renderer in case of error. + @param reload - callback for re-rendering the faulty component + */ +export interface ErrorRenderer { + (error: any, info: any, reload: () => void): JSX.Element; +} + +export interface CatchProps { + /** Name of the error boundary. */ + label?: string; + /** Alternative renderer callback in case of errors. */ + onError?: JSX.Element | ErrorRenderer; +} + +interface CatchState { + error?: any; + info?: any; +} + +/** + React Error Boundaries. + */ +export class Catch extends React.Component<CatchProps, CatchState, {}> { + + constructor(props: CatchProps) { + super(props); + this.state = {}; + this.logerr = this.logerr.bind(this); + this.reload = this.reload.bind(this); + } + + componentDidCatch(error: any, info: any) { + this.setState({ error, info }); + } + + logerr() { + const { error, info } = this.state; + console.error('[dome] Catched error:', error, info); + } + + reload() { + this.setState({ error: undefined, info: undefined }); + } + + render() { + const { error, info } = this.state; + if (error) { + const { onError, label = 'Error' } = this.props; + if (typeof (onError) === 'function') + return onError(error, info, this.reload); + return ( + <div> + <Button + icon="WARNING" + kind="warning" + title={error} + onClick={this.logerr} + /> + <Button icon="RELOAD" onClick={this.reload} /> + <Label>{label}</Label> + </div> + ); + } + return this.props.children || null; + } +} + +// -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/layout/boxes.js b/ivette/src/dome/src/renderer/layout/boxes.js deleted file mode 100644 index e7848a9ddcc671c3b9c0dfeb80a733708875357d..0000000000000000000000000000000000000000 --- a/ivette/src/dome/src/renderer/layout/boxes.js +++ /dev/null @@ -1,184 +0,0 @@ -// -------------------------------------------------------------------------- -// --- Box Layout -// -------------------------------------------------------------------------- - -/** - @packageDocumentation - @module dome/layout/boxes - @description - - This modules offers several `<div>` containers with various - predefined layout. - - Boxes are the very elementary way to layout components horizontally - or vertically. The different kinds of proposed boxes differ in how they - extends in both directions: normal boxes extends - along their layout direction, _pack_ boxes don't extends and _fill_ boxes - extends along both directions. - - Grids layout their component from left-to-right inside predefined _columns_, - then vertically by wrapping cells in rows. - - The various containers layout and extensibility is listed below: - - [[Hbox]] horizontal, fixed height - - [[Vbox]] vertical, fixed width - - [[Hpack]] horizontal, fixed dimensions - - [[Vpack]] vertical, fixed dimensions - - [[Hfill]] horizontal, extends in both directions - - [[Vfill]] vertical, extends in both directions - - [[Grid]] uses CSS grid columns, extends in both directions - - [[Scroll]] scrolls its content - - Inside a box, you may add `<Space/>` and `<Filler/>` to separate items. - Inside a grid, you may also use `<Space/>` or an empty `<div/>` for empty cells. - - <strong>Warning:</strong> large elements will be clipped if they overflow. - If you want to add scrolling capabilities to some item that does not manage overflow - natively, place it inside a `<Scroll/>` sub-container. -*/ - -import React from 'react'; -import * as Dome from 'dome'; -import { Title } from 'dome/controls/labels' ; -import './style.css' ; - -// -------------------------------------------------------------------------- -// --- Generic Box -// -------------------------------------------------------------------------- - -const makeBox = ( boxClasses, props, morestyle ) => { - const { children, className, style, ...others } = props ; - const allClasses = className ? boxClasses + ' ' + className : boxClasses ; - const allStyles = morestyle ? (style ? Object.assign( {}, style, morestyle ) : morestyle) : style ; - return ( - <div className={allClasses} style={allStyles} {...others} > - {children} - </div> - ); -}; - -// -------------------------------------------------------------------------- -// --- Predefined Defaults -// -------------------------------------------------------------------------- - -/** - @summary Horizontal box (extends horizontally, no overflow). - @property {object} [...props] - Extra properties passed to the `<div>` container - @description - <strong>Warning:</strong> large elements will be clipped if they overflow. -*/ -export const Hbox = (props) => makeBox( 'dome-xBoxes-hbox dome-xBoxes-box' , props ); - -/** - @summary Vertical box (extends vertically, no overflow). - @property {object} [...props] - Extra properties passed to the `<div>` container - @description - <strong>Warning:</strong> large elements will be clipped if they overflow. -*/ -export const Vbox = (props) => makeBox( 'dome-xBoxes-vbox dome-xBoxes-box' , props ); - -/** - @summary Compact Horizontal box (fixed dimensions, no overflow). - @property {object} [...props] - Extra properties passed to the `<div>` container - @description - <strong>Warning:</strong> large elements would be clipped if they overflow. -*/ -export const Hpack = (props) => makeBox( 'dome-xBoxes-hbox dome-xBoxes-pack' , props ); - -/** - @summary Compact Vertical box (fixed dimensions, no overflow). - @property {object} [...props] - Extra properties passed to the `<div>` container - @description - <strong>Warning:</strong> large elements will be clipped if they overflow. -*/ -export const Vpack = (props) => makeBox( 'dome-xBoxes-vbox dome-xBoxes-pack' , props ); - -/** - @summary Horizontally filled box (fixed height, maximal width, no overflow). - @property {object} [...props] - Extra properties passed to the `<div>` container - @description - <strong>Warning:</strong> large elements will be clipped if they overflow. -*/ -export const Hfill = (props) => makeBox( 'dome-xBoxes-hbox dome-xBoxes-fill' , props ); - -/** - @summary Vertically filled box (fixed width, maximal height, no overflow). - @property {object} [...props] - Extra properties passed to the `<div>` container - @description - <strong>Warning:</strong> large elements will be clipped if they overflow. -*/ -export const Vfill = (props) => makeBox( 'dome-xBoxes-vbox dome-xBoxes-fill' , props ); - -// -------------------------------------------------------------------------- -// --- Scrolling & Spacing -// -------------------------------------------------------------------------- - -/** - @summary Scrolling container. - @property {object} [...props] - Extra properties passed to the `<div>` container -*/ -export const Scroll = (props) => makeBox( 'dome-xBoxes-scroll dome-container' , props ); - -/** - @summary Rigid space between items in a box. - @property {object} [...props] - Extra properties passed to the `<div>` separator -*/ -export const Space = (props) => makeBox( 'dome-xBoxes-space' , props ); - -/** - @summary Extensible space between items in a box. - @property {object} [...props] - Extra properties passed to the `<div>` separator -*/ -export const Filler = (props) => makeBox( 'dome-xBoxes-filler' , props ); - -// -------------------------------------------------------------------------- -// --- Grids -// -------------------------------------------------------------------------- - -/** - @summary Grid box container. - @property {string} [columns] - Grid column specifications - @property {object} [...props] - Extra properties passed to the `<div>` container - @description - Layout its children in a multi-column grid. Each column is specified by its - width, following the CSS Grid Layout `grid-template-columns` property. - - The rows are populated with children from left-to-right, using one column at a time. - Items layout can be modified by adding CSS Grid Layout item properties. - - Example: `<Grid columns="25% auto auto"> ... </Grid>` -*/ -export const Grid = ({columns='auto',...props}) => - makeBox( 'dome-xBoxes-grid', props , { gridTemplateColumns: columns }); - -// -------------------------------------------------------------------------- -// --- Folders -// -------------------------------------------------------------------------- - -/** - @summary Foldable Vpack box. - @property {string} label - box label - @property {string} [title] - box label tooltip - @property {string} [settings] - window setting to store the fold/unfold state - @property {boolean} [defaultUnfold] - initial state (default is `false`) - @property {React.Children} [children] - content of the vertical box - @description - A vertical `Vpack` box with a clickable head label to fold/unfold its content. -*/ -export const Folder = - ({ settings, defaultUnfold=false, indent=18, label, title, children }) => -{ - const [ unfold , setUnfold ] = Dome.useState( settings, defaultUnfold ); - const icon = unfold ? 'TRIANGLE.DOWN' : 'TRIANGLE.RIGHT' ; - const onClick = () => setUnfold( !unfold ); - return ( - <Vpack> - <Hpack onClick={onClick}><Title icon={icon} label={label} title={title} /></Hpack> - <Vpack style={{ marginLeft:indent }}> - { unfold && children } - </Vpack> - </Vpack> - ); -}; - -// -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/layout/boxes.tsx b/ivette/src/dome/src/renderer/layout/boxes.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2d98e79fd1753cd726bf857c092db42f18cb9f52 --- /dev/null +++ b/ivette/src/dome/src/renderer/layout/boxes.tsx @@ -0,0 +1,198 @@ +// -------------------------------------------------------------------------- +// --- Box Layout +// -------------------------------------------------------------------------- + +/** + This modules offers several `<div>` containers with various + predefined layout. + + Boxes are the very elementary way to layout components horizontally + or vertically. The different kinds of proposed boxes differ in how they + extends in both directions: normal boxes extends + along their layout direction, _pack_ boxes don't extends and _fill_ boxes + extends along both directions. + + Grids layout their component from left-to-right inside predefined _columns_, + then vertically by wrapping cells in rows. + + The various containers layout and extensibility is listed below: + - [[Hbox]] horizontal, fixed height + - [[Vbox]] vertical, fixed width + - [[Hpack]] horizontal, fixed dimensions + - [[Vpack]] vertical, fixed dimensions + - [[Hfill]] horizontal, extends in both directions + - [[Vfill]] vertical, extends in both directions + - [[Grid]] uses CSS grid columns, extends in both directions + - [[Scroll]] scrolls its content + + Inside a box, you may add `<Space/>` and `<Filler/>` to separate items. + Inside a grid, you may also use `<Space/>` or an empty `<div/>` for empty + cells. + + <strong>Warning:</strong> large elements will be clipped if they overflow. + If you want to add scrolling capabilities to some item that does not manage + overflow natively, place it inside a `<Scroll/>` sub-container. + + @packageDocumentation + @module dome/layout/boxes + */ + +import React from 'react'; +import * as Dome from 'dome'; +import { Title } from 'dome/controls/labels'; +import { classes, styles } from 'dome/misc/utils'; +import './style.css'; + +// -------------------------------------------------------------------------- +// --- Generic Box +// -------------------------------------------------------------------------- + +/** Div properties that you can also use in boxes. */ +export type DivProps = React.HTMLAttributes<HTMLDivElement>; + +const makeBox = ( + boxClasses: string, + props: DivProps, + morestyle?: React.CSSProperties, +) => { + const { children, className, style, ...others } = props; + const allClasses = classes(className, boxClasses); + const allStyles = styles(style, morestyle); + return ( + <div className={allClasses} style={allStyles} {...others}> + {children} + </div> + ); +}; + +// -------------------------------------------------------------------------- +// --- Predefined Defaults +// -------------------------------------------------------------------------- + +/** + Horizontal box (extends horizontally, no overflow). +*/ +export const Hbox = + (props: DivProps) => makeBox('dome-xBoxes-hbox dome-xBoxes-box', props); + +/** + Vertical box (extends vertically, no overflow). +*/ +export const Vbox = + (props: DivProps) => makeBox('dome-xBoxes-vbox dome-xBoxes-box', props); + +/** + Compact Horizontal box (fixed dimensions, no overflow). +*/ +export const Hpack = + (props: DivProps) => makeBox('dome-xBoxes-hbox dome-xBoxes-pack', props); + +/** + Compact Vertical box (fixed dimensions, no overflow). +*/ +export const Vpack = + (props: DivProps) => makeBox('dome-xBoxes-vbox dome-xBoxes-pack', props); + +/** + Horizontally filled box (fixed height, maximal width, no overflow). +*/ +export const Hfill = + (props: DivProps) => makeBox('dome-xBoxes-hbox dome-xBoxes-fill', props); + +/** + Vertically filled box (fixed width, maximal height, no overflow). +*/ +export const Vfill = + (props: DivProps) => makeBox('dome-xBoxes-vbox dome-xBoxes-fill', props); + +// -------------------------------------------------------------------------- +// --- Scrolling & Spacing +// -------------------------------------------------------------------------- + +/** + Scrolling container. +*/ +export const Scroll = + (props: DivProps) => makeBox('dome-xBoxes-scroll dome-container', props); + +/** + Rigid space between items in a box. +*/ +export const Space = + (props: DivProps) => makeBox('dome-xBoxes-space', props); + +/** + Extensible space between items in a box. +*/ +export const Filler = + (props: DivProps) => makeBox('dome-xBoxes-filler', props); + +// -------------------------------------------------------------------------- +// --- Grids +// -------------------------------------------------------------------------- + +export interface GridProps extends DivProps { columns?: string } + +/** + Grid box container. + + Layout its children in a multi-column grid. Each column is specified by its + width, following the CSS Grid Layout `grid-template-columns` property. + + The rows are populated with children from left-to-right, using one column at + a time. Items layout can be modified by using CSS Grid Layout item + properties. + + Example: `<Grid columns="25% auto auto"> ... </Grid>` +*/ +export const Grid = (props: GridProps) => { + const { columns, ...others } = props; + return makeBox('dome-xBoxes-grid', others, { gridTemplateColumns: columns }); +}; + +// -------------------------------------------------------------------------- +// --- Folders +// -------------------------------------------------------------------------- + +export interface FolderProps { + /** Title bar label. */ + label: string; + /** Title bar tooltip. */ + title?: string; + /** Window settings key. */ + settings?: string; + /** Default state (`false`). */ + defaultUnfold?: boolean; + /** Contents left margin (in pixels, defaults to 18). */ + indent?: number; + /** Children JSX elements. */ + children: any; +} + +/** + Foldable (vertical, packed) box. + The head label is clickable to fold/unfold its contents. +*/ +export const Folder = (props: FolderProps) => { + const { + settings, + defaultUnfold = false, + indent = 18, + label, title, children, + } = props; + const [unfold, onClick] = Dome.useSwitch(settings, defaultUnfold); + const icon = unfold ? 'TRIANGLE.DOWN' : 'TRIANGLE.RIGHT'; + const display = unfold ? 'none' : 'block'; + return ( + <Vpack> + <Hpack onClick={onClick}> + <Title icon={icon} label={label} title={title} /> + </Hpack> + <Vpack style={{ display, marginLeft: indent }}> + {children} + </Vpack> + </Vpack> + ); +}; + +// -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/table/arrays.ts b/ivette/src/dome/src/renderer/table/arrays.ts index e506d8237fc7e5ff1fdd635a9b0b630e761c5161..5abe5992daa086172bd98a05b63c30ec72c72908 100644 --- a/ivette/src/dome/src/renderer/table/arrays.ts +++ b/ivette/src/dome/src/renderer/table/arrays.ts @@ -10,7 +10,9 @@ import * as Compare from 'dome/data/compare'; import type { ByFields, Order } from 'dome/data/compare'; import { - SortingInfo, Sorting, Filter, Filtering, Model, Collection, forEach + SortingInfo, Sorting, + Filter, Filtering, + Model, Collection, forEach, } from './models'; // -------------------------------------------------------------------------- @@ -23,7 +25,7 @@ interface PACK<Key, Row> { index: number | undefined; key: Key; row: Row; -}; +} type SORT<K, R> = Order<PACK<K, R>>; @@ -76,8 +78,7 @@ export class ArrayModel<Key, Row> private array?: Row[]; // Filtered-out Row Count - private filtered: number = 0; - + private filtered = 0; // Filtering function private filter?: Filter<Key, Row>; @@ -102,16 +103,18 @@ export class ArrayModel<Key, Row> protected sorter(): SORT<Key, Row> { let current = this.order; if (current) return current; - current = this.order = orderByRing(this.natural, this.columns, this.ring); + current = this.order = // eslint-disable-line no-multi-assign + orderByRing(this.natural, this.columns, this.ring); return current; } - // Lazily compute table + // Lazily compute table ; modifies packed entries in place + /* eslint-disable no-param-reassign */ protected rebuild(): PACK<Key, Row>[] { const current = this.table; let filtered = 0; if (current !== undefined) return current; - let table: PACK<Key, Row>[] = []; + const table: PACK<Key, Row>[] = []; try { this.index.forEach((packed) => { packed.index = undefined; @@ -125,11 +128,12 @@ export class ArrayModel<Key, Row> } catch (err) { console.warn('[Dome] error when rebuilding table:', err); } - table.forEach((pack, index) => pack.index = index); + table.forEach((packed, index) => { packed.index = index; }); this.table = table; this.filtered = filtered; return table; } + /* eslint-enable no-param-reassign */ // -------------------------------------------------------------------------- // --- Proxy @@ -191,8 +195,9 @@ export class ArrayModel<Key, Row> setOrderingByFields(byfields: ByFields<Row>) { this.natural = Compare.byFields(byfields); const columns = this.columns ?? {}; - for (let k of Object.keys(byfields)) { - const dataKey = k as (string & keyof Row); + const keys = Object.keys(byfields); + for (let i = 0; i < keys.length; i++) { + const dataKey = keys[i] as (string & keyof Row); const fn = byfields[dataKey]; if (fn) columns[dataKey] = (x: Row, y: Row) => { const dx = x[dataKey]; @@ -211,9 +216,9 @@ export class ArrayModel<Key, Row> Remove the sorting function for the provided column. */ deleteColumnOrder(dataKey: string) { - const columns = this.columns; + const { columns } = this; if (columns) delete columns[dataKey]; - this.ring = this.ring.filter(ord => ord.sortBy !== dataKey); + this.ring = this.ring.filter((ord) => ord.sortBy !== dataKey); this.reload(); } @@ -222,7 +227,7 @@ export class ArrayModel<Key, Row> Use `undefined` or `null` to reset the natural ordering. */ setSorting(ord?: undefined | null | SortingInfo) { if (ord) { - const ring = this.ring; + const { ring } = this; const cur = ring[0]; const fd = ord.sortBy; if ( @@ -235,11 +240,9 @@ export class ArrayModel<Key, Row> this.ring = newRing; this.reload(); } - } else { - if (this.ring.length > 0) { - this.ring = []; - this.reload(); - } + } else if (this.ring.length > 0) { + this.ring = []; + this.reload(); } } @@ -294,11 +297,11 @@ export class ArrayModel<Key, Row> const k = pack.index ?? -1; const n = current ? current.length : 0; const phi = this.filter; - const old_ok = 0 <= k && k < n; - const now_ok = phi ? phi(pack.row, pack.key) : true; - if (old_ok !== now_ok) return true; + const oldOk = 0 <= k && k < n; + const nowOk = phi ? phi(pack.row, pack.key) : true; + if (oldOk !== nowOk) return true; // Case where element was not displayed and will still not be - if (!old_ok) return false; + if (!oldOk) return false; // Detecting if ordering is preserved const order = this.sorter(); const prev = k - 1; @@ -360,11 +363,11 @@ export class ArrayModel<Key, Row> if (row === null) { // Nop return; - } else { - const newPack = { key, row, index: undefined }; - this.index.set(key, newPack); - doReload = this.needReloadForInsert(newPack); } + const newPack = { key, row, index: undefined }; + this.index.set(key, newPack); + doReload = this.needReloadForInsert(newPack); + } if (doReload) this.reload(); } @@ -417,7 +420,8 @@ export class ArrayModel<Key, Row> getArray(): Row[] { let arr = this.array; if (arr === undefined) { - arr = this.array = this.rebuild().map((e) => e.row); + arr = this.array = // eslint-disable-line no-multi-assign + this.rebuild().map((e) => e.row); } return arr; } diff --git a/ivette/src/dome/src/renderer/table/models.ts b/ivette/src/dome/src/renderer/table/models.ts index 0717180890e3c193eb320ce0c90437a30a7d035d..76c516f2950beaa5843e3c07a68e09647a863700 100644 --- a/ivette/src/dome/src/renderer/table/models.ts +++ b/ivette/src/dome/src/renderer/table/models.ts @@ -100,7 +100,8 @@ export function forEach<A>(data: Collection<A>, fn: (elt: A) => void) { individual updates. The model might not hold the entire collection of data at the same time, but - serves as a proxy for fetching data on demand. A model makes a distinction between: + serves as a proxy for fetching data on demand. A model makes a distinction + between: - `Key`: a key identifies a given entry in the table at any time; - `Row`: the row data associated to some `Key` at a given time; @@ -115,10 +116,13 @@ export function forEach<A>(data: Collection<A>, fn: (elt: A) => void) { When your data change over time, you shall invoke the following methods of the model to keep views in sync: - - [[update]] or [[updateIndex]] when single or contiguous row data changes over time; - - [[reload]] when the number of rows, their ordering, or (many) row data has been changed. + - [[update]] or [[updateIndex]] when single or contiguous row data + changes over time; + - [[reload]] when the number of rows, their ordering, or (many) row data + has been changed. - It is always safe to use `reload` instead of `update` although it might be less performant. + It is always safe to use `reload` instead of `update` although it might be + less performant. @template Key - identification of some entry @template Row - dynamic row data associated to some key @@ -139,16 +143,16 @@ export abstract class Model<Key, Row> { abstract getRowCount(): number; /** - Shall return the current row data at a given index in the table, with respect to - current filtering and ordering (if any). - Might return `undefined` if the index is invalid or not (yet) available. + Shall return the current row data at a given index in the table, with + respect to current filtering and ordering (if any). Might return + `undefined` if the index is invalid or not (yet) available. */ abstract getRowAt(index: number): undefined | Row; /** - Shall return the key at the given index. The specified index and data - are those of the last corresponding call to [[getRowAt]]. - Might return `undefined` if the index is invalid. + Shall return the key at the given index. The specified index and data are + those of the last corresponding call to [[getRowAt]]. Might return + `undefined` if the index is invalid. */ abstract getKeyAt(index: number): undefined | Key; @@ -160,11 +164,12 @@ export abstract class Model<Key, Row> { abstract getKeyFor(index: number, data: Row): undefined | Key; /** - Shall return the index of a given entry in the table, identified by its key, with - respect to current filtering and ordering (if any). - Shall return `undefined` if the specified key no longer belong to the table or - when it is currently filtered out. - Out-of-range indices would be treated as `undefined`. + Shall return the index of a given entry in the table, identified by its + key, with respect to current filtering and ordering (if any). + Shall return + `undefined` if the specified key no longer belong to the table or when it + is currently filtered out. Out-of-range indices would be treated as + `undefined`. */ abstract getIndexOf(key: Key): undefined | number; @@ -205,8 +210,10 @@ export abstract class Model<Key, Row> { The initial watching range is empty with no trigger. You normally never call this method directly. It is automatically called by table views. - @param onReload - optional callback for reloads (and updates, unless specified) - @param onUpdate - optional callback for updates (when different from reloads) + @param onReload - optional callback for reloads + (and updates, unless specified) + @param onUpdate - optional callback for updates + (when different from reloads) */ link(onReload?: Trigger, onUpdate?: Trigger): Client { const id = this.clientsId++; @@ -222,7 +229,7 @@ export abstract class Model<Key, Row> { watch(lower: number, upper: number) { w.lower = lower; w.upper = upper; - } + }, }; m.set(id, w); return w; diff --git a/ivette/src/dome/src/renderer/table/views.tsx b/ivette/src/dome/src/renderer/table/views.tsx index 3089b35483683f8efa470dc7e241091877333937..671cc8f8438cd53b47aa12fd09bec96521b246ec 100644 --- a/ivette/src/dome/src/renderer/table/views.tsx +++ b/ivette/src/dome/src/renderer/table/views.tsx @@ -30,7 +30,7 @@ import { Trigger, Client, Sorting, SortingInfo, Model } from './models'; import './style.css'; -const SVG = SVGraw as (props: { id: string, size?: number }) => JSX.Element; +const SVG = SVGraw as (props: { id: string; size?: number }) => JSX.Element; // -------------------------------------------------------------------------- // --- Rendering Interfaces @@ -56,7 +56,7 @@ export type RenderByFields<Row> = { You may use hierarchical index to order columns. See [[ColumnGroup]]. */ -export type index = number | number[] +export type index = number | number[]; /** Column Properties. @@ -169,7 +169,7 @@ interface ColumnData { title?: string; headerMenu: () => void; headerRef: divRef; -}; +} interface PopupItem { label: string; @@ -177,11 +177,11 @@ interface PopupItem { enabled?: boolean; display?: boolean; onClick?: Trigger; -}; +} type PopupMenu = ('separator' | PopupItem)[]; -type Cmap<A> = Map<string, A> +type Cmap<A> = Map<string, A>; type Cprops = ColProps<any>; type ColProps<R> = ColumnProps<R, any>; @@ -206,13 +206,13 @@ const defaultGetter = (row: any, dataKey: string) => { const defaultRenderer = (d: any) => ( <div className="dome-xTable-renderer dome-text-label"> - {new String(d)} + {String(d)} </div> ); function makeRowGetter<Key, Row>(model?: Model<Key, Row>) { return ({ index }: Index) => model && model.getRowAt(index); -}; +} function makeDataGetter( getter: ((row: any, dataKey: string) => any) = defaultGetter, @@ -237,8 +237,8 @@ function makeDataRenderer( render: ((data: any) => ReactNode) = defaultRenderer, onContextMenu?: (row: any, index: number, dataKey: string) => void, ): TableCellRenderer { - return (props => { - const cellData = props.cellData; + return ((props) => { + const { cellData } = props; try { const contents = cellData ? render(cellData) : null; if (onContextMenu) { @@ -270,7 +270,7 @@ type ColSettings<A> = { [id: string]: undefined | null | A }; type TableSettings = { resize?: ColSettings<number>; visible?: ColSettings<boolean>; -} +}; // -------------------------------------------------------------------------- // --- Table State @@ -384,7 +384,7 @@ class TableState<Key, Row> { const wl = cwl ? cwl + offset : 0; const wr = cwr ? cwr - offset : 0; if (wl > 40 && wr > 40) { - const resize = this.resize; + const { resize } = this; resize.set(lcol, wl); resize.set(rcol, wr); this.offset = offset; @@ -405,8 +405,8 @@ class TableState<Key, Row> { if (userSettings) { const cws: ColSettings<number> = {}; const cvs: ColSettings<boolean> = {}; - const resize = this.resize; - const visible = this.visible; + const { resize } = this; + const { visible } = this; this.columns.forEach(({ id }) => { const cw = resize.get(id); const cv = visible.get(id); @@ -422,8 +422,8 @@ class TableState<Key, Row> { importSettings(settings?: string) { if (settings !== this.settings) { this.settings = settings; - const resize = this.resize; - const visible = this.visible; + const { resize } = this; + const { visible } = this; resize.clear(); visible.clear(); const theSettings: undefined | TableSettings = @@ -482,11 +482,12 @@ class TableState<Key, Row> { onSelection?: (data: Row, key: Key, index: number) => void; onRowClick(info: RowMouseEventHandlerParams) { - const index = info.index; + const { index } = info; const data = info.rowData as (Row | undefined); - const model = this.model; - const key = (data !== undefined) ? model?.getKeyFor(index, data) : undefined; - const onSelection = this.onSelection; + const { model } = this; + const key = + (data !== undefined) ? model?.getKeyFor(index, data) : undefined; + const { onSelection } = this; if (key !== undefined && data !== undefined && onSelection) onSelection(data, key, index); } @@ -498,11 +499,11 @@ class TableState<Key, Row> { rowClassName({ index }: Index): string { if (this.selectedIndex === index) return 'dome-xTable-selected'; - return (index & 1 ? 'dome-xTable-even' : 'dome-xTable-odd'); + return (index & 1 ? 'dome-xTable-even' : 'dome-xTable-odd'); // eslint-disable-line no-bitwise } keyStepper(index: number) { - const onSelection = this.onSelection; + const { onSelection } = this; const key = this.model?.getKeyAt(index); const data = this.model?.getRowAt(index); if (key !== undefined && data !== undefined && onSelection) { @@ -521,7 +522,7 @@ class TableState<Key, Row> { } onSorting(ord?: SortingInfo) { - const sorting = this.sorting; + const { sorting } = this; if (sorting) { sorting.setSorting(ord); this.sortBy = ord?.sortBy; @@ -561,16 +562,16 @@ class TableState<Key, Row> { // ---- Header Context Menu onHeaderMenu() { - let has_order = false; - let has_resize = false; - let has_visible = false; - const visible = this.visible; - const columns = this.columns; - columns.forEach(col => { - if (!col.disableSort) has_order = true; - if (!col.fixed) has_resize = true; + let hasOrder = false; + let hasResize = false; + let hasVisible = false; + const { visible } = this; + const { columns } = this; + columns.forEach((col) => { + if (!col.disableSort) hasOrder = true; + if (!col.fixed) hasResize = true; if (col.visible !== 'never' && col.visible !== 'always') - has_visible = true; + hasVisible = true; }); const resetSizing = () => { this.resize.clear(); @@ -584,27 +585,27 @@ class TableState<Key, Row> { const items: PopupMenu = [ { label: 'Reset ordering', - display: has_order && this.sorting, + display: hasOrder && this.sorting, onClick: this.onSorting, }, { label: 'Reset column widths', - display: has_resize, + display: hasResize, onClick: resetSizing, }, { label: 'Restore column defaults', - display: has_visible, + display: hasVisible, onClick: resetColumns, }, 'separator', ]; - columns.forEach(col => { + columns.forEach((col) => { switch (col.visible) { case 'never': case 'always': break; - default: + default: { const { id, label, title } = col; const checked = isVisible(visible, col); const onClick = () => { @@ -612,6 +613,7 @@ class TableState<Key, Row> { this.updateSettings(); }; items.push({ label: label || title || id, checked, onClick }); + } } }); Dome.popupMenu(items); @@ -654,7 +656,7 @@ class TableState<Key, Row> { path: number[], index: number, ): Trigger { - const id = props.id; + const { id } = props; const theIndex = props.index ?? index; const thePath = path.concat(theIndex); this.setRegistry(id, { ...props, index: thePath }); @@ -705,7 +707,7 @@ export function Column<Row, Cell>(props: ColumnProps<Row, Cell>) { function spawnIndex( state: TableState<any, any>, path: number[], - children: any + children: any, ) { const indexChild = (elt: React.ReactElement, k: number) => ( <ColumnContext.Provider value={{ state, path, index: k }}> @@ -758,7 +760,7 @@ function spawnIndex( this implicit root column group, just pack your columns inside a classical React fragment: `<Table … ><>{children}</></Table>`. */ -export function ColumnGroup(props: { index?: index, children: any }) { +export function ColumnGroup(props: { index?: index; children: any }) { const context = React.useContext(ColumnContext); if (!context) return null; const { state, path, index: defaultIndex } = context; @@ -787,7 +789,7 @@ function makeColumn<Key, Row>( }; const width = state.resize.get(id) || props.width || 60; const flexGrow = fill ? 1 : 0; - const sorting = state.sorting; + const { sorting } = state; const disableSort = props.disableSort || !sorting || !sorting.canSortBy(dataKey); const getter = state.computeGetter(id, dataKey, props); @@ -807,7 +809,7 @@ function makeColumn<Key, Row>( style={align} /> ); -}; +} const byIndex = (a: Cprops, b: Cprops) => { const ak = a.index ?? 0; @@ -815,7 +817,7 @@ const byIndex = (a: Cprops, b: Cprops) => { if (ak < bk) return -1; if (bk < ak) return 1; return 0; -} +}; function makeCprops<Key, Row>(state: TableState<Key, Row>) { const cols: Cprops[] = []; @@ -846,20 +848,24 @@ function makeColumns<Key, Row>(state: TableState<Key, Row>, cols: Cprops[]) { const headerIcon = (icon?: string) => ( icon && - (<div className='dome-xTable-header-icon'> - <SVG id={icon} /> - </div>) + ( + <div className="dome-xTable-header-icon"> + <SVG id={icon} /> + </div> + ) ); const headerLabel = (label?: string) => ( label && - (<label className='dome-xTable-header-label dome-text-label'> - {label} - </label>) + ( + <label className="dome-xTable-header-label dome-text-label"> + {label} + </label> + ) ); const makeSorter = (id: string) => ( - <div className='dome-xTable-header-sorter'> + <div className="dome-xTable-header-sorter"> <SVG id={id} size={8} /> </div> ); @@ -889,7 +895,7 @@ function headerRenderer(props: TableHeaderProps) { : undefined; return ( <div - className='dome-xTable-header' + className="dome-xTable-header" title={title} ref={headerRef} onContextMenu={headerMenu} @@ -930,15 +936,17 @@ const Resizer = (props: ResizerProps) => ( </DraggableCore> ); -type ResizeInfo = { id: string, fixed: boolean, left?: string, right?: string }; +type ResizeInfo = { id: string; fixed: boolean; left?: string; right?: string }; function makeResizers( state: TableState<any, any>, columns: Cprops[], ): null | JSX.Element[] { if (columns.length < 2) return null; - const resizing: ResizeInfo[] = columns.map(({ id, fixed = false }) => ({ id, fixed })); - var k: number, cid; // last non-fixed from left/right + const resizing: ResizeInfo[] = + columns.map(({ id, fixed = false }) => ({ id, fixed })); + let k: number; let + cid; // last non-fixed from left/right for (cid = undefined, k = 0; k < columns.length; k++) { const r = resizing[k]; r.left = cid; @@ -949,8 +957,9 @@ function makeResizers( r.right = cid; if (!r.fixed) cid = r.id; } - const cwidth = columns.map(col => state.computeWidth(col.id)); - var position = 0, resizers = []; + const cwidth = columns.map((col) => state.computeWidth(col.id)); + let position = 0; const + resizers = []; for (k = 0; k < columns.length - 1; k++) { const width = cwidth[k]; if (!width) return null; @@ -991,6 +1000,7 @@ function makeResizers( const CSS_HEADER_HEIGHT = 22; const CSS_ROW_HEIGHT = 20; +// Modifies state in place function makeTable<Key, Row>( props: TableProps<Key, Row>, state: TableState<Key, Row>, @@ -998,7 +1008,7 @@ function makeTable<Key, Row>( ) { const { width, height } = size; - const model = props.model; + const { model } = props; const itemCount = model.getRowCount(); const tableHeight = CSS_HEADER_HEIGHT + CSS_ROW_HEIGHT * itemCount; const smallHeight = itemCount > 0 && tableHeight < height; @@ -1008,11 +1018,13 @@ function makeTable<Key, Row>( const columns = makeColumns(state, cprops); const resizers = makeResizers(state, cprops); + /* eslint-disable no-param-reassign */ state.rowCount = rowCount; if (state.width !== width) { state.width = width; setImmediate(state.forceUpdate); } + /* eslint-enable no-param-reassign */ return ( <div onKeyDown={state.onKeyDown}> @@ -1041,9 +1053,9 @@ function makeTable<Key, Row>( {columns} </VTable> {resizers} - </div > + </div> ); -}; +} // -------------------------------------------------------------------------- // --- Table View @@ -1052,7 +1064,7 @@ function makeTable<Key, Row>( /** Table View. This component is base on - [React-Virtualized](https://bvaughn.github.io/react-virtualized/#/components/Table) + [React-Virtualized](https://bvaughn.github.io/react-virtualized) which offers a super-optimized lazy rendering process that scales on huge datasets. @@ -1078,7 +1090,7 @@ function makeTable<Key, Row>( @template Key - unique identifiers of table entries. @template Row - data associated to each key in the table entries. -*/ + */ export function Table<Key, Row>(props: TableProps<Key, Row>) { @@ -1096,14 +1108,14 @@ export function Table<Key, Row>(props: TableProps<Key, Row>) { }); Dome.useEvent('dome.defaults', state.clearSettings); return ( - <div className='dome-xTable'> - <React.Fragment key='columns'> + <div className="dome-xTable"> + <React.Fragment key="columns"> {spawnIndex(state, [], props.children)} </React.Fragment> - <AutoSizer key='table'> + <AutoSizer key="table"> {(size: Size) => makeTable(props, state, size)} </AutoSizer> - </div > + </div> ); } diff --git a/ivette/src/dome/src/renderer/text/buffers.ts b/ivette/src/dome/src/renderer/text/buffers.ts index c0db788100d9cf3019b8c6a492061b33338c7e26..f0a587f849fa9616c6add252a9533f632576fb8a 100644 --- a/ivette/src/dome/src/renderer/text/buffers.ts +++ b/ivette/src/dome/src/renderer/text/buffers.ts @@ -8,9 +8,9 @@ */ import Emitter from 'events'; -import CodeMirror from 'codemirror/lib/codemirror.js'; +import CodeMirror from 'codemirror/lib/codemirror'; -export type Range = { from: CodeMirror.Position, to: CodeMirror.Position }; +export type Range = { from: CodeMirror.Position; to: CodeMirror.Position }; export interface Decorator { /** @return a className to apply on markers with the identifier. */ @@ -24,8 +24,10 @@ export interface TextMarkerProxy { } /** - Text Marker options. Inherits - CodeMirror [TextMerkerOptions](https://codemirror.net/doc/manual.html#api_marker). + Text Marker options. + + Inherits CodeMirror + [TextMerkerOptions](https://codemirror.net/doc/manual.html#api_marker). */ export interface MarkerProps extends CodeMirror.TextMarkerOptions { id?: string; @@ -48,25 +50,25 @@ export interface CSSMarker { // --- Batched Update // -------------------------------------------------------------------------- -const BATCH_OPS = 500 -const BATCH_DELAY = 5 -const BATCH_RMAX = 1000 // max tag range for sorting -const BATCH_MARGINS = 20 // visible lines above the viewport +const BATCH_OPS = 500; +const BATCH_DELAY = 5; +const BATCH_RMAX = 1000; // max tag range for sorting +const BATCH_MARGINS = 20; // visible lines above the viewport interface MarkerOptions { - id?: string, - hover?: boolean, - className?: string, - options: CodeMirror.TextMarkerOptions, + id?: string; + hover?: boolean; + className?: string; + options: CodeMirror.TextMarkerOptions; } interface StackedMarker extends MarkerOptions { - startIndex: number, + startIndex: number; } interface BufferedMarker extends MarkerOptions { - startIndex: number, - stopIndex: number, + startIndex: number; + stopIndex: number; } type BufferedTag = BufferedMarker | undefined; @@ -91,7 +93,9 @@ function byVisibleTag(lmin: number, lmax: number) { export interface RichTextBufferProps { - /** CodeMirror [mode](https://codemirror.net/mode/index.html) specification. */ + /** + * CodeMirror [mode](https://codemirror.net/mode/index.html) specification. + */ mode?: any; /** Maximum number of lines in the buffer. */ @@ -124,9 +128,9 @@ export interface RichTextBufferProps { the _edited_ state to `false`, but sill emit an `'edited'` event if the buffer was not empty. - Buffers can also be updated programmatically by various methods. In addition to - specified CodeMirror modes, you can also attach text markers programmatically with - a push/pop API. + Buffers can also be updated programmatically by various methods. In addition + to specified CodeMirror modes, you can also attach text markers + programmatically with a push/pop API. Text markers can be associated with an identifier, that can be used for dynamic highlighting, called Decorations. Decorations are class names that @@ -146,13 +150,13 @@ export interface RichTextBufferProps { export class RichTextBuffer extends Emitter { private document: CodeMirror.Doc; - private maxlines: number = 10000; + private maxlines = 10000; private editors: CodeMirror.Editor[] = []; - private cacheIndex: number = 0; // document index, negative if not computed + private cacheIndex = 0; // document index, negative if not computed private bufferedText: string; // buffered text to append private bufferedTags: BufferedTag[]; private stacked: StackedMarker[] = []; - private batched: boolean = false; + private batched = false; // Indexed by CSS property dome-xHover-nnnn private cssmarkers = new Map<string, CSSMarker>(); @@ -222,9 +226,7 @@ export class RichTextBuffer extends Emitter { const start = doc.posFromIndex(Infinity); if (start.ch > 0) doc.replaceRange('\n', start, undefined, 'buffer'); this.cacheIndex = -1; - } else { - if (buf[buf.length - 1] !== '\n') this.bufferedText += '\n'; - } + } else if (buf[buf.length - 1] !== '\n') this.bufferedText += '\n'; } /** @@ -271,11 +273,11 @@ export class RichTextBuffer extends Emitter { inserted between its associated [[openTextMarker]] and [[closeTextMarker]] calls. - The returned text marker is actually a _proxy_ to the text marker that will be - eventually created by [[closeTextMarker]]. Its methods are automatically - forwarded to the actual `CodeMirror.TextMarker` - instance, once created. Hence, you can safely invoke these methods on either - the _proxy_ or the _final_ text marker at your convenience. + The returned text marker is actually a _proxy_ to the text marker that will + be eventually created by [[closeTextMarker]]. Its methods are automatically + forwarded to the actual `CodeMirror.TextMarker` instance, once created. + Hence, you can safely invoke these methods on either the _proxy_ or the + _final_ text marker at your convenience. */ openTextMarker(props: MarkerProps) { const { id, hover, className, ...options } = props; @@ -301,13 +303,14 @@ export class RichTextBuffer extends Emitter { } } - /** Lookup for the text markers associated with a marker identifier. */ + /** Lookup for the text markers associated with a marker identifier. + Remove the marked tags from the buffered tag array. */ findTextMarker(id: string): CodeMirror.TextMarker[] { this.doFlushText(); - this.bufferedTags.forEach((tg, idx, arr) => { + this.bufferedTags.forEach((tg, idx) => { if (tg?.id === id) { this.doMark(tg); - arr[idx] = undefined; + this.bufferedTags[idx] = undefined; } }); return this.textmarkers.get(id) ?? []; @@ -459,7 +462,7 @@ export class RichTextBuffer extends Emitter { private onChange( _editor: CodeMirror.Editor, - change: CodeMirror.EditorChangeLinkedList + change: CodeMirror.EditorChangeLinkedList, ) { if (change.origin !== 'buffer') { this.setEdited(true); @@ -477,7 +480,9 @@ export class RichTextBuffer extends Emitter { @param cm - code mirror instance to link this document in. */ link(cm: CodeMirror.Editor) { - const newDoc = this.document.linkedDoc({ sharedHist: true, mode: undefined }); + const newDoc = this.document.linkedDoc( + { sharedHist: true, mode: undefined }, + ); cm.swapDoc(newDoc); cm.on('change', this.onChange); this.editors.push(cm); @@ -511,7 +516,7 @@ export class RichTextBuffer extends Emitter { this.editors.forEach((cm) => { try { fn(cm); } catch (e) { console.error('[Dome.text]', e); } }); - }; + } // -------------------------------------------------------------------------- // --- Update Operations @@ -540,7 +545,7 @@ export class RichTextBuffer extends Emitter { '', { line: p, ch: 0 }, { line: q, ch: 0 }, - 'buffer' + 'buffer', ); this.cacheIndex = -1; } @@ -551,7 +556,7 @@ export class RichTextBuffer extends Emitter { const { id, hover, className, startIndex, stopIndex } = tag; let markerId; if (id || hover) { - markerId = 'dome-xHover-' + (this.markid++); + markerId = `dome-xHover-${this.markid++}`; const cmark = { id, classNameId: markerId, @@ -562,7 +567,7 @@ export class RichTextBuffer extends Emitter { } const fullClassName = [ 'dome-xMarked', - id && ('dome-xMark-' + id), + id && (`dome-xMark-${id}`), markerId, className, ].filter((s) => !!s).join(' '); @@ -601,13 +606,12 @@ export class RichTextBuffer extends Emitter { } private getLastIndex() { - let idx = this.cacheIndex; - if (idx < 0) { + if (this.cacheIndex < 0) { const doc = this.document; const line = doc.lastLine() + 1; - this.cacheIndex = idx = doc.indexFromPos({ line, ch: 0 }); + this.cacheIndex = doc.indexFromPos({ line, ch: 0 }); } - return idx; + return this.cacheIndex; } // -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/text/editors.tsx b/ivette/src/dome/src/renderer/text/editors.tsx index 6a59be9f1e25361277408aa0d0d1309f3b00b681..0a5d45ec867f57d868dc7e2f85df920f2a71ce25 100644 --- a/ivette/src/dome/src/renderer/text/editors.tsx +++ b/ivette/src/dome/src/renderer/text/editors.tsx @@ -11,7 +11,7 @@ import _ from 'lodash'; import React from 'react'; import * as Dome from 'dome'; import { Vfill } from 'dome/layout/boxes'; -import CodeMirror, { EditorConfiguration } from 'codemirror/lib/codemirror.js'; +import CodeMirror, { EditorConfiguration } from 'codemirror/lib/codemirror'; import { RichTextBuffer, CSSMarker, Decorator } from './buffers'; import './style.css'; @@ -133,33 +133,33 @@ class CodeMirrorWrapper extends React.Component<TextProps> { // Mounting... const { buffer } = this.props; const config = getConfig(this.props); - const cm = this.codeMirror = CodeMirror(elt, { value: '' }); + this.codeMirror = CodeMirror(elt, { value: '' }); if (buffer) { - buffer.link(cm); + buffer.link(this.codeMirror); buffer.on('decorated', this.handleUpdate); buffer.on('scroll', this.handleScrollTo); } // Passing all options to constructor does not work (Cf. CodeMirror's BTS) - forEachOption(config, (opt) => cm.setOption(opt, config[opt])); + forEachOption( + config, (opt) => this.codeMirror?.setOption(opt, config[opt]), + ); // Binding events to view - cm.on('update', this.handleUpdate); - cm.on('keyHandled', this.handleKey); + this.codeMirror.on('update', this.handleUpdate); + this.codeMirror.on('keyHandled', this.handleKey); Dome.on('dome.update', this.refresh); // Auto refresh this.refreshPolling = setInterval(this.autoRefresh, 250); this.handleUpdate(); } else { // Unmounting... - const polling = this.refreshPolling; - if (polling) { - clearInterval(polling); + if (this.refreshPolling) { + clearInterval(this.refreshPolling); this.refreshPolling = undefined; } - const cm = this.codeMirror; Dome.off('dome.update', this.refresh); const { buffer } = this.props; - if (cm && buffer) { - buffer.unlink(cm); + if (this.codeMirror && buffer) { + buffer.unlink(this.codeMirror); buffer.off('decorated', this.handleUpdate); buffer.off('scroll', this.handleScrollTo); } @@ -210,7 +210,7 @@ class CodeMirrorWrapper extends React.Component<TextProps> { _findMarker(elt: Element): CSSMarker | undefined { const { buffer } = this.props; if (buffer) { - var best: CSSMarker | undefined; + let best: CSSMarker | undefined; elt.classList.forEach((name) => { const marker = buffer.findHover(name); if (marker && (!best || marker.length < best.length)) best = marker; @@ -220,33 +220,35 @@ class CodeMirrorWrapper extends React.Component<TextProps> { return undefined; } + // eslint-disable-next-line class-methods-use-this _findDecoration( classes: DOMTokenList, buffer: RichTextBuffer, decorator: Decorator, ) { - var best_marker: CSSMarker | undefined; - var best_decorated: CSSMarker | undefined; - var best_decoration: string | undefined; + let bestMarker: CSSMarker | undefined; + let bestDecorated: CSSMarker | undefined; + let bestDecoration: string | undefined; classes.forEach((name) => { const marker = buffer.findHover(name); const id = marker && marker.id; const decoration = id && decorator(id); - if (marker && (!best_marker || marker.length < best_marker.length)) { - best_marker = marker; + if (marker && (!bestMarker || marker.length < bestMarker.length)) { + bestMarker = marker; } - if (marker && decoration && (!best_decorated || marker.length < best_decorated.length)) { - best_decorated = marker; - best_decoration = decoration; + if (marker && decoration && + (!bestDecorated || marker.length < bestDecorated.length)) { + bestDecorated = marker; + bestDecoration = decoration; } }); - return best_marker ? { - classNameId: best_marker.classNameId, - decoration: best_decoration, + return bestMarker ? { + classNameId: bestMarker.classNameId, + decoration: bestDecoration, } : undefined; } @@ -256,7 +258,7 @@ class CodeMirrorWrapper extends React.Component<TextProps> { if (toMark) { const n = toMark.length; if (n === 0) return; - for (var k = 0; k < n; k++) toMark[k].classList.add(className); + for (let k = 0; k < n; k++) toMark[k].classList.add(className); } } @@ -266,21 +268,21 @@ class CodeMirrorWrapper extends React.Component<TextProps> { if (toUnmark) { const n = toUnmark.length; if (n === 0) return; - const elts: Element[] = new Array(n);; - for (var k = 0; k < n; k++) elts[k] = toUnmark[k]; + const elts: Element[] = new Array(n); + for (let k = 0; k < n; k++) elts[k] = toUnmark[k]; elts.forEach((elt) => elt.classList.remove(className)); } } handleHover(target: Element) { // Throttled (see constructor) - const old_marker = this.marker; - const new_marker = this._findMarker(target); - if (old_marker !== new_marker) { - if (old_marker) this._unmarkElementsWith(CSS_HOVERED); - if (new_marker && new_marker.hover) - this._markElementsWith(new_marker.classNameId, CSS_HOVERED); - this.marker = new_marker; + const oldMarker = this.marker; + const newMarker = this._findMarker(target); + if (oldMarker !== newMarker) { + if (oldMarker) this._unmarkElementsWith(CSS_HOVERED); + if (newMarker && newMarker.hover) + this._markElementsWith(newMarker.classNameId, CSS_HOVERED); + this.marker = newMarker; } } @@ -290,15 +292,15 @@ class CodeMirrorWrapper extends React.Component<TextProps> { if (!marked) return; const n = marked.length; if (n === 0) return; - const marker = this.marker; + const { marker } = this; const hovered = (marker && marker.hover) ? marker.classNameId : undefined; - const selection = this.props.selection; - const selected = selection && ('dome-xMark-' + selection); + const { selection } = this.props; + const selected = selection && (`dome-xMark-${selection}`); const { buffer } = this.props; const decorator = buffer?.getDecorator(); if (!hovered && !selection && !decorator) return; const newDecorations = new Map<string, string>(); - for (var k = 0; k < n; k++) { + for (let k = 0; k < n; k++) { const elt = marked[k]; const classes = elt.classList; if (hovered && classes.contains(hovered)) classes.add(CSS_HOVERED); @@ -327,7 +329,7 @@ class CodeMirrorWrapper extends React.Component<TextProps> { onMouseClick(evt: MouseEvt, callback: MarkerCallback | undefined) { // No need for throttling - const target = evt.target; + const { target } = evt; if (target instanceof Element && callback) { const marker = this._findMarker(target); if (marker && marker.id) callback(marker.id); @@ -350,8 +352,10 @@ class CodeMirrorWrapper extends React.Component<TextProps> { handleScrollTo(line: number) { try { const cm = this.codeMirror; - cm && cm.scrollIntoView({ line, ch: 0 }); - } catch (_error) { } // Out of range + return cm && cm.scrollIntoView({ line, ch: 0 }); + } catch (_error) { + console.warn(`[Dome] Unable to scroll to line ${line}: out of range.`); + } } // -------------------------------------------------------------------------- @@ -392,12 +396,12 @@ class CodeMirrorWrapper extends React.Component<TextProps> { const { buffer: oldBuffer, selection: oldSelect, - fontSize: oldFont + fontSize: oldFont, } = this.props; const { buffer: newBuffer, selection: newSelect, - fontSize: newFont + fontSize: newFont, } = newProps; if (oldBuffer !== newBuffer) { if (oldBuffer) oldBuffer.unlink(cm); @@ -423,7 +427,7 @@ class CodeMirrorWrapper extends React.Component<TextProps> { }); // Update selection if (oldSelect !== newSelect) { - const selected = 'dome-xMark-' + newSelect; + const selected = `dome-xMark-${newSelect}`; if (oldSelect) this._unmarkElementsWith(CSS_SELECTED); if (newSelect) this._markElementsWith(selected, CSS_SELECTED); } @@ -436,7 +440,8 @@ class CodeMirrorWrapper extends React.Component<TextProps> { render() { return ( - <div className={'dome-xText'} + <div + className="dome-xText" ref={this.mountPoint} onClick={this.onClick} onContextMenu={this.onContextMenu} @@ -444,7 +449,8 @@ class CodeMirrorWrapper extends React.Component<TextProps> { onFocus={this.onFocus} onScroll={this.onScroll} onMouseMove={this.onMouseMove} - />); + /> + ); } } @@ -459,23 +465,24 @@ class CodeMirrorWrapper extends React.Component<TextProps> { A component rendering the content of a text buffer, that shall be instances of the `Buffer` base class. - The view is based on a [CodeMirror](https://codemirror.net) component linked with - the internal Code Mirror Document from the associated buffer. + The view is based on a [CodeMirror](https://codemirror.net) component linked + with the internal Code Mirror Document from the associated buffer. - Multiple views might share the same buffer as source content. The buffer will be - kept in sync with all its linked views. + Multiple views might share the same buffer as source content. The buffer will + be kept in sync with all its linked views. - The Text component never update its mounted NODE element, however, all property - modifications (including buffer) are propagated to the internal CodeMirror instance. - Undefined properties are set (or reset) to the CodeMirror defaults. + The Text component never update its mounted NODE element, however, all + property modifications (including buffer) are propagated to the internal + CodeMirror instance. Undefined properties are set (or reset) to the + CodeMirror defaults. #### Themes The CodeMirror `theme` option allow you to style your document, especially when using modes. Themes are only accessible if you load the associated CSS style sheet. - For instance, to use the `'ambiance'` theme provided with CodeMirror, you shall - import `'codemirror/theme/ambiance.css'` somewhere in your application. + For instance, to use the `'ambiance'` theme provided with CodeMirror, you + shall import `'codemirror/theme/ambiance.css'` somewhere in your application. #### Modes & Adds-On @@ -491,16 +498,13 @@ class CodeMirrorWrapper extends React.Component<TextProps> { `import CodeMirror from 'codemirror/lib/codemirror.js'` ; using `from 'codemirror'` returns a different instance of `CodeMirror` class and will not work. - */ export function Text(props: TextProps) { let { className, style, fontSize, ...cmprops } = props; if (fontSize !== undefined && fontSize < 4) fontSize = 4; if (fontSize !== undefined && fontSize > 48) fontSize = 48; - const theStyle = Object.assign({}, style); - theStyle.fontSize = fontSize; return ( - <Vfill className={className} style={theStyle}> + <Vfill className={className} style={{ ...style, fontSize }}> <CodeMirrorWrapper fontSize={fontSize} {...cmprops} /> </Vfill> ); diff --git a/ivette/src/frama-c/LabViews.tsx b/ivette/src/frama-c/LabViews.tsx index 52bc3151cac0951f6f9642dee1e7ceb83b05dc6b..fe74698d0346f10153cd61714e1086a9a3027761 100644 --- a/ivette/src/frama-c/LabViews.tsx +++ b/ivette/src/frama-c/LabViews.tsx @@ -329,7 +329,7 @@ const makeGridItem = (customize: any, onClose: any) => (comp: any) => { <Vfill className="labview-content"> <Hbox className="labview-titlebar"> <Hfill> - <Catch title={id}> + <Catch label={id}> <RenderItem id={`labview.title.${id}`}> <Label className="labview-handle" label={label} title={title} /> </RenderItem> @@ -338,7 +338,7 @@ const makeGridItem = (customize: any, onClose: any) => (comp: any) => { {CLOSING} </Hbox> <TitleContext.Provider value={{ id, label, title }}> - <Catch title={id}>{children}</Catch> + <Catch label={id}>{children}</Catch> </TitleContext.Provider> </Vfill> </Grids.GridItem> diff --git a/ivette/tsconfig.json b/ivette/tsconfig.json index 209897047d79c96fd790c4f1efe2a20a49871f7d..0cbb1f1af838676ae822f3048a4df3ca1086e745 100644 --- a/ivette/tsconfig.json +++ b/ivette/tsconfig.json @@ -49,7 +49,7 @@ "dome/system": [ "src/dome/src/misc/system.js" ], "dome/misc/*": [ "src/dome/src/misc/*"], "dome/*": [ "src/dome/src/renderer/*" ], - "codemirror/lib/codemirror.js": ["node_modules/@types/codemirror/index.d.ts"], + "codemirror/lib/codemirror": ["node_modules/@types/codemirror/index.d.ts"], }, // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ @@ -101,6 +101,7 @@ "doc/pages", "src/frama-c", "api", "src/dome/src/renderer", + "src/dome/src/misc", ] } } diff --git a/src/plugins/server/package.ml b/src/plugins/server/package.ml index 8734ab5fd50b8a46da21ed0eea3e80a9fc2d32b3..309cae632565703288b5cc95ca1a5793215cfba5 100644 --- a/src/plugins/server/package.ml +++ b/src/plugins/server/package.ml @@ -170,7 +170,7 @@ type jtype = | Jkey of string (* kind of a string used for indexing *) | Jindex of string (* kind of an integer used for indexing *) | Joption of jtype - | Jdict of string * jtype (* kind of keys *) + | Jdict of jtype (* dictionaries *) | Jlist of jtype (* order does not matter *) | Jarray of jtype (* order matters *) | Jtuple of jtype list @@ -291,14 +291,14 @@ let rec isRecursive = function | Jdata _ | Jenum _ | Jany | Jnull | Jboolean | Jnumber | Jstring | Jalpha | Jkey _ | Jindex _ | Jtag _ -> false - | Joption js | Jdict(_,js) | Jarray js | Jlist js -> isRecursive js + | Joption js | Jdict js | Jarray js | Jlist js -> isRecursive js | Jtuple js | Junion js -> List.exists isRecursive js | Jrecord fjs -> List.exists (fun (_,js) -> isRecursive js) fjs let rec visit_jtype fn = function | Jany | Jself | Jnull | Jboolean | Jnumber | Jstring | Jalpha | Jkey _ | Jindex _ | Jtag _ -> () - | Joption js | Jdict(_,js) | Jarray js | Jlist js -> visit_jtype fn js + | Joption js | Jdict js | Jarray js | Jlist js -> visit_jtype fn js | Jtuple js | Junion js -> List.iter (visit_jtype fn) js | Jrecord fjs -> List.iter (fun (_,js) -> visit_jtype fn js) fjs | Jdata id | Jenum id -> @@ -471,8 +471,8 @@ let rec md_jtype pp = function | Junion js -> md_jlist pp "|" js | Jarray js | Jlist js -> protect pp js @ Md.code "[]" | Jrecord fjs -> Md.code "{" @ fields pp fjs @ Md.code "}" - | Jdict (id,js) -> - Md.code "{[" @ key id @ Md.code "]:" @ md_jtype pp js @ Md.code "}" + | Jdict js -> + Md.code "{[key]:" @ md_jtype pp js @ Md.code "}" and md_jlist pp sep js = Md.glue ~sep:(Md.plain sep) (List.map (md_jtype pp) js) diff --git a/src/plugins/server/package.mli b/src/plugins/server/package.mli index 85af7f86923ac5df9fda9ad6fc5b066800ace281..952f0dd05659e32932bce624cb57c19fcc2e8741 100644 --- a/src/plugins/server/package.mli +++ b/src/plugins/server/package.mli @@ -38,7 +38,7 @@ type jtype = | Jkey of string (** kind of a string used for indexing *) | Jindex of string (** kind of an integer used for indexing *) | Joption of jtype - | Jdict of string * jtype (** kind of keys *) + | Jdict of jtype (** dictionaries *) | Jlist of jtype (** order does not matter *) | Jarray of jtype (** order matters *) | Jtuple of jtype list