Skip to content

Commit

Permalink
Merge pull request #274 from victorsoares96/feat/manipulate-annotatio…
Browse files Browse the repository at this point in the history
…ns-by-tag-id

✨ feat: manipulate annotations by tag id
  • Loading branch information
victorsoares96 authored May 30, 2024
2 parents 587cdd0 + e61494d commit 32edcb2
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 0 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,11 @@ const { changeFontSize, goToLocation, ... } = useReader();
| `getCurrentLocation` | | Returns the current location of the book |
| `getMeta` | | Returns an object containing the book's metadata. |
| `addAnnotation` | `annotation` | Attach annotation in the book. |
| `addAnnotationByTagId` | `annotation` | Attach annotation in the book by dom element tag id. |
| `updateAnnotation` | `annotation, data, styles` | Update annotation data and style |
| `updateAnnotationByTagId` | `annotation, data, styles` | Update annotation data and style by dom element tag id |
| `removeAnnotation` | `annotation` | Detach annotation in the book. |
| `removeAnnotationByTagId` | `annotation` | Detach annotation in the book by dom element tag id. |
| `removeAnnotationByCfi` `cfi` | | Detach annotations in the book by provided cfi. |
| `removeAnnotations` | `type?: optional` | Detach all annotations in the book. Can be detach by type |
| `removeSelection` | | Remove selection |
Expand Down
91 changes: 91 additions & 0 deletions src/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -438,19 +438,58 @@ export interface ReaderContextProps {
iconClass?: string
) => void;

addAnnotationByTagId: (
type: AnnotationType,
tagId: string,
data?: object,
styles?: AnnotationStyles,
/**
* The name of the css class defined in the applied theme that will be used as the icon for the markup.
* Example of how the class should be defined in the theme file:
* ```html
* <style type="text/css">
* [ref="epubjs-mk-heart"] {
* background: url("data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPScxLj4...") no-repeat;
* width: 20px;
* height: 20px;
* cursor: pointer;
* margin-left: 0;
* }
* </style>
* ```
*
*
* And how it should be defined:
*
*
* ```js
* addAnnotation('mark', 'epubCfi(20/14...)', {}, undefined, 'epubjs-mk-heart');
* ```
*/
iconClass?: string
) => void;

updateAnnotation: (
annotation: Annotation,
data?: object,
styles?: AnnotationStyles
) => void;

updateAnnotationByTagId: (
tagId: string,
data?: object,
styles?: AnnotationStyles
) => void;

removeAnnotation: (annotation: Annotation) => void;

/**
* Remove all annotations matching with provided cfi
*/
removeAnnotationByCfi: (cfiRange: ePubCfi) => void;

removeAnnotationByTagId: (tagId: string) => void;

removeAnnotations: (type?: AnnotationType) => void;

setAnnotations: (annotations: Annotation[]) => void;
Expand Down Expand Up @@ -660,8 +699,11 @@ const ReaderContext = createContext<ReaderContextProps>({
removeSelection: () => {},

addAnnotation: () => {},
addAnnotationByTagId: () => {},
updateAnnotation: () => {},
updateAnnotationByTagId: () => {},
removeAnnotation: () => {},
removeAnnotationByTagId: () => {},
removeAnnotationByCfi: () => {},
removeAnnotations: () => {},
setAnnotations: () => {},
Expand Down Expand Up @@ -921,6 +963,32 @@ function ReaderProvider({ children }: { children: React.ReactNode }) {
[]
);

const addAnnotationByTagId = useCallback(
(
type: AnnotationType,
tagId: string,
data?: object,
styles?: {
color?: string;
opacity?: number;
thickness?: number;
},
iconClass = ''
) => {
webViewInjectFunctions.injectJavaScript(
book,
webViewInjectFunctions.addAnnotationByTagId(
type,
tagId,
data,
iconClass,
styles
)
);
},
[]
);

const updateAnnotation = useCallback(
(annotation: Annotation, data = {}, styles?: AnnotationStyles) => {
webViewInjectFunctions.injectJavaScript(
Expand All @@ -931,6 +999,16 @@ function ReaderProvider({ children }: { children: React.ReactNode }) {
[]
);

const updateAnnotationByTagId = useCallback(
(tagId: string, data = {}, styles?: AnnotationStyles) => {
webViewInjectFunctions.injectJavaScript(
book,
webViewInjectFunctions.updateAnnotationByTagId(tagId, data, styles)
);
},
[]
);

const removeAnnotation = useCallback((annotation: Annotation) => {
webViewInjectFunctions.injectJavaScript(
book,
Expand All @@ -942,6 +1020,13 @@ function ReaderProvider({ children }: { children: React.ReactNode }) {
);
}, []);

const removeAnnotationByTagId = useCallback((tagId: string) => {
webViewInjectFunctions.injectJavaScript(
book,
webViewInjectFunctions.removeAnnotationByTagId(tagId)
);
}, []);

const removeAnnotationByCfi = useCallback((cfiRange: string) => {
webViewInjectFunctions.injectJavaScript(
book,
Expand Down Expand Up @@ -1198,8 +1283,11 @@ function ReaderProvider({ children }: { children: React.ReactNode }) {
removeSelection,

addAnnotation,
addAnnotationByTagId,
updateAnnotation,
updateAnnotationByTagId,
removeAnnotation,
removeAnnotationByTagId,
removeAnnotationByCfi,
removeAnnotations,
setAnnotations,
Expand Down Expand Up @@ -1253,8 +1341,11 @@ function ReaderProvider({ children }: { children: React.ReactNode }) {
setTotalLocations,
removeSelection,
addAnnotation,
addAnnotationByTagId,
updateAnnotation,
updateAnnotationByTagId,
removeAnnotation,
removeAnnotationByTagId,
removeAnnotationByCfi,
removeAnnotations,
setAnnotations,
Expand Down
9 changes: 9 additions & 0 deletions src/hooks/useReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ export function useReader() {
key,
searchResults,
addAnnotation,
addAnnotationByTagId,
updateAnnotation,
updateAnnotationByTagId,
removeAnnotation,
removeAnnotationByTagId,
removeAnnotationByCfi,
removeSelection,
annotations,
Expand Down Expand Up @@ -59,8 +62,11 @@ export function useReader() {
search,
clearSearchResults,
addAnnotation,
addAnnotationByTagId,
updateAnnotation,
updateAnnotationByTagId,
removeAnnotation,
removeAnnotationByTagId,
removeAnnotationByCfi,
removeSelection,
theme,
Expand Down Expand Up @@ -111,8 +117,11 @@ export function useReader() {
| 'key'
| 'searchResults'
| 'addAnnotation'
| 'addAnnotationByTagId'
| 'updateAnnotation'
| 'updateAnnotationByTagId'
| 'removeAnnotation'
| 'removeAnnotationByTagId'
| 'removeAnnotationByCfi'
| 'removeSelection'
| 'annotations'
Expand Down
177 changes: 177 additions & 0 deletions src/utils/webViewInjectFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,67 @@ export function addAnnotation(
`;
}

export function addAnnotationByTagId(
type: AnnotationType,
tagId: ePubCfi,
data?: object,
iconClass?: string,
styles?: AnnotationStyles,
cfiRangeText?: string,
noEmit = false
) {
const epubStyles = mapAnnotationStylesToEpubStyles(type, styles);

if (type === 'mark') {
// eslint-disable-next-line no-param-reassign
iconClass = iconClass || 'epubjs-mk-balloon';
}

return `
async function addAnnotationByTagId(tagId) {
return Promise.all(book.spine.spineItems.map((item) => {
return item.load(book.load.bind(book)).then(() => {
const element = item.document.getElementById(tagId);
if (!element) return null;
const range = item.document.createRange();
range.selectNodeContents(element);
let textOffset = element.textContent.length;
if (element.childNodes.length > 1) {
const lastChildNode = element.childNodes[element.childNodes.length - 1];
textOffset = lastChildNode.textContent.length;
}
const cfi = item.cfiFromElement(element).split(')')[0].concat(',/1:0,/').concat(range.endOffset).concat(':').concat(textOffset).concat(')');
const data = ${JSON.stringify(data)} || { epubcfi: cfi }
const cfiRangeText = ${JSON.stringify(cfiRangeText)} || range.toString();
const annotation = rendition.annotations.add('${type}', cfi, data, () => {}, ${JSON.stringify(iconClass)}, ${JSON.stringify(epubStyles)}, cfiRangeText);
const noEmit = ${noEmit};
if (!noEmit) {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'onAddAnnotation',
annotation: ${mapObjectToAnnotation('annotation')}
}));
}
item.unload();
return Promise.resolve();
});
}));
}
addAnnotationByTagId(${JSON.stringify(tagId)})
.then(() => {
${onChangeAnnotations()}
})
.catch(error => alert(JSON.stringify(error?.message)))
`;
}

export function updateAnnotation(
annotation: Annotation,
data = {},
Expand All @@ -135,3 +196,119 @@ export function updateAnnotation(
${onChangeAnnotations()}
`;
}

export function updateAnnotationByTagId(
tagId: string,
data = {},
styles?: AnnotationStyles
) {
return `
async function getCfiByTagId(tagId) {
const results = await Promise.all(book.spine.spineItems.map((item) => {
return item.load(book.load.bind(book)).then(() => {
const element = item.document.getElementById(tagId);
if (!element) return null;
const range = item.document.createRange();
range.selectNodeContents(element);
let textOffset = element.textContent.length;
if (element.childNodes.length > 1) {
const lastChildNode = element.childNodes[element.childNodes.length - 1];
textOffset = lastChildNode.textContent.length;
}
const cfi = item.cfiFromElement(element).split(')')[0].concat(',/1:0,/').concat(range.endOffset).concat(':').concat(textOffset).concat(')');
item.unload();
return Promise.resolve(cfi);
});
}));
if (results.length === 0) return null;
return results.filter(result => result)[0];
}
getCfiByTagId(${JSON.stringify(tagId)})
.then((cfi) => {
let annotations = Object.values(rendition.annotations._annotations);
annotations = annotations.filter(item => item.cfiRange === cfi);
annotations.forEach(annotation => {
let epubStyles = {};
const styles = ${JSON.stringify(styles)};
if (annotation.type === 'highlight') {
epubStyles = {
'fill': styles?.color || 'yellow',
'fill-opacity': styles?.opacity || 0.3,
};
}
if (annotation.type === 'underline') {
epubStyles = {
'stroke': styles?.color || 'yellow',
'stroke-opacity': styles?.opacity || 0.3,
'stroke-width': styles?.thickness || 1,
};
}
annotation.update(${JSON.stringify(data)}, epubStyles);
});
rendition.views().forEach(view => view.pane ? view.pane.render() : null);
${onChangeAnnotations()}
})
.catch(error => alert(JSON.stringify(error?.message)))
`;
}

export function removeAnnotationByTagId(tagId: string) {
return `
async function getCfiByTagId(tagId) {
const results = await Promise.all(book.spine.spineItems.map((item) => {
return item.load(book.load.bind(book)).then(() => {
const element = item.document.getElementById(tagId);
if (!element) return null;
const range = item.document.createRange();
range.selectNodeContents(element);
let textOffset = element.textContent.length;
if (element.childNodes.length > 1) {
const lastChildNode = element.childNodes[element.childNodes.length - 1];
textOffset = lastChildNode.textContent.length;
}
const cfi = item.cfiFromElement(element).split(')')[0].concat(',/1:0,/').concat(range.endOffset).concat(':').concat(textOffset).concat(')');
item.unload();
return Promise.resolve(cfi);
});
}));
if (results.length === 0) return null;
return results.filter(result => result)[0];
}
getCfiByTagId(${JSON.stringify(tagId)})
.then((cfi) => {
let annotations = Object.values(rendition.annotations._annotations);
annotations = annotations.filter(item => item.cfiRange === cfi);
annotations.forEach(annotation => {
rendition.annotations.remove(annotation.cfiRange, annotation.type);
});
rendition.views().forEach(view => view.pane ? view.pane.render() : null);
${onChangeAnnotations()}
})
.catch(error => alert(error?.message))
`;
}

0 comments on commit 32edcb2

Please sign in to comment.