diff --git a/src/components/CollectionsDropdown.vue b/src/components/CollectionsDropdown.vue new file mode 100644 index 0000000..c9cbebb --- /dev/null +++ b/src/components/CollectionsDropdown.vue @@ -0,0 +1,39 @@ + + + diff --git a/src/constants/ContentScriptMessageTypes.ts b/src/constants/ContentScriptMessageTypes.ts index e1296d3..3e16966 100644 --- a/src/constants/ContentScriptMessageTypes.ts +++ b/src/constants/ContentScriptMessageTypes.ts @@ -1,4 +1,6 @@ export default { GET_CONTENT_SCRIPT_STATUS: 'get-content-script-status', - GET_COLLECTION_SAVED_ITEMS: 'get-collection-saved-items' + GET_COLLECTION_SAVED_ITEMS: 'get-collection-saved-items', + GET_COLLECTIONS_LIST: 'get-collections-list', + REDIRECT_TO_COLLECTION: 'redirect-to-collection', } \ No newline at end of file diff --git a/src/models/Collection.ts b/src/models/Collection.ts new file mode 100644 index 0000000..bba4237 --- /dev/null +++ b/src/models/Collection.ts @@ -0,0 +1,4 @@ +export interface Collection { + _id: string, + name: string, +} \ No newline at end of file diff --git a/src/services/content/content-script.ts b/src/services/content/content-script.ts index 2195c70..3ac454c 100644 --- a/src/services/content/content-script.ts +++ b/src/services/content/content-script.ts @@ -6,13 +6,17 @@ import { SavedItem } from '@/models/SavedItem'; import { SOCIAL_SHARE_BUTTON_IMG_SELECTOR, locateColumnsWrapper, - unNestElement + getUserCollections, + unNestElement, } from '@/services/content/utils/extractTools'; import { getUid } from '@/utils' const { + GET_COLLECTIONS_LIST, + GET_COLLECTION_SAVED_ITEMS, GET_CONTENT_SCRIPT_STATUS, + REDIRECT_TO_COLLECTION, } = ContentScriptMessageTypes; const { @@ -34,12 +38,53 @@ chrome.runtime.onConnect.addListener((port) => { } }) +// Switch case handlers +type HandlerArgs = { + port: chrome.runtime.Port, + opts?: any, +}; + +function handle_GET_COLLECTION_SAVED_ITEMS({ port }: HandlerArgs) { + port.postMessage(extractSavedItemsData()); +} + +function handle_REDIRECT_TO_COLLECTION({ port, opts }: HandlerArgs) { + port.disconnect() + const { url } = opts; + location.href = url; +} + +function handle_GET_COLLECTIONS_LIST({ port }: HandlerArgs) { + port.postMessage(getCollectionsList()); +} + function handleResponse(port: chrome.runtime.Port) { + + // Middleware to attach port instance to handler + const call = (handlerFunc: Function, opts?: Object) => { + handlerFunc({ port, opts }); + } + port.onMessage.addListener(request => { - if (request) { - port.postMessage(extractSavedItemsData()) + const { type } = request; + + switch(type){ + case GET_COLLECTION_SAVED_ITEMS: + call(handle_GET_COLLECTION_SAVED_ITEMS); + break; + + case GET_COLLECTIONS_LIST: + call(handle_GET_COLLECTIONS_LIST); + break; + + case REDIRECT_TO_COLLECTION: + call(handle_REDIRECT_TO_COLLECTION, { url: request.url }); + break; + + default: + break; } - }) + }); } /** @@ -135,3 +180,10 @@ function handleResponse(port: chrome.runtime.Port) { return data; } + +function getCollectionsList() { + return getUserCollections(); +} + +// Need to craft the URL manually for each collection +// We can extract the data-id from one of the span's children diff --git a/src/services/content/utils/extractTools.ts b/src/services/content/utils/extractTools.ts index fe910e9..6270614 100644 --- a/src/services/content/utils/extractTools.ts +++ b/src/services/content/utils/extractTools.ts @@ -1,8 +1,22 @@ +import { Collection } from "@/models/Collection"; + +/** + * Query selector strings + */ const SOCIAL_SHARE_BUTTON_IMG_SELECTOR = 'img[src="https://www.gstatic.com/save/icons/share_blue.svg"]'; +const COLLECTIONS_QUERY = 'span[role="option"]'; +const DATA_ID_ATTR = 'data-id'; + +/** + * Index paths for traversing children + */ +const COLLECTION_TITLE_DIV = [0,0,0,1,0]; +const COLLECTION_DATAID_DIV = [1]; + /** * Go up "n" parent nodes from passed element. */ -const unNestElement = (element: any, levels = 7) => { +const unNestElement = (element: any, levels: number) => { let i = 0; let destElement = element; try { @@ -17,6 +31,37 @@ const unNestElement = (element: any, levels = 7) => { } } +// Notes, if we want to access a children that is between the traversing path, +// could update to return the target and a second option that is the array of seen +// children (dynamic programming?) +/** + * Function to traverse children of an element through the indexPath + * + * Ex: indicesPath = [0,1,2] => element.children[0].children[1].children[2] + * @param element + * @param indicesPath array of indices where to traverse children + * @returns + */ +const traverseChildren = (element: Element, indicesPath: number[]) => { + let targetElem: Element | null = element; + + try { + + indicesPath.forEach((childrenIndex) => { + if (!targetElem?.hasChildNodes()) { + throw new Error('Element has no children elements'); + } + + targetElem = targetElem.children[childrenIndex]; + }); + + return targetElem; + + } catch (e) { + throw new Error('Error while traversing children'); + } +} + const locateColumnsWrapper = (savedItemsGrid: any | null) => { let columnsWrapperFound = false; let div = savedItemsGrid; @@ -30,8 +75,50 @@ const locateColumnsWrapper = (savedItemsGrid: any | null) => { return div; } +const getUserCollections = () => { + // Helper function + // @TODO: Recheck typing + /** + * Receive element wrapper and extract collection's relevant + * information + */ + const extractCollectionData = (collectionElem: HTMLSpanElement) => { + const divWithDataId = traverseChildren( + collectionElem, + COLLECTION_DATAID_DIV + ); + + const _id = divWithDataId.getAttribute(DATA_ID_ATTR); + const divWithTitle = traverseChildren( + divWithDataId, + COLLECTION_TITLE_DIV + ); + + if (!divWithTitle || !_id) { + throw new Error('Error while getting collection data.'); + } + + return { + _id, + name: divWithTitle.textContent || 'Couldn\'t find name' + } + }; + + // Variables + const collections: Collection[] = []; + + // Logic + const collectionElements: NodeList = document.querySelectorAll(COLLECTIONS_QUERY); + collectionElements.forEach((collectionElem) => { + const data = extractCollectionData(collectionElem as HTMLSpanElement); + collections.push(data); + }); + return collections; +} + export { SOCIAL_SHARE_BUTTON_IMG_SELECTOR, unNestElement, - locateColumnsWrapper + locateColumnsWrapper, + getUserCollections } \ No newline at end of file diff --git a/src/store/savedItemsStore.ts b/src/store/savedItemsStore.ts index 13767d9..0e64d03 100644 --- a/src/store/savedItemsStore.ts +++ b/src/store/savedItemsStore.ts @@ -16,8 +16,13 @@ export const useSavedItemsStore= defineStore('savedItemsStore', () => { savedItems.value.set(newSavedItem._id, newSavedItem); } + function clearItems() { + savedItems.value.clear(); + } + return { addSavedItem, + clearItems, itemsCount, savedItems, savedItemsAsArray diff --git a/src/store/toExportItemsStore.ts b/src/store/toExportItemsStore.ts index c2edd4d..4dbf38d 100644 --- a/src/store/toExportItemsStore.ts +++ b/src/store/toExportItemsStore.ts @@ -11,14 +11,17 @@ export const useToExportItemsStore= defineStore('toExportItemsStore', () => { itemsToExport.value, ([_, { _id, url, title }]) => ({ _id, url, title }) )); - - + function addItemToExport(savedItem: SavedItem | undefined) { if (savedItem && savedItem._id) { itemsToExport.value.set(savedItem._id, savedItem); } } + function clearItems() { + itemsToExport.value.clear(); + } + function removeItemFromExport(_id: string) { const entryExists = itemsToExport.value.has(_id); if (entryExists) { @@ -47,6 +50,7 @@ export const useToExportItemsStore= defineStore('toExportItemsStore', () => { return { addItemToExport, + clearItems, itemsCount, itemsToExport, removeItemFromExport, diff --git a/src/utils/index.ts b/src/utils/index.ts index cee88ad..491d603 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -8,7 +8,10 @@ const getUid = function(){ return Date.now().toString(36) + Math.random().toString(36).substr(2); } +const buildCollectionRedirectURL = (collectionId: string) => `https://www.google.com/save/list/${collectionId}`; + export { + buildCollectionRedirectURL, getCurrentTabID, getUid } \ No newline at end of file diff --git a/src/views/Home.vue b/src/views/Home.vue index c6be322..53e9abb 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -1,10 +1,14 @@