diff --git a/app/assets/javascripts/comments.js b/app/assets/javascripts/comments.js index e56a4b428..78b81fec1 100644 --- a/app/assets/javascripts/comments.js +++ b/app/assets/javascripts/comments.js @@ -380,7 +380,7 @@ $(() => { const id = $item.data('user-id'); $tgt[0].selectionStart = caretPos - posInWord; $tgt[0].selectionEnd = caretPos - posInWord + currentWord.length; - QPixel.replaceSelection($tgt, `@#${id}`); + QPixel.MD.replaceSelection($tgt, `@#${id}`); popup.destroy(); $tgt.focus(); }; diff --git a/app/assets/javascripts/markdown.js b/app/assets/javascripts/markdown.js index 68a4fd2e1..852cd2794 100644 --- a/app/assets/javascripts/markdown.js +++ b/app/assets/javascripts/markdown.js @@ -1,20 +1,4 @@ $(() => { - const stringInsert = (str, idx, insert) => str.slice(0, idx) + insert + str.slice(idx); - - const insertIntoField = ($field, start, end) => { - let value = $field.val(); - value = stringInsert(value, $field[0].selectionStart, start); - if (end) { - value = stringInsert(value, $field[0].selectionEnd + start.length, end); - } - $field.val(value).trigger('markdown'); - }; - - const replaceSelection = ($field, text) => { - const prev = $field.val(); - $field.val(prev.substring(0, $field[0].selectionStart) + text + prev.substring($field[0].selectionEnd)); - }; - $(document).on('click', '.js-markdown-tool', (ev) => { const $tgt = $(ev.target); const $button = $tgt.is('a') ? $tgt : $tgt.parents('a'); @@ -38,7 +22,7 @@ $(() => { if (Object.keys(actions).indexOf(action) !== -1) { const preSelection = [$field[0].selectionStart, $field[0].selectionEnd]; - insertIntoField($field, actions[action][0], actions[action][1]); + QPixel.MD.insertIntoField($field, actions[action][0], actions[action][1]); $field.focus(); $field[0].selectionStart = preSelection[0] + actions[action][0].length; $field[0].selectionEnd = preSelection[1] + actions[action][0].length; @@ -99,10 +83,10 @@ $(() => { const $field = $('.js-post-field'); if ($field[0].selectionStart != null && $field[0].selectionStart !== $field[0].selectionEnd) { - replaceSelection($field, markdown); + QPixel.MD.replaceSelection($field, markdown); } else { - insertIntoField($field, markdown); + QPixel.MD.insertIntoField($field, markdown); } $field.trigger('markdown'); diff --git a/app/assets/javascripts/posts.js b/app/assets/javascripts/posts.js index 74542f705..44aad0f44 100644 --- a/app/assets/javascripts/posts.js +++ b/app/assets/javascripts/posts.js @@ -20,15 +20,6 @@ $(() => { /** @type {JQuery} */ const $uploadForm = $('.js-upload-form'); - /** - * Inserts text at a given {@link idx} in a given {@link str} - * @param {string} str text to insert into - * @param {number} idx position to insert at - * @param {string} insert text to insert - * @returns {string} - */ - const stringInsert = (str, idx, insert) => str.slice(0, idx) + insert + str.slice(idx); - const placeholder = '![Uploading, please wait...]()'; $uploadForm.find('input[type="file"]').on('change', async (evt) => { @@ -37,7 +28,7 @@ $(() => { const postText = postField.value; const cursorPos = postField.selectionStart; - postField.value = stringInsert(postText, cursorPos, placeholder); + postField.value = QPixel.MD.stringInsert(postText, cursorPos, placeholder); $uploadForm.trigger('submit'); }); diff --git a/app/assets/javascripts/qpixel_api.js b/app/assets/javascripts/qpixel_api.js index 51a9336e6..e0f87ca70 100644 --- a/app/assets/javascripts/qpixel_api.js +++ b/app/assets/javascripts/qpixel_api.js @@ -98,11 +98,6 @@ window.QPixel = { } }, - replaceSelection: ($field, text) => { - const prev = $field.val()?.toString(); - $field.val(prev.substring(0, $field[0].selectionStart) + text + prev.substring($field[0].selectionEnd)); - }, - /** * @type {QPixelFilter[]|null} */ diff --git a/app/assets/javascripts/qpixel_markdown.js b/app/assets/javascripts/qpixel_markdown.js index 52f4435fc..f15caf361 100644 --- a/app/assets/javascripts/qpixel_markdown.js +++ b/app/assets/javascripts/qpixel_markdown.js @@ -1,12 +1,33 @@ window.QPixel = window.QPixel || {}; QPixel.MD = { + insertIntoField: ($field, start, end) => { + let value = $field.val(); + + value = QPixel.MD.stringInsert(value, $field[0].selectionStart, start); + + if (end) { + value = QPixel.MD.stringInsert(value, $field[0].selectionEnd + start.length, end); + } + + $field.val(value).trigger('markdown'); + }, + + replaceSelection: ($field, text) => { + const prev = $field.val()?.toString(); + $field.val(prev.substring(0, $field[0].selectionStart) + text + prev.substring($field[0].selectionEnd)); + }, + + stringInsert: (str, idx, insert) => { + return str.slice(0, idx) + insert + str.slice(idx); + }, + stripMarkdown: (content, options = {}) => { const stripped = content .replace(/(?:^#+ +|^-{3,}|^\[[^\]]+\]: ?.+$|^!\[[^\]]+\](?:\([^)]+\)|\[[^\]]+\])$|<[^>]+>)/g, '') .replace(/[*_~]+/g, '') .replace(/!?\[([^\]]+)\](?:\([^)]+\)|\[[^\]]+\])/g, '$1'); - + if (options.removeLeadingQuote ?? false) { return stripped.replace(/^>.+?$/g, ''); } diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index d5c5bda91..34ae47704 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -27,7 +27,7 @@ $(() => { const id = $item.data('post-type-id'); $tgt[0].selectionStart = caretPos - posInWord; $tgt[0].selectionEnd = (caretPos - posInWord) + currentWord.length; - QPixel.replaceSelection($tgt, `post_type:${id}`); + QPixel.MD.replaceSelection($tgt, `post_type:${id}`); popup.destroy(); $tgt.focus(); }; diff --git a/app/views/layouts/_sidebar.html.erb b/app/views/layouts/_sidebar.html.erb index e288e5289..b2908c621 100644 --- a/app/views/layouts/_sidebar.html.erb +++ b/app/views/layouts/_sidebar.html.erb @@ -1,10 +1,10 @@ - diff --git a/global.d.ts b/global.d.ts index 0153e8907..89aff8c56 100644 --- a/global.d.ts +++ b/global.d.ts @@ -62,7 +62,7 @@ interface QPixelDOM { * Checks common modifier states on a given keyboard event * @param event */ - getModifierState: (event: KeyboardEvent | MouseEvent | JQuery.KeyboardEventBase) => boolean; + getModifierState?: (event: KeyboardEvent | MouseEvent | JQuery.KeyboardEventBase) => boolean; /** * Is a given event target an HTMLElement? * @param target event target to check @@ -91,10 +91,30 @@ interface StripMarkdownOptions { } interface QPixelMD { + /** + * Inserts text around a given {@link $field}'s selection + * @param $field field to insert text into + * @param start text to insert at selection start + * @param end text to insert at selection end, if any + */ + insertIntoField?: ($field: JQuery, start: string, end?: string | null) => void; + /** + * Replace the selected text in an input field with a provided replacement. + * @param $field the field in which to replace text + * @param text the text with which to replace the selection + */ + replaceSelection?: ($field: JQuery, text: string) => void; + /** + * Inserts text at a given {@link idx} in a given {@link str} + * @param str text to insert into + * @param idx position to insert at + * @param insert text to insert + */ + stringInsert?: (str: string, idx: number, insert: string) => string; /** * See [strip_markdown](app/helpers/application_helper.rb) application helper */ - stripMarkdown(content: string, options?: StripMarkdownOptions): string; + stripMarkdown?: (content: string, options?: StripMarkdownOptions) => string; } interface QPixelStorageGetOptions { @@ -468,12 +488,6 @@ interface QPixel { */ preference?: (name: string, community?: boolean) => Promise; - /** - * Replace the selected text in an input field with a provided replacement. - * @param $field the field in which to replace text - * @param text the text with which to replace the selection - */ - replaceSelection?: ($field: JQuery, text: string) => void; setFilter?: (name: string, filter: QPixelFilter, category: string, isDefault: boolean) => Promise; /** @@ -688,6 +702,22 @@ declare var hljs: any; declare var MathJax: any; // DOMPurify lib, TODO: types declare var DOMPurify: any; +// Sefaria Linker (no known types), see https://developers.sefaria.org/docs/linker-v2 +declare var sefaria: { + link: (options?: { + contentLang?: 'bilingual' | 'english' | 'hebrew', + dynamic?: boolean, + excludeFromLinking?: string, + excludeFromTracking?: string, + hidePopupsOnMobile?: boolean, + interfaceLang?: 'english' | 'hebrew', + mode?: 'link' | 'popup-click', + parenthesesOnly?: boolean, + popupStyles?: Record, + selector?: string, + quotationOnly?: boolean + }) => void +} | undefined; declare var QPixel: QPixel; declare var getCaretCoordinates: ( diff --git a/public/assets/community/codegolf.js b/public/assets/community/codegolf.js index b5522ace4..bdf188f7b 100644 --- a/public/assets/community/codegolf.js +++ b/public/assets/community/codegolf.js @@ -15,6 +15,7 @@ * variant?: string * extensions?: string * code?: string + * placement?: number * score?: number * }} ChallengeEntry * @@ -31,6 +32,10 @@ } const CHALLENGE_ID = match[0]; + + /** + * @type {ChallengeEntry[] | undefined} + */ let leaderboard; /** @@ -53,14 +58,13 @@ return this._get('groupByLanguage'); }, set groupByLanguage(value) { - return this._set('groupByLanguage', value); + this._set('groupByLanguage', value); }, - get showPlacements() { return this._get('showPlacements'); }, set showPlacements(value) { - return this._set('showPlacements', value); + this._set('showPlacements', value); }, _get(name) { @@ -92,7 +96,7 @@ const doc = dom_parser.parseFromString(text.toString(), 'text/html'); const pagination = doc.querySelector('.pagination'); - const num_pages = pagination ? parseInt(pagination.querySelector('.next').previousElementSibling.innerText) : 1; + const num_pages = pagination ? parseInt(pagination.querySelector('.next').previousElementSibling.textContent) : 1; const pagePromises = []; for (let i = 1; i <= num_pages; i++) { @@ -111,13 +115,15 @@ for (const answerPost of non_deleted_answers) { /** @type {HTMLElement | null} */ const header = answerPost.querySelector('h1, h2, h3'); - const code = header?.parentElement.querySelector(':scope > pre > code'); + /** @type {HTMLElement | null} */ + const codeEl = header?.parentElement.querySelector(':scope > pre > code'); const full_language = header?.innerText.split(',')[0].trim(); const regexGroups = full_language?.match(/(?.+?)(?: \((?.+)\))?(?: \+ (?.+))?$/)?.groups ?? {}; const { language, variant, extensions } = regexGroups; - const userlink = answerPost.querySelector( - ".user-card--content .user-card--link", - ); + /** @type {HTMLAnchorElement | null} */ + const userlinkEl = answerPost.querySelector(".user-card--content .user-card--link"); + /** @type {HTMLAnchorElement | null} */ + const answerLinkEl = answerPost.querySelector('.js-permalink'); // https://regex101.com/r/BjIjk5/2 const matchedScore = header?.innerText.match(/\d+(?:\.\d+)?/g)?.pop(); @@ -125,15 +131,15 @@ /** @type {ChallengeEntry} */ const entry = { answerID: answerPost.id, - answerURL: answerPost.querySelector('.js-permalink').href, + answerURL: answerLinkEl?.href, page: i + 1, // +1 because pages are 1-indexed while arrays are 0-indexed - username: userlink?.firstChild?.data?.trim() || 'deleted user', - userid: userlink?.href?.match(/\d+/)?.[0] || '', + username: userlinkEl?.firstChild?.textContent?.trim() || 'deleted user', + userid: userlinkEl?.href?.match(/\d+/)?.[0] || '', full_language, language, variant, extensions, - code: code?.innerText, + code: codeEl?.innerText, score: isFinite(+matchedScore) ? +matchedScore : void 0 }; @@ -192,6 +198,7 @@
`; + /** @type {HTMLElement | null} */ const leaderboardsTable = embed.querySelector('#toc-rows'); const toggle = embed.querySelector('#leaderboards-header'); toggle.addEventListener('click', (_) => { @@ -202,7 +209,10 @@ leaderboardsTable.style.display = 'none'; } }); + + /** @type {HTMLInputElement | null} */ const groupByLanguageInput = embed.querySelector('#group-by-lang'); + /** @type {HTMLInputElement | null} */ const showPlacementsInput = embed.querySelector('#show-placement'); groupByLanguageInput.addEventListener('click', (_) => { @@ -237,6 +247,7 @@ * @returns {Record} */ function createGroups(array, categorizer) { + /** @type {Record} */ const groups = {}; for (const item of array) { @@ -267,13 +278,19 @@ : (settings.showPlacements ? `
#${answer.placement}
` : '')}
`; - row.querySelector('.username').innerText = answer.username - row.querySelector('.language-badge').innerText = answer.full_language ?? 'N/A'; + /** @type {HTMLElement | null} */ + const usernameEl = row.querySelector('.username'); + /** @type {HTMLElement | null} */ + const langBadgeEl = row.querySelector('.language-badge'); + + usernameEl.innerText = answer.username; + langBadgeEl.innerText = answer.full_language ?? 'N/A'; + if (answer.code) { - row.querySelector('.username').after(document.createElement('code')); + usernameEl.after(document.createElement('code')); row.querySelector('code').innerText = answer.code.split('\n')[0].substring(0, 200); } else if (answer.code !== '') { - row.querySelector('.username').insertAdjacentHTML('afterend', 'Invalid entry format'); + usernameEl.insertAdjacentHTML('afterend', 'Invalid entry format'); } return row; @@ -313,19 +330,23 @@ } window.addEventListener("DOMContentLoaded", (_) => { - const categoryName = document.querySelector(".category-header--name").innerText.trim(); + /** @type {HTMLElement | null} */ + const categoryNameEl = document.querySelector(".category-header--name"); + + const categoryName = categoryNameEl.innerText.trim(); if (categoryName !== 'Challenges') { return; } - const question_tags = [ - ...document.querySelector(".post--tags").children, - ].map((el) => el.innerText); + /** @type {NodeListOf} */ + const questionTagsElements = document.querySelectorAll(".post--tags > a"); + + const questionTags = [...questionTagsElements].map((el) => el.innerText); if ( - question_tags.includes("code-golf") || - question_tags.includes("lowest-score") + questionTags.includes("code-golf") || + questionTags.includes("lowest-score") ) { // If x were undefined, it would be automatically sorted to the end, but not so if x.score is undefined, so this needs to be stated explicitly. sort = (x, y) => typeof x.score === "undefined" ? 1 : x.score - y.score; @@ -334,8 +355,8 @@ refreshBoard(sort); } else if ( - question_tags.includes("code-bowling") || - question_tags.includes("highest-score") + questionTags.includes("code-bowling") || + questionTags.includes("highest-score") ) { // If x were undefined, it would be automatically sorted to the end, but not so if x.score is undefined, so this needs to be stated explicitly. sort = (x, y) => typeof x.score === "undefined" ? 1 : y.score - x.score; diff --git a/public/assets/community/judaism.js b/public/assets/community/judaism.js index c4627020d..d8b12e18d 100644 --- a/public/assets/community/judaism.js +++ b/public/assets/community/judaism.js @@ -38,6 +38,7 @@ THE SOFTWARE. }, }; + /** @type {JQuery} */ var currentTextfield = $('textarea, input[type=text]'); $(document).ready(function(){ $(document).on('focus', 'textarea, input[type=text]', function(){ @@ -150,7 +151,7 @@ THE SOFTWARE. /* Event handling for buttons and checkboxes*/ kb.find('.hbkey').click(function () { - t = currentTextfield[0]; + var t = currentTextfield[0]; var start = t.selectionStart, end = t.selectionEnd, text = t.value, @@ -164,7 +165,7 @@ THE SOFTWARE. }); kb.find('.hbins').click(function () { - t = currentTextfield[0]; + var t = currentTextfield[0]; var start = t.selectionStart, end = t.selectionEnd, text = t.value, @@ -265,13 +266,19 @@ THE SOFTWARE. $(() => { + const link = () => { + sefaria.link({ + excludeFromLinking: '.js-post-field', + }); + }; + const el = document.createElement('script'); el.src = 'https://www.sefaria.org/linker.js'; el.addEventListener('load', () => { - sefaria.link(); + link(); $(document).on('ajax:success', '.post--comments', () => { - sefaria.link(); + link(); }); let linkTimeout = null; @@ -282,7 +289,7 @@ $(() => { } linkTimeout = setTimeout(() => { - sefaria.link(); + link(); }, 1000); }); }); @@ -303,10 +310,11 @@ $(() => { const doReplacement = (ev) => { ev.preventDefault(); + /** @type {JQuery} */ const $field = $('.js-post-field'); const $tgt = $(ev.target); const text = $tgt.attr('data-text'); - QPixel.replaceSelection($field, text); + QPixel.MD.replaceSelection($field, text); $field.trigger('markdown'); }; @@ -318,6 +326,7 @@ $(() => { }; QPixel.addEditorButton(``, 'Suggest Reference', async () => { + /** @type {JQuery} */ const $field = $('.js-post-field'); const selection = $field.val().substring($field[0].selectionStart, $field[0].selectionEnd) || ''; if (!selection) { @@ -355,8 +364,14 @@ window.addEventListener("load", async () => { container.innerHTML = "
Today is:
loading date...
"; container.classList.add('widget', 'has-margin-4'); - const disclaimerNotice = document.querySelector('.widget.is-yellow:first-child'); - disclaimerNotice.parentNode.insertBefore(container, disclaimerNotice.nextSibling); + const disclaimerNotice = document.querySelector('.js-sidebar-notice'); + const sidebar = document.querySelector('.js-sidebar'); + + if (disclaimerNotice) { + disclaimerNotice.insertAdjacentElement('afterend', container); + } else { + sidebar?.prepend(container); + } let todayDate = new Date(); diff --git a/tsconfig.json b/tsconfig.json index 5556dbb7b..9b9686a17 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ "target": "ES2021", "types": ["./global.d.ts", "@types/jquery", "@types/select2"] }, - "include": ["./app/assets/javascripts"], + "include": ["./app/assets/javascripts", "./public/assets/community"], "exclude": [ "config", "db", @@ -17,7 +17,6 @@ "img", "lib", "log", - "public", "scripts", "static", "test",