Add fullscreen editor and proportional resize

Introduce a fullscreen editor mode and a proportional resize handle for the memo editor. Updates include:

- UI: add fullscreen button and resize handle to popup.html and related i18n keys for en/ja/ko/zh_CN.
- CSS: styles for .memo-editor, fullscreen state, and #editor-resize-handle, plus layout tweaks for fullscreen.
- Background: enhance context menu handler to retrieve selection text from the active tab using chrome.scripting.executeScript, support opening the extension popup programmatically (tryOpenActionPopup), and factor appendContent logic.
- Oper: implement isFullscreenMode(), openFullscreenTab(), proportional editor resize logic with pointer events (initProportionalEditorResize), focus handling adjustments, and init call. Added helper focusTextareaToEnd().
- Manifest: request scripting and windows permissions required for selection injection and window focus.

These changes enable sending accurate selection text from web pages, allow users to open a fullscreen editor tab, and provide a draggable, proportional resize experience in the popup editor.
This commit is contained in:
jonny
2026-03-07 14:42:31 +08:00
parent 8f51bb399b
commit adfd797e84
10 changed files with 385 additions and 32 deletions
+9
View File
@@ -163,5 +163,14 @@
},
"langKorean": {
"message": "한국어"
},
"fullscreenBtn": {
"message": "Fullscreen"
},
"tipFullscreen": {
"message": "Open fullscreen editor"
},
"tipResize": {
"message": "Drag to resize (min: default size)"
}
}
+9
View File
@@ -163,5 +163,14 @@
},
"langKorean": {
"message": "한국어"
},
"fullscreenBtn": {
"message": "全画面"
},
"tipFullscreen": {
"message": "全画面で編集"
},
"tipResize": {
"message": "ドラッグで拡大/縮小(最小:初期サイズ)"
}
}
+9
View File
@@ -163,5 +163,14 @@
},
"langKorean": {
"message": "한국어"
},
"fullscreenBtn": {
"message": "전체화면"
},
"tipFullscreen": {
"message": "전체화면 편집"
},
"tipResize": {
"message": "드래그로 확대/축소(최소: 기본 크기)"
}
}
+9
View File
@@ -163,5 +163,14 @@
},
"langKorean": {
"message": "한국어"
},
"fullscreenBtn": {
"message": "全屏"
},
"tipFullscreen": {
"message": "全屏编辑"
},
"tipResize": {
"message": "拖拽缩放编辑框(最小为默认大小)"
}
}
+66 -3
View File
@@ -48,6 +48,37 @@ a{color: #555;}
transition-timing-function: cubic-bezier(.4,0,.2,1);
transition-duration: .15s;
}
.memo-editor{
position: relative;
resize: none;
overflow: hidden;
box-sizing: border-box;
contain: layout paint;
}
#editor-resize-handle{
position: absolute;
right: .35rem;
bottom: .35rem;
width: 14px;
height: 14px;
border-right: 2px solid #bbb;
border-bottom: 2px solid #bbb;
cursor: nwse-resize;
opacity: .8;
user-select: none;
touch-action: none;
}
#editor-resize-handle:hover{
opacity: 1;
border-right-color: #888;
border-bottom-color: #888;
}
.body.fullscreen #editor-resize-handle{
display: none;
}
.random-item{
border: 1px solid rgb(229,231,235);
color: #666;
@@ -66,6 +97,25 @@ a{color: #555;}
overflow-wrap: anywhere;
word-break: normal;}
.btns-container{text-align:right;}
.memo-editor #fullscreen{
position: absolute;
right: .5rem;
top: .5rem;
z-index: 2;
border: 1px solid rgb(229,231,235);
border-radius: .25rem;
background-color: rgb(255,255,255);
color: #666;
font-size: .75rem;
line-height: 1.5rem;
padding: 0 .4rem;
cursor: pointer;
opacity: .9;
}
.memo-editor #fullscreen:hover{
opacity: 1;
background-color: rgb(243,244,246);
}
.common-editor-inputer,input.inputer{
font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;
height: 100%;
@@ -77,10 +127,25 @@ a{color: #555;}
background-color: transparent;
font-size: 1rem;
min-height: 40px;
max-height: 400px;
scrollbar-width: none;
line-height: 1.5rem;
}
.common-editor-inputer{
padding-top: 1.8rem;
padding-right: 3.2rem;
}
.body.fullscreen{
min-width: 0;
width: 100vw;
height: 100vh;
box-sizing: border-box;
}
.body.fullscreen .common-editor-inputer{
min-height: 60vh;
}
input.inputer{border-bottom: 1px solid #ccc;width:75%;}
#saveKey{margin:0;flex:1;}
@@ -248,9 +313,7 @@ input.inputer{border-bottom: 1px solid #ccc;width:75%;}
padding: .15rem .35rem;
cursor: pointer;
}
#blog_info{
}
.tip{
margin-left: 36%;
+118 -27
View File
@@ -23,6 +23,89 @@ function updateContextMenu(id, update) {
})
}
function pageReadSelectionText() {
try {
const active = document.activeElement
const isTextInput =
active &&
(active.tagName === 'TEXTAREA' ||
(active.tagName === 'INPUT' &&
/^(text|search|url|tel|email|password)$/i.test(active.type || 'text')))
if (isTextInput && typeof active.selectionStart === 'number' && typeof active.selectionEnd === 'number') {
return String(active.value || '').slice(active.selectionStart, active.selectionEnd).replace(/\r\n?/g, '\n')
}
const sel = window.getSelection && window.getSelection()
if (!sel) return ''
return String(sel.toString() || '').replace(/\r\n?/g, '\n')
} catch (_) {
return ''
}
}
function getSelectionTextFromTab(tabId, fallbackText) {
return new Promise((resolve) => {
const fallback = typeof fallbackText === 'string' ? fallbackText : ''
if (!tabId || !chrome.scripting || typeof chrome.scripting.executeScript !== 'function') {
resolve(fallback)
return
}
try {
chrome.scripting.executeScript(
{
target: { tabId },
func: pageReadSelectionText
},
(results) => {
if (chrome.runtime.lastError) {
resolve(fallback)
return
}
const first = Array.isArray(results) ? results[0] : null
const text = first && typeof first.result === 'string' ? first.result : ''
resolve(text || fallback)
}
)
} catch (_) {
resolve(fallback)
}
})
}
function tryOpenActionPopup(tab) {
try {
if (!chrome.action || typeof chrome.action.openPopup !== 'function') return
const windowId = tab && typeof tab.windowId === 'number' ? tab.windowId : undefined
const open = () => {
try {
if (typeof windowId === 'number') {
chrome.action.openPopup({ windowId }, () => void chrome.runtime.lastError)
} else {
chrome.action.openPopup({}, () => void chrome.runtime.lastError)
}
} catch (_) {
// best-effort only
}
}
// Avoid: "Cannot show popup for an inactive window".
if (typeof windowId === 'number' && chrome.windows && typeof chrome.windows.update === 'function') {
chrome.windows.update(windowId, { focused: true }, () => {
void chrome.runtime.lastError
open()
})
return
}
open()
} catch (_) {
// best-effort only
}
}
let cachedUiLanguage = null
let cachedOverrideMessages = null
@@ -93,35 +176,43 @@ chrome.storage.onChanged.addListener((changes, areaName) => {
refreshContextMenus()
})
chrome.contextMenus.onClicked.addListener((info) => {
let tempCont = ''
switch (info.menuItemId) {
case 'Memos-send-selection':
tempCont =
info.selectionText +
'\n' +
`[Reference Link](${info.linkUrl || info.pageUrl})` +
'\n'
break
case 'Memos-send-link':
tempCont = (info.linkUrl || info.pageUrl) + '\n'
break
case 'Memos-send-image':
tempCont = `![](${info.srcUrl})` + '\n'
break
}
chrome.storage.sync.get(
{ open_action: 'save_text', open_content: '' },
function (items) {
chrome.contextMenus.onClicked.addListener((info, tab) => {
const appendContent = (tempCont, { openPopup } = { openPopup: false }) => {
chrome.storage.sync.get({ open_action: 'save_text', open_content: '' }, function (items) {
if (items.open_action === 'upload_image') {
t('picPending').then((m) => alert(m))
} else {
chrome.storage.sync.set({
return
}
chrome.storage.sync.set(
{
open_action: 'save_text',
open_content: items.open_content + tempCont
})
}
}
)
},
function () {
if (openPopup) tryOpenActionPopup(tab)
}
)
})
}
if (info.menuItemId === 'Memos-send-selection') {
const ref = info.linkUrl || info.pageUrl
const tabId = tab && tab.id
getSelectionTextFromTab(tabId, info.selectionText).then((selectionText) => {
const tempCont = selectionText + '\n' + `[Reference Link](${ref})` + '\n'
appendContent(tempCont, { openPopup: true })
})
return
}
if (info.menuItemId === 'Memos-send-link') {
appendContent((info.linkUrl || info.pageUrl) + '\n')
return
}
if (info.menuItemId === 'Memos-send-image') {
appendContent(`![](${info.srcUrl})` + '\n')
}
})
+3
View File
@@ -86,6 +86,7 @@ function applyStaticI18n() {
setText('lockPublic', 'lockPublic')
setText('content_submit_text', 'submitBtn')
setText('fullscreen', 'fullscreenBtn')
setPlaceholder('hideInput', 'placeHideInput')
setPlaceholder('showInput', 'placeShowInput')
@@ -111,6 +112,8 @@ function applyStaticI18n() {
setTitle('search', 'tipSearch')
setTitle('lock', 'tipVisibility')
setTitle('content_submit_text', 'tipSend')
setTitle('fullscreen', 'tipFullscreen')
setTitle('editor-resize-handle', 'tipResize')
}
async function setUiLanguage(nextLang, { persist = true } = {}) {
+158 -2
View File
@@ -1,6 +1,136 @@
dayjs.extend(window.dayjs_plugin_relativeTime)
let currentMemoLock = ''
function isFullscreenMode() {
try {
const params = new URLSearchParams(window.location.search || '')
return params.get('mode') === 'full'
} catch (_) {
return false
}
}
function openFullscreenTab() {
try {
const url = chrome.runtime.getURL('popup.html?mode=full')
chrome.tabs.create({ url })
} catch (_) {
// best-effort only
}
}
function initProportionalEditorResize() {
try {
if (isFullscreenMode()) return
const editor = document.querySelector('.memo-editor')
const tools = document.querySelector('.common-tools-wrapper')
const handle = document.getElementById('editor-resize-handle')
if (!editor || !tools || !handle) return
const safety = 8
const initialRect = editor.getBoundingClientRect()
const baseW = Math.ceil(initialRect.width)
const baseH = Math.ceil(initialRect.height)
// Lock the base size. Scaling will be applied by setting width/height.
editor.style.width = `${baseW}px`
editor.style.height = `${baseH}px`
editor.style.minWidth = `${baseW}px`
editor.style.minHeight = `${baseH}px`
let maxScale = 1
const computeMaxScale = () => {
// In popup mode, allow scaling up to Chrome's max popup size.
// Do not clamp by current window.innerWidth/innerHeight, otherwise the popup can't grow to the max.
const viewportW = 800
const viewportH = 600
const editorRect = editor.getBoundingClientRect()
const toolsRect = tools.getBoundingClientRect()
const toolsStyle = window.getComputedStyle(tools)
const gap = parseFloat(toolsStyle.marginTop || '0') || 0
const availW = Math.max(0, viewportW - safety - editorRect.left)
const availH = Math.max(0, viewportH - safety - toolsRect.height - editorRect.top - gap)
const scaleW = baseW > 0 ? availW / baseW : 1
const scaleH = baseH > 0 ? availH / baseH : 1
maxScale = Math.max(1, Math.min(scaleW, scaleH))
}
computeMaxScale()
window.addEventListener('resize', computeMaxScale)
let dragging = false
let startX = 0
let startY = 0
let startScale = 1
let rafId = 0
let pendingScale = null
const readCurrentScale = () => {
const w = parseFloat(editor.style.width || '')
const h = parseFloat(editor.style.height || '')
const sw = baseW > 0 && Number.isFinite(w) ? w / baseW : 1
const sh = baseH > 0 && Number.isFinite(h) ? h / baseH : 1
return Math.max(1, sw, sh)
}
const applyScale = (scale) => {
const s = Math.max(1, Math.min(maxScale, scale))
editor.style.width = `${Math.round(baseW * s)}px`
editor.style.height = `${Math.round(baseH * s)}px`
}
const scheduleApply = () => {
if (rafId) return
rafId = window.requestAnimationFrame(() => {
rafId = 0
if (pendingScale == null) return
const s = pendingScale
pendingScale = null
applyScale(s)
})
}
handle.addEventListener('pointerdown', (ev) => {
dragging = true
startX = ev.clientX
startY = ev.clientY
startScale = readCurrentScale()
computeMaxScale()
try { handle.setPointerCapture(ev.pointerId) } catch (_) {}
ev.preventDefault()
})
handle.addEventListener('pointermove', (ev) => {
if (!dragging) return
const dx = ev.clientX - startX
const dy = ev.clientY - startY
// Proportional scale based on diagonal length for smoother, more linear feel.
const diag0 = Math.hypot(baseW, baseH)
const targetW = baseW * startScale + dx
const targetH = baseH * startScale + dy
const diag1 = Math.hypot(targetW, targetH)
const next = diag0 > 0 ? diag1 / diag0 : startScale
pendingScale = next
scheduleApply()
})
const endDrag = () => {
dragging = false
}
handle.addEventListener('pointerup', endDrag)
handle.addEventListener('pointercancel', endDrag)
} catch (_) {
// best-effort only
}
}
function msg(key) {
if (typeof window.t === 'function') return window.t(key)
return chrome.i18n.getMessage(key) || ''
@@ -57,6 +187,10 @@ function updateLockNowText(lockType) {
applyDayjsLocaleByUiLanguage(typeof window.getUiLanguage === 'function' ? window.getUiLanguage() : 'auto')
if (isFullscreenMode()) {
document.body.classList.add('fullscreen')
}
window.addEventListener('i18n:changed', (ev) => {
applyDayjsLocaleByUiLanguage(ev && ev.detail ? ev.detail.lang : 'auto')
updateLockNowText(currentMemoLock)
@@ -143,11 +277,14 @@ get_info(function (info) {
//打开的时候就是上传图片
uploadImage(info.open_content)
} else {
$("textarea[name=text]").val(info.open_content)
const $textarea = $("textarea[name=text]")
$textarea.val(info.open_content)
focusTextareaToEnd($textarea)
}
relistNow = Array.isArray(info.resourceIdList) ? info.resourceIdList : []
renderUploadList(relistNow)
initProportionalEditorResize()
//从localstorage 里面读取数据
setTimeout(get_info, 1)
})
@@ -161,7 +298,7 @@ chrome.storage.onChanged.addListener(function (changes, areaName) {
renderUploadList(relistNow)
})
$("textarea[name=text]").focus()
// focus is handled after textarea content is set
//监听输入结束,保存未发送内容到本地
$("textarea[name=text]").blur(function () {
@@ -176,6 +313,11 @@ $("textarea[name=text]").on('keydown', function (ev) {
}
})
$('#fullscreen').on('click', function () {
if (isFullscreenMode()) return
openFullscreenTab()
})
//监听拖拽事件,实现拖拽到窗口上传图片
initDrag()
@@ -239,6 +381,20 @@ function escapeHtml(input) {
.replace(/'/g, ''')
}
function focusTextareaToEnd($textarea) {
try {
const el = $textarea && $textarea[0]
if (!el) return
el.focus()
const len = typeof el.value === 'string' ? el.value.length : 0
if (typeof el.setSelectionRange === 'function') {
el.setSelectionRange(len, len)
}
} catch (_) {
// best-effort only
}
}
function buildV1ResourceStreamUrl(info, resource) {
if (!info || !info.apiUrl || !resource) return ''
// Use the configured apiUrl as the base (may include a reverse-proxy subpath).
+2
View File
@@ -21,6 +21,8 @@
},
"permissions": [
"tabs",
"scripting",
"windows",
"storage",
"activeTab",
"contextMenus"
+2
View File
@@ -51,6 +51,7 @@
</div>
<div class="memo-editor">
<button id="fullscreen" class="action-btn" type="button"></button>
<textarea
class="common-editor-inputer"
rows="4"
@@ -59,6 +60,7 @@
placeholder=""
required=""
></textarea>
<div id="editor-resize-handle" aria-label="Resize"></div>
</div>
<div class="common-tools-wrapper">