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

Gutter enrichment manager #42

Merged
merged 3 commits into from
Jun 20, 2022
Merged
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@laws-africa/indigo-akn",
"version": "5.0.0",
"version": "5.1.0",
"description": "Akoma Ntoso support libraries for the Indigo platform.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
122 changes: 122 additions & 0 deletions src/enrichments/gutter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { rangeToTarget, IRangeTarget } from "../ranges";

export interface IGutterEnrichmentProvider {
/** If the provider wants to add an enrichment for this target, return a button element.
* @param target
*/
getButton(target: IRangeTarget): HTMLButtonElement | null;

/**
* The user clicked the provider's button, add an enrichment for the provided target.
* @param target
*/
addEnrichment(target: IRangeTarget): void;
}

/**
* This manager provides support for creating gutter item enrichments from selected text in the document body.
* Providers register themselves with the manager and will be called when a new range is selected.
*
* The manager must be created on an element that has an `la-gutter` and an `la-akoma-ntoso` element as descendants.
*/
export class GutterEnrichmentManager {
protected root: Element;
protected gutter: Element | null;
protected akn: Element | null;
protected providers: IGutterEnrichmentProvider[];
protected floaterTimeout: number | null;
protected target: IRangeTarget | null;
protected floatingContainer: HTMLElement;

constructor (root: Element) {
this.root = root;
this.gutter = root.querySelector('la-gutter');
this.akn = root.querySelector('la-akoma-ntoso');
this.providers = [];
this.floatingContainer = this.createFloatingContainer();
this.floaterTimeout = null;
this.target = null;
document.addEventListener('selectionchange', this.selectionChanged.bind(this));
}

addProvider (provider: IGutterEnrichmentProvider) {
this.providers.push(provider);
}

createFloatingContainer () {
const item = document.createElement('la-gutter-item');
const btnGroup = document.createElement('div');
btnGroup.className = 'gutter-enrichment-new-buttons btn-group-vertical btn-group-sm bg-white';
Copy link

@musangowope musangowope Jun 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can assume that indigo-akn will always be used in bootstrap-dependent projects 🤔 and if not we can always style it accordingly🤷🏾

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ja, that's the assumption I'm making. Otherwise we have to style it explicitly here, and we don't have a mechanism for injecting CSS here. :(

item.appendChild(btnGroup);
return item;
}

/**
* When the selection in the document changes, transform it into a target description and, if successful,
* show the floating button container in the gutter.
*/
selectionChanged () {
const sel = document.getSelection();

if (!(this.akn && this.gutter)) {
return;
}

if (sel && sel.rangeCount > 0 && !sel.getRangeAt(0).collapsed) {
if (this.floaterTimeout) window.clearTimeout(this.floaterTimeout);

const range = sel.getRangeAt(0);

// is the common ancestor inside the akn container?
if (range.commonAncestorContainer.compareDocumentPosition(this.akn) & Node.DOCUMENT_POSITION_CONTAINS) {
// find first element
let root: Node | null = range.startContainer;
while (root && root.nodeType !== Node.ELEMENT_NODE) root = root.parentElement;

// stash the range as converted to a target; this may be null!
this.target = rangeToTarget(range, this.akn);
if (this.target) {
this.addProviderButtons(this.target);

// @ts-ignore
this.floatingContainer.anchor = root;

// add it to the gutter if it is not already there
if (!this.gutter.contains(this.floatingContainer)) {
this.gutter.appendChild(this.floatingContainer);
}
} else {
this.removeFloater();
}
}
} else {
// this needs to stick around for a little bit, for the case
// where the selection has been cleared because the button is
// being clicked
this.floaterTimeout = window.setTimeout(this.removeFloater.bind(this), 200);
}
}

addProviderButtons (target: IRangeTarget) {
const btnGroup = this.floatingContainer.firstElementChild;
if (btnGroup) {
btnGroup.innerHTML = '';

for (const provider of this.providers) {
const btn = provider.getButton(target);
if (btn) {
btn.addEventListener('click', () => {
this.removeFloater();
provider.addEnrichment(target);
});
btnGroup.appendChild(btn);
}
}
}
}

removeFloater () {
this.floatingContainer.remove();
this.floaterTimeout = null;
}
}
1 change: 1 addition & 0 deletions src/enrichments/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './popups';
export * from './gutter';
8 changes: 3 additions & 5 deletions src/ranges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export function withoutForeignElements (root: Element, callback: () => any, sele
const removed: RemovedElement[] = [];

// remove the foreign elements
for (const elem of root.querySelectorAll(selector)) {
for (const elem of Array.from(root.querySelectorAll(selector))) {
const info: RemovedElement = {
e: elem,
before: null,
Expand Down Expand Up @@ -197,8 +197,7 @@ export function targetToRange (target: IRangeTarget, root: Element): Range | nul
} else {
// no selectors, the anchor is the range
const range = root.ownerDocument.createRange();
range.setStartBefore(anchor);
range.setEndAfter(anchor);
range.selectNodeContents(anchor);
return range;
}
}
Expand Down Expand Up @@ -241,8 +240,7 @@ export function targetToAknRange (target: IRangeTarget, root: Element): Range |
} else {
// no selectors, the anchor is the range
const range = new Range();
range.setStartBefore(anchor);
range.setEndAfter(anchor);
range.selectNodeContents(anchor);
return range;
}
}
Expand Down