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

GC-11 Add logic to detect and list other collections #8

39 changes: 39 additions & 0 deletions src/components/CollectionsDropdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { Collection } from '@/models/Collection';
import { emit } from 'process';
import { computed, ref } from 'vue';

const emit = defineEmits(['collectionSelect']);

const props = defineProps<{
options: Map<string, Collection>;
}>();

const optionsAsArray = computed(() => {
return Array.from(props.options, ([_, { _id, name }]) => ({ _id, name }));
});

// span[aria-selected="true"] to get currently selected collection
const selectedCollection = ref('');

function selectCollection() {
const collection = props.options.get(selectedCollection.value);
emit('collectionSelect', collection);
}
</script>

<template>
<input
list="collectionsDatalist"
type="search"
v-model="selectedCollection"
@change="selectCollection"
/>
<datalist id="collectionsDatalist">
<option
v-for="collectionOpt in optionsAsArray"
:key="collectionOpt._id"
:value="collectionOpt.name"
></option>
</datalist>
</template>
4 changes: 3 additions & 1 deletion src/constants/ContentScriptMessageTypes.ts
Original file line number Diff line number Diff line change
@@ -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',
}
4 changes: 4 additions & 0 deletions src/models/Collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Collection {
_id: string,
name: string,
}
60 changes: 56 additions & 4 deletions src/services/content/content-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
})
});
}

/**
Expand Down Expand Up @@ -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
91 changes: 89 additions & 2 deletions src/services/content/utils/extractTools.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
Expand All @@ -30,8 +75,50 @@ const locateColumnsWrapper = (savedItemsGrid: any | null) => {
return div;
}

const getUserCollections = () => {
// Helper function
// @TODO: Recheck typing
/**
* Receive <span> 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
}
5 changes: 5 additions & 0 deletions src/store/savedItemsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions src/store/toExportItemsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -47,6 +50,7 @@ export const useToExportItemsStore= defineStore('toExportItemsStore', () => {

return {
addItemToExport,
clearItems,
itemsCount,
itemsToExport,
removeItemFromExport,
Expand Down
3 changes: 3 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading