feat: refresh Zhihu dashboard and draft autofill
Refresh the Zhihu hotlist screen presentation and update the article draft autofill flow to match the latest interaction changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -44,12 +44,48 @@ function dispatchTextInput(node) {
|
||||
node.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
function dispatchRichInput(node, inputType, data) {
|
||||
try {
|
||||
node.dispatchEvent(new InputEvent('beforeinput', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
inputType,
|
||||
data,
|
||||
}));
|
||||
} catch (_) {}
|
||||
try {
|
||||
node.dispatchEvent(new InputEvent('input', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
inputType,
|
||||
data,
|
||||
}));
|
||||
} catch (_) {
|
||||
node.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
||||
}
|
||||
node.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, composed: true, key: 'Enter' }));
|
||||
node.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
node.dispatchEvent(new Event('blur', { bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
function fillInput(node, value) {
|
||||
node.focus();
|
||||
if ('value' in node) {
|
||||
node.value = '';
|
||||
// React overrides the value setter on input/textarea to track changes.
|
||||
// Calling .value = x directly bypasses React's tracking, so the
|
||||
// component's onChange never fires and React state stays stale.
|
||||
// Using the native prototype setter makes React detect the update.
|
||||
var proto = node instanceof HTMLTextAreaElement
|
||||
? HTMLTextAreaElement.prototype
|
||||
: HTMLInputElement.prototype;
|
||||
var nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value');
|
||||
var set = (nativeSetter && nativeSetter.set)
|
||||
? function(v) { nativeSetter.set.call(node, v); }
|
||||
: function(v) { node.value = v; };
|
||||
|
||||
set('');
|
||||
dispatchTextInput(node);
|
||||
node.value = value;
|
||||
set(value);
|
||||
dispatchTextInput(node);
|
||||
return;
|
||||
}
|
||||
@@ -58,17 +94,111 @@ function fillInput(node, value) {
|
||||
dispatchTextInput(node);
|
||||
}
|
||||
|
||||
function selectEditableContents(node) {
|
||||
const selection = window.getSelection && window.getSelection();
|
||||
if (!selection) {
|
||||
return null;
|
||||
}
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
range.collapse(true);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
return selection;
|
||||
}
|
||||
|
||||
function fillEditable(node, value) {
|
||||
const normalized = String(value || '').replace(/\r\n/g, '\n');
|
||||
node.focus();
|
||||
node.innerHTML = '';
|
||||
const lines = String(value || '').split(/\n/);
|
||||
lines.forEach((line, index) => {
|
||||
if (index > 0) {
|
||||
node.appendChild(document.createElement('br'));
|
||||
}
|
||||
node.appendChild(document.createTextNode(line));
|
||||
});
|
||||
dispatchTextInput(node);
|
||||
|
||||
// Strategy 1: Simulate a clipboard paste event.
|
||||
// Rich-text frameworks (Draft.js, ProseMirror, Slate) have explicit paste
|
||||
// handlers that parse pasted content and update their internal content
|
||||
// model. This keeps word counts, undo stacks, and toolbar / publish-button
|
||||
// states in sync — something direct DOM writes and execCommand cannot
|
||||
// guarantee.
|
||||
var pasteWorked = false;
|
||||
try {
|
||||
selectEditableContents(node);
|
||||
try { document.execCommand('selectAll', false, null); } catch (_) {}
|
||||
try { document.execCommand('delete', false, null); } catch (_) {}
|
||||
|
||||
var htmlLines = normalized.split('\n').map(function(line) {
|
||||
var escaped = (line || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
return '<p>' + (escaped || '<br>') + '</p>';
|
||||
}).join('');
|
||||
|
||||
var dt = new DataTransfer();
|
||||
dt.setData('text/plain', normalized);
|
||||
dt.setData('text/html', htmlLines);
|
||||
node.dispatchEvent(new ClipboardEvent('paste', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clipboardData: dt,
|
||||
}));
|
||||
|
||||
var afterPaste = cleanText(node.innerText || node.textContent || '');
|
||||
pasteWorked = !!afterPaste && afterPaste !== '请输入正文' &&
|
||||
afterPaste !== cleanText(attrText(node, 'placeholder'));
|
||||
} catch (_) {}
|
||||
|
||||
if (pasteWorked) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Strategy 2: document.execCommand('insertText')
|
||||
selectEditableContents(node);
|
||||
|
||||
let inserted = false;
|
||||
try {
|
||||
document.execCommand('selectAll', false, null);
|
||||
} catch (_) {}
|
||||
try {
|
||||
document.execCommand('delete', false, null);
|
||||
} catch (_) {}
|
||||
try {
|
||||
inserted = document.execCommand('insertText', false, normalized);
|
||||
} catch (_) {
|
||||
inserted = false;
|
||||
}
|
||||
|
||||
if (!inserted) {
|
||||
// Strategy 3: Direct DOM write (visual only — editor state may not update)
|
||||
node.innerHTML = '';
|
||||
const lines = normalized.split(/\n/);
|
||||
lines.forEach((line, index) => {
|
||||
if (index > 0) {
|
||||
node.appendChild(document.createElement('p'));
|
||||
}
|
||||
const container = index > 0 ? node.lastChild : node;
|
||||
const textNode = document.createTextNode(line || '');
|
||||
if (container === node) {
|
||||
node.appendChild(textNode);
|
||||
} else {
|
||||
container.appendChild(textNode);
|
||||
}
|
||||
if (!line && container !== node) {
|
||||
container.appendChild(document.createElement('br'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispatchRichInput(node, 'insertText', normalized);
|
||||
|
||||
const actualText = cleanText(node.innerText || node.textContent || '');
|
||||
if (!actualText || actualText === cleanText(attrText(node, 'placeholder')) || actualText === '请输入正文') {
|
||||
node.innerHTML = '';
|
||||
normalized.split(/\n/).forEach((line) => {
|
||||
const paragraph = document.createElement('p');
|
||||
if (line) {
|
||||
paragraph.textContent = line;
|
||||
} else {
|
||||
paragraph.appendChild(document.createElement('br'));
|
||||
}
|
||||
node.appendChild(paragraph);
|
||||
});
|
||||
dispatchRichInput(node, 'insertParagraph', normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function findVisible(selectors) {
|
||||
@@ -101,6 +231,21 @@ function findBodyEditor(titleInput) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function isDisabledButton(node) {
|
||||
if (!node) {
|
||||
return true;
|
||||
}
|
||||
if (node.disabled) {
|
||||
return true;
|
||||
}
|
||||
const ariaDisabled = attrText(node, 'aria-disabled');
|
||||
if (ariaDisabled === 'true') {
|
||||
return true;
|
||||
}
|
||||
const className = typeof node.className === 'string' ? node.className : '';
|
||||
return /disabled|is-disabled/.test(className);
|
||||
}
|
||||
|
||||
function findButtonByText(fragment) {
|
||||
const candidates = Array.from(document.querySelectorAll("button, [role='button'], a"));
|
||||
const wanted = cleanText(fragment);
|
||||
@@ -113,6 +258,38 @@ function findButtonByText(fragment) {
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function findPreferredButtonByText(fragment) {
|
||||
const candidates = Array.from(document.querySelectorAll("button, [role='button'], a"))
|
||||
.filter((node) => isVisible(node));
|
||||
const wanted = cleanText(fragment);
|
||||
const exactMatch = candidates.find((node) => cleanText(node.textContent) === wanted);
|
||||
if (exactMatch) {
|
||||
return exactMatch;
|
||||
}
|
||||
return candidates.find((node) => cleanText(node.textContent).includes(wanted)) || null;
|
||||
}
|
||||
|
||||
function nodeText(node) {
|
||||
if (!node) {
|
||||
return '';
|
||||
}
|
||||
if ('value' in node && typeof node.value === 'string') {
|
||||
return cleanText(node.value);
|
||||
}
|
||||
return cleanText(node.innerText || node.textContent || '');
|
||||
}
|
||||
|
||||
function collectDraftState(titleInput, bodyEditor) {
|
||||
const titleText = nodeText(titleInput);
|
||||
const bodyText = nodeText(bodyEditor);
|
||||
const normalizedBodyText = bodyText === '请输入正文' ? '' : bodyText;
|
||||
return {
|
||||
titleText,
|
||||
bodyText: normalizedBodyText,
|
||||
ready: !!titleText && !!normalizedBodyText,
|
||||
};
|
||||
}
|
||||
|
||||
const currentUrl = location.href;
|
||||
const text = pageText();
|
||||
if (isLoginBlocked(currentUrl, text)) {
|
||||
@@ -140,9 +317,82 @@ if (!titleInput || !bodyEditor) {
|
||||
};
|
||||
}
|
||||
|
||||
const liveInputMode = String(args.input_mode || '').toLowerCase() === 'live_input';
|
||||
if (liveInputMode) {
|
||||
var draftState = collectDraftState(titleInput, bodyEditor);
|
||||
const publishMode = String(args.publish_mode || '').toLowerCase() === 'true';
|
||||
|
||||
// If keyboard simulation hasn't populated the fields yet (fire-and-forget
|
||||
// may still be in progress or may have missed), fall back to direct DOM fill.
|
||||
if (!draftState.titleText && args.title) {
|
||||
fillInput(titleInput, String(args.title || ''));
|
||||
}
|
||||
if (!draftState.bodyText && args.body) {
|
||||
fillEditable(bodyEditor, String(args.body || ''));
|
||||
}
|
||||
draftState = collectDraftState(titleInput, bodyEditor);
|
||||
|
||||
if (!draftState.ready) {
|
||||
return {
|
||||
status: 'editor_not_ready',
|
||||
current_url: currentUrl,
|
||||
title: draftState.titleText || cleanText(args.title),
|
||||
body_text: draftState.bodyText,
|
||||
};
|
||||
}
|
||||
if (!publishMode) {
|
||||
return {
|
||||
status: 'draft_ready',
|
||||
current_url: currentUrl,
|
||||
title: draftState.titleText || cleanText(args.title),
|
||||
body_text: draftState.bodyText,
|
||||
};
|
||||
}
|
||||
|
||||
const publishButton = findPreferredButtonByText('发布');
|
||||
if (!publishButton || isDisabledButton(publishButton)) {
|
||||
return {
|
||||
status: 'publish_button_missing',
|
||||
current_url: currentUrl,
|
||||
title: draftState.titleText || cleanText(args.title),
|
||||
publish_button_disabled: !!publishButton && isDisabledButton(publishButton),
|
||||
body_text: draftState.bodyText,
|
||||
};
|
||||
}
|
||||
publishButton.click();
|
||||
|
||||
const confirmButton = findPreferredButtonByText('确认发布');
|
||||
if (!confirmButton || isDisabledButton(confirmButton)) {
|
||||
return {
|
||||
status: 'publish_clicked',
|
||||
current_url: currentUrl,
|
||||
title: draftState.titleText || cleanText(args.title),
|
||||
body_text: draftState.bodyText,
|
||||
};
|
||||
}
|
||||
confirmButton.click();
|
||||
|
||||
return {
|
||||
status: 'publish_submitted',
|
||||
current_url: currentUrl,
|
||||
title: draftState.titleText || cleanText(args.title),
|
||||
body_text: draftState.bodyText,
|
||||
};
|
||||
}
|
||||
|
||||
fillInput(titleInput, String(args.title || ''));
|
||||
fillEditable(bodyEditor, String(args.body || ''));
|
||||
|
||||
const bodyTextAfterFill = cleanText(bodyEditor.innerText || bodyEditor.textContent || '');
|
||||
if (!bodyTextAfterFill || bodyTextAfterFill === '请输入正文') {
|
||||
return {
|
||||
status: 'editor_not_ready',
|
||||
current_url: currentUrl,
|
||||
title: cleanText(args.title),
|
||||
body_text: bodyTextAfterFill,
|
||||
};
|
||||
}
|
||||
|
||||
const publishMode = String(args.publish_mode || '').toLowerCase() === 'true';
|
||||
if (!publishMode) {
|
||||
return {
|
||||
@@ -153,17 +403,19 @@ if (!publishMode) {
|
||||
}
|
||||
|
||||
const publishButton = findButtonByText('发布');
|
||||
if (!publishButton) {
|
||||
if (!publishButton || isDisabledButton(publishButton)) {
|
||||
return {
|
||||
status: 'publish_button_missing',
|
||||
current_url: currentUrl,
|
||||
title: cleanText(args.title),
|
||||
publish_button_disabled: !!publishButton && isDisabledButton(publishButton),
|
||||
body_text: bodyTextAfterFill,
|
||||
};
|
||||
}
|
||||
publishButton.click();
|
||||
|
||||
const confirmButton = findButtonByText('确认发布');
|
||||
if (!confirmButton) {
|
||||
if (!confirmButton || isDisabledButton(confirmButton)) {
|
||||
return {
|
||||
status: 'publish_clicked',
|
||||
current_url: currentUrl,
|
||||
|
||||
Reference in New Issue
Block a user