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:
File diff suppressed because it is too large
Load Diff
@@ -44,12 +44,48 @@ function dispatchTextInput(node) {
|
|||||||
node.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
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) {
|
function fillInput(node, value) {
|
||||||
node.focus();
|
node.focus();
|
||||||
if ('value' in node) {
|
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);
|
dispatchTextInput(node);
|
||||||
node.value = value;
|
set(value);
|
||||||
dispatchTextInput(node);
|
dispatchTextInput(node);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -58,17 +94,111 @@ function fillInput(node, value) {
|
|||||||
dispatchTextInput(node);
|
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) {
|
function fillEditable(node, value) {
|
||||||
|
const normalized = String(value || '').replace(/\r\n/g, '\n');
|
||||||
node.focus();
|
node.focus();
|
||||||
|
|
||||||
|
// 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 = '';
|
node.innerHTML = '';
|
||||||
const lines = String(value || '').split(/\n/);
|
const lines = normalized.split(/\n/);
|
||||||
lines.forEach((line, index) => {
|
lines.forEach((line, index) => {
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
node.appendChild(document.createElement('br'));
|
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'));
|
||||||
}
|
}
|
||||||
node.appendChild(document.createTextNode(line));
|
|
||||||
});
|
});
|
||||||
dispatchTextInput(node);
|
}
|
||||||
|
|
||||||
|
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) {
|
function findVisible(selectors) {
|
||||||
@@ -101,6 +231,21 @@ function findBodyEditor(titleInput) {
|
|||||||
return null;
|
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) {
|
function findButtonByText(fragment) {
|
||||||
const candidates = Array.from(document.querySelectorAll("button, [role='button'], a"));
|
const candidates = Array.from(document.querySelectorAll("button, [role='button'], a"));
|
||||||
const wanted = cleanText(fragment);
|
const wanted = cleanText(fragment);
|
||||||
@@ -113,6 +258,38 @@ function findButtonByText(fragment) {
|
|||||||
}) || null;
|
}) || 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 currentUrl = location.href;
|
||||||
const text = pageText();
|
const text = pageText();
|
||||||
if (isLoginBlocked(currentUrl, text)) {
|
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 || ''));
|
fillInput(titleInput, String(args.title || ''));
|
||||||
fillEditable(bodyEditor, String(args.body || ''));
|
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';
|
const publishMode = String(args.publish_mode || '').toLowerCase() === 'true';
|
||||||
if (!publishMode) {
|
if (!publishMode) {
|
||||||
return {
|
return {
|
||||||
@@ -153,17 +403,19 @@ if (!publishMode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const publishButton = findButtonByText('发布');
|
const publishButton = findButtonByText('发布');
|
||||||
if (!publishButton) {
|
if (!publishButton || isDisabledButton(publishButton)) {
|
||||||
return {
|
return {
|
||||||
status: 'publish_button_missing',
|
status: 'publish_button_missing',
|
||||||
current_url: currentUrl,
|
current_url: currentUrl,
|
||||||
title: cleanText(args.title),
|
title: cleanText(args.title),
|
||||||
|
publish_button_disabled: !!publishButton && isDisabledButton(publishButton),
|
||||||
|
body_text: bodyTextAfterFill,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
publishButton.click();
|
publishButton.click();
|
||||||
|
|
||||||
const confirmButton = findButtonByText('确认发布');
|
const confirmButton = findButtonByText('确认发布');
|
||||||
if (!confirmButton) {
|
if (!confirmButton || isDisabledButton(confirmButton)) {
|
||||||
return {
|
return {
|
||||||
status: 'publish_clicked',
|
status: 'publish_clicked',
|
||||||
current_url: currentUrl,
|
current_url: currentUrl,
|
||||||
|
|||||||
Reference in New Issue
Block a user