From 5fd441986c3cec114bfcc27ab91849f24b17277e Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Wed, 3 Mar 2021 03:14:22 +0300
Subject: [PATCH 01/16] Fix file embed regexp
The previous version went into a lengthy loop that handed up the execution.
---
src/js/boot.js | 8 ++++----
src/js/wikitext.js | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/js/boot.js b/src/js/boot.js
index 2c232bb4..41fdf5f4 100644
--- a/src/js/boot.js
+++ b/src/js/boot.js
@@ -431,12 +431,12 @@ function initPatterns() {
const fileNamespacesPattern = fileNamespaces.map(anySpace).join('|');
cd.g.FILE_PREFIX_PATTERN = `(?:${fileNamespacesPattern}):`;
- // Actually, only text from "mini" format images should be captured, as in the standard format,
- // the text is not displayed. See "img_thumbnail" in
+ // Actually, only text from "mini" format images should be captured, because in the standard
+ // format the text is not displayed. See "img_thumbnail" in
// https://ru.wikipedia.org/w/api.php?action=query&meta=siteinfo&siprop=magicwords&formatversion=2.
// Unfortunately, that would add like 100ms to the server's response time.
- cd.g.FILE_LINK_REGEXP = new RegExp(
- `\\[\\[${cd.g.FILE_PREFIX_PATTERN}[^]+?(?:\\|[^]+?\\|((?:\\[\\[[^]+?\\]\\]|[^|])+?))?\\]\\]`,
+ cd.g.FILE_EMBED_REGEXP = new RegExp(
+ `\\[\\[${cd.g.FILE_PREFIX_PATTERN}[^\\]]+?(?:\\|[^\\]]+?\\|((?:\\[\\[[^\\]]+?\\]\\]|[^|\\]])+))?\\]\\]`,
'ig'
);
diff --git a/src/js/wikitext.js b/src/js/wikitext.js
index c5dbb9e9..aa0eb3c5 100644
--- a/src/js/wikitext.js
+++ b/src/js/wikitext.js
@@ -92,7 +92,7 @@ export function removeWikiMarkup(code) {
.replace(/(\[\[:?(?:[^|[\]<>\n:]+:)?([^|[\]<>\n]+)\|)(\]\])/g, '$1$2$3')
// Extract displayed text from file embeddings
- .replace(cd.g.FILE_LINK_REGEXP, '$1')
+ .replace(cd.g.FILE_EMBED_REGEXP, '$1')
// Extract displayed text from [[wikilinks]]
.replace(/\[\[:?(?:[^|[\]<>\n]+\|)?(.+?)\]\]/g, '$1')
From bcddd86eddfd0af99dd95aa8c3d711424b9f6735 Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Wed, 3 Mar 2021 22:08:27 +0300
Subject: [PATCH 02/16] w-ru: Fix regexp for page in whitelist
---
config/w-ru.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/config/w-ru.js b/config/w-ru.js
index d2a27607..66aacd52 100644
--- a/config/w-ru.js
+++ b/config/w-ru.js
@@ -76,7 +76,7 @@ export default {
/^Википедия:К объединению\//,
/^Википедия:К оценке источников/,
/^Википедия:К переименованию\//,
- /^Википедия:К посредничеству\//,
+ /^Википедия:К посредничеству/,
/^Википедия:К разделению\//,
/^Википедия:К удалению\//,
/^Википедия:К улучшению\//,
From 069e716e269feb7c380549c9540691cbda633dee Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Wed, 3 Mar 2021 22:12:58 +0300
Subject: [PATCH 03/16] Fix detecting interwikis in wikilinks autocomplete
---
src/js/boot.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/boot.js b/src/js/boot.js
index 41fdf5f4..0f797604 100644
--- a/src/js/boot.js
+++ b/src/js/boot.js
@@ -306,7 +306,7 @@ function initPatterns() {
const allNamespaces = Object.keys(namespaceIds).filter((ns) => ns);
const allNamespacesPattern = allNamespaces.join('|');
- cd.g.ALL_NAMESPACES_REGEXP = new RegExp(`(?:^|:)(?:${allNamespacesPattern}):`, 'i');
+ cd.g.ALL_NAMESPACES_REGEXP = new RegExp(`^(?:${allNamespacesPattern}):`, 'i');
const contribsPagePattern = anySpace(cd.g.CONTRIBS_PAGE);
cd.g.CAPTURE_USER_NAME_PATTERN = (
From dbfa6a4b88f565daf5c886da84861a32c4b6a446 Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Wed, 3 Mar 2021 22:17:56 +0300
Subject: [PATCH 04/16] Improve comment menu appearance
---
src/js/Comment.js | 4 ++++
src/less/commentLayers.less | 6 +-----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/js/Comment.js b/src/js/Comment.js
index 746840b5..50ad583c 100644
--- a/src/js/Comment.js
+++ b/src/js/Comment.js
@@ -1536,6 +1536,10 @@ export default class Comment extends CommentSkeleton {
text = text.replace(lineRegexp, (s) => s.replace(regexp, '\n\n'));
}
+ if (this.level !== 0) {
+ text = text.replace(/\n\n+/g, '\n\n');
+ }
+
return text.trim();
}
diff --git a/src/less/commentLayers.less b/src/less/commentLayers.less
index 54b6cf39..446ad60b 100644
--- a/src/less/commentLayers.less
+++ b/src/less/commentLayers.less
@@ -51,7 +51,7 @@
&-content {
display: inline-flex;
- padding: 0 0.5em 1px;
+ padding: 0 0.5em;
background-color: var(--cd-comment-focused-color);
flex-direction: row;
pointer-events: all;
@@ -61,10 +61,6 @@
.cd-button.oo-ui-buttonElement-frameless.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active {
background-color: transparent;
}
-
- .cd-commentButton.oo-ui-buttonElement > .oo-ui-buttonElement-button {
- border-top: 0;
- }
}
.ltr .cd-commentOverlay {
From da645ac3e3f641450f7e2fa86af2024aa4589c28 Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Fri, 5 Mar 2021 05:22:56 +0300
Subject: [PATCH 05/16] Fix reverse sorting of tags in tag autocomplete
---
src/js/Autocomplete.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/Autocomplete.js b/src/js/Autocomplete.js
index f86de7ac..c9dfdf1f 100644
--- a/src/js/Autocomplete.js
+++ b/src/js/Autocomplete.js
@@ -685,7 +685,7 @@ export default class Autocomplete {
config.default.sort((item1, item2) => {
const s1 = typeof item1 === 'string' ? item1 : item1[0];
const s2 = typeof item2 === 'string' ? item2 : item2[0];
- return s1 < s2;
+ return s1 > s2;
});
break;
}
From 13a382b2453bbc603baece71e68f963c4c2f69fb Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Fri, 5 Mar 2021 05:40:24 +0300
Subject: [PATCH 06/16] Streamline processPage() based on several categories of
page activeness
In the process, fix losing session when an old revision page is closed and empty session saved. Fix processing the "editondblclick" and "editsectiononrightclick" options (they didn't make any difference as the module they rely on in the code no longer exists). Don't suppress the behaviors brought by these options, as it was before.
---
src/js/boot.js | 9 +-
src/js/eventHandlers.js | 1 +
src/js/processPage.js | 214 +++++++++++++++++++++++-----------------
3 files changed, 130 insertions(+), 94 deletions(-)
diff --git a/src/js/boot.js b/src/js/boot.js
index 0f797604..900ff8bb 100644
--- a/src/js/boot.js
+++ b/src/js/boot.js
@@ -31,7 +31,12 @@ import {
transparentize,
unhideText,
} from './util';
-import { createWindowManager, rescueCommentFormsContent } from './modal';
+import {
+ createWindowManager,
+ editWatchedSections,
+ rescueCommentFormsContent,
+ settingsDialog,
+} from './modal';
import { getLocalOverridingSettings, getSettings, setSettings } from './options';
import { getUserInfo } from './apiWrappers';
import { initTimestampParsingTools, loadData } from './siteData';
@@ -245,6 +250,8 @@ function initGlobals() {
// Useful for testing
cd.g.processPageInBackground = updateChecker.processPage;
+ cd.g.editWatchedSections = editWatchedSections;
+ cd.g.settingsDialog = settingsDialog;
/* Some static methods for external use */
diff --git a/src/js/eventHandlers.js b/src/js/eventHandlers.js
index f065c235..482913c7 100644
--- a/src/js/eventHandlers.js
+++ b/src/js/eventHandlers.js
@@ -24,6 +24,7 @@ export function handleWindowResize() {
commentForm.adjustLabels();
});
pageNav.updateWidth();
+ handleScroll();
}
/**
diff --git a/src/js/processPage.js b/src/js/processPage.js
index ef84dbf2..198c6c3f 100644
--- a/src/js/processPage.js
+++ b/src/js/processPage.js
@@ -25,7 +25,7 @@ import {
handleScroll,
handleWindowResize,
} from './eventHandlers';
-import { confirmDialog, editWatchedSections, notFound, settingsDialog } from './modal';
+import { confirmDialog, notFound } from './modal';
import { generateCommentAnchor, parseCommentAnchor, resetCommentAnchors } from './timestamp';
import { getSettings, getVisits, getWatchedSections } from './options';
import { init, removeLoadingOverlay, restoreCommentForms, saveSession } from './boot';
@@ -365,12 +365,10 @@ function processSections(parser, watchedSectionsRequest) {
Section.adjust();
- if (watchedSectionsRequest) {
- watchedSectionsRequest.then(() => {
- Section.cleanUpWatched();
- toc.highlightWatchedSections();
- });
- }
+ watchedSectionsRequest.then(() => {
+ Section.cleanUpWatched();
+ toc.highlightWatchedSections();
+ });
/**
* The script has processed the sections.
@@ -844,62 +842,86 @@ export default async function processPage(keptData = {}) {
cd.debug.stopTimer('preparations');
cd.debug.startTimer('main code');
+ const articleId = mw.config.get('wgArticleId');
+
+ /*
+ To make things systematized, we have 4 possible assessments of page activeness as a talk page,
+ sorted by the scope of enabled features. Each level includes the next ones; 3 is the
+ intersection of 2.1 and 2.2.
+ 1. The page is a wikitext page.
+ 2. The page is likely a talk page. The "isLikelyTalkPage" variable is used to reflect that. We
+ may reevaluate page as being not a talk page if we don't find any comments on it and
+ several other criteria are not met. Likely talk pages are divided into two categories:
+ 2.1. The page is eligible to create comment forms on. (This includes 404 pages where the user
+ could create a section, but excludes archive pages and old revisions.) The
+ "isPageCommentable" variable is used to reflect this level.
+ 2.2. The page exists (not a 404 page). (This includes archive pages and old revisions, which
+ are not eligible to create comment forms on.) Such pages are parsed, the page navigation
+ block is added to them.
+ 3. The page is active. This means, it's not a 404 page, not an archive page, and not an old
+ revision. The "cd.g.isPageActive" property is true when the page is of this level. The
+ navigation panel is added to such pages, new comments are highlighted.
+
+ We need to be accurate regarding which functionality should be turned on on which level. We
+ should also make sure we only add this functionality once. The "isPageFirstParsed" variable is
+ used to reflect the run at which the page is parsed for the first time.
+ */
+
// This property isn't static: a 404 page doesn't have an ID and is considered inactive, but if
// the user adds a topic to it, it will become active and get an ID. At the same time (on a really
// rare occasion), an active page may become inactive if it becomes identified as an archive page.
cd.g.isPageActive = (
- mw.config.get('wgArticleId') &&
+ articleId &&
!cd.g.CURRENT_PAGE.isArchivePage() &&
mw.config.get('wgRevisionId') === mw.config.get('wgCurRevisionId')
);
- // For testing
- cd.g.editWatchedSections = editWatchedSections;
- cd.g.settingsDialog = settingsDialog;
-
let watchedSectionsRequest;
- if (mw.config.get('wgArticleId')) {
+ let visitsRequest;
+ let parser;
+ if (articleId) {
watchedSectionsRequest = getWatchedSections(true, keptData);
watchedSectionsRequest.catch((e) => {
console.warn('Couldn\'t load the settings from the server.', e);
});
- }
- let visitsRequest;
- if (cd.g.isPageActive) {
- visitsRequest = getVisits(true);
- }
-
- /**
- * The script is going to parse the page.
- *
- * @event beforeParse
- * @type {module:cd~convenientDiscussions}
- */
- mw.hook('convenientDiscussions.beforeParse').fire(cd);
-
- findSpecialElements();
+ if (cd.g.isPageActive) {
+ visitsRequest = getVisits(true);
+ }
- cd.debug.startTimer('process comments');
+ /**
+ * The script is going to parse the page.
+ *
+ * @event beforeParse
+ * @type {module:cd~convenientDiscussions}
+ */
+ mw.hook('convenientDiscussions.beforeParse').fire(cd);
+
+ findSpecialElements();
+
+ cd.debug.startTimer('process comments');
+
+ parser = new Parser({
+ CommentClass: Comment,
+ SectionClass: Section,
+ childElementsProperty: 'children',
+ document,
+ follows: (el1, el2) => Boolean(
+ el2.compareDocumentPosition(el1) & Node.DOCUMENT_POSITION_FOLLOWING
+ ),
+ getAllTextNodes,
+ getElementByClassName: (node, className) => node.querySelector(`.${className}`),
+ });
- const parser = new Parser({
- CommentClass: Comment,
- SectionClass: Section,
- childElementsProperty: 'children',
- document,
- follows: (el1, el2) => el1.compareDocumentPosition(el2) & Node.DOCUMENT_POSITION_PRECEDING,
- getAllTextNodes,
- getElementByClassName: (node, className) => node.querySelector(`.${className}`),
- });
+ try {
+ processComments(parser, feivData);
+ } catch (e) {
+ console.error(e);
+ }
- try {
- processComments(parser, feivData);
- } catch (e) {
- console.error(e);
+ cd.debug.stopTimer('process comments');
}
- cd.debug.stopTimer('process comments');
-
// Reevaluate if this is likely a talk page.
const isLikelyTalkPage = (
!cd.g.isFirstRun ||
@@ -908,14 +930,19 @@ export default async function processPage(keptData = {}) {
cd.g.PAGE_WHITELIST_REGEXP?.test(cd.g.CURRENT_PAGE.name)
);
+ const isPageCommentable = cd.g.isPageActive || !articleId;
+ const isPageFirstParsed = cd.g.isFirstRun || keptData.wasPageCreated;
+
if (isLikelyTalkPage) {
- cd.debug.startTimer('process sections');
+ if (articleId) {
+ cd.debug.startTimer('process sections');
- processSections(parser, watchedSectionsRequest);
+ processSections(parser, watchedSectionsRequest);
- cd.debug.stopTimer('process sections');
+ cd.debug.stopTimer('process sections');
+ }
- if (cd.g.isPageActive || !mw.config.get('wgArticleId')) {
+ if (isPageCommentable) {
addAddTopicButton();
connectToAddTopicButtons();
}
@@ -925,19 +952,21 @@ export default async function processPage(keptData = {}) {
// Operations that need reflow, such as getBoundingClientRect(), go in this section.
cd.debug.startTimer('final code and rendering');
- // Restore the initial viewport position in terms of visible elements which is how the user sees
- // it.
- if (feivData) {
- const y = window.scrollY + feivData.element.getBoundingClientRect().top - feivData.top;
- window.scrollTo(0, y);
- }
+ if (articleId) {
+ // Restore the initial viewport position in terms of visible elements which is how the user sees
+ // it.
+ if (feivData) {
+ const y = window.scrollY + feivData.element.getBoundingClientRect().top - feivData.top;
+ window.scrollTo(0, y);
+ }
- highlightOwnComments();
+ highlightOwnComments();
- processFragment(keptData);
+ processFragment(keptData);
+ }
if (cd.g.isPageActive) {
- if (cd.g.isFirstRun || keptData.wasPageCreated) {
+ if (isPageFirstParsed) {
navPanel.mount();
} else {
navPanel.reset();
@@ -954,32 +983,45 @@ export default async function processPage(keptData = {}) {
}
}
- if (cd.g.isPageActive || !mw.config.get('wgArticleId')) {
+ if (isPageCommentable) {
// This should be below the viewport position restoration and own comments highlighting as it
// may rely on the elements that are made invisible during the comment forms restoration. It
// should also be below the navPanel mount/reset methods as it runs
// navPanel.updateCommentFormButton() which depends on the navPanel being mounted.
restoreCommentForms();
- }
- if (cd.g.isFirstRun) {
- mw.hook('wikipage.content').add(highlightMentions);
+ if (isPageFirstParsed) {
+ const alwaysConfirmLeavingPage = (
+ mw.user.options.get('editondblclick') ||
+ mw.user.options.get('editsectiononrightclick')
+ );
+ addPreventUnloadCondition('commentForms', () => {
+ saveSession(true);
+ return (
+ mw.user.options.get('useeditwarning') &&
+ (
+ CommentForm.getLastActiveAltered() ||
+ (alwaysConfirmLeavingPage && cd.commentForms.length)
+ )
+ );
+ });
+ }
+ }
+ // keptData.wasPageCreated? articleId? но resize + adjustLabels ok на 404. resize
+ // orientationchange у document + window
+ if (isPageFirstParsed) {
pageNav.mount();
- // `mouseover` allows to capture the event when the cursor is not moving but ends up above the
- // element (for example, as a result of scrolling).
- $(document).on('mousemove mouseover', Comment.highlightFocused);
+ $(document)
+ // `mouseover` allows to capture the event when the cursor is not moving but ends up above
+ // the element (for example, as a result of scrolling).
+ .on('mousemove mouseover', Comment.highlightFocused)
+
+ .on('scroll', handleScroll);
$(window).on('resize orientationchange', handleWindowResize);
- addPreventUnloadCondition('commentForms', () => {
- saveSession(true);
- return (
- mw.user.options.get('useeditwarning') &&
- (CommentForm.getLastActiveAltered() || (alwaysConfirmLeavingPage && cd.commentForms.length))
- );
- });
- mw.hook('wikipage.content').add(connectToCommentLinks);
+ mw.hook('wikipage.content').add(highlightMentions, connectToCommentLinks);
mw.hook('convenientDiscussions.previewReady').add(connectToCommentLinks);
// Mutation observer doesn't follow all possible cases (for example, initiated with adding new
@@ -991,41 +1033,27 @@ export default async function processPage(keptData = {}) {
const observer = new MutationObserver((records) => {
const areLayers = records
.every((record) => /^cd-comment(Underlay|Overlay|Layers)/.test(record.target.className));
- if (areLayers) return;
- commentLayers.redrawIfNecessary();
+ if (!areLayers) {
+ commentLayers.redrawIfNecessary();
+ }
});
observer.observe(cd.g.$content.get(0), {
attributes: true,
childList: true,
subtree: true,
});
-
- $(document)
- .on('keydown', handleGlobalKeyDown)
- .on('scroll resize orientationchange', handleScroll);
} else {
pageNav.update();
}
- let alwaysConfirmLeavingPage = false;
- if (mw.user.options.get('editondblclick')) {
- mw.loader.using('mediawiki.action.view.dblClickEdit').then(() => {
- $('#ca-edit').off('click');
- alwaysConfirmLeavingPage = true;
- });
- }
-
- if (mw.user.options.get('editsectiononrightclick')) {
- mw.loader.using('mediawiki.action.view.rightClickEdit').then(() => {
- $('.mw-editsection a').off('click');
- alwaysConfirmLeavingPage = true;
- });
- }
-
if (cd.g.isFirstRun) {
confirmDesktopNotifications();
}
+ if (isPageCommentable) {
+ $(document).on('keydown', handleGlobalKeyDown);
+ }
+
/**
* The script has processed the page.
*
From 475241dddc0cd3a4520358174ee768964344851e Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Fri, 5 Mar 2021 05:52:52 +0300
Subject: [PATCH 07/16] Add copy link item even to unactionable sections
---
src/js/Section.js | 124 +++++++++++++++++++++++-----------------------
1 file changed, 63 insertions(+), 61 deletions(-)
diff --git a/src/js/Section.js b/src/js/Section.js
index 83e55855..4c47a59c 100644
--- a/src/js/Section.js
+++ b/src/js/Section.js
@@ -37,7 +37,7 @@ export default class Section extends SectionSkeleton {
*
* @param {Parser} parser A relevant instance of {@link module:Parser Parser}.
* @param {Element} headingElement
- * @param {Promise} [watchedSectionsRequest]
+ * @param {Promise} watchedSectionsRequest
* @throws {CdError}
*/
constructor(parser, headingElement, watchedSectionsRequest) {
@@ -108,9 +108,7 @@ export default class Section extends SectionSkeleton {
!cd.g.closedDiscussionElements.some((el) => el.contains(headingElement))
);
- if (this.isActionable) {
- this.extendSectionMenu(watchedSectionsRequest);
- }
+ this.extendSectionMenu(watchedSectionsRequest);
}
/**
@@ -296,68 +294,70 @@ export default class Section extends SectionSkeleton {
* @private
*/
extendSectionMenu(watchedSectionsRequest) {
- if (
- this.comments.length &&
- this.comments[0].isOpeningSection &&
- this.comments[0].openingSectionOfLevel === this.level &&
- (this.comments[0].isOwn || cd.settings.allowEditOthersComments) &&
- this.comments[0].isActionable
- ) {
- this.addMenuItem({
- label: cd.s('sm-editopeningcomment'),
- tooltip: cd.s('sm-editopeningcomment-tooltip'),
- func: () => {
- this.comments[0].edit();
- },
- class: 'cd-sectionLink-editOpeningComment',
- });
- }
+ if (this.isActionable) {
+ if (
+ this.comments.length &&
+ this.comments[0].isOpeningSection &&
+ this.comments[0].openingSectionOfLevel === this.level &&
+ (this.comments[0].isOwn || cd.settings.allowEditOthersComments) &&
+ this.comments[0].isActionable
+ ) {
+ this.addMenuItem({
+ label: cd.s('sm-editopeningcomment'),
+ tooltip: cd.s('sm-editopeningcomment-tooltip'),
+ func: () => {
+ this.comments[0].edit();
+ },
+ class: 'cd-sectionLink-editOpeningComment',
+ });
+ }
- if (this.level >= 2 && this.level !== 6) {
- this.addMenuItem({
- label: cd.s('sm-addsubsection'),
- tooltip: cd.s('sm-addsubsection-tooltip'),
- func: () => {
- this.addSubsection();
- },
- class: 'cd-sectionLink-addSubsection',
- });
- }
+ if (this.level >= 2 && this.level !== 6) {
+ this.addMenuItem({
+ label: cd.s('sm-addsubsection'),
+ tooltip: cd.s('sm-addsubsection-tooltip'),
+ func: () => {
+ this.addSubsection();
+ },
+ class: 'cd-sectionLink-addSubsection',
+ });
+ }
- if (this.level === 2) {
- this.addMenuItem({
- label: cd.s('sm-move'),
- tooltip: cd.s('sm-move-tooltip'),
- func: () => {
- this.move();
- },
- class: 'cd-sectionLink-moveSection',
- });
+ if (this.level === 2) {
+ this.addMenuItem({
+ label: cd.s('sm-move'),
+ tooltip: cd.s('sm-move-tooltip'),
+ func: () => {
+ this.move();
+ },
+ class: 'cd-sectionLink-moveSection',
+ });
+ }
}
- if (watchedSectionsRequest) {
- const finallyCallback = () => {
- if (this.headline) {
- // We put this instruction here to make it always appear after the "watch" item.
- this.addMenuItem({
- label: cd.s('sm-copylink'),
- // We need the event object to be passed to the function.
- func: this.copyLink.bind(this),
- class: 'cd-sectionLink-copyLink',
- tooltip: cd.s('sm-copylink-tooltip'),
- href: `${cd.g.CURRENT_PAGE.getUrl()}#${this.anchor}`,
- });
- }
-
- /**
- * Section menu has been extneded.
- *
- * @event sectionMenuExtended
- * @type {module:cd~convenientDiscussions}
- */
- mw.hook('convenientDiscussions.sectionMenuExtended').fire(this);
+ const addCopyLinkMenuItem = () => {
+ if (this.headline) {
+ // We put this instruction here to make it always appear after the "watch" item.
+ this.addMenuItem({
+ label: cd.s('sm-copylink'),
+ // We need the event object to be passed to the function.
+ func: this.copyLink.bind(this),
+ class: 'cd-sectionLink-copyLink',
+ tooltip: cd.s('sm-copylink-tooltip'),
+ href: `${cd.g.CURRENT_PAGE.getUrl()}#${this.anchor}`,
+ });
}
+ /**
+ * Section menu has been extneded.
+ *
+ * @event sectionMenuExtended
+ * @type {module:cd~convenientDiscussions}
+ */
+ mw.hook('convenientDiscussions.sectionMenuExtended').fire(this);
+ }
+
+ if (this.isActionable) {
watchedSectionsRequest
.then(
() => {
@@ -383,7 +383,9 @@ export default class Section extends SectionSkeleton {
},
() => {}
)
- .then(finallyCallback, finallyCallback);
+ .then(addCopyLinkMenuItem, addCopyLinkMenuItem);
+ } else {
+ addCopyLinkMenuItem();
}
}
From ca9584f4aca1140e797e6d61acb5523c39024501 Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Fri, 5 Mar 2021 07:38:52 +0300
Subject: [PATCH 08/16] Adjust refresh button tooltip text, streamline navPanel
module's code
Don't show "Hold Ctrl to mark all currently visible comments as read" text in the tooltip when there is no new comments visible on the page.
---
i18n/en.json | 2 +-
src/js/navPanel.js | 199 ++++++++++++++++++------------------------
src/less/pageNav.less | 6 +-
3 files changed, 88 insertions(+), 119 deletions(-)
diff --git a/i18n/en.json b/i18n/en.json
index e6829739..2afca7e2 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -211,7 +211,7 @@
"navpanel-newcomments-names": "$1 → $2",
"navpanel-newcomments-unknowndate": "unknown date",
"navpanel-newcomments-refresh": "Click to refresh the page",
- "navpanel-markasread": "Hold Ctrl to mark all comments as read",
+ "navpanel-markasread": "Hold Ctrl to mark all currently visible comments as read",
"toc-more": "…$1 more",
"toc-watched": "You watch this section",
diff --git a/src/js/navPanel.js b/src/js/navPanel.js
index b8357eb8..8683072d 100644
--- a/src/js/navPanel.js
+++ b/src/js/navPanel.js
@@ -7,78 +7,15 @@
import Comment from './Comment';
import cd from './cd';
import updateChecker from './updateChecker';
+import { focusInput, reorderArray } from './util';
import { reloadPage } from './boot';
import { removeWikiMarkup } from './wikitext';
-import { focusInput, reorderArray } from './util';
-let newCount;
let lastFirstUnseenCommentId;
-/**
- * Generate tooltip text displaying statistics of unseen or not yet displayed comments.
- *
- * @param {number} commentsCount
- * @param {Map} [commentsBySection]
- * @returns {?string}
- * @private
- */
-function generateRefreshButtonTooltipText(commentsCount, commentsBySection) {
- let tooltipText = null;
- if (commentsCount) {
- tooltipText = (
- cd.s('navpanel-newcomments-count', commentsCount) +
- ' ' +
- cd.s('navpanel-newcomments-refresh') +
- ' ' +
- cd.mws('parentheses', 'R') +
- '\n' +
- cd.s('navpanel-markasread')
- );
- const bullet = removeWikiMarkup(cd.s('bullet'));
- commentsBySection.forEach((comments, sectionOrAnchor) => {
- let headline;
- if (typeof sectionOrAnchor === 'string') {
- headline = comments[0].section.headline;
- } else if (sectionOrAnchor !== null) {
- headline = sectionOrAnchor.headline;
- }
- tooltipText += headline ? `\n\n${headline}` : '\n';
- comments.forEach((comment) => {
- tooltipText += `\n`;
- const names = comment.parent?.author && comment.level > 1 ?
- cd.s('navpanel-newcomments-names', comment.author.name, comment.parent.author.name) :
- comment.author.name;
- const date = comment.date ?
- cd.util.formatDate(comment.date) :
- cd.s('navpanel-newcomments-unknowndate');
- tooltipText += (
- bullet +
- ' ' +
- names +
- (cd.g.CONTENT_DIR === 'rtl' ? '\u200F' : '') +
- cd.mws('comma-separator') +
- date
- );
- });
- });
- } else {
- tooltipText = (
- cd.s('navpanel-refresh') +
- ' ' +
- cd.mws('parentheses', 'R') +
- '\n' +
- cd.s('navpanel-markasread')
- );
- }
-
- return tooltipText;
-}
-
-const navPanel = {
+export default {
/**
* Render the navigation panel. This is done when the page is first loaded or created.
- *
- * @memberof module:navPanel
*/
mount() {
/**
@@ -100,12 +37,13 @@ const navPanel = {
this.$refreshButton = $('
')
.addClass('cd-navPanel-button')
.attr('id', 'cd-navPanel-refreshButton')
- .attr('title', generateRefreshButtonTooltipText(0))
.on('click', (e) => {
this.refreshClick(e.ctrlKey);
})
.appendTo(this.$element);
+ this.updateRefreshButtonTooltip(0);
+
/**
* "Go to the previous new comment" button element.
*
@@ -185,7 +123,6 @@ const navPanel = {
* `convenientDiscussions.g.isPageActive` check.
*
* @returns {boolean}
- * @memberof module:navPanel
*/
isMounted() {
return Boolean(this.$element);
@@ -194,15 +131,12 @@ const navPanel = {
/**
* Reset the navigation panel to the initial state. This is done after page refreshes. (Comment
* forms are expected to be restored already.)
- *
- * @memberof module:navPanel
*/
reset() {
lastFirstUnseenCommentId = null;
- this.$refreshButton
- .empty()
- .attr('title', generateRefreshButtonTooltipText(0));
+ this.$refreshButton.empty();
+ this.updateRefreshButtonTooltip(0);
this.$previousButton.hide();
this.$nextButton.hide();
this.$firstUnseenButton.hide();
@@ -211,40 +145,20 @@ const navPanel = {
/**
* Count the new and unseen comments on the page, and update the navigation panel to reflect that.
- *
- * @memberof module:navPanel
*/
fill() {
- newCount = cd.comments.filter((comment) => comment.isNew).length;
- if (newCount) {
- this.$nextButton.show();
+ if (cd.comments.some((comment) => comment.isNew)) {
+ this.updateRefreshButtonTooltip(0);
this.$previousButton.show();
+ this.$nextButton.show();
this.updateFirstUnseenButton();
}
},
- /**
- * Update the state of the "Go to the first unseen comment" button.
- *
- * @memberof module:navPanel
- */
- updateFirstUnseenButton() {
- if (!navPanel.isMounted()) return;
-
- const unseenCount = cd.comments.filter((comment) => comment.isSeen === false).length;
- if (unseenCount) {
- this.$firstUnseenButton.show().text(unseenCount);
- } else {
- this.$firstUnseenButton.hide();
- }
- },
-
/**
* Perform routines at the refresh button click.
*
* @param {boolean} markAsRead Whether to mark all comments as read.
- *
- * @memberof module:navPanel
*/
refreshClick(markAsRead) {
// There was reload confirmation here, but after session restore was introduced, the
@@ -257,8 +171,6 @@ const navPanel = {
/**
* Scroll to the previous new comment.
- *
- * @memberof module:navPanel
*/
goToPreviousNewComment() {
if (cd.g.autoScrollInProgress) return;
@@ -279,8 +191,6 @@ const navPanel = {
/**
* Scroll to the next new comment.
- *
- * @memberof module:navPanel
*/
goToNextNewComment() {
if (cd.g.autoScrollInProgress) return;
@@ -301,8 +211,6 @@ const navPanel = {
/**
* Scroll to the first unseen comment.
- *
- * @memberof module:navPanel
*/
goToFirstUnseenComment() {
if (cd.g.autoScrollInProgress) return;
@@ -324,7 +232,6 @@ const navPanel = {
* true.
*
* @param {boolean} [first=false]
- * @memberof module:navPanel
*/
goToNextCommentForm(first = false) {
const commentForm = cd.commentForms
@@ -352,32 +259,96 @@ const navPanel = {
* @param {number} commentCount
* @param {Map} commentsBySection
* @param {boolean} areThereInteresting
- * @private
- * @memberof module:navPanel
*/
updateRefreshButton(commentCount, commentsBySection, areThereInteresting) {
- this.$refreshButton
- .empty()
- .attr('title', generateRefreshButtonTooltipText(commentCount, commentsBySection));
+ this.$refreshButton.empty();
+ this.updateRefreshButtonTooltip(commentCount, commentsBySection);
if (commentCount) {
$('
')
.text(`+${commentCount}`)
.appendTo(this.$refreshButton);
}
- if (areThereInteresting) {
- this.$refreshButton.addClass('cd-navPanel-refreshButton-interesting');
+ this.$refreshButton.toggleClass('cd-navPanel-refreshButton-interesting', areThereInteresting);
+ },
+
+ /**
+ * Update the tooltip of the refresh button, displaying statistics of comments not yet displayed if
+ * there are such.
+ *
+ * @param {number} commentsCount
+ * @param {Map} [commentsBySection]
+ * @private
+ */
+ updateRefreshButtonTooltip(commentsCount, commentsBySection) {
+ let tooltipText = null;
+ const areThereNew = cd.comments.some((comment) => comment.isNew);
+ if (commentsCount) {
+ tooltipText = (
+ cd.s('navpanel-newcomments-count', commentsCount) +
+ ' ' +
+ cd.s('navpanel-newcomments-refresh') +
+ ' ' +
+ cd.mws('parentheses', 'R')
+ );
+ if (areThereNew) {
+ tooltipText += '\n' + cd.s('navpanel-markasread');
+ }
+ const bullet = removeWikiMarkup(cd.s('bullet'));
+ commentsBySection.forEach((comments, sectionOrAnchor) => {
+ let headline;
+ if (typeof sectionOrAnchor === 'string') {
+ headline = comments[0].section.headline;
+ } else if (sectionOrAnchor !== null) {
+ headline = sectionOrAnchor.headline;
+ }
+ tooltipText += headline ? `\n\n${headline}` : '\n';
+ comments.forEach((comment) => {
+ tooltipText += `\n`;
+ const names = comment.parent?.author && comment.level > 1 ?
+ cd.s('navpanel-newcomments-names', comment.author.name, comment.parent.author.name) :
+ comment.author.name;
+ const date = comment.date ?
+ cd.util.formatDate(comment.date) :
+ cd.s('navpanel-newcomments-unknowndate');
+ tooltipText += (
+ bullet +
+ ' ' +
+ names +
+ (cd.g.CONTENT_DIR === 'rtl' ? '\u200F' : '') +
+ cd.mws('comma-separator') +
+ date
+ );
+ });
+ });
+ } else {
+ tooltipText = cd.s('navpanel-refresh') + ' ' + cd.mws('parentheses', 'R');
+ if (areThereNew) {
+ tooltipText += '\n' + cd.s('navpanel-markasread');
+ }
+ }
+
+ this.$refreshButton.attr('title', tooltipText);
+ },
+
+ /**
+ * Update the state of the "Go to the first unseen comment" button.
+ */
+ updateFirstUnseenButton() {
+ if (!this.isMounted()) return;
+
+ const unseenCount = cd.comments.filter((comment) => comment.isSeen === false).length;
+ if (unseenCount) {
+ this.$firstUnseenButton.show().text(unseenCount);
} else {
- this.$refreshButton.removeClass('cd-navPanel-refreshButton-interesting');
+ this.$firstUnseenButton.hide();
}
},
/**
* Update the "Go to the next comment form out of sight" button visibility.
- *
- * @memberof module:navPanel
*/
updateCommentFormButton() {
- if (cd.g.autoScrollInProgress || !navPanel.isMounted()) return;
+ if (cd.g.autoScrollInProgress || !this.isMounted()) return;
if (cd.commentForms.some((commentForm) => !commentForm.$element.cdIsInViewport(true))) {
this.$commentFormButton.show();
@@ -386,5 +357,3 @@ const navPanel = {
}
},
};
-
-export default navPanel;
diff --git a/src/less/pageNav.less b/src/less/pageNav.less
index 5cea59cd..836ae005 100644
--- a/src/less/pageNav.less
+++ b/src/less/pageNav.less
@@ -58,7 +58,7 @@
cursor: pointer;
}
-.sitedir-ltr.ltr .cd-pageNav-level {
+.sitedir-ltr.ltr .cd-pageNav-item-level {
&-1 {
margin-left: 1em;
}
@@ -80,8 +80,8 @@
}
}
-.sitedir-rtl.ltr .cd-pageNav-level,
-.rtl .cd-pageNav-level {
+.sitedir-rtl.ltr .cd-pageNav-item-level,
+.rtl .cd-pageNav-item-level {
&-1 {
margin-right: 1em;
}
From eeae5ea4d4d1bcaac63de4e6251aafb8b345a84c Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Fri, 5 Mar 2021 07:40:51 +0300
Subject: [PATCH 09/16] Remove "trim" parameter from
CommentForm#encapsulateSelection
Trim in methods calling it instead (currently only CommentForm#quote).
---
src/js/CommentForm.js | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/src/js/CommentForm.js b/src/js/CommentForm.js
index 1c97ae78..143e0016 100644
--- a/src/js/CommentForm.js
+++ b/src/js/CommentForm.js
@@ -3428,10 +3428,11 @@ export default class CommentForm {
* selection is empty.
*/
quote(allowEmptySelection = true) {
- const selection = isInputFocused() ?
+ let selection = isInputFocused() ?
document.activeElement.value
.substring(document.activeElement.selectionStart, document.activeElement.selectionEnd) :
window.getSelection().toString();
+ selection = selection.trim();
// With just "Q" pressed, empty selection doesn't count.
if (selection || allowEmptySelection) {
@@ -3452,7 +3453,6 @@ export default class CommentForm {
peri: cd.s('cf-quote-placeholder'),
post: cd.config.quoteFormatting[1],
selection,
- trim: true,
ownline: true,
});
}
@@ -3470,7 +3470,6 @@ export default class CommentForm {
* @param {string} [options.replace=false] If there is a selection, replace it with pre, peri,
* post instead of leaving it alone.
* @param {string} [options.selection] The selected text. Use if it is out of the input.
- * @param {boolean} [options.trim=false] Trim the selection.
* @param {boolean} [options.ownline=false] Put the inserted text on a line of its own.
*/
encapsulateSelection({
@@ -3479,7 +3478,6 @@ export default class CommentForm {
post = '',
selection,
replace = false,
- trim = false,
ownline = false,
}) {
const range = this.commentInput.getRange();
@@ -3503,9 +3501,6 @@ export default class CommentForm {
} else {
selection = selection || '';
}
- if (trim) {
- selection = selection.trim();
- }
// Wrap text moving the leading and trailing spaces to the sides of the resulting text.
const [leadingSpace] = selection.match(/^ */);
From 825b054cd61f6ef129c194c393e89db3cc0e97ef Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Fri, 5 Mar 2021 07:42:47 +0300
Subject: [PATCH 10/16] Don't run script on main pages anywhere
$('.flow-board-page').length check is redundant also.
---
src/js/app.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/js/app.js b/src/js/app.js
index e0fc313b..3855e78e 100644
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -532,11 +532,10 @@ async function app() {
*/
cd.isRunning = true;
- // Doesn't work in the mobile version, isn't needed on Structured Discussions pages.
if (
/(^|\.)m\./.test(location.hostname) ||
mw.config.get('wgPageContentModel') !== 'wikitext' ||
- $('.flow-board-page').length
+ mw.config.get('wgIsMainPage')
) {
return;
}
From 74584f3dfc0fe44e21dc14bf3671780818c20ca0 Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Fri, 5 Mar 2021 07:55:48 +0300
Subject: [PATCH 11/16] Don't insert heading markup if headline is not entered
This is done mostly for the ability to use substituted templates containing a heading in them. The other possible use case is when the "nosummary" parameter is missed in the preload configuration (it works to disable the headline input in this case), but the headline input is left alone by the user (as was here before the fix: https://ru.wikipedia.org/w/index.php?diff=112451882).
---
src/js/CommentForm.js | 37 ++++++++++++++++++++-----------------
1 file changed, 20 insertions(+), 17 deletions(-)
diff --git a/src/js/CommentForm.js b/src/js/CommentForm.js
index 143e0016..684c3157 100644
--- a/src/js/CommentForm.js
+++ b/src/js/CommentForm.js
@@ -659,7 +659,7 @@ export default class CommentForm {
.toLowerCase();
}
- this.editingSectionOpeningComment = this.mode === 'edit' && this.target.isOpeningSection;
+ this.isSectionOpeningCommentEdited = this.mode === 'edit' && this.target.isOpeningSection;
/**
* The main form element.
@@ -672,7 +672,7 @@ export default class CommentForm {
if (this.containerListType === 'ol') {
this.$element.addClass('cd-commentForm-inNumberedList');
}
- if (this.editingSectionOpeningComment) {
+ if (this.isSectionOpeningCommentEdited) {
this.$element.addClass('cd-commentForm-sectionOpeningComment');
}
if (this.mode === 'addSubsection') {
@@ -693,7 +693,7 @@ export default class CommentForm {
if (
(['addSection', 'addSubsection'].includes(this.mode) && !this.preloadConfig?.noHeadline) ||
- this.editingSectionOpeningComment
+ this.isSectionOpeningCommentEdited
) {
if (this.mode === 'addSubsection') {
this.headlineInputPurpose = cd.s('cf-headline-subsection', this.targetSection.headline);
@@ -2323,21 +2323,24 @@ export default class CommentForm {
// Add the headline
if (this.headlineInput) {
- let level;
- if (this.mode === 'addSection') {
- level = 2;
- } else if (this.mode === 'addSubsection') {
- level = this.target.level + 1;
- } else {
- level = this.target.inCode.headingLevel;
- }
- const equalSigns = '='.repeat(level);
+ const headline = this.headlineInput.getValue().trim();
+ if (headline) {
+ let level;
+ if (this.mode === 'addSection') {
+ level = 2;
+ } else if (this.mode === 'addSubsection') {
+ level = this.target.level + 1;
+ } else {
+ level = this.target.inCode.headingLevel;
+ }
+ const equalSigns = '='.repeat(level);
- if (this.editingSectionOpeningComment && /^\n/.test(this.target.inCode.code)) {
- // To have pretty diffs.
- code = '\n' + code;
+ if (this.isSectionOpeningCommentEdited && /^\n/.test(this.target.inCode.code)) {
+ // To have pretty diffs.
+ code = '\n' + code;
+ }
+ code = `${equalSigns} ${headline} ${equalSigns}\n${code}`;
}
- code = `${equalSigns} ${this.headlineInput.getValue().trim()} ${equalSigns}\n${code}`;
}
// Add the signature
@@ -3063,7 +3066,7 @@ export default class CommentForm {
if (this.watchSectionCheckbox) {
if (this.watchSectionCheckbox.isSelected()) {
const isHeadlineAltered = (
- this.editingSectionOpeningComment &&
+ this.isSectionOpeningCommentEdited &&
this.headlineInput.getValue() !== this.originalHeadline
);
if (this.mode === 'addSection' || this.mode === 'addSubsection' || isHeadlineAltered) {
From 1803c7da3072a259ddcef8d3553b4f77eb75b5f7 Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Fri, 5 Mar 2021 07:58:36 +0300
Subject: [PATCH 12/16] Improve configuration documentation
---
src/js/defaultConfig.js | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/src/js/defaultConfig.js b/src/js/defaultConfig.js
index 6e852337..1cfb2f54 100644
--- a/src/js/defaultConfig.js
+++ b/src/js/defaultConfig.js
@@ -55,9 +55,8 @@ export default {
/**
* Pages where the script should run. If `[]`, all pages in the {@link
* module:defaultConfig.customTalkNamespaces} namespaces will pass. If you add at least one value,
- * {@link module:defaultConfig.customTalkNamespaces} will not be used. In this case, you may
- * specify entire namespaces in this value, e.g., /^Wikipedia:/. The blacklist has priority over
- * the whitelist.
+ * {@link module:defaultConfig.customTalkNamespaces} will not be used. You may specify entire
+ * namespaces in this value, e.g., /^Wikipedia:/. The blacklist has priority over the whitelist.
*
* @type {RegExp[]}
* @default []
@@ -314,7 +313,7 @@ export default {
* https://en.wikipedia.org/wiki/Template:Smalldiv}. Used when the whole comment is wrapped in
* `` (with some exceptions when that could break the layout).
*
- * @type {?string}
+ * @type {string[]}
* @default []
*/
smallDivTemplates: [],
@@ -630,7 +629,11 @@ export default {
* module:defaultConfig.customBadCommentBeginnings}.
*
* The second parameter is a "context", i.e., a collection of classes, functions, and other
- * properties that perform the tasks we need in the current context (window or worker).
+ * properties that perform the tasks we need in the current context (window or worker). Examples
+ * are the name of the child elements property (the worker context uses `childElements` instead of
+ * `children`) and the document element. Contexts are predefined in the script like {@link
+ * https://github.com/jwbth/convenient-discussions/blob/c6b177bce7588949b46e0e8d52c5e0f4e76cb3ee/src/js/processPage.js#L885}
+ * this.
*
* @type {?Function}
* @kind function
From eed231cf0f0dda8297fee64e0487df0e4ca6f97e Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Fri, 5 Mar 2021 08:00:39 +0300
Subject: [PATCH 13/16] Apply minor improvements
---
buildConfigs.js | 1 +
config/w-ru.js | 4 ++--
src/js/Comment.js | 11 +++++++----
src/js/CommentForm.js | 12 +++---------
src/js/SectionStatic.js | 13 ++++++-------
src/js/boot.js | 5 +----
src/js/commentLinks.js | 2 +-
src/js/eventHandlers.js | 1 +
src/js/htmlparser2Extended.js | 2 +-
src/js/pageNav.js | 6 ++----
src/js/processPage.js | 5 ++---
src/js/siteData.js | 2 +-
src/js/staticGlobals.js | 4 ++--
src/js/updateChecker.js | 2 +-
14 files changed, 31 insertions(+), 39 deletions(-)
diff --git a/buildConfigs.js b/buildConfigs.js
index 2471ff9b..7ef67fc7 100644
--- a/buildConfigs.js
+++ b/buildConfigs.js
@@ -41,6 +41,7 @@ function getStrings() {
return lang !== 'en';
})
.map((lang) => mw.loader.getScript('https://commons.wikimedia.org/w/index.php?title=User:Jack_who_built_the_house/convenientDiscussions-i18n/' + lang + '.js&action=raw&ctype=text/javascript'));
+
// We assume it's OK to fall back to English if the translation is unavailable for any reason.
return Promise.all(requests).catch(function () {});
}
diff --git a/config/w-ru.js b/config/w-ru.js
index 66aacd52..db8d49f5 100644
--- a/config/w-ru.js
+++ b/config/w-ru.js
@@ -406,7 +406,7 @@ mw.hook('convenientDiscussions.pageReady').add(function () {
mw.notify($text, { autoHide: false } );
mw.cookie.set('cd-hlmConflict', '1', {
path: '/',
- expires: cd.g.SECONDS_IN_A_DAY * 30,
+ expires: cd.g.SECONDS_IN_DAY * 30,
});
}
@@ -415,7 +415,7 @@ mw.hook('convenientDiscussions.pageReady').add(function () {
mw.notify($text, { autoHide: false });
mw.cookie.set('cd-ptaConflict', '1', {
path: '/',
- expires: cd.g.SECONDS_IN_A_DAY * 30,
+ expires: cd.g.SECONDS_IN_DAY * 30,
});
}
diff --git a/src/js/Comment.js b/src/js/Comment.js
index 50ad583c..30182f3c 100644
--- a/src/js/Comment.js
+++ b/src/js/Comment.js
@@ -52,7 +52,7 @@ function cleanUpThanks(data) {
Object.keys(newData).forEach((key) => {
if (
!newData[key].thankUnixTime ||
- newData[key].thankUnixTime < Date.now() - 60 * cd.g.SECONDS_IN_A_DAY * 1000
+ newData[key].thankUnixTime < Date.now() - 60 * cd.g.SECONDS_IN_DAY * 1000
) {
delete newData[key];
}
@@ -531,6 +531,8 @@ export default class Comment extends CommentSkeleton {
options.update = true;
}
+ // FIXME: it is possible that a floating element that is on above in the DOM is below spacially.
+ // In this case, rectTop and rectBottom will be swapped.
options.rectTop = this.highlightables[0].getBoundingClientRect();
options.rectBottom = this.elements.length === 1 ?
options.rectTop :
@@ -1115,8 +1117,8 @@ export default class Comment extends CommentSkeleton {
}
// Search for the edit in the range of 2 minutes before to 2 minutes later.
- const rvstart = new Date(this.date.getTime() - cd.g.MILLISECONDS_IN_A_MINUTE * 2).toISOString();
- const rvend = new Date(this.date.getTime() + cd.g.MILLISECONDS_IN_A_MINUTE * 2).toISOString();
+ const rvstart = new Date(this.date.getTime() - cd.g.MILLISECONDS_IN_MINUTE * 2).toISOString();
+ const rvend = new Date(this.date.getTime() + cd.g.MILLISECONDS_IN_MINUTE * 2).toISOString();
const revisions = await this.getSourcePage().getArchivedPage().getRevisions({
rvprop: ['ids', 'comment', 'parsedcomment', 'timestamp'],
rvdir: 'newer',
@@ -1511,6 +1513,7 @@ export default class Comment extends CommentSkeleton {
.replace(/^(?![:*# ]).*
.*$/gmi, (s) => (
s.replace(/
\n? */gi, () => '\n')
))
+
// Remove indentation characters
.replace(/\n([:*#]*[:*])([ \t]*)/g, (s, chars, spacing) => {
const newChars = chars.slice(indentationChars.length);
@@ -1932,7 +1935,7 @@ export default class Comment extends CommentSkeleton {
start: /^/,
end: /<\/small>[ \u00A0\t]*$/,
}];
- if (cd.config.smallDivTemplates?.[0]) {
+ if (cd.config.smallDivTemplates.length) {
smallWrappers.push({
start: new RegExp(
`^(?:\\{\\{(${cd.config.smallDivTemplates.join('|')})\\|(?: *1 *= *|(?![^{]*=)))`,
diff --git a/src/js/CommentForm.js b/src/js/CommentForm.js
index 684c3157..f61d5795 100644
--- a/src/js/CommentForm.js
+++ b/src/js/CommentForm.js
@@ -666,9 +666,7 @@ export default class CommentForm {
*
* @type {JQuery}
*/
- this.$element = $('')
- .addClass('cd-commentForm')
- .addClass(`cd-commentForm-${this.mode}`);
+ this.$element = $('
').addClass(`cd-commentForm cd-commentForm-${this.mode}`);
if (this.containerListType === 'ol') {
this.$element.addClass('cd-commentForm-inNumberedList');
}
@@ -2367,7 +2365,7 @@ export default class CommentForm {
} else {
before = '';
}
- if (cd.config.smallDivTemplates?.[0] && !/^[:*#;]/m.test(code)) {
+ if (cd.config.smallDivTemplates.length && !/^[:*#;]/m.test(code)) {
const adjustedCode = code.replace(/\|/g, '{{!}}') + signature;
code = `{{${cd.config.smallDivTemplates[0]}|1=${adjustedCode}}}`;
} else {
@@ -2735,11 +2733,7 @@ export default class CommentForm {
.html(html)
.prepend($label)
.cdAddCloseButton();
- if (imitateList) {
- this.$previewArea.addClass('cd-previewArea-indentedComment');
- } else {
- this.$previewArea.removeClass('cd-previewArea-indentedComment');
- }
+ this.$previewArea.toggleClass('cd-previewArea-indentedComment', imitateList);
/**
* A comment preview has been rendered.
diff --git a/src/js/SectionStatic.js b/src/js/SectionStatic.js
index 98cc4ba7..3049251a 100644
--- a/src/js/SectionStatic.js
+++ b/src/js/SectionStatic.js
@@ -313,19 +313,18 @@ export default {
reloadPage({ commentAnchor });
});
- let $lastElement;
+ let $last;
if (section.$addSubsectionButtonContainer && !section.getChildren().length) {
- $lastElement = section.$addSubsectionButtonContainer;
+ $last = section.$addSubsectionButtonContainer;
} else if (section.$replyButton) {
- $lastElement = section.$replyButton.closest('ul, ol, dl');
+ $last = section.$replyButton.closest('ul, ol, dl');
} else {
- $lastElement = section.$elements[section.$elements.length - 1];
+ $last = section.$elements.last();
}
$('
')
- .addClass('cd-refreshButtonContainer')
- .addClass('cd-sectionButtonContainer')
+ .addClass('cd-refreshButtonContainer cd-sectionButtonContainer')
.append(button.$element)
- .insertAfter($lastElement);
+ .insertAfter($last);
});
},
diff --git a/src/js/boot.js b/src/js/boot.js
index 900ff8bb..5cf95b2c 100644
--- a/src/js/boot.js
+++ b/src/js/boot.js
@@ -830,7 +830,7 @@ function cleanUpSessions(data) {
if (
!newData[key].commentForms?.length ||
- newData[key].saveUnixTime < Date.now() - 60 * cd.g.SECONDS_IN_A_DAY * 1000
+ newData[key].saveUnixTime < Date.now() - 60 * cd.g.SECONDS_IN_DAY * 1000
) {
delete newData[key];
}
@@ -1051,9 +1051,6 @@ export function restoreCommentForms() {
}
}
saveSession();
-
- // Navigation panel doesn't appear on non-existent pages, but sessions are saved and restored on
- // them.
navPanel.updateCommentFormButton();
}
diff --git a/src/js/commentLinks.js b/src/js/commentLinks.js
index f1dc6eca..5b6c2305 100644
--- a/src/js/commentLinks.js
+++ b/src/js/commentLinks.js
@@ -616,7 +616,7 @@ async function processDiff() {
// minutes for the watchlist time and not higher than 1 minute for the script-generated
// time.
for (let gap = 1; !comment && gap <= 5; gap++) {
- const dateToFind = new Date(date.getTime() - cd.g.MILLISECONDS_IN_A_MINUTE * gap);
+ const dateToFind = new Date(date.getTime() - cd.g.MILLISECONDS_IN_MINUTE * gap);
commentAnchorToCheck = generateCommentAnchor(dateToFind, author);
comment = Comment.getByAnchor(commentAnchorToCheck);
}
diff --git a/src/js/eventHandlers.js b/src/js/eventHandlers.js
index 482913c7..81941d07 100644
--- a/src/js/eventHandlers.js
+++ b/src/js/eventHandlers.js
@@ -66,6 +66,7 @@ export function handleGlobalKeyDown(e) {
if (
// Ctrl+Alt+Q
keyCombination(e, 81, ['ctrl', 'alt']) ||
+
// Q
(keyCombination(e, 81) && !isInputFocused())
) {
diff --git a/src/js/htmlparser2Extended.js b/src/js/htmlparser2Extended.js
index 238fa9cb..fd621a61 100644
--- a/src/js/htmlparser2Extended.js
+++ b/src/js/htmlparser2Extended.js
@@ -4,8 +4,8 @@ import cd from './cd';
import { decodeHtmlEntities } from './wikitext';
self.Node = {
- TEXT_NODE: 3,
ELEMENT_NODE: 1,
+ TEXT_NODE: 3,
};
/**
diff --git a/src/js/pageNav.js b/src/js/pageNav.js
index 1f420521..cd095177 100644
--- a/src/js/pageNav.js
+++ b/src/js/pageNav.js
@@ -25,8 +25,7 @@ export default {
.appendTo(document.body);
this.$bottomElement = $('
')
.attr('id', 'cd-pageNav-bottom')
- .addClass('cd-pageNav')
- .addClass('cd-pageNav-list')
+ .addClass('cd-pageNav cd-pageNav-list')
.appendTo(document.body);
this.updateWidth();
@@ -179,8 +178,7 @@ export default {
) ?
$sectionWithBackLink :
$('- ')
- .addClass('cd-pageNav-item')
- .addClass(`cd-pageNav-level-${level}`)
+ .addClass(`cd-pageNav-item cd-pageNav-item-level-${level}`)
.data('section', sectionInTree)
.text(sectionInTree.headline)
.on('click', () => {
diff --git a/src/js/processPage.js b/src/js/processPage.js
index 198c6c3f..dc375c17 100644
--- a/src/js/processPage.js
+++ b/src/js/processPage.js
@@ -395,8 +395,7 @@ function addAddTopicButton() {
CommentForm.createAddSectionForm();
});
cd.g.$addSectionButtonContainer = $('
')
- .addClass('cd-addTopicButtonContainer')
- .addClass('cd-sectionButtonContainer')
+ .addClass('cd-addTopicButtonContainer cd-sectionButtonContainer')
.append(cd.g.addSectionButton.$element)
.appendTo(cd.g.rootElement);
}
@@ -572,7 +571,7 @@ async function processFragment(keptData) {
// There can be a time difference between the time we know (taken from the watchlist) and the
// time on the page. We take it to be not higher than 5 minutes for the watchlist.
for (let gap = 1; !comment && gap <= 5; gap++) {
- const dateToFind = new Date(date.getTime() - cd.g.MILLISECONDS_IN_A_MINUTE * gap);
+ const dateToFind = new Date(date.getTime() - cd.g.MILLISECONDS_IN_MINUTE * gap);
commentAnchorToCheck = generateCommentAnchor(dateToFind, author);
comment = Comment.getByAnchor(commentAnchorToCheck);
}
diff --git a/src/js/siteData.js b/src/js/siteData.js
index 46314d6f..9581fa2a 100644
--- a/src/js/siteData.js
+++ b/src/js/siteData.js
@@ -914,7 +914,7 @@ function setLocalTimestampParser() {
return new Date(
Date.UTC(year, monthIdx, day, hour, minute) -
- timezoneOffset * cd.g.MILLISECONDS_IN_A_MINUTE
+ timezoneOffset * cd.g.MILLISECONDS_IN_MINUTE
);
};
diff --git a/src/js/staticGlobals.js b/src/js/staticGlobals.js
index b9cd8197..aa529d77 100644
--- a/src/js/staticGlobals.js
+++ b/src/js/staticGlobals.js
@@ -99,7 +99,7 @@ export default {
* @type {number}
* @memberof module:cd~convenientDiscussions.g
*/
- SECONDS_IN_A_DAY: 60 * 60 * 24,
+ SECONDS_IN_DAY: 60 * 60 * 24,
/**
* Number of milliseconds in a minute.
@@ -107,7 +107,7 @@ export default {
* @type {number}
* @memberof module:cd~convenientDiscussions.g
*/
- MILLISECONDS_IN_A_MINUTE: 1000 * 60,
+ MILLISECONDS_IN_MINUTE: 1000 * 60,
/**
* Popular elements that don't have the `display: inline` property in the default browser styles.
diff --git a/src/js/updateChecker.js b/src/js/updateChecker.js
index 223bedf9..86bc2660 100644
--- a/src/js/updateChecker.js
+++ b/src/js/updateChecker.js
@@ -167,7 +167,7 @@ function cleanUpSeenRenderedEdits(data) {
const newData = Object.assign({}, data);
Object.keys(newData).forEach((key) => {
const seenUnixTime = Object.keys(newData[key])[0]?.seenUnixTime;
- if (!seenUnixTime || seenUnixTime < Date.now() - 60 * cd.g.SECONDS_IN_A_DAY * 1000) {
+ if (!seenUnixTime || seenUnixTime < Date.now() - 60 * cd.g.SECONDS_IN_DAY * 1000) {
delete newData[key];
}
});
From cab1588722f5343e183515793c0fabece05f9d2e Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Fri, 5 Mar 2021 21:15:36 +0300
Subject: [PATCH 14/16] Fix data request not being passed to processPage()
Rename siteData.loadData to loadSiteData.
---
src/js/app.js | 18 +++++++++---------
src/js/boot.js | 9 ++++-----
src/js/commentLinks.js | 17 ++++++++---------
src/js/processPage.js | 17 +++++++++--------
src/js/siteData.js | 2 +-
5 files changed, 31 insertions(+), 32 deletions(-)
diff --git a/src/js/app.js b/src/js/app.js
index 3855e78e..1fc6decc 100644
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -21,7 +21,7 @@ import {
setLoadingOverlay,
setTalkPageCssVariables,
} from './boot';
-import { loadData } from './siteData';
+import { loadSiteData } from './siteData';
import { setVisits } from './options';
let config;
@@ -151,7 +151,7 @@ function addCommentLinksToSpecialSearch() {
if (commentAnchor) {
mw.loader.using('mediawiki.api').then(
async () => {
- await loadData();
+ await loadSiteData();
$('.mw-search-result-heading').each((i, el) => {
const $a = $('
')
.attr(
@@ -316,9 +316,9 @@ function go() {
// the background, this request is made and the execution stops at mw.loader.using, which
// results in overriding the renewed visits setting of one tab by another tab (the visits are
// loaded by one tab, then another tab, then written by one tab, then by another tab).
- let dataRequest;
+ let siteDataRequest;
if (mw.loader.getState('mediawiki.api') === 'ready') {
- dataRequest = loadData();
+ siteDataRequest = loadSiteData();
}
let modulesRequest = mw.loader.using([
@@ -342,10 +342,10 @@ function go() {
'user.options',
]);
- Promise.all([modulesRequest, dataRequest].filter(defined)).then(
+ Promise.all([modulesRequest, siteDataRequest].filter(defined)).then(
async () => {
try {
- await processPage({ dataRequest });
+ await processPage(undefined, siteDataRequest);
} catch (e) {
mw.notify(cd.s('error-processpage'), { type: 'error' });
removeLoadingOverlay();
@@ -411,9 +411,9 @@ function go() {
) {
// Make some requests in advance if the API module is ready in order not to make 2 requests
// sequentially.
- let dataRequest;
+ let siteDataRequest;
if (mw.loader.getState('mediawiki.api') === 'ready') {
- dataRequest = loadData();
+ siteDataRequest = loadSiteData();
if (!cd.g.IS_DIFF_PAGE) {
getUserInfo(true).catch((e) => {
console.warn(e);
@@ -435,7 +435,7 @@ function go() {
'oojs-ui.styles.icons-alerts',
]).then(
() => {
- commentLinks({ dataRequest });
+ commentLinks(siteDataRequest);
// See the comment above: "Additions of CSS...".
require('../less/global.less');
diff --git a/src/js/boot.js b/src/js/boot.js
index 5cf95b2c..f93ac7f8 100644
--- a/src/js/boot.js
+++ b/src/js/boot.js
@@ -39,7 +39,7 @@ import {
} from './modal';
import { getLocalOverridingSettings, getSettings, setSettings } from './options';
import { getUserInfo } from './apiWrappers';
-import { initTimestampParsingTools, loadData } from './siteData';
+import { initTimestampParsingTools, loadSiteData } from './siteData';
let notificationsData = [];
let isPageBeingReloaded = false;
@@ -608,14 +608,13 @@ function initOouiAndElementPrototypes() {
* Create various global objects' (`convenientDiscussions`, `$`) properties and methods. Executed at
* the first run.
*
- * @param {object} [data] Data passed from the main module.
- * @param {Promise} [data.messagesRequest] Promise returned by {@link module:siteData.loadData}.
+ * @param {Promise} siteDataRequest Promise returned by {@link module:siteData.loadSiteData}.
*/
-export async function init({ messagesRequest }) {
+export async function init(siteDataRequest) {
cd.g.api = cd.g.api || new mw.Api();
cd.g.worker = new Worker();
- await (messagesRequest || loadData());
+ await (siteDataRequest || loadSiteData());
initGlobals();
await initSettings();
initTimestampParsingTools();
diff --git a/src/js/commentLinks.js b/src/js/commentLinks.js
index 5b6c2305..3cdd0ff9 100644
--- a/src/js/commentLinks.js
+++ b/src/js/commentLinks.js
@@ -21,7 +21,7 @@ import { editWatchedSections, settingsDialog } from './modal';
import { generateCommentAnchor, parseTimestamp } from './timestamp';
import { getWatchedSections } from './options';
import { initSettings } from './boot';
-import { initTimestampParsingTools, loadData } from './siteData';
+import { initTimestampParsingTools, loadSiteData } from './siteData';
let serverName;
let colon;
@@ -39,11 +39,10 @@ let processDiffFirstRun = true;
/**
* Prepare variables.
*
- * @param {object} [data] Data passed from the main module.
- * @param {Promise} [data.dataRequest] Promise returned by {@link module:siteData.loadData}.
+ * @param {Promise} [siteDataRequest] Promise returned by {@link module:siteData.loadSiteData}.
* @private
*/
-async function prepare({ dataRequest }) {
+async function prepare(siteDataRequest) {
cd.g.api = cd.g.api || new mw.Api();
// Loading the watched sections is not critical, as opposed to messages, so we catch the possible
@@ -51,10 +50,10 @@ async function prepare({ dataRequest }) {
const watchedSectionsRequest = getWatchedSections(true).catch((e) => {
console.warn('Couldn\'t load the settings from the server.', e);
});
- dataRequest = dataRequest || loadData();
+ siteDataRequest = siteDataRequest || loadSiteData();
try {
- await Promise.all([watchedSectionsRequest, dataRequest]);
+ await Promise.all([watchedSectionsRequest, siteDataRequest]);
} catch (e) {
throw ['Couldn\'t load the messages required for the script.', e];
}
@@ -697,11 +696,11 @@ async function addCommentLinks($content) {
/**
* The entry function for the comment links adding mechanism.
*
- * @param {object} [data] Data passed from the main module.
+ * @param {Promise} [siteDataRequest] Promise returned by {@link module:siteData.loadSiteData}.
*/
-export default async function commentLinks({ dataRequest }) {
+export default async function commentLinks(siteDataRequest) {
try {
- await prepare({ dataRequest });
+ await prepare(siteDataRequest);
} catch (e) {
console.warn(...e);
return;
diff --git a/src/js/processPage.js b/src/js/processPage.js
index dc375c17..19f2db7c 100644
--- a/src/js/processPage.js
+++ b/src/js/processPage.js
@@ -36,11 +36,10 @@ import { setSettings, setVisits } from './options';
* Prepare (initialize or reset) various properties, mostly global ones. DOM preparations related to
* comment layers are also made here.
*
- * @param {object} [data] Data passed from the main module.
- * @param {Promise} [data.messagesRequest] Promise returned by {@link module:siteData.loadData}.
+ * @param {Promise} siteDataRequest Promise returned by {@link module:siteData.loadSiteData}.
* @private
*/
-async function prepare({ messagesRequest }) {
+async function prepare(siteDataRequest) {
cd.g.$root = cd.g.$content.children('.mw-parser-output');
if (!cd.g.$root.length) {
cd.g.$root = cd.g.$content;
@@ -68,7 +67,9 @@ async function prepare({ messagesRequest }) {
cd.sections = [];
if (cd.g.isFirstRun) {
- await init({ messagesRequest });
+ cd.debug.startTimer('init')
+ await init(siteDataRequest);
+ cd.debug.stopTimer('init')
} else {
resetCommentAnchors();
commentLayers.reset();
@@ -815,23 +816,23 @@ function debugLog() {
* @property {string} [justUnwatchedSection] Section just unwatched so that there could be not
* enough time for it to be saved to the server.
* @property {boolean} [didSubmitCommentForm] Did the user just submitted a comment form.
- * @property {Promise} [messagesRequest] Promise returned by {@link module:siteData.loadData}.
*/
/**
* Process the current web page.
*
- * @param {KeptData} [keptData={}] Data passed from the previous page state or the main module.
+ * @param {KeptData} [keptData={}] Data passed from the previous page state.
+ * @param {Promise} [siteDataRequest] Promise returned by {@link module:siteData.loadSiteData}.
* @fires beforeParse
* @fires commentsReady
* @fires sectionsReady
* @fires pageReady
*/
-export default async function processPage(keptData = {}) {
+export default async function processPage(keptData = {}, siteDataRequest) {
cd.debug.stopTimer(cd.g.isFirstRun ? 'loading data' : 'laying out HTML');
cd.debug.startTimer('preparations');
- await prepare(keptData);
+ await prepare(siteDataRequest);
let feivData;
if (cd.g.isFirstRun) {
diff --git a/src/js/siteData.js b/src/js/siteData.js
index 9581fa2a..673b0cc2 100644
--- a/src/js/siteData.js
+++ b/src/js/siteData.js
@@ -512,7 +512,7 @@ function setFormats() {
*
* @returns {Promise}
*/
-export function loadData() {
+export function loadSiteData() {
const requests = [];
mw.messages.set(cd.config.messages);
From 74ade8d867f32cd11d1e26f5972cf098d5a25bbf Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Fri, 5 Mar 2021 21:53:45 +0300
Subject: [PATCH 15/16] Move defaultConfig module to /config/default
---
src/js/defaultConfig.js => config/default.js | 0
src/js/app.js | 2 +-
2 files changed, 1 insertion(+), 1 deletion(-)
rename src/js/defaultConfig.js => config/default.js (100%)
diff --git a/src/js/defaultConfig.js b/config/default.js
similarity index 100%
rename from src/js/defaultConfig.js
rename to config/default.js
diff --git a/src/js/app.js b/src/js/app.js
index 1fc6decc..48a56fbf 100644
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -8,7 +8,7 @@ import cd from './cd';
import commentLinks from './commentLinks';
import configUrls from './../../config/urls.json';
import debug from './debug';
-import defaultConfig from './defaultConfig';
+import defaultConfig from '../../config/default';
import g from './staticGlobals';
import processPage from './processPage';
import util from './globalUtil';
From c9daffd9ef8bb6c1908885ed04a81e5709e14bc3 Mon Sep 17 00:00:00 2001
From: jwbth <33615628+jwbth@users.noreply.github.com>
Date: Fri, 5 Mar 2021 23:25:19 +0300
Subject: [PATCH 16/16] Fix not working hotkeys on 404 pages
---
src/js/eventHandlers.js | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/js/eventHandlers.js b/src/js/eventHandlers.js
index 81941d07..6ddafea9 100644
--- a/src/js/eventHandlers.js
+++ b/src/js/eventHandlers.js
@@ -61,7 +61,7 @@ export function removePreventUnloadCondition(name) {
* @param {Event} e
*/
export function handleGlobalKeyDown(e) {
- if (!cd.g.isPageActive || cd.util.isPageOverlayOn()) return;
+ if (cd.util.isPageOverlayOn()) return;
if (
// Ctrl+Alt+Q
@@ -70,8 +70,11 @@ export function handleGlobalKeyDown(e) {
// Q
(keyCombination(e, 81) && !isInputFocused())
) {
- e.preventDefault();
- CommentForm.getLastActive()?.quote(e.ctrlKey);
+ const lastActiveCommentForm = CommentForm.getLastActive();
+ if (lastActiveCommentForm) {
+ e.preventDefault();
+ lastActiveCommentForm.quote(e.ctrlKey);
+ }
}
if (navPanel.isMounted()) {