Veche: v2.20.0 - 16-09-2023 - Make selector for tab labels more precise.
Closes #139.
Loop through tabpanels on open to reset scroll.
I do not think there is a way to make this work on iframes.
Closes #138.
Implement v R to view a posts reposts.
Closes #109.
Nouă: v2.20.1 - 17-09-2023 - Fix typo in comment.
Factor out some code from the LinkedIn class into a helper class.
While a bit of a nuisance, this does eliminate the current
no-use-before-define linter issues.
Issue #117.
JSDoc: Normalize how @type items are described.
@type/@param {TYPE} - Description.
If possible, fit on a single line using /** ... */.
If Emacs decides it wants to reflow onto two lines, then use
blockquotes.
Purposefully did not touch simple methods.
Move URL monitoring into the SPA class.
Closes #117.
Switch many Notifications methods to arrow functions.
Issue #97.
Bump version number.
- @@ -3,7 +3,7 @@
- // @namespace [email protected]
- // @match https://www.linkedin.com/*
- // @noframes
-// @version 2.20.0
- +// @version 2.20.1
- // @author Mike Castle
- // @description Minor enhancements to LinkedIn. Mostly just hotkeys.
- // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0-standalone.html
- @@ -64,32 +64,22 @@
- this.trace = trace;
- }
-
- /**
- * @type {string} - Name for this logger.
- */
- + /** @type {string} - Name for this logger. */
- get name() {
- return this._name;
- }
-
- /**
- * Whether logging is currently enabled.
- * @type {boolean}
- */
- + /** @type {boolean} - Whether logging is currently enabled. */
- get enabled() {
- return this._enabled;
- }
-
- /**
- * Indicates whether messages include a stack trace.
- * @type {boolean}
- */
- + /** @type {boolean} - Indicates whether messages include a stack trace. */
- get trace() {
- return this._trace;
- }
-
- /**
- * @param {boolean} val - Set inclusion of stack traces.
- */
- + /** @param {boolean} val - Set inclusion of stack traces. */
- set trace(val) {
- this._trace = Boolean(val);
- }
- @@ -584,7 +574,7 @@
- return this._container;
- }
-
- /** Map<string,TabEntry> */
- + /** @type {Map<string,TabEntry>} */
- get tabs() {
- const entries = new Map();
- for (const label of this._nav.querySelectorAll(':scope > label[data-tabbed-name]')) {
- @@ -933,10 +923,7 @@
- return this._dispatcher;
- }
-
- /**
- * Represents the current item.
- * @type {Element}
- */
- + /** @type {Element} - Represents the current item. */
- get item() {
- const me = 'get item';
- this._log.entered(me);
- @@ -960,9 +947,7 @@
- return item;
- }
-
- /**
- * @param {Element} val - Update the current item with val.
- */
- + /** @param {Element} val - Set the current item. */
- set item(val) {
- const me = 'set item';
- this._log.entered(me, val);
- @@ -1226,6 +1211,58 @@
- }
-
- /**
- + * This class exists solely to avoid some `no-use-before-define`
- + * linter issues.
- + */
- + class LinkedInGlobals {
- +
- + _navBarHeightPixels = 0;
- +
- + /** @type{number} - The height of the navbar in pixels. */
- + get navBarHeightPixels() {
- + return this._navBarHeightPixels;
- + }
- +
- + /** @param {number} val - Set height of the navbar in pixels. */
- + set navBarHeightPixels(val) {
- + this._navBarHeightPixels = val;
- + }
- +
- + /** @type {string} - The height of the navbar as CSS string. */
- + get navBarHeightCss() {
- + return `${this._navBarHeightPixels}px`;
- + }
- +
- + /**
- + * Scroll common sidebar into view and move focus to it.
- + */
- + focusOnSidebar = () => {
- + const sidebar = document.querySelector('div.scaffold-layout__sidebar');
- + if (sidebar) {
- + sidebar.style.scrollMarginTop = this.navBarHeightCss;
- + sidebar.scrollIntoView();
- + focusOnElement(sidebar);
- + }
- + }
- +
- + /**
- + * Scroll common aside (right-hand sidebar) into view and move
- + * focus to it.
- + */
- + focusOnAside = () => {
- + const aside = document.querySelector('aside.scaffold-layout__aside');
- + if (aside) {
- + aside.style.scrollMarginTop = this.navBarHeightCss;
- + aside.scrollIntoView();
- + focusOnElement(aside);
- + }
- + }
- +
- + }
- +
- + const linkedInGlobals = new LinkedInGlobals();
- +
- + /**
- * Base class for handling various views of a single-page
- * application.
- *
- @@ -1238,17 +1275,16 @@
- // The immediate following can be set in subclasses.
-
- /**
- * What pathname part of the URL this page should handle. The
- * special case of null is used by the {@link SPA} class to
- * represent global keys.
- * @type {string}
-
- + * @type {string} - What pathname part of the URL this page should
- + * handle. The special case of null is used by the {@link SPA}
- + * class to represent global keys.
- */
- _pathname;
-
- /**
- * CSS selector for capturing clicks on this page. If overridden,
- * then the class should also provide a _onClick() method.
- * @type {string}
-
- + * @type {string} - CSS selector for capturing clicks on this
- + * page. If overridden, then the class should also provide a
- + * _onClick() method.
- */
- _onClickSelector = null;
-
- @@ -1262,33 +1298,25 @@
- * form of `this.methodName`.
- */
-
- /**
- * List of {@link Shortcut}s to register automatically. The
- * function is bound to `this` before registering it with
- * VM.shortcut.
- * @type {Shortcut[]}
- */
- + /** @type {Shortcut[]} - List of {@link Shortcut}s to register automatically. */
- get _autoRegisteredKeys() { // eslint-disable-line class-methods-use-this
- return [];
- }
-
- // Private members.
-
- /**
- * @type {KeyboardService}
- */
- + /** @type {KeyboardService} */
- _keyboard = new VM.shortcut.KeyboardService();
-
- /**
- * Tracks which HTMLElement holds the `onclick` function.
- * @type {Element}
- + * @type {Element} - Tracks which HTMLElement holds the `onclick`
- + * function.
- */
- _onClickElement = null;
-
- /**
- * Magic for VM.shortcut. This disables keys when focus is on an
- * input type field or when viewing the help.
- * @type {IShortcutOptions}
-
- + * @type {IShortcutOptions} - Disables keys when focus is on an
- + * element or info view.
- +
- */
- static _navOption = {
- caseSensitive: true,
- @@ -1342,17 +1370,14 @@
- this._disableOnClick();
- }
-
- /**
- * Describes what the header should be.
- * @type {string}
- */
- + /** @type {string} - Describes what the header should be. */
- get helpHeader() {
- return this.constructor.name;
- }
-
- /**
- * The `key` and `desc` properties are important here.
- * @type {Shortcut[]}
-
- + * @type {Shortcut[]} - The `key` and `desc` properties are
- + * important here.
- */
- get helpContent() {
- return this._autoRegisteredKeys;
- @@ -1455,8 +1480,8 @@
- {seq: 'g p', desc: 'Go to Profile (aka, Me)', func: Global._gotoProfile},
- {seq: 'g b', desc: 'Go to Business', func: Global._gotoBusiness},
- {seq: 'g l', desc: 'Go to Learning', func: Global._gotoLearning},
- {seq: ',', desc: 'Focus on the left/top sidebar (not always present)', func: linkedIn.focusOnSidebar}, // eslint-disable-line no-use-before-define
- {seq: '.', desc: 'Focus on the right/bottom sidebar (not always present)', func: linkedIn.focusOnAside}, // eslint-disable-line no-use-before-define
- + {seq: ',', desc: 'Focus on the left/top sidebar (not always present)', func: linkedInGlobals.focusOnSidebar},
- + {seq: '.', desc: 'Focus on the right/bottom sidebar (not always present)', func: linkedInGlobals.focusOnAside},
- ];
- }
-
- @@ -1622,7 +1647,7 @@
- constructor() {
- super();
- this._postScroller = new Scroller(Feed._postsWhat, Feed._postsHow);
- this._postScroller.dispatcher.on('out-of-range', linkedIn.focusOnSidebar); // eslint-disable-line no-use-before-define
- + this._postScroller.dispatcher.on('out-of-range', linkedInGlobals.focusOnSidebar);
- this._postScroller.dispatcher.on('change', this._onPostChange);
- }
-
- @@ -1919,7 +1944,7 @@
- */
- static _gotoShare() {
- const share = document.querySelector('div.share-box-feed-entry__top-bar').parentElement;
- share.style.scrollMarginTop = linkedIn.navBarHeightCss; // eslint-disable-line no-use-before-define
- + share.style.scrollMarginTop = linkedInGlobals.navBarHeightCss;
- share.scrollIntoView();
- share.querySelector('button').focus();
- }
- @@ -2053,7 +2078,7 @@
- constructor() {
- super();
- this._sectionScroller = new Scroller(Jobs._sectionsWhat, Jobs._sectionsHow);
- this._sectionScroller.dispatcher.on('out-of-range', linkedIn.focusOnSidebar); // eslint-disable-line no-use-before-define
- + this._sectionScroller.dispatcher.on('out-of-range', linkedInGlobals.focusOnSidebar);
- this._sectionScroller.dispatcher.on('change', this._onChange);
- this._sectionsMO1 = new MutationObserver(this._mutationHandler);
- this._sectionsMO2 = new MutationObserver(this._mutationHandler);
- @@ -2425,7 +2450,7 @@
- constructor() {
- super();
- this._notificationScroller = new Scroller(Notifications._notificationsWhat, Notifications._notificationsHow);
- this._notificationScroller.dispatcher.on('out-of-range', linkedIn.focusOnSidebar); // eslint-disable-line no-use-before-define
- + this._notificationScroller.dispatcher.on('out-of-range', linkedInGlobals.focusOnSidebar);
- }
-
- /** @inheritdoc */
- @@ -2482,21 +2507,21 @@
- /**
- * Select the next notification.
- */
- _nextNotification() {
- + _nextNotification = () => {
- this._notifications.next();
- }
-
- /**
- * Select the previous notification.
- */
- _prevNotification() {
- + _prevNotification = () => {
- this._notifications.prev();
- }
-
- /**
- * Change browser focus to the current notification.
- */
- _focusBrowser() {
- + _focusBrowser = () => {
- this._notifications.show();
- focusOnElement(this._notifications.item);
- }
- @@ -2504,28 +2529,28 @@
- /**
- * Select the first notification.
- */
- _firstNotification() {
- + _firstNotification = () => {
- this._notifications.first();
- }
-
- /**
- * Select the last notification.
- */
- _lastNotification() {
- + _lastNotification = () => {
- this._notifications.last();
- }
-
- /**
- * Open the (⋯) menu for the current notification.
- */
- _openMeatballMenu() {
- + _openMeatballMenu = () => {
- clickElement(this._notifications.item, ['button[aria-label^="Settings menu"]']);
- }
-
- /**
- * Activate the current notification.
- */
- _activateNotification() {
- + _activateNotification = () => {
- const ONE_ITEM = 1;
- const notification = this._notifications.item;
- if (notification) {
- @@ -2556,7 +2581,7 @@
- /**
- * Toggles deletion of the current notification.
- */
- async _deleteNotification() {
- + _deleteNotification = async () => {
- const notification = this._notifications.item;
-
- /**
- @@ -2647,6 +2672,12 @@
- /** @type {SetupIssue[]} */
- _setupIssues = [];
-
- + /**
- + * @type {string} - CSS selector to monitor if self-managing URL
- + * changes. The selector must resolve to an element that, once it
- + * exists, will continue to exist for the lifetime of the SPA.
- + */
- + urlChangeMonitorSelector = 'body';
-
- /** @type {TabbedUI} */
- _ui = null;
- @@ -2757,6 +2788,8 @@
- /** LinkedIn specific information. */
- class LinkedIn extends SPADetails {
-
- + urlChangeMonitorSelector = 'div.authentication-outlet';
- +
- static _icon =
- '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">' +
- '<defs>' +
- @@ -2774,8 +2807,6 @@
- '<circle cx="18" cy="6" r="5" mask="url(#b)"/>' +
- '</svg>';
-
- _navBarHeightPixels = 0;
-
- _navBarScrollerFixups = [
- Feed._postsHow,
- Feed._commentsHow,
- @@ -2784,9 +2815,14 @@
- Notifications._notificationsHow,
- ];
-
- /** Create a LinkedIn instance. */
- constructor() {
- + /**
- + * Create a LinkedIn instance.
- + * @param {LinkedInGlobals} globals - Instance of a helper class to avoid
- + * circular dependencies.
- + */
- + constructor(globals) {
- super();
- + this._globals = globals;
- this.ready = this._waitUntilPageLoadedEnough();
- }
-
- @@ -2800,27 +2836,15 @@
- this._log.leaving(me);
- }
-
- /** @type{number} - The height of the navbar in pixels. */
- get navBarHeightPixels() {
- return this._navBarHeightPixels;
- }
-
- /** @type {string} - The height of the navbar as CSS string. */
- get navBarHeightCss() {
- return `${this._navBarHeightPixels}px`;
- }
-
- /**
- * The element.id used to identify the help pop-up.
- * @type {string}
- + * @type {string} - The element.id used to identify the help
- + * pop-up.
- */
- get helpId() {
- return this._helpId;
- }
-
- /**
- * @param {string} val - Set the value of the help element.id.
- */
- + /** @param {string} val - Set the value of the help element.id. */
- set helpId(val) {
- this._helpId = val;
- }
- @@ -2830,6 +2854,8 @@
- * @property {string} name - Name of the license.
- * @property {string} url - License URL.
- */
- +
- + /** @type{LicenseData} */
- get licenseData() {
- const me = 'licenseData';
- this._log.entered(me);
- @@ -2997,12 +3023,12 @@
- _setNavBarInfo() {
- const fudgeFactor = 4;
-
- this._navBarHeightPixels = this._navbar.clientHeight + fudgeFactor;
- + this._globals.navBarHeightPixels = this._navbar.clientHeight + fudgeFactor;
- // XXX: These {Scroller~How} items are static, so they need to
- // be configured after we figure out what the values should be.
- for (const how of this._navBarScrollerFixups) {
- how.topMarginPixels = this.navBarHeightPixels;
- how.topMarginCss = this.navBarHeightCss;
- + how.topMarginPixels = this._globals.navBarHeightPixels;
- + how.topMarginCss = this._globals.navBarHeightCss;
- how.bottomMarginCss = '3em';
- }
- }
- @@ -3063,31 +3089,6 @@
- return infoTab;
- }
-
- /**
- * Scroll common sidebar into view and move focus to it.
- */
- focusOnSidebar = () => {
- const sidebar = document.querySelector('div.scaffold-layout__sidebar');
- if (sidebar) {
- sidebar.style.scrollMarginTop = this.navBarHeightCss;
- sidebar.scrollIntoView();
- focusOnElement(sidebar);
- }
- }
-
- /**
- * Scroll common aside (right-hand sidebar) into view and move
- * focus to it.
- */
- focusOnAside = () => {
- const aside = document.querySelector('aside.scaffold-layout__aside');
- if (aside) {
- aside.style.scrollMarginTop = this.navBarHeightCss;
- aside.scrollIntoView();
- focusOnElement(aside);
- }
- }
-
- }
-
- /**
- @@ -3102,33 +3103,22 @@
-
- static _errorMarker = '---';
-
- /**
- * A special {Page} that handles global keys.
- * @type {Page}
- */
- + /** @type {Page} - A special {Page} that handles global keys. */
- _global = null;
-
- /**
- * Current {Page}.
- * @type {Page}
- */
- + /** @type {Page} - Current {Page}. */
- _page = null;
-
- /**
- * Collect of {Page} mapped by the pathname they support.
- * @type {Page}
- + * @type {Map<string,Page>} - {Page}s mapped by the pathname they
- + * support.
- */
- _pages = new Map();
-
- /**
- * The most recent element to receive focus.
- * @type {Element}
- */
- + /** @type {Element} - The most recent element to receive focus. */
- _lastInputElement = null;
-
- /**
- * @type {KeyboardService}
- */
- + /** @type {KeyboardService} */
- _helpKeyboard = null;
-
- /**
- @@ -3152,10 +3142,89 @@
- }
- document.addEventListener('focus', this._onFocus, true);
- document.addEventListener('urlchange', this._onUrlChange, true);
- + this._startUrlMonitor();
- this._details.done();
- }
-
- /**
- + * Tampermonkey was the first(?) userscript manager to provide
- + * events about URLs changing. Hence the need for `@grant
- + * window.onurlchange` in the UserScript header.
- + * @fires Event#urlchange
- + */
- + _startUserscriptManagerUrlMonitor() {
- + this._log.log('Using Userscript Manager provided URL monitor.');
- + window.addEventListener('urlchange', (info) => {
- + // The info that TM gives is not really an event. So we turn
- + // it into one and throw it again, this time onto `document`
- + // where something is listening for it.
- + const newUrl = new URL(info.url);
- + const evt = new CustomEvent('urlchange', {detail: {url: newUrl}});
- + document.dispatchEvent(evt);
- + });
- + }
- +
- + /**
- + * Install a long lived MutationObserver that watches
- + * {SPADetails.urlChangeMonitorSelector}. Whenever it is
- + * triggered, it will check to see if the current URL has changed,
- + * and if so, send an appropriate event.
- + * @fires Event#urlchange
- + */
- + async _startMutationObserverUrlMonitor() {
- + this._log.log('Using MutationObserver for monitoring URL changes.');
- +
- + const observeOptions = {childList: true, subtree: true};
- +
- + /**
- + * Watch for the initial {SPADetails.urlChangeMonitorSelector}
- + * to show up.
- + * @implements {Monitor}
- + * @returns {Continuation} - Indicate whether done monitoring.
- + */
- + const monitor = () => {
- + // The default selector is 'body', so we need to query
- + // 'document', not 'document.body'.
- + const element = document.querySelector(this._details.urlChangeMonitorSelector);
- + if (element) {
- + return {done: true, results: element};
- + }
- + return {done: false};
- + };
- + const what = {
- + name: 'SPA URL initializer observer',
- + base: document.body,
- + };
- + const how = {
- + observeOptions: observeOptions,
- + monitor: monitor,
- + };
- + const element = await otmot(what, how);
- + this._log.log('element exists:', element);
- +
- + this._oldUrl = new URL(window.location);
- + new MutationObserver(() => {
- + const newUrl = new URL(window.location);
- + if (this._oldUrl.href !== newUrl.href) {
- + const evt = new CustomEvent('urlchange', {detail: {url: newUrl}});
- + this._oldUrl = newUrl;
- + document.dispatchEvent(evt);
- + }
- + }).observe(element, observeOptions);
- + }
- +
- + /**
- + * Select which way to monitor the URL for changes and start it.
- + */
- + _startUrlMonitor() {
- + if (window.onurlchange === null) {
- + this._startUserscriptManagerUrlMonitor();
- + } else {
- + this._startMutationObserverUrlMonitor();
- + }
- + }
- +
- + /**
- * Set the context (used by VM.shortcut) to a specific value.
- * @param {string} context - The name of the context.
- * @param {object} state - What the value should be.
- @@ -3603,7 +3672,7 @@
-
- }
-
- const linkedIn = new LinkedIn();
- + const linkedIn = new LinkedIn(linkedInGlobals);
- linkedIn.ready.then(() => {
- log.log('proceeding...');
- const spa = new SPA(linkedIn);
- @@ -3615,67 +3684,6 @@
- spa.activate(window.location.pathname);
- });
-
- if (window.onurlchange === null) {
- // We are likely running on Tampermonkey, so use native support.
- log.log('Using window.onurlchange for monitoring URL updates.');
- window.addEventListener('urlchange', (info) => {
- // The info that TM gives is not really an event. So we turn it
- // into one and throw it again, this time onto `document` where
- // `spa` is listening for it.
- const newUrl = new URL(info.url);
- const evt = new CustomEvent('urlchange', {detail: {url: newUrl}});
- document.dispatchEvent(evt);
- });
- } else {
- log.log('Using MutationObserver for monitoring URL updates.');
-
- let oldUrl = new URL(window.location);
-
- /**
- * Constantly watch the web page. Whenever anything changes,
- * compare the current URL to the previous one, and if change,
- * send out an event.
- * @param {Element} element - Element to observe, ideally the
- * smallest thing that stays consistent throughout the lifetime of
- * the app.
- */
- function createUrlObserver(element) { // eslint-disable-line no-inner-declarations
- const observer = new MutationObserver(() => {
- const newUrl = new URL(window.location);
- if (oldUrl.href !== newUrl.href) {
- const evt = new CustomEvent('urlchange', {detail: {url: newUrl}});
- oldUrl = newUrl;
- document.dispatchEvent(evt);
- }
- });
- observer.observe(element, {childList: true, subtree: true});
- }
-
- /**
- * Watch for the initial `authentication-outlet` to show up, then
- * attach the URL observer to it.
- * @implements {Monitor}
- * @returns {Continuation} - Indicate whether done monitoring.
- */
- function authenticationOutletMonitor() { // eslint-disable-line no-inner-declarations
- const div = document.body.querySelector('div.authentication-outlet');
- if (div) {
- return {done: true, results: div};
- }
- return {done: false};
- }
-
- const authOutletWhat = {
- name: 'authOutletMonitor',
- base: document.body,
- };
- const autoOutletHow = {
- observeOptions: {childList: true, subtree: true},
- monitor: authenticationOutletMonitor,
- };
- otmot(authOutletWhat, autoOutletHow).then(el => createUrlObserver(el));
- }
-
- if (_runTests) {
- for (const test of _tests) {
- test();