Skip to content

Commit

Permalink
feat(interface): 🚸 add indicator for unsaved changes (#53)
Browse files Browse the repository at this point in the history
Squashed commit of the following:

commit c2cc0ff
Author: James <james@jamesnzl.xyz>
Date:   Fri Jul 15 13:44:25 2022 +1200

    feat(interface): :lipstick: show key value unsaved indicator in changed group(s)

commit 958c188
Author: James <james@jamesnzl.xyz>
Date:   Fri Jul 15 13:26:17 2022 +1200

    refactor(elements): :art: delegate to `markModified` method

commit 686904f
Author: James <james@jamesnzl.xyz>
Date:   Fri Jul 15 13:00:34 2022 +1200

    fix(elements): :bug: properly implement `getLabels()` on key value group

commit dcb02fb
Author: James <james@jamesnzl.xyz>
Date:   Fri Jul 15 12:59:33 2022 +1200

    refactor(elements): :recycle: refactor `getLabels()` to return array

commit 7672040
Author: James <james@jamesnzl.xyz>
Date:   Fri Jul 15 12:58:04 2022 +1200

    fix(types): :label: update input compositional interfaces

commit 503ba85
Author: James <james@jamesnzl.xyz>
Date:   Fri Jul 15 09:45:26 2022 +1200

    fix(types): :construction: fix type for `inputs` values

commit cd756b4
Author: James <james@jamesnzl.xyz>
Date:   Fri Jul 15 09:23:57 2022 +1200

    feat(interface): :children_crossing: add indicator for unsaved changes
  • Loading branch information
JamesNZL committed Jul 15, 2022
1 parent cce212f commit 7a8166d
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 10 deletions.
8 changes: 5 additions & 3 deletions src/elements/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class Element {

public show() {
this.removeClass('hidden');
this.getLabels()?.forEach(label => label.classList.remove('hidden'));
this.getLabels().forEach(label => label.classList.remove('hidden'));

if (!this.element.parentElement) return;

Expand All @@ -117,7 +117,7 @@ export class Element {

public hide() {
this.addClass('hidden');
this.getLabels()?.forEach(label => label.classList.add('hidden'));
this.getLabels().forEach(label => label.classList.add('hidden'));

if (!this.element.parentElement) return;

Expand All @@ -143,9 +143,11 @@ export class Element {
}

public getLabels() {
return (this.element instanceof HTMLInputElement)
const nodeList = (this.element instanceof HTMLInputElement)
? this.element.labels
: document.querySelectorAll(`label[for='${this.element.id}']`);

return Array.from(nodeList ?? []);
}

public safelySetInnerHTML(html: string) {
Expand Down
12 changes: 12 additions & 0 deletions src/elements/Input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ export class Input extends Element {
throw new Error(`Failed to set unexpected value ${value} of type ${typeof value} on element ${this.id}`);
}

public markModified(comparand: SupportedTypes) {
const isModified = (!this.isHidden() && this.getValue() !== comparand);

this.getLabels().forEach(label => {
(isModified)
? label.classList.add('unsaved')
: label.classList.remove('unsaved');
});

return isModified;
}

public setPlaceholder(placeholder: SupportedTypes) {
if (!(this.element instanceof HTMLInputElement) && !(this.element instanceof HTMLTextAreaElement)) return;

Expand Down
38 changes: 38 additions & 0 deletions src/elements/KeyValueGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,40 @@ export class KeyValueGroup extends Element {
return this;
}

public markModified(comparand: SupportedTypes) {
if (typeof comparand !== 'string') throw new Error(`Invalid comparand value ${comparand} of type ${typeof comparand} on KeyValueGroup ${this.id}`);

const currentValue = this.getValue();
if (typeof currentValue !== 'string') throw new Error(`Invalid currentValue value ${currentValue} of type ${typeof currentValue} on KeyValueGroup ${this.id}`);

const isModified = (!this.isHidden() && currentValue !== comparand);

if (!isModified) {
this.getLabels().forEach(label => label.classList.remove('unsaved'));

return false;
}

const currentObject = JSON.parse(currentValue);
const comparandObject = JSON.parse(comparand);

const keysMatch = JSON.stringify(Object.keys(currentObject)) === JSON.stringify(Object.keys(comparandObject));
const valuesMatch = JSON.stringify(Object.values(currentObject)) === JSON.stringify(Object.values(comparandObject));

this.keyGroup.getLabels().forEach(keyLabel => {
(!keysMatch)
? keyLabel.classList.add('unsaved')
: keyLabel.classList.remove('unsaved');
});
this.valueGroup.getLabels().forEach(valueLabel => {
(!valuesMatch)
? valueLabel.classList.add('unsaved')
: valueLabel.classList.remove('unsaved');
});

return true;
}

public async validate(force = false) {
if (!force) {
return (this.isValid)
Expand Down Expand Up @@ -266,6 +300,10 @@ export class KeyValueGroup extends Element {
this.removeRow(this.rows.indexOf(emptyRows[1]));
}

public override getLabels() {
return [...this.keyGroup.getLabels(), ...this.valueGroup.getLabels()];
}

public dispatchInputEvent(bubbles = true) {
this.dispatchEvent(new Event('input', { bubbles }));
}
Expand Down
12 changes: 12 additions & 0 deletions src/elements/SegmentedControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ export class SegmentedControl extends Element {
this.dispatchInputEvent();
}

public markModified(comparand: SupportedTypes) {
const isModified = (!this.isHidden() && this.getValue() !== comparand);

this.getLabels().forEach(label => {
(isModified)
? label.classList.add('unsaved')
: label.classList.remove('unsaved');
});

return isModified;
}

public override show() {
super.show();
this.dispatchInputEvent();
Expand Down
12 changes: 9 additions & 3 deletions src/options/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ import { valueof } from '../types/utils';
export type SupportedTypes = NullIfEmpty<string> | boolean;

interface Readable {
getValue(): SupportedTypes;
validate(force?: boolean): Promise<SupportedTypes | typeof InputFieldValidator.INVALID_INPUT>;
getValue(): SupportedTypes;
}

interface Settable {
setValue(value: SupportedTypes, dispatchEvent?: boolean): void;
markModified(comparand: SupportedTypes): boolean;
}

interface Displayable {
isHidden(): boolean;
show(): void;
hide(): void;
}
Expand All @@ -38,13 +40,17 @@ interface Subscribable {
dispatchInputEvent(bubbles?: boolean): void;
}

interface HasLabels {
getLabels(): globalThis.Element[];
}

interface HasPlaceholder {
setPlaceholder(placeholder: unknown): void;
}

interface OptionConfiguration<T> {
export interface OptionConfiguration<T> {
readonly defaultValue: T;
input: Readable & Settable & Displayable & Dependable & Subscribable & Partial<HasPlaceholder>;
input: Readable & Settable & Displayable & Dependable & Subscribable & HasLabels & Partial<HasPlaceholder>;
// default to 'input' if undefined
readonly validateOn?: 'input' | 'change';
readonly dependents?: readonly InputElementId[];
Expand Down
15 changes: 11 additions & 4 deletions src/options/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { OAuth2 } from '../apis/oauth';

import { SavedFields } from './';
import { EmojiField, InputFieldValidator } from './validator';
import { CONFIGURATION, SupportedTypes } from './configuration';
import { CONFIGURATION, OptionConfiguration, SupportedTypes } from './configuration';

import { Element, Button, Input, Select, KeyValueGroup } from '../elements';
import { Element, Button, Select, KeyValueGroup } from '../elements';

import { valueof } from '../types/utils';

Expand Down Expand Up @@ -60,7 +60,7 @@ class RestoreDefaultsButton extends Button {
protected static override instances: Record<string, RestoreDefaultsButton> = {};

protected restoreKeys: (keyof SavedFields)[];
protected inputs: Partial<Record<keyof SavedFields, Input>>;
protected inputs: Partial<Record<keyof SavedFields, OptionConfiguration<SupportedTypes>['input']>>;

protected constructor(id: string, restoreKeys: (keyof SavedFields)[]) {
super(id);
Expand Down Expand Up @@ -109,7 +109,14 @@ class RestoreSavedButton extends RestoreDefaultsButton {
public override async toggle() {
const savedFields = await Storage.getSavedFields();

(Object.entries(this.inputs).some(([key, input]) => !input.isHidden() && input.getValue() !== savedFields[<keyof SavedFields>key]))
const anyUnsavedInputs = Object.entries(this.inputs)
.reduce((hasUnsaved, [key, input]) => {
const isUnsaved = input.markModified(savedFields[<keyof SavedFields>key]);

return hasUnsaved || isUnsaved;
}, false);

(anyUnsavedInputs)
? this.show()
: this.hide();
}
Expand Down
15 changes: 15 additions & 0 deletions src/style/stylesheet.scss
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ label {
font-weight: 500;
margin-inline-start: 20px;
font-size: 0.75rem;
display: block;
}

&.required {
Expand All @@ -309,6 +310,20 @@ label {
right: 100%;
}
}

&.unsaved {
position: relative;

&::after {
content: '';
font-size: 0.875em;
color: orange;
position: absolute;
top: 0.0625em;
right: 0;
margin-inline-end: 10px;
}
}
}

input,
Expand Down

0 comments on commit 7a8166d

Please sign in to comment.