Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#268 Show a diff (kinda) #290

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/sass/diff.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@use "colours"

.vbl-dirty
background-color: colours.$light-blue !important

input[type=checkbox].vbl-dirty
box-shadow: 0 0 0 2px colours.$blue
19 changes: 10 additions & 9 deletions src/ts/common/ui/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,29 @@ import "../../../sass/modal.sass";
import {createOverlay} from "./overlay";
import {UIColour} from "./colour";

interface ModalElement {
kind: string;
type BaseModalElement = {
text: string;
colour: UIColour;
}
};

interface ModalButton extends ModalElement {
type ModalButton = BaseModalElement & {
kind: "button";
onClick?: () => Promise<void>;
}
};

interface ModalInput extends ModalElement {
type ModalInput = BaseModalElement & {
kind: "input";
placeholder: string;
ensureNonEmpty: boolean;
onSelect: (userText: string) => Promise<void>;
}
};

type ModalElement = ModalButton | ModalInput;

interface ModalOptions {
text: string;
subText?: string[];
elements: (ModalButton | ModalInput)[];
elements: ModalElement[];
onCancel?: () => Promise<void>;
colour: UIColour;
/**
Expand Down Expand Up @@ -94,7 +95,7 @@ const createModalInput = (exit: () => void, {text, onSelect, colour, ensureNonEm
return container;
};

const createModalElement = (exit: () => void) => (element: ModalButton | ModalInput) => {
const createModalElement = (exit: () => void) => (element: ModalElement) => {
if (element.kind === "button") {
return createModalButton(exit, element);
} else {
Expand Down
37 changes: 25 additions & 12 deletions src/ts/content/entities/bookForm/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const FORM_META_DATA_KEY = "___metadata_";

const matchFactory = matchFactoryFromDescriptors(collections, ...physicalDescription);

const extractFromFormDataStrict = matchFactory(
"fromFormDataStrict",
(formData, element) => formData[element.id] ?? false
);

const extractFromFormData = matchFactory("fromFormData", (formData, element) => formData[element.id] ?? element);

const extractFromElement = matchFactory(
Expand All @@ -40,29 +45,37 @@ const getFormMetadata = (document: Document): FormData => ({
const isFormDataElement = (element: FormAreaElement): boolean =>
element && element.type !== "hidden" && internalIsFormDataElement(null, element);

const getFormDataElements = (_document = document) => getFormElements(_document).filter(isFormDataElement);

const getFormDataFromElement = (element: FormAreaElement) => extractFromElement({}, element);

const getFormData = (_document = document) =>
getFormElements(_document).reduce((formData: FormData, element: any) => {
isFormDataElement(element) && extractFromElement(formData, element);
getFormDataElements(_document).reduce((formData: FormData, element: any) => {
extractFromElement(formData, element);
return formData;
}, getFormMetadata(_document));

const insertFormData = (formData: FormData) => {
const metaData = formData?.[FORM_META_DATA_KEY] ?? {};
ensureRolesInputCount(metaData);
ensurePhysicalDescriptionInputCounts(metaData);
getFormElements(document).forEach((element: any) => {
if (isFormDataElement(element)) {
const {value, checked} = transformIncomingData(extractFromFormData(formData, element), element);
if (element.value !== value || element.checked !== checked) {
element.value = value;
element.checked = checked;
element.dispatchEvent(new Event("change"));
ensureVisible(element);
}
getFormDataElements(document).forEach((element: any) => {
const {value, checked} = transformIncomingData(extractFromFormData(formData, element), element);
if (element.value !== value || element.checked !== checked) {
element.value = value;
element.checked = checked;
element.dispatchEvent(new Event("change"));
ensureVisible(element);
}
});
};

const ensureVisible = (element: Element) => match(element).case(isCollectionsElement, show).yield();

export {getFormData, insertFormData};
export {
getFormData,
getFormDataElements,
getFormDataFromElement,
extractFromFormDataStrict as getFormDataForElement,
insertFormData,
};
12 changes: 9 additions & 3 deletions src/ts/content/entities/bookForm/data/uniqueData/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,28 @@ const COLLECTIONS_KEY = "___collections_";

const isCollectionsElement = (element: Element): boolean => element.id.startsWith(COLLECTIONS_ID_PREFIX);

const extractCollectionsDataFromFormData = (formData: FormData, element) => {
const extractCollectionsDataFromFormDataStrict = (formData: FormData, element) => {
const span = element.parentElement.getElementsByTagName("span")[0];
return formData[COLLECTIONS_KEY]?.[span?.textContent] ?? element;
return formData[COLLECTIONS_KEY]?.[span?.textContent] ?? false;
};

const extractCollectionsDataFromFormData = (formData: FormData, element) =>
extractCollectionsDataFromFormDataStrict(formData, element) || element;

const extractCollectionsDataFromElement = (formData: FormData, element) => {
const collections = formData[COLLECTIONS_KEY] ?? {};
const span = element.parentElement.getElementsByTagName("span")?.[0];
collections[span.textContent] = {value: element.value, checked: element.checked};
const record = {value: element.value, checked: element.checked};
collections[span.textContent] = record;
formData[COLLECTIONS_KEY] = collections;
return record;
};

const collections = uniqueFormElement({
predicate: isCollectionsElement,
fromElement: extractCollectionsDataFromElement,
fromFormData: extractCollectionsDataFromFormData,
fromFormDataStrict: extractCollectionsDataFromFormDataStrict,
});

export {collections, isCollectionsElement};
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,27 @@ const calculateRow = (element: Element): number => {

const fromPhysicalDescriptionElement = (key: string, name: string) => (formData: FormData, element) => {
const row = calculateRow(element);
if (row >= 0) {
formData[key] ??= [];
formData[key][row] ??= {};
formData[key][row][name] ??= {};
formData[key][row][name] = {value: element.value, checked: element.checked};
}
formData[key] ??= [];
formData[key][row] ??= {};
formData[key][row][name] ??= {};
return (formData[key][row][name] = {value: element.value, checked: element.checked});
};

const fromPhysicalDescriptionFormData = (key: string, name: string) => (formData: FormData, element) => {
const fromPhysicalDescriptionFormDataStrict = (key: string, name: string) => (formData: FormData, element) => {
const row = calculateRow(element);
return formData[key]?.[row]?.[name] ?? element;
return formData[key]?.[row]?.[name] ?? false;
};

const fromPhysicalDescriptionFormData = (key: string, name: string) => (formData: FormData, element) =>
fromPhysicalDescriptionFormDataStrict(key, name)(formData, element) || element;

const physicalDescriptionFormElement = (id: string) => (name: string) => {
const key = `_vbl_${id}`;
return uniqueFormElement({
predicate: isPhysicalDescriptionElement(id, name),
fromElement: fromPhysicalDescriptionElement(key, name),
fromFormData: fromPhysicalDescriptionFormData(key, name),
fromFormDataStrict: fromPhysicalDescriptionFormDataStrict(key, name),
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,36 @@ import {FormData} from "../../types";

type Predicate = (element) => boolean;
type FromFormData = (formData: FormData, element) => {value: string; checked: boolean};
type FromElement = (formData: FormData, element) => void;
type FromFormDataStrict = (formData: FormData, element) => {value: string; checked: boolean} | false;
type FromElement = (formData: FormData, element) => {value: string; checked: boolean};

interface UniqueFormElementDescriptor {
isFormData: () => readonly [Predicate, () => true];
fromFormData: (formData: FormData) => readonly [Predicate, (element) => {value: string; checked: boolean}];
fromElement: (formData: FormData) => readonly [Predicate, (element) => void];
fromFormDataStrict: (
formData: FormData
) => readonly [Predicate, (element) => {value: string; checked: boolean} | false];
}

interface UniqueFormElementOptions {
predicate: Predicate;
fromFormData: FromFormData;
fromElement: FromElement;
fromFormDataStrict: FromFormDataStrict;
}

const uniqueFormElement = ({
predicate,
fromElement,
fromFormData,
fromFormDataStrict,
}: UniqueFormElementOptions): UniqueFormElementDescriptor => ({
isFormData: () => [predicate, () => true] as const,
fromFormData: (formData: FormData) => [predicate, (element) => fromFormData(formData, element)] as const,
fromElement: (formData: FormData) => [predicate, (element) => fromElement(formData, element)] as const,
fromFormDataStrict: (formData: FormData) =>
[predicate, (element) => fromFormDataStrict(formData, element)] as const,
});

const matchFactoryFromDescriptors =
Expand Down
2 changes: 1 addition & 1 deletion src/ts/content/entities/bookForm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "./render";
import {formDataEquals, formExists} from "./util";
import {getFormData, insertFormData} from "./data";
import {OnSave, OffSave} from "./save";
import {OnSave, OffSave} from "./state";

export type {FormData, ForEachFormElement, FormRenderListener, OnSave, OffSave};
export {
Expand Down
55 changes: 39 additions & 16 deletions src/ts/content/entities/bookForm/render.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
import {FormAreaElement} from "./types";
import {formExists, getForm, getFormElements} from "./util";
import {createOnSave, OffSave, OnConfirm, OnSave} from "./save";
import {FormAreaElement, FormData} from "./types";
import {formExists, getForm, getFormElementsFromSubtree} from "./util";
import {createFormState, FormState, OffSave, OnConfirm, OnSave} from "./state";
import {getFormDataElements} from "./data";

type ForEachFormElement = (callback: (element: FormAreaElement) => void) => void;
type FormRenderListener = (
form: HTMLElement,
forEachElement: ForEachFormElement,
onSave: OnSave,
offSave: OffSave,
onConfirm: OnConfirm
) => void;
type FormRenderEnvironment = {
form: HTMLElement;
forEachElement: ForEachFormElement;
onSave: OnSave;
offSave: OffSave;
onConfirm: OnConfirm;
getCleanFormData: () => FormData;
};
type FormRenderListener = (env: FormRenderEnvironment) => void;

const FORM_RENDER_EVENT = "library-thing-form-rendered";
const FORM_REMOVED_EVENT = "library-thing-form-removed";

const forEachFormElement: ForEachFormElement = (callback: (element: FormAreaElement) => void): void =>
getFormElements(document).forEach(callback);
const forEachFormElement: ForEachFormElement = (callback: (element: FormAreaElement) => void): void => {
getFormDataElements(document).forEach(callback); // view -> edit. things called twice.
state.onFormElement(callback);
};

const listeners = new Map<FormRenderListener, () => void>();
// kinda gross but i don't have a better idea without a bIG refactor
let save: {onSave: OnSave; offSave: OffSave; onConfirm: OnConfirm};
let state: FormState;

const encloseCallbackArguments = (callback: FormRenderListener) => () =>
callback(getForm(document), forEachFormElement, save.onSave, save.offSave, save.onConfirm);
callback({
form: getForm(document),
forEachElement: forEachFormElement,
onSave: (callback) => state.onSave(callback),
offSave: (callback) => state.offSave(callback),
onConfirm: (callback) => state.onConfirm(callback),
getCleanFormData: () => state.getCleanFormData(),
});

const onFormRender = (callback: FormRenderListener): void => {
const listener = encloseCallbackArguments(callback);
Expand All @@ -45,7 +57,7 @@ const onceFormRemoved = (callback: () => void): void =>

const handleFormMutation = () => {
if (formExists()) {
save = createOnSave();
state = createFormState();
window.dispatchEvent(new Event(FORM_RENDER_EVENT));
} else {
window.dispatchEvent(new Event(FORM_REMOVED_EVENT));
Expand All @@ -55,7 +67,18 @@ const handleFormMutation = () => {
window.addEventListener("pageshow", () => {
const editForm = getForm(document);
if (editForm) {
new MutationObserver(handleFormMutation).observe(editForm, {subtree: false, childList: true});
new MutationObserver(handleFormMutation).observe(editForm, {childList: true});
new MutationObserver(
(mutations) =>
state?.registerFormElement &&
mutations.forEach((mutation) =>
mutation.addedNodes.forEach(
(node) =>
node instanceof HTMLElement &&
getFormElementsFromSubtree(node).forEach(state.registerFormElement)
)
)
).observe(editForm, {subtree: true, childList: true});
handleFormMutation();
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import {FormAreaElement, FormData} from "./types";
import {getFormData} from "./data";

type OnSave = (callback: OnSaveListener) => void;
type OffSave = OnSave;
type OnConfirm = (callback: OnConfirmedListener) => void;
type OnSaveListener = () => Promise<boolean>;
type OnConfirmedListener = () => void;
type FormElementListener = (element: FormAreaElement) => void;
type OnFormElement = (callback: FormElementListener) => void;
type FormState = {
onSave: OnSave;
offSave: OffSave;
onConfirm: OnConfirm;
registerFormElement: FormElementListener;
onFormElement: OnFormElement;
getCleanFormData: () => FormData;
};

const maybeClick =
(
Expand Down Expand Up @@ -36,16 +49,21 @@ const replaceButton = (
button.insertAdjacentElement("beforebegin", td);
};

const createOnSave = (): {onSave: OnSave; offSave: OffSave; onConfirm: OnConfirm} => {
const createFormState = (): FormState => {
const formData = getFormData();
const listeners: Set<OnSaveListener> = new Set<OnSaveListener>();
const confirmListeners: Set<OnConfirmedListener> = new Set<OnConfirmedListener>();
const formElementListeners: Set<FormElementListener> = new Set<FormElementListener>();
replaceButton(document.getElementById("book_editTabTextSave1"), listeners, confirmListeners);
replaceButton(document.getElementById("book_editTabTextSave2"), listeners, confirmListeners);
const onSave = (callback: OnSaveListener) => listeners.add(callback);
const offSave = (callback: OnSaveListener) => listeners.delete(callback);
const onConfirm = (callback: OnConfirmedListener) => confirmListeners.add(callback);
return {onSave, offSave, onConfirm};
const onFormElement = (callback: FormElementListener) => formElementListeners.add(callback);
const registerFormElement = (formAreaElement: FormAreaElement) =>
formElementListeners.forEach((callback) => callback(formAreaElement));
return {onSave, offSave, onConfirm, onFormElement, registerFormElement, getCleanFormData: () => formData};
};

export type {OnSave, OffSave, OnConfirm};
export {createOnSave};
export type {OnSave, OffSave, OnConfirm, FormState, FormElementListener};
export {createFormState};
8 changes: 5 additions & 3 deletions src/ts/content/entities/bookForm/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ const getElementsByTag = (parent: HTMLElement) => (tag: string) => Array.from(pa

const getElementsByTags = (parent: HTMLElement, tags: string[]) => tags.flatMap(getElementsByTag(parent));

const getFormElements = (document: Document): FormAreaElement[] =>
getElementsByTags(getForm(document), FORM_DATA_ELEMENT_TAGS) as FormAreaElement[];
const getFormElements = (document: Document): FormAreaElement[] => getFormElementsFromSubtree(getForm(document));

const getFormElementsFromSubtree = (element: HTMLElement): FormAreaElement[] =>
getElementsByTags(element, FORM_DATA_ELEMENT_TAGS) as FormAreaElement[];

// This is relying on the fact that when the edit form is available, the html matches this selector,
// and fails to match in all other cases. This IS brittle. If LibraryThing changes
Expand All @@ -26,4 +28,4 @@ const getForm = (document: Document): HTMLElement => document.getElementById("bo
*/
const formDataEquals = (formA: FormData, formB: FormData): boolean => JSON.stringify(formA) === JSON.stringify(formB);

export {getForm, getFormElements, formExists, formDataEquals};
export {getForm, getFormElements, getFormElementsFromSubtree, formExists, formDataEquals};
2 changes: 1 addition & 1 deletion src/ts/content/extensions/copy/onEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ const onHoverPasteButton = (editTooltip: (text: string) => void) => async () =>
}
};

onFormRender((form: HTMLElement) => Array.from(form.getElementsByClassName("book_bitTable")).forEach(appendCopyPaste));
onFormRender(({form}) => Array.from(form.getElementsByClassName("book_bitTable")).forEach(appendCopyPaste));
Loading