Allow multi-panel to keep one open only
This commit is contained in:
@ -1,8 +1,10 @@
|
||||
import './styles.css';
|
||||
import * as style from './styles.css';
|
||||
|
||||
const openOneOnlyAttr = 'open-one-only';
|
||||
|
||||
function getClosestHeading(el: Element) {
|
||||
const closestEl = el.closest('multi-panel > *');
|
||||
if (closestEl && closestEl.classList.contains('panel-heading')) {
|
||||
if (closestEl && closestEl.classList.contains(style.panelHeading)) {
|
||||
return closestEl;
|
||||
}
|
||||
return undefined;
|
||||
@ -14,6 +16,7 @@ function getClosestHeading(el: Element) {
|
||||
* and odd index element becomes the expandable content.
|
||||
*/
|
||||
export default class MultiPanel extends HTMLElement {
|
||||
static get observedAttributes() { return [openOneOnlyAttr]; }
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@ -31,12 +34,18 @@ export default class MultiPanel extends HTMLElement {
|
||||
this._childrenChange();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
||||
if (name === openOneOnlyAttr && newValue === null) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
// Click event handler
|
||||
private _onClick(event: MouseEvent) {
|
||||
const el = event.target as Element;
|
||||
const heading = getClosestHeading(el);
|
||||
if (!heading) return;
|
||||
this._expand(heading);
|
||||
this._toggle(heading);
|
||||
}
|
||||
|
||||
// KeyDown event handler
|
||||
@ -77,7 +86,7 @@ export default class MultiPanel extends HTMLElement {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
case 'Spacebar':
|
||||
this._expand(heading);
|
||||
this._toggle(heading);
|
||||
break;
|
||||
|
||||
// Any other key press is ignored and passed back to the browser.
|
||||
@ -93,7 +102,7 @@ export default class MultiPanel extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _expand(heading: Element) {
|
||||
private _toggle(heading: Element) {
|
||||
if (!heading) return;
|
||||
const content = heading.nextElementSibling;
|
||||
|
||||
@ -105,11 +114,21 @@ export default class MultiPanel extends HTMLElement {
|
||||
content.removeAttribute('expanded');
|
||||
content.setAttribute('aria-expanded', 'false');
|
||||
} else {
|
||||
if (this.openOneOnly) this._closeAll();
|
||||
content.setAttribute('expanded', '');
|
||||
content.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
private _closeAll(): void {
|
||||
const els = [...this.children].filter(el => el.matches('[expanded]'));
|
||||
|
||||
for (const el of els) {
|
||||
el.removeAttribute('expanded');
|
||||
el.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
// children of multi-panel should always be even number (heading/content pair)
|
||||
private _childrenChange() {
|
||||
let preserveTabIndex : boolean = false;
|
||||
@ -130,10 +149,10 @@ export default class MultiPanel extends HTMLElement {
|
||||
// When odd number of elements were inserted in the middle,
|
||||
// what was heading before may become content after the insertion.
|
||||
// Remove classes and attributes to prepare for this change.
|
||||
heading.classList.remove('panel-content');
|
||||
heading.classList.remove(style.panelContent);
|
||||
|
||||
if (content.classList.contains('panel-heading')) {
|
||||
content.classList.remove('panel-heading');
|
||||
if (content.classList.contains(style.panelHeading)) {
|
||||
content.classList.remove(style.panelHeading);
|
||||
}
|
||||
if (heading.hasAttribute('expanded') && heading.hasAttribute('aria-expanded')) {
|
||||
heading.removeAttribute('expanded');
|
||||
@ -146,8 +165,8 @@ export default class MultiPanel extends HTMLElement {
|
||||
}
|
||||
|
||||
// Assign heading and content classes
|
||||
heading.classList.add('panel-heading');
|
||||
content.classList.add('panel-content');
|
||||
heading.classList.add(style.panelHeading);
|
||||
content.classList.add(style.panelContent);
|
||||
|
||||
// Assign ids and aria-X for heading/content pair.
|
||||
heading.id = `panel-heading-${randomId}`;
|
||||
@ -208,7 +227,7 @@ export default class MultiPanel extends HTMLElement {
|
||||
private _lastHeading() {
|
||||
// if the last element is heading, return last element
|
||||
const lastEl = this.lastElementChild as HTMLElement;
|
||||
if (lastEl && lastEl.classList.contains('panel-heading')) {
|
||||
if (lastEl && lastEl.classList.contains(style.panelHeading)) {
|
||||
return lastEl;
|
||||
}
|
||||
// otherwise return 2nd from the last
|
||||
@ -217,6 +236,21 @@ export default class MultiPanel extends HTMLElement {
|
||||
return lastContent.previousElementSibling as HTMLElement;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, only one panel can be open at once. When one opens, others close.
|
||||
*/
|
||||
get openOneOnly() {
|
||||
return this.hasAttribute(openOneOnlyAttr);
|
||||
}
|
||||
|
||||
set openOneOnly(val: boolean) {
|
||||
if (val) {
|
||||
this.setAttribute(openOneOnlyAttr, '');
|
||||
} else {
|
||||
this.removeAttribute(openOneOnlyAttr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('multi-panel', MultiPanel);
|
9
src/components/compress/custom-els/MultiPanel/missing-types.d.ts
vendored
Normal file
9
src/components/compress/custom-els/MultiPanel/missing-types.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
interface MultiPanelAttributes extends JSX.HTMLAttributes {
|
||||
'open-one-only'?: boolean;
|
||||
}
|
||||
|
||||
declare namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'multi-panel': MultiPanelAttributes;
|
||||
}
|
||||
}
|
11
src/components/compress/custom-els/MultiPanel/styles.css
Normal file
11
src/components/compress/custom-els/MultiPanel/styles.css
Normal file
@ -0,0 +1,11 @@
|
||||
.panel-heading {
|
||||
background:gray;
|
||||
}
|
||||
.panel-content {
|
||||
height:0px;
|
||||
overflow:scroll;
|
||||
transition: height 1s;
|
||||
}
|
||||
.panel-content[expanded] {
|
||||
height:auto;
|
||||
}
|
@ -24,16 +24,15 @@ import {
|
||||
EncoderOptions,
|
||||
encoderMap,
|
||||
} from '../../codecs/encoders';
|
||||
|
||||
import {
|
||||
PreprocessorState,
|
||||
defaultPreprocessorState,
|
||||
} from '../../codecs/preprocessors';
|
||||
|
||||
import { decodeImage } from '../../codecs/decoders';
|
||||
import { cleanMerge, cleanSet } from '../../lib/clean-modify';
|
||||
import Processor from '../../codecs/processor';
|
||||
import { VectorResizeOptions, BitmapResizeOptions } from '../../codecs/resize/processor-meta';
|
||||
import './custom-els/MultiPanel';
|
||||
|
||||
export interface SourceImage {
|
||||
file: File | Fileish;
|
||||
@ -397,6 +396,23 @@ export default class Compress extends Component<Props, State> {
|
||||
const [leftImage, rightImage] = images;
|
||||
const [leftImageData, rightImageData] = images.map(i => i.data);
|
||||
|
||||
const options = images.map((image, index) => (
|
||||
<Options
|
||||
loading={loading || image.loading}
|
||||
source={source}
|
||||
mobileView={mobileView}
|
||||
imageIndex={index}
|
||||
imageFile={image.file}
|
||||
downloadUrl={image.downloadUrl}
|
||||
preprocessorState={image.preprocessorState}
|
||||
encoderState={image.encoderState}
|
||||
onEncoderTypeChange={this.onEncoderTypeChange.bind(this, index)}
|
||||
onEncoderOptionsChange={this.onEncoderOptionsChange.bind(this, index)}
|
||||
onPreprocessorOptionsChange={this.onPreprocessorOptionsChange.bind(this, index)}
|
||||
onCopyToOtherClick={this.onCopyToOtherClick.bind(this, index)}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<div class={style.compress}>
|
||||
<Output
|
||||
@ -407,22 +423,16 @@ export default class Compress extends Component<Props, State> {
|
||||
leftImgContain={leftImage.preprocessorState.resize.fitMethod === 'cover'}
|
||||
rightImgContain={rightImage.preprocessorState.resize.fitMethod === 'cover'}
|
||||
/>
|
||||
{images.map((image, index) => (
|
||||
<Options
|
||||
loading={loading || image.loading}
|
||||
source={source}
|
||||
mobileView={mobileView}
|
||||
imageIndex={index}
|
||||
imageFile={image.file}
|
||||
downloadUrl={image.downloadUrl}
|
||||
preprocessorState={image.preprocessorState}
|
||||
encoderState={image.encoderState}
|
||||
onEncoderTypeChange={this.onEncoderTypeChange.bind(this, index)}
|
||||
onEncoderOptionsChange={this.onEncoderOptionsChange.bind(this, index)}
|
||||
onPreprocessorOptionsChange={this.onPreprocessorOptionsChange.bind(this, index)}
|
||||
onCopyToOtherClick={this.onCopyToOtherClick.bind(this, index)}
|
||||
/>
|
||||
))}
|
||||
{mobileView
|
||||
? (
|
||||
<multi-panel class={style.multiPanel} open-one-only>
|
||||
<div>Top</div>
|
||||
{options[0]}
|
||||
<div>Bottom</div>
|
||||
{options[1]}
|
||||
</multi-panel>
|
||||
) : options
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -16,3 +16,7 @@
|
||||
grid-template-rows: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-panel {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
multi-panel > .panel-heading {
|
||||
background:gray;
|
||||
}
|
||||
multi-panel > .panel-content {
|
||||
height:0px;
|
||||
overflow:scroll;
|
||||
transition: height 1s;
|
||||
}
|
||||
multi-panel > .panel-content[expanded] {
|
||||
height:auto;
|
||||
}
|
Reference in New Issue
Block a user