2025-04-24 17:01:52 +08:00
import { enableLuteMarkdownSyntax , getTextStar , paste , restoreLuteMarkdownSyntax } from "../util/paste" ;
2022-05-26 15:18:53 +08:00
import {
hasClosestBlock ,
hasClosestByAttribute ,
2025-04-19 20:23:31 +08:00
hasClosestByClassName ,
hasClosestByTag ,
2024-11-25 22:52:02 +08:00
hasTopClosestByClassName ,
isInEmbedBlock ,
2022-05-26 15:18:53 +08:00
} from "../util/hasClosest" ;
import {
focusBlock ,
focusByRange ,
focusByWbr ,
focusSideBlock ,
getEditorRange ,
2024-11-25 22:52:02 +08:00
getSelectionOffset ,
2025-04-19 20:23:31 +08:00
setFirstNodeRange ,
setInsertWbrHTML ,
2023-02-09 12:21:02 +08:00
setLastNodeRange ,
2022-05-26 15:18:53 +08:00
} from "../util/selection" ;
import { Constants } from "../../constants" ;
2024-04-26 23:27:50 +08:00
import { isMobile } from "../../util/functions" ;
2023-09-22 20:24:00 +08:00
import { previewDocImage } from "../preview/image" ;
2023-06-29 22:07:41 +08:00
import {
2023-08-29 23:02:10 +08:00
contentMenu ,
enterBack ,
2023-06-29 22:07:41 +08:00
fileAnnotationRefMenu ,
2024-11-25 22:52:02 +08:00
imgMenu ,
inlineMathMenu ,
2023-06-29 22:07:41 +08:00
linkMenu ,
refMenu ,
setFold ,
tagMenu ,
zoomOut
} from "../../menus/protyle" ;
2022-05-26 15:18:53 +08:00
import * as dayjs from "dayjs" ;
import { dropEvent } from "../util/editorCommonEvent" ;
import { input } from "./input" ;
import {
getContenteditableElement ,
2023-02-09 12:21:02 +08:00
getNextBlock ,
2022-05-26 15:18:53 +08:00
getTopAloneElement ,
2023-02-09 12:21:02 +08:00
hasNextSibling ,
2025-04-19 20:23:31 +08:00
hasPreviousSibling ,
isEndOfBlock ,
2022-05-26 15:18:53 +08:00
isNotEditBlock
} from "./getBlock" ;
import { transaction , updateTransaction } from "./transaction" ;
import { hideElements } from "../ui/hideElements" ;
/// #if !BROWSER
2024-04-30 09:06:52 +08:00
import { ipcRenderer } from "electron" ;
2022-05-26 15:18:53 +08:00
/// #endif
2022-11-24 23:16:56 +08:00
import { getEnableHTML , removeEmbed } from "./removeEmbed" ;
2022-05-26 15:18:53 +08:00
import { keydown } from "./keydown" ;
import { openMobileFileById } from "../../mobile/editor" ;
import { removeBlock } from "./remove" ;
2023-06-07 10:36:20 +08:00
import { highlightRender } from "../render/highlightRender" ;
2022-05-26 15:18:53 +08:00
import { openAttr } from "../../menus/commonMenuItem" ;
2023-06-07 10:36:20 +08:00
import { blockRender } from "../render/blockRender" ;
2022-06-29 15:36:44 +08:00
/// #if !MOBILE
2022-06-29 20:25:30 +08:00
import { getAllModels } from "../../layout/getAll" ;
import { pushBack } from "../../util/backForward" ;
2024-05-04 14:45:21 +08:00
import { openFileById } from "../../editor/util" ;
2022-06-30 15:56:44 +08:00
import { openGlobalSearch } from "../../search/util" ;
2023-04-21 14:54:21 +08:00
/// #else
import { popSearch } from "../../mobile/menu/search" ;
2022-06-29 15:36:44 +08:00
/// #endif
2022-05-26 15:18:53 +08:00
import { BlockPanel } from "../../block/Panel" ;
2025-09-08 09:44:52 +08:00
import { copyPlainText , isInIOS , isMac , isOnlyMeta , readClipboard , encodeBase64 } from "../util/compatibility" ;
2022-05-26 15:18:53 +08:00
import { MenuItem } from "../../menus/Menu" ;
2025-09-06 18:02:18 +08:00
import { fetchPost , fetchSyncPost } from "../../util/fetch" ;
2022-05-26 15:18:53 +08:00
import { onGet } from "../util/onGet" ;
2025-05-08 17:58:26 +08:00
import { clearTableCell , isIncludeCell , setTableAlign } from "../util/table" ;
2022-06-28 10:19:15 +08:00
import { countBlockWord , countSelectWord } from "../../layout/status" ;
2022-06-29 10:35:35 +08:00
import { showMessage } from "../../dialog/message" ;
2022-09-30 18:05:24 +08:00
import { getBacklinkHeadingMore , loadBreadcrumb } from "./renderBacklink" ;
2023-03-07 10:42:20 +08:00
import { removeSearchMark } from "../toolbar/util" ;
2025-04-17 10:12:22 +08:00
import { activeBlur } from "../../mobile/util/keyboardToolbar" ;
2023-04-12 19:02:35 +08:00
import { commonClick } from "./commonClick" ;
2023-07-03 23:02:19 +08:00
import { avClick , avContextmenu , updateAVName } from "../render/av/action" ;
2025-06-21 10:14:07 +08:00
import { selectRow , stickyRow } from "../render/av/row" ;
2023-12-01 11:48:20 +08:00
import { showColMenu } from "../render/av/col" ;
import { openViewMenu } from "../render/av/view" ;
2023-12-12 12:09:31 +08:00
import { checkFold } from "../../util/noRelyPCFunction" ;
2024-01-14 18:02:14 +08:00
import {
2024-03-22 11:08:21 +08:00
addDragFill ,
2024-01-14 22:06:03 +08:00
dragFillCellsValue ,
2024-01-14 18:02:14 +08:00
genCellValueByElement ,
getCellText ,
getPositionByCellElement ,
getTypeByCellElement ,
updateCellsValue
} from "../render/av/cell" ;
2024-02-02 21:57:47 +08:00
import { openEmojiPanel , unicode2Emoji } from "../../emoji" ;
2024-05-04 14:45:21 +08:00
import { openLink } from "../../editor/openLink" ;
2024-05-09 16:35:04 +08:00
import { mathRender } from "../render/mathRender" ;
2024-05-15 11:17:34 +08:00
import { editAssetItem } from "../render/av/asset" ;
2024-12-20 12:23:49 +08:00
import { img3115 } from "../../boot/compatibleVersion" ;
2025-01-15 11:42:08 +08:00
import { globalClickHideMenu } from "../../boot/globalEvent/click" ;
2025-02-05 17:26:32 +08:00
import { hideTooltip } from "../../dialog/tooltip" ;
2025-06-19 18:00:52 +08:00
import { openGalleryItemMenu } from "../render/av/gallery/util" ;
2025-06-20 10:58:34 +08:00
import { clearSelect } from "../util/clearSelect" ;
2025-09-28 19:56:04 +08:00
import { chartRender } from "../render/chartRender" ;
2022-05-26 15:18:53 +08:00
export class WYSIWYG {
public lastHTMLs : { [ key : string ] : string } = { } ;
public element : HTMLDivElement ;
public preventKeyup : boolean ;
2024-01-13 22:34:46 +08:00
private preventClick : boolean ;
2023-10-03 13:28:08 +08:00
2023-06-01 20:50:49 +08:00
constructor ( protyle : IProtyle ) {
2022-05-26 15:18:53 +08:00
this . element = document . createElement ( "div" ) ;
this . element . className = "protyle-wysiwyg" ;
this . element . setAttribute ( "spellcheck" , "false" ) ;
2023-07-19 22:45:19 +08:00
if ( isMobile ( ) ) {
2022-05-26 15:18:53 +08:00
// iPhone, iPad 端输入 contenteditable 为 true 时会在块中间插入 span
2023-07-19 22:45:19 +08:00
// Android 端空块输入法弹出会收起 https://ld246.com/article/1689713888289
2022-05-26 15:18:53 +08:00
this . element . setAttribute ( "contenteditable" , "false" ) ;
} else {
this . element . setAttribute ( "contenteditable" , "true" ) ;
}
if ( window . siyuan . config . editor . displayBookmarkIcon ) {
this . element . classList . add ( "protyle-wysiwyg--attr" ) ;
}
2022-08-31 22:29:52 +08:00
this . bindCommonEvent ( protyle ) ;
2022-08-31 01:14:45 +08:00
if ( protyle . options . action . includes ( Constants . CB_GET_HISTORY ) ) {
return ;
}
2022-05-26 15:18:53 +08:00
this . bindEvent ( protyle ) ;
2023-06-01 20:50:49 +08:00
keydown ( protyle , this . element ) ;
dropEvent ( protyle , this . element ) ;
2022-05-26 15:18:53 +08:00
}
public renderCustom ( ial : IObject ) {
2023-09-09 14:54:05 +08:00
let isFullWidth = ial [ Constants . CUSTOM_SY_FULLWIDTH ] ;
2023-09-08 21:59:18 +08:00
if ( ! isFullWidth ) {
isFullWidth = window . siyuan . config . editor . fullWidth ? "true" : "false" ;
}
2023-09-13 22:42:57 +08:00
if ( isFullWidth === "true" ) {
2023-09-08 21:59:18 +08:00
this . element . parentElement . setAttribute ( "data-fullwidth" , "true" ) ;
} else {
this . element . parentElement . removeAttribute ( "data-fullwidth" ) ;
}
2022-05-26 15:18:53 +08:00
const ialKeys = Object . keys ( ial ) ;
for ( let i = 0 ; i < this . element . attributes . length ; i ++ ) {
const oldKey = this . element . attributes [ i ] . nodeName ;
2024-06-12 11:13:09 +08:00
if ( ! [ "type" , "class" , "spellcheck" , "contenteditable" , "data-doc-type" , "style" , "data-realwidth" , "data-readonly" ] . includes ( oldKey ) &&
2022-05-26 15:18:53 +08:00
! ialKeys . includes ( oldKey ) ) {
this . element . removeAttribute ( oldKey ) ;
i -- ;
}
}
ialKeys . forEach ( ( key : string ) = > {
2024-06-12 11:13:09 +08:00
if ( ! [ "title-img" , "title" , "updated" , "icon" , "id" , "type" , "class" , "spellcheck" , "contenteditable" , "data-doc-type" , "style" , "data-realwidth" , "data-readonly" ] . includes ( key ) ) {
2022-05-26 15:18:53 +08:00
this . element . setAttribute ( key , ial [ key ] ) ;
}
} ) ;
}
2022-09-17 17:55:02 +08:00
// text block-ref file-annotation-ref a 结尾处打字应为普通文本
2022-05-26 15:18:53 +08:00
private escapeInline ( protyle : IProtyle , range : Range , event : InputEvent ) {
2024-06-20 11:08:09 +08:00
if ( ! event . data && event . inputType !== "insertLineBreak" ) {
2022-05-26 15:18:53 +08:00
return ;
}
2024-06-20 11:08:09 +08:00
2022-05-26 15:18:53 +08:00
const inputData = event . data ;
protyle . toolbar . range = range ;
const inlineElement = range . startContainer . parentElement ;
2022-09-18 20:20:53 +08:00
const currentTypes = protyle . toolbar . getCurrentType ( ) ;
2022-09-22 22:52:05 +08:00
2024-06-20 11:08:09 +08:00
// https://github.com/siyuan-note/siyuan/issues/11766
if ( event . inputType === "insertLineBreak" ) {
if ( currentTypes . length > 0 && range . toString ( ) === "" && inlineElement . tagName === "SPAN" &&
inlineElement . textContent . startsWith ( "\n" ) &&
range . startContainer . previousSibling && range . startContainer . previousSibling . textContent === "\n" ) {
inlineElement . before ( range . startContainer . previousSibling ) ;
}
return ;
}
2022-09-22 22:52:05 +08:00
let dataLength = inputData . length ;
if ( inputData === "<" || inputData === ">" ) {
// 使用 inlineElement.innerHTML 会出现 https://ld246.com/article/1627185027423 中的第2个问题
dataLength = 4 ;
2024-08-17 11:50:07 +08:00
} else if ( inputData === "&" ) {
// https://github.com/siyuan-note/siyuan/issues/12239
dataLength = 5 ;
2022-09-22 22:52:05 +08:00
}
2022-09-23 09:10:31 +08:00
// https://github.com/siyuan-note/siyuan/issues/5924
2024-06-20 11:08:09 +08:00
if ( currentTypes . length > 0 && range . toString ( ) === "" && range . startOffset === inputData . length &&
inlineElement . tagName === "SPAN" &&
2022-09-23 09:10:31 +08:00
inlineElement . textContent . replace ( Constants . ZWSP , "" ) !== inputData &&
2022-09-30 18:05:24 +08:00
inlineElement . textContent . replace ( Constants . ZWSP , "" ) . length >= inputData . length &&
2022-09-22 22:52:05 +08:00
! hasPreviousSibling ( range . startContainer ) && ! hasPreviousSibling ( inlineElement ) ) {
const html = inlineElement . innerHTML . replace ( Constants . ZWSP , "" ) ;
inlineElement . innerHTML = html . substr ( dataLength ) ;
const textNode = document . createTextNode ( inputData ) ;
inlineElement . before ( textNode ) ;
range . selectNodeContents ( textNode ) ;
range . collapse ( false ) ;
return ;
}
2022-05-26 15:18:53 +08:00
if ( // 表格行内公式之前无法插入文字 https://github.com/siyuan-note/siyuan/issues/3908
2022-09-18 20:20:53 +08:00
inlineElement . tagName === "SPAN" &&
2022-09-23 09:10:31 +08:00
inlineElement . textContent !== inputData &&
2023-03-07 11:17:00 +08:00
! currentTypes . includes ( "search-mark" ) && // https://github.com/siyuan-note/siyuan/issues/7586
2025-02-17 17:51:43 +08:00
! currentTypes . includes ( "code" ) && // https://github.com/siyuan-note/siyuan/issues/13871
! currentTypes . includes ( "kbd" ) &&
! currentTypes . includes ( "tag" ) &&
2022-09-17 17:55:02 +08:00
range . toString ( ) === "" && range . startContainer . nodeType === 3 &&
2025-03-07 10:56:13 +08:00
( currentTypes . includes ( "inline-memo" ) || currentTypes . includes ( "block-ref" ) || currentTypes . includes ( "file-annotation-ref" ) || currentTypes . includes ( "a" ) ) &&
2022-09-17 17:55:02 +08:00
! hasNextSibling ( range . startContainer ) && range . startContainer . textContent . length === range . startOffset &&
2022-09-23 09:10:31 +08:00
inlineElement . textContent . length > inputData . length
2022-09-17 17:55:02 +08:00
) {
2022-05-26 15:18:53 +08:00
const position = getSelectionOffset ( inlineElement , protyle . wysiwyg . element , range ) ;
2022-09-23 09:10:31 +08:00
const html = inlineElement . innerHTML ;
2022-05-26 15:18:53 +08:00
if ( position . start === inlineElement . textContent . length ) {
// 使用 inlineElement.textContent **$a$b** 中数学公式消失
inlineElement . innerHTML = html . substr ( 0 , html . length - dataLength ) ;
const textNode = document . createTextNode ( inputData ) ;
2022-09-17 17:55:02 +08:00
inlineElement . after ( textNode ) ;
2022-05-26 15:18:53 +08:00
range . selectNodeContents ( textNode ) ;
range . collapse ( false ) ;
}
}
}
private setEmptyOutline ( protyle : IProtyle , element : HTMLElement ) {
let nodeElement = element ;
if ( ! element . getAttribute ( "data-node-id" ) ) {
const tempElement = hasClosestBlock ( element ) ;
if ( ! tempElement ) {
return ;
}
nodeElement = tempElement ;
}
2022-06-29 20:25:30 +08:00
/// #if !MOBILE
2022-05-26 15:18:53 +08:00
if ( protyle . model ) {
getAllModels ( ) . outline . forEach ( item = > {
if ( item . blockId === protyle . block . rootID ) {
2023-03-03 10:39:58 +08:00
item . setCurrent ( nodeElement ) ;
2022-05-26 15:18:53 +08:00
}
} ) ;
}
2024-05-13 23:41:44 +08:00
/// #else
if ( protyle . disabled ) {
protyle . toolbar . range = getEditorRange ( nodeElement ) ;
}
2022-06-29 20:25:30 +08:00
/// #endif
2022-05-26 15:18:53 +08:00
}
2023-04-18 11:57:19 +08:00
private emojiToMd ( element : HTMLElement ) {
2023-04-14 22:26:34 +08:00
element . querySelectorAll ( ".emoji" ) . forEach ( ( item : HTMLElement ) = > {
2023-04-14 22:42:21 +08:00
item . outerHTML = ` : ${ item . getAttribute ( "alt" ) } : ` ;
} ) ;
2023-04-14 22:26:34 +08:00
}
2022-09-15 20:19:56 +08:00
private bindCommonEvent ( protyle : IProtyle ) {
2025-09-06 18:02:18 +08:00
this . element . addEventListener ( "copy" , async ( event : ClipboardEvent & { target : HTMLElement } ) = > {
2022-10-27 23:10:29 +08:00
window . siyuan . ctrlIsPressed = false ; // https://github.com/siyuan-note/siyuan/issues/6373
2022-05-26 15:18:53 +08:00
// https://github.com/siyuan-note/siyuan/issues/4600
2024-03-13 15:41:10 +08:00
if ( event . target . tagName === "PROTYLE-HTML" || event . target . localName === "input" ) {
2022-05-26 15:18:53 +08:00
event . stopPropagation ( ) ;
return ;
}
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
const range = getEditorRange ( protyle . wysiwyg . element ) ;
const nodeElement = hasClosestBlock ( range . startContainer ) ;
if ( ! nodeElement ) {
return ;
}
2022-05-29 10:06:02 +08:00
const selectImgElement = nodeElement . querySelector ( ".img--select" ) ;
2023-12-15 12:04:18 +08:00
const selectAVElement = nodeElement . querySelector ( ".av__row--select, .av__cell--select" ) ;
2024-10-29 23:00:39 +08:00
const selectTableElement = nodeElement . querySelector ( ".table__select" ) ? . clientWidth > 0 ;
2022-05-26 15:18:53 +08:00
let selectElements = Array . from ( protyle . wysiwyg . element . querySelectorAll ( ".protyle-wysiwyg--select" ) ) ;
if ( selectElements . length === 0 && range . toString ( ) === "" && ! range . cloneContents ( ) . querySelector ( "img" ) &&
2024-10-29 23:00:39 +08:00
! selectImgElement && ! selectAVElement && ! selectTableElement ) {
2022-05-26 15:18:53 +08:00
nodeElement . classList . add ( "protyle-wysiwyg--select" ) ;
2022-06-28 10:20:55 +08:00
countBlockWord ( [ nodeElement . getAttribute ( "data-node-id" ) ] ) ;
2022-05-26 15:18:53 +08:00
selectElements = [ nodeElement ] ;
}
let html = "" ;
let textPlain = "" ;
2025-09-05 12:01:46 +08:00
let isInCodeBlock = false ;
2025-09-06 18:02:18 +08:00
let needClipboardWrite = false ;
2022-05-26 15:18:53 +08:00
if ( selectElements . length > 0 ) {
2023-02-06 21:54:26 +08:00
const isRefText = selectElements [ 0 ] . getAttribute ( "data-reftext" ) === "true" ;
2022-11-12 00:36:53 +08:00
if ( selectElements [ 0 ] . getAttribute ( "data-type" ) === "NodeListItem" &&
selectElements [ 0 ] . parentElement . classList . contains ( "list" ) && // 反链复制列表项 https://github.com/siyuan-note/siyuan/issues/6555
selectElements [ 0 ] . parentElement . childElementCount - 1 === selectElements . length ) {
2025-10-10 13:06:31 +08:00
const hasNoLiElement = selectElements . find ( item = > {
2025-10-17 22:57:52 +08:00
if ( ! selectElements [ 0 ] . parentElement . contains ( item ) ) {
2025-10-10 13:06:31 +08:00
return true ;
}
} ) ;
if ( ! hasNoLiElement ) {
selectElements = [ selectElements [ 0 ] . parentElement ] ;
}
}
let listHTML = "" ;
for ( let i = 0 ; i < selectElements . length ; i ++ ) {
const item = selectElements [ i ] as HTMLElement ;
// 复制列表项中的块会变为复制列表项,因此不能使用 getTopAloneElement https://github.com/siyuan-note/siyuan/issues/8925
2023-02-04 15:40:54 +08:00
if ( isRefText ) {
2025-10-10 13:06:31 +08:00
html += getTextStar ( item ) + "\n\n" ;
2023-02-04 15:40:54 +08:00
} else {
2025-10-13 11:32:18 +08:00
let itemHTML = "" ;
2025-10-10 13:06:31 +08:00
if ( item . getAttribute ( "data-type" ) === "NodeHeading" && item . getAttribute ( "fold" ) === "1" ) {
needClipboardWrite = true ;
const response = await fetchSyncPost ( "/api/block/getHeadingChildrenDOM" , {
id : item.getAttribute ( "data-node-id" ) ,
removeFoldAttr : false
} ) ;
itemHTML = response . data ;
} else if ( item . getAttribute ( "data-type" ) !== "NodeBlockQueryEmbed" && item . querySelector ( '[data-type="NodeHeading"][fold="1"]' ) ) {
needClipboardWrite = true ;
const response = await fetchSyncPost ( "/api/block/getBlockDOM" , {
id : item.getAttribute ( "data-node-id" ) ,
} ) ;
itemHTML = response . data . dom ;
2023-02-04 15:40:54 +08:00
} else {
2025-10-10 13:06:31 +08:00
itemHTML = removeEmbed ( item ) ;
}
2025-10-18 20:47:01 +08:00
if ( item . getAttribute ( "data-type" ) === "NodeListItem" ) {
if ( ! listHTML ) {
listHTML = ` <div data-subtype=" ${ item . getAttribute ( "data-subtype" ) } " data-node-id=" ${ Lute . NewNodeID ( ) } " data-type="NodeList" class="list"> ` ;
}
listHTML += itemHTML ;
if ( i === selectElements . length - 1 ||
selectElements [ i + 1 ] . getAttribute ( "data-type" ) !== "NodeListItem" ||
selectElements [ i + 1 ] . getAttribute ( "data-subtype" ) !== item . getAttribute ( "data-subtype" )
) {
html += ` ${ listHTML } <div class="protyle-attr" contenteditable="false"> ${ Constants . ZWSP } </div></div> ` ;
listHTML = "" ;
}
2025-10-10 13:06:31 +08:00
} else {
html += itemHTML ;
2023-02-04 15:40:54 +08:00
}
2025-09-06 18:02:18 +08:00
}
2022-05-26 15:18:53 +08:00
}
2024-04-01 17:33:18 +08:00
if ( isRefText ) {
2025-10-10 13:06:31 +08:00
html = html . slice ( 0 , - 2 ) ;
2024-04-01 17:33:18 +08:00
selectElements [ 0 ] . removeAttribute ( "data-reftext" ) ;
}
2023-12-15 12:04:18 +08:00
} else if ( selectAVElement ) {
2024-01-27 22:06:22 +08:00
const cellElements : Element [ ] = Array . from ( nodeElement . querySelectorAll ( ".av__cell--active, .av__cell--select" ) ) || [ ] ;
2023-12-15 12:04:18 +08:00
if ( cellElements . length === 0 ) {
nodeElement . querySelectorAll ( ".av__row--select:not(.av__row--header)" ) . forEach ( rowElement = > {
rowElement . querySelectorAll ( ".av__cell" ) . forEach ( cellElement = > {
2023-12-17 16:24:11 +08:00
cellElements . push ( cellElement ) ;
} ) ;
2023-12-15 12:04:18 +08:00
} ) ;
}
2024-01-27 22:06:22 +08:00
if ( cellElements . length > 0 ) {
html = "[" ;
2024-01-31 11:02:32 +08:00
cellElements . forEach ( ( item : HTMLElement , index ) = > {
2024-01-27 22:06:22 +08:00
const cellText = getCellText ( item ) ;
2025-07-15 12:36:19 +08:00
if ( index === 0 || (
2025-07-23 13:08:38 +08:00
cellElements [ index - 1 ] !== item . previousElementSibling &&
! ( item . previousElementSibling ? . classList . contains ( "av__colsticky" ) && ! cellElements [ index - 1 ] . nextElementSibling &&
cellElements [ index - 1 ] . parentElement === item . previousElementSibling )
2025-07-15 12:36:19 +08:00
) ) {
2024-03-23 10:25:15 +08:00
html += "[" ;
}
2024-01-27 22:06:22 +08:00
html += JSON . stringify ( genCellValueByElement ( getTypeByCellElement ( item ) , item ) ) + "," ;
2025-07-15 12:36:19 +08:00
if ( index === cellElements . length - 1 || (
2025-07-23 13:08:38 +08:00
cellElements [ index + 1 ] !== item . nextElementSibling &&
! ( ! item . nextElementSibling && item . parentElement . nextElementSibling === cellElements [ index + 1 ] )
2025-07-15 12:36:19 +08:00
) ) {
2024-03-23 10:25:15 +08:00
html = html . substring ( 0 , html . length - 1 ) + "]," ;
2025-07-15 12:36:19 +08:00
textPlain += cellText + "\n" ;
} else {
textPlain += cellText + "\t" ;
2024-03-23 10:25:15 +08:00
}
2024-01-27 22:06:22 +08:00
} ) ;
2025-07-16 10:31:50 +08:00
textPlain = textPlain . substring ( 0 , textPlain . length - 1 ) ;
2024-01-27 22:06:22 +08:00
html = html . substring ( 0 , html . length - 1 ) + "]" ;
}
2024-10-29 23:00:39 +08:00
} else if ( selectTableElement ) {
const selectCellElements : HTMLTableCellElement [ ] = [ ] ;
const scrollLeft = nodeElement . firstElementChild . scrollLeft ;
2025-05-08 17:58:26 +08:00
const scrollTop = nodeElement . querySelector ( "table" ) . scrollTop ;
2024-10-29 23:00:39 +08:00
const tableSelectElement = nodeElement . querySelector ( ".table__select" ) as HTMLElement ;
2024-10-29 23:01:31 +08:00
html = "<table>" ;
2024-10-29 23:00:39 +08:00
nodeElement . querySelectorAll ( "th, td" ) . forEach ( ( item : HTMLTableCellElement ) = > {
2025-05-08 17:58:26 +08:00
if ( ! item . classList . contains ( "fn__none" ) && isIncludeCell ( {
tableSelectElement ,
scrollLeft ,
scrollTop ,
item ,
} ) ) {
2024-10-29 23:00:39 +08:00
selectCellElements . push ( item ) ;
}
} ) ;
selectCellElements . forEach ( ( item , index ) = > {
if ( index === 0 || ! item . previousElementSibling ||
2025-07-23 13:08:38 +08:00
item . previousElementSibling !== selectCellElements [ index - 1 ] ) {
2024-10-29 23:01:31 +08:00
html += "<tr>" ;
2024-10-29 23:00:39 +08:00
}
html += item . outerHTML ;
if ( ! item . nextElementSibling || ! selectCellElements [ index + 1 ] ||
2025-07-23 13:08:38 +08:00
item . nextElementSibling !== selectCellElements [ index + 1 ] ) {
2024-10-29 23:00:39 +08:00
html += "</tr>" ;
}
2024-10-29 23:01:31 +08:00
} ) ;
html += "</table>" ;
2024-10-29 23:00:39 +08:00
textPlain = protyle . lute . HTML2Md ( html ) ;
2022-05-26 15:18:53 +08:00
} else {
const tempElement = document . createElement ( "div" ) ;
2022-07-30 16:29:11 +08:00
// https://github.com/siyuan-note/siyuan/issues/5540
2022-07-31 10:18:05 +08:00
const selectTypes = protyle . toolbar . getCurrentType ( range ) ;
2025-02-17 12:27:28 +08:00
const spanElement = hasClosestByTag ( range . startContainer , "SPAN" ) ;
const headingElement = hasClosestByAttribute ( range . startContainer , "data-type" , "NodeHeading" ) ;
2025-07-02 17:19:46 +08:00
const matchHeading = headingElement && headingElement . textContent . replace ( Constants . ZWSP , "" ) === range . toString ( ) ;
2025-02-17 12:27:28 +08:00
if ( ( selectTypes . length > 0 && spanElement && spanElement . textContent . replace ( Constants . ZWSP , "" ) === range . toString ( ) ) ||
2025-07-02 17:19:46 +08:00
matchHeading ) {
if ( matchHeading ) {
2022-05-26 15:18:53 +08:00
// 复制标题 https://github.com/siyuan-note/insider/issues/297
2025-02-17 12:27:28 +08:00
tempElement . append ( headingElement . cloneNode ( true ) ) ;
2025-09-06 18:02:18 +08:00
// https://github.com/siyuan-note/siyuan/issues/13232
headingElement . removeAttribute ( "fold" ) ;
2022-09-19 16:47:38 +08:00
} else if ( ! [ "DIV" , "TD" , "TH" , "TR" ] . includes ( range . startContainer . parentElement . tagName ) ) {
2022-05-26 15:18:53 +08:00
// 复制行内元素 https://github.com/siyuan-note/insider/issues/191
tempElement . append ( range . startContainer . parentElement . cloneNode ( true ) ) ;
2023-04-14 22:42:21 +08:00
this . emojiToMd ( tempElement ) ;
2022-05-26 15:18:53 +08:00
} else {
// 直接复制块 https://github.com/siyuan-note/insider/issues/318
tempElement . append ( range . cloneContents ( ) ) ;
2023-04-14 22:42:21 +08:00
this . emojiToMd ( tempElement ) ;
2022-05-26 15:18:53 +08:00
}
html = tempElement . innerHTML ;
2025-01-14 22:28:03 +08:00
textPlain = range . toString ( ) ;
2022-05-26 15:18:53 +08:00
} else if ( selectImgElement ) {
html = selectImgElement . outerHTML ;
2025-01-14 22:28:03 +08:00
textPlain = selectImgElement . querySelector ( "img" ) . getAttribute ( "data-src" ) ;
2025-02-17 12:27:28 +08:00
} else if ( selectTypes . length > 0 && range . startContainer . nodeType === 3 &&
range . startContainer . parentElement . tagName === "SPAN" &&
2025-07-23 13:08:38 +08:00
range . startContainer . parentElement === range . endContainer . parentElement ) {
2022-09-16 11:54:27 +08:00
// 复制粗体等字体中的一部分
2022-09-18 17:51:19 +08:00
const attributes = range . startContainer . parentElement . attributes ;
2022-09-16 11:54:27 +08:00
const spanElement = document . createElement ( "span" ) ;
for ( let i = 0 ; i < attributes . length ; i ++ ) {
spanElement . setAttribute ( attributes [ i ] . name , attributes [ i ] . value ) ;
}
2022-09-30 22:26:02 +08:00
if ( spanElement . getAttribute ( "data-type" ) . indexOf ( "block-ref" ) > - 1 &&
spanElement . getAttribute ( "data-subtype" ) === "d" ) {
// 需变为静态锚文本
spanElement . setAttribute ( "data-subtype" , "s" ) ;
}
2022-09-18 17:51:19 +08:00
spanElement . textContent = range . toString ( ) ;
2022-09-16 11:54:27 +08:00
html = spanElement . outerHTML ;
2025-01-14 22:28:03 +08:00
textPlain = range . toString ( ) ;
2022-05-26 15:18:53 +08:00
} else {
tempElement . append ( range . cloneContents ( ) ) ;
2023-04-14 22:42:21 +08:00
this . emojiToMd ( tempElement ) ;
2022-05-26 15:18:53 +08:00
const inlineMathElement = hasClosestByAttribute ( range . commonAncestorContainer , "data-type" , "inline-math" ) ;
if ( inlineMathElement ) {
// 表格内复制数学公式 https://ld246.com/article/1631708573504
html = inlineMathElement . outerHTML ;
} else {
html = tempElement . innerHTML ;
}
// 不能使用 commonAncestorContainer https://ld246.com/article/1643282894693
2025-02-05 10:21:23 +08:00
textPlain = tempElement . textContent ;
2025-02-07 11:18:32 +08:00
if ( hasClosestByAttribute ( range . startContainer , "data-type" , "NodeCodeBlock" ) ) {
2025-02-08 09:25:14 +08:00
if ( isEndOfBlock ( range ) ) {
2025-02-04 21:55:49 +08:00
textPlain = textPlain . replace ( /\n$/ , "" ) ;
}
2025-09-05 12:01:46 +08:00
isInCodeBlock = true ;
2025-04-16 20:57:17 +08:00
} else if ( hasClosestByTag ( range . startContainer , "TD" ) || hasClosestByTag ( range . startContainer , "TH" ) ) {
tempElement . innerHTML = tempElement . innerHTML . replace ( /<br>/g , "\n" ) . replace ( /<br\/>/g , "\n" ) ;
2025-04-17 23:33:04 +08:00
textPlain = tempElement . textContent . endsWith ( "\n" ) ? tempElement . textContent . replace ( /\n$/ , "" ) : tempElement . textContent ;
2025-02-09 12:04:40 +08:00
} else if ( ! hasClosestByTag ( range . startContainer , "CODE" ) ) {
2025-01-14 22:28:03 +08:00
textPlain = range . toString ( ) ;
2022-05-26 15:18:53 +08:00
}
}
}
2022-11-24 23:16:56 +08:00
if ( protyle . disabled ) {
2022-11-25 23:09:16 +08:00
html = getEnableHTML ( html ) ;
2022-11-24 23:16:56 +08:00
}
2023-10-09 21:45:36 +08:00
textPlain = textPlain || protyle . lute . BlockDOM2StdMd ( html ) . trimEnd ( ) ;
2025-02-03 11:19:37 +08:00
textPlain = textPlain . replace ( /\u00A0/g , " " ) // Replace non-breaking spaces with normal spaces when copying https://github.com/siyuan-note/siyuan/issues/9382
// Remove ZWSP when copying inline elements https://github.com/siyuan-note/siyuan/issues/13882
. replace ( new RegExp ( Constants . ZWSP , "g" ) , "" ) ;
2023-10-09 21:45:36 +08:00
event . clipboardData . setData ( "text/plain" , textPlain ) ;
2025-07-17 17:26:58 +08:00
2025-09-06 18:02:18 +08:00
if ( ! isInCodeBlock ) {
enableLuteMarkdownSyntax ( protyle ) ;
const textSiyuan = selectTableElement ? protyle . lute . HTML2BlockDOM ( html ) : html ;
event . clipboardData . setData ( "text/siyuan" , textSiyuan ) ;
restoreLuteMarkdownSyntax ( protyle ) ;
// 在 text/html 中插入注释节点,用于右键菜单粘贴时获取 text/siyuan 数据
const textHTML = ` <!--data-siyuan=' ${ encodeBase64 ( textSiyuan ) } '--> ` + ( selectTableElement ? html : protyle.lute.BlockDOM2HTML ( selectAVElement ? textPlain : html ) ) ;
event . clipboardData . setData ( "text/html" , textHTML ) ;
if ( needClipboardWrite ) {
try {
await navigator . clipboard . write ( [ new ClipboardItem ( {
[ "text/plain" ] : textPlain ,
[ "text/html" ] : textHTML ,
} ) ] ) ;
} catch ( e ) {
console . log ( "Copy write clipboard error:" , e ) ;
}
}
2025-09-05 12:01:46 +08:00
}
2022-05-26 15:18:53 +08:00
} ) ;
2023-10-03 13:28:08 +08:00
2022-08-31 22:29:52 +08:00
this . element . addEventListener ( "mousedown" , ( event : MouseEvent ) = > {
2025-02-05 17:08:35 +08:00
protyle . wysiwyg . element . classList . remove ( "protyle-wysiwyg--hiderange" ) ;
2025-06-20 13:35:12 +08:00
if ( event . button === 2 ) {
2022-08-31 22:29:52 +08:00
// 右键
2022-05-26 15:18:53 +08:00
return ;
}
2025-06-30 22:27:15 +08:00
const documentSelf = document ;
documentSelf . onmouseup = null ;
2025-06-20 13:35:12 +08:00
let target = event . target as HTMLElement ;
2025-06-21 10:13:44 +08:00
let nodeElement = hasClosestBlock ( target ) as HTMLElement ;
2025-06-20 13:35:12 +08:00
const hasSelectClassElement = this . element . querySelector ( ".protyle-wysiwyg--select" ) ;
const galleryItemElement = hasClosestByClassName ( target , "av__gallery-item" ) ;
if ( event . shiftKey ) {
let startElement ;
let endElement = nodeElement ;
// Electron 更新后 shift 向上点击获取的 range 不为上一个位置的 https://github.com/siyuan-note/siyuan/issues/9334
if ( getSelection ( ) . rangeCount > 0 ) {
startElement = hasClosestBlock ( getSelection ( ) . getRangeAt ( 0 ) . startContainer ) as HTMLElement ;
}
// shift 多选
if ( ! hasSelectClassElement && galleryItemElement ) {
galleryItemElement . classList . add ( "av__gallery-item--select" ) ;
let sideElement = galleryItemElement . previousElementSibling ;
let previousList : Element [ ] = [ ] ;
while ( sideElement ) {
if ( sideElement . classList . contains ( "av__gallery-item--select" ) ) {
break ;
} else {
previousList . push ( sideElement ) ;
}
sideElement = sideElement . previousElementSibling ;
if ( ! sideElement ) {
previousList = [ ] ;
break ;
}
}
sideElement = galleryItemElement . nextElementSibling ;
let nextList : Element [ ] = [ ] ;
while ( sideElement ) {
if ( sideElement . classList . contains ( "av__gallery-item--select" ) ) {
break ;
} else {
nextList . push ( sideElement ) ;
}
sideElement = sideElement . nextElementSibling as HTMLElement ;
2025-06-21 11:20:21 +08:00
if ( ! sideElement || sideElement . classList . contains ( "av__gallery-add" ) ) {
2025-06-20 13:35:12 +08:00
nextList = [ ] ;
break ;
}
}
previousList . concat ( nextList ) . forEach ( item = > {
item . classList . add ( "av__gallery-item--select" ) ;
} ) ;
2025-07-03 10:46:50 +08:00
event . preventDefault ( ) ;
2025-07-23 13:08:38 +08:00
} else if ( startElement && endElement && startElement !== endElement ) {
2025-06-20 13:35:12 +08:00
let toDown = true ;
const startRect = startElement . getBoundingClientRect ( ) ;
const endRect = endElement . getBoundingClientRect ( ) ;
let startTop = startRect . top ;
let endTop = endRect . top ;
if ( startTop === endTop ) {
// 横排 https://ld246.com/article/1663036247544
startTop = startRect . left ;
endTop = endRect . left ;
}
if ( startTop > endTop ) {
const tempElement = endElement ;
endElement = startElement ;
startElement = tempElement ;
const tempTop = endTop ;
endTop = startTop ;
startTop = tempTop ;
toDown = false ;
}
let selectElements : Element [ ] = [ ] ;
let currentElement : HTMLElement = startElement ;
let hasJump = false ;
while ( currentElement ) {
if ( currentElement && ! currentElement . classList . contains ( "protyle-attr" ) ) {
const currentRect = currentElement . getBoundingClientRect ( ) ;
if ( startRect . top === endRect . top ? ( currentRect . left <= endTop ) : ( currentRect . top <= endTop ) ) {
if ( hasJump ) {
// 父节点的下个节点在选中范围内才可使用父节点作为选中节点
if ( currentElement . nextElementSibling && ! currentElement . nextElementSibling . classList . contains ( "protyle-attr" ) ) {
const currentNextRect = currentElement . nextElementSibling . getBoundingClientRect ( ) ;
if ( startRect . top === endRect . top ?
( currentNextRect . left <= endTop && currentNextRect . bottom <= endRect . bottom ) :
( currentNextRect . top <= endTop ) ) {
selectElements = [ currentElement ] ;
currentElement = currentElement . nextElementSibling as HTMLElement ;
hasJump = false ;
} else if ( currentElement . parentElement . classList . contains ( "sb" ) ) {
currentElement = hasClosestBlock ( currentElement . parentElement ) as HTMLElement ;
hasJump = true ;
} else {
break ;
}
} else {
currentElement = hasClosestBlock ( currentElement . parentElement ) as HTMLElement ;
hasJump = true ;
}
} else {
selectElements . push ( currentElement ) ;
currentElement = currentElement . nextElementSibling as HTMLElement ;
}
} else if ( currentElement . parentElement . classList . contains ( "sb" ) ) {
// 跳出超级块横向排版中的未选中元素
currentElement = hasClosestBlock ( currentElement . parentElement ) as HTMLElement ;
hasJump = true ;
} else {
break ;
}
} else {
currentElement = hasClosestBlock ( currentElement . parentElement ) as HTMLElement ;
hasJump = true ;
}
}
if ( selectElements . length === 1 && ! selectElements [ 0 ] . classList . contains ( "list" ) && ! selectElements [ 0 ] . classList . contains ( "bq" ) && ! selectElements [ 0 ] . classList . contains ( "sb" ) ) {
// 单个 p 不选中
} else {
const ids : string [ ] = [ ] ;
if ( ! hasSelectClassElement && protyle . scroll && ! protyle . scroll . element . classList . contains ( "fn__none" ) && ! protyle . scroll . keepLazyLoad &&
( startElement . getBoundingClientRect ( ) . top < - protyle . contentElement . clientHeight * 2 || endElement . getBoundingClientRect ( ) . bottom > protyle . contentElement . clientHeight * 2 ) ) {
showMessage ( window . siyuan . languages . crossKeepLazyLoad ) ;
}
selectElements . forEach ( item = > {
2025-06-23 10:21:37 +08:00
if ( ! hasClosestByClassName ( currentElement , "protyle-wysiwyg--select" ) ) {
item . classList . add ( "protyle-wysiwyg--select" ) ;
ids . push ( item . getAttribute ( "data-node-id" ) ) ;
// 清除选中的子块 https://ld246.com/article/1667826582251
item . querySelectorAll ( ".protyle-wysiwyg--select" ) . forEach ( subItem = > {
subItem . classList . remove ( "protyle-wysiwyg--select" ) ;
} ) ;
}
2025-06-20 13:35:12 +08:00
} ) ;
countBlockWord ( ids ) ;
if ( toDown ) {
focusBlock ( selectElements [ selectElements . length - 1 ] , protyle . wysiwyg . element , false ) ;
} else {
focusBlock ( selectElements [ 0 ] , protyle . wysiwyg . element , false ) ;
}
}
2025-07-03 10:46:50 +08:00
event . preventDefault ( ) ;
2025-06-20 13:35:12 +08:00
}
return ;
2023-10-03 13:28:08 +08:00
}
2025-06-20 13:35:12 +08:00
if ( isOnlyMeta ( event ) && ! event . shiftKey && ! event . altKey ) {
let ctrlElement = nodeElement ;
if ( ! hasSelectClassElement && galleryItemElement ) {
galleryItemElement . classList . toggle ( "av__gallery-item--select" ) ;
} else if ( ctrlElement ) {
const embedBlockElement = isInEmbedBlock ( ctrlElement ) ;
if ( embedBlockElement ) {
ctrlElement = embedBlockElement ;
}
ctrlElement = getTopAloneElement ( ctrlElement ) as HTMLElement ;
if ( ctrlElement . classList . contains ( "protyle-wysiwyg--select" ) ) {
ctrlElement . classList . remove ( "protyle-wysiwyg--select" ) ;
ctrlElement . removeAttribute ( "select-start" ) ;
ctrlElement . removeAttribute ( "select-end" ) ;
} else {
ctrlElement . classList . add ( "protyle-wysiwyg--select" ) ;
}
ctrlElement . querySelectorAll ( ".protyle-wysiwyg--select" ) . forEach ( item = > {
item . classList . remove ( "protyle-wysiwyg--select" ) ;
item . removeAttribute ( "select-start" ) ;
item . removeAttribute ( "select-end" ) ;
} ) ;
const ctrlParentElement = hasClosestByClassName ( ctrlElement . parentElement , "protyle-wysiwyg--select" ) ;
if ( ctrlParentElement ) {
ctrlParentElement . classList . remove ( "protyle-wysiwyg--select" ) ;
ctrlParentElement . removeAttribute ( "select-start" ) ;
ctrlParentElement . removeAttribute ( "select-end" ) ;
}
const ids : string [ ] = [ ] ;
protyle . wysiwyg . element . querySelectorAll ( ".protyle-wysiwyg--select" ) . forEach ( item = > {
ids . push ( item . getAttribute ( "data-node-id" ) ) ;
} ) ;
countBlockWord ( ids ) ;
}
return ;
2022-08-31 01:14:45 +08:00
}
2025-06-30 22:27:15 +08:00
2025-06-22 18:06:26 +08:00
// https://github.com/siyuan-note/siyuan/issues/15100
2025-07-04 11:03:15 +08:00
if ( galleryItemElement && ! hasClosestByAttribute ( target , "data-type" , "av-gallery-more" ) ) {
2025-06-29 22:34:08 +08:00
documentSelf . onmouseup = ( ) = > {
documentSelf . onmousemove = null ;
documentSelf . onmouseup = null ;
documentSelf . ondragstart = null ;
documentSelf . onselectstart = null ;
documentSelf . onselect = null ;
clearSelect ( [ "galleryItem" ] , protyle . wysiwyg . element ) ;
return false ;
} ;
2025-06-22 18:06:26 +08:00
return ;
}
2025-06-29 17:01:12 +08:00
const avDragFillElement = hasClosestByClassName ( target , "av__drag-fill" ) ;
2025-06-20 13:35:12 +08:00
// https://github.com/siyuan-note/siyuan/issues/3026
hideElements ( [ "select" ] , protyle ) ;
if ( hasClosestByAttribute ( target , "data-type" , "av-gallery-more" ) ) {
clearSelect ( [ "img" , "row" , "cell" ] , protyle . wysiwyg . element ) ;
2025-06-29 17:01:12 +08:00
} else if ( ! hasClosestByClassName ( target , "av__firstcol" ) && ! avDragFillElement ) {
2025-06-20 13:35:12 +08:00
clearSelect ( [ "img" , "av" ] , protyle . wysiwyg . element ) ;
}
2025-07-01 17:40:49 +08:00
if ( ( hasClosestByClassName ( target , "protyle-action" ) && ! hasClosestByClassName ( target , "code-block" ) ) ||
2024-01-14 12:09:42 +08:00
( hasClosestByClassName ( target , "av__cell--header" ) && ! hasClosestByClassName ( target , "av__widthdrag" ) ) ) {
2022-05-26 15:18:53 +08:00
return ;
}
2025-05-19 21:07:53 +08:00
const wysiwygRect = protyle . wysiwyg . element . getBoundingClientRect ( ) ;
const wysiwygStyle = window . getComputedStyle ( protyle . wysiwyg . element ) ;
2025-05-19 23:29:23 +08:00
const mostLeft = wysiwygRect . left + ( parseInt ( wysiwygStyle . paddingLeft ) || 24 ) + 1 ;
const mostRight = wysiwygRect . right - ( parseInt ( wysiwygStyle . paddingRight ) || 16 ) - 2 ;
const protyleRect = protyle . element . getBoundingClientRect ( ) ;
2025-05-19 21:07:53 +08:00
const mostBottom = protyleRect . bottom ;
2022-08-31 22:29:52 +08:00
const y = event . clientY ;
2024-06-19 22:56:34 +08:00
const contentRect = protyle . contentElement . getBoundingClientRect ( ) ;
2023-07-02 23:58:00 +08:00
// av col resize
if ( ! protyle . disabled && target . classList . contains ( "av__widthdrag" ) ) {
if ( ! nodeElement ) {
return ;
}
const avId = nodeElement . getAttribute ( "data-av-id" ) ;
2024-03-04 17:42:54 +08:00
const blockID = nodeElement . dataset . nodeId ;
2023-07-02 23:58:00 +08:00
const dragElement = target . parentElement ;
const oldWidth = dragElement . clientWidth ;
2023-07-09 23:54:32 +08:00
const dragColId = dragElement . getAttribute ( "data-col-id" ) ;
2024-04-02 23:58:00 +08:00
let newWidth : number ;
2023-11-15 18:40:01 +08:00
const scrollElement = nodeElement . querySelector ( ".av__scroll" ) ;
2023-07-02 23:58:00 +08:00
documentSelf . onmousemove = ( moveEvent : MouseEvent ) = > {
2024-04-02 23:58:00 +08:00
newWidth = Math . max ( oldWidth + ( moveEvent . clientX - event . clientX ) , 25 ) ;
2023-11-15 18:40:01 +08:00
scrollElement . querySelectorAll ( ".av__row, .av__row--footer" ) . forEach ( item = > {
2024-06-17 10:35:13 +08:00
( item . querySelector ( ` [data-col-id=" ${ dragColId } "] ` ) as HTMLElement ) . style . width = newWidth + "px" ;
2023-11-15 18:40:01 +08:00
} ) ;
stickyRow ( nodeElement , contentRect , "bottom" ) ;
2023-07-02 23:58:00 +08:00
} ;
documentSelf . onmouseup = ( ) = > {
documentSelf . onmousemove = null ;
documentSelf . onmouseup = null ;
documentSelf . ondragstart = null ;
documentSelf . onselectstart = null ;
documentSelf . onselect = null ;
2024-04-02 23:58:00 +08:00
if ( ! newWidth || newWidth === oldWidth ) {
2023-10-27 11:34:53 +08:00
return ;
}
2025-11-14 23:55:07 +08:00
const viewID = nodeElement . getAttribute ( Constants . CUSTOM_SY_AV_VIEW ) ;
2023-07-02 23:58:00 +08:00
transaction ( protyle , [ {
action : "setAttrViewColWidth" ,
2023-07-09 23:54:32 +08:00
id : dragColId ,
2023-07-11 23:13:19 +08:00
avID : avId ,
2024-04-04 10:29:47 +08:00
data : newWidth + "px" ,
2024-04-20 10:04:21 +08:00
blockID ,
2025-11-14 23:55:07 +08:00
viewID // https://github.com/siyuan-note/siyuan/issues/11019
2023-07-02 23:58:00 +08:00
} ] , [ {
action : "setAttrViewColWidth" ,
2023-07-09 23:54:32 +08:00
id : dragColId ,
2023-07-11 23:13:19 +08:00
avID : avId ,
2024-03-04 17:42:54 +08:00
data : oldWidth + "px" ,
2024-04-20 10:04:21 +08:00
blockID ,
2025-11-14 23:55:07 +08:00
viewID
2023-07-02 23:58:00 +08:00
} ] ) ;
} ;
2024-01-13 22:34:46 +08:00
this . preventClick = true ;
2023-07-02 23:58:00 +08:00
event . preventDefault ( ) ;
return ;
}
2024-01-14 18:02:14 +08:00
// av drag fill
if ( ! protyle . disabled && avDragFillElement ) {
if ( ! nodeElement ) {
return ;
}
2024-01-16 10:56:55 +08:00
const originData : { [ key : string ] : IAVCellValue [ ] } = { } ;
2025-08-12 01:23:47 +08:00
let lastOriginCellElement : HTMLElement ;
2024-01-16 10:56:55 +08:00
const originCellIds : string [ ] = [ ] ;
nodeElement . querySelectorAll ( ".av__cell--active" ) . forEach ( ( item : HTMLElement ) = > {
2024-01-14 18:02:14 +08:00
const rowElement = hasClosestByClassName ( item , "av__row" ) ;
if ( rowElement ) {
if ( ! originData [ rowElement . dataset . id ] ) {
originData [ rowElement . dataset . id ] = [ ] ;
}
originData [ rowElement . dataset . id ] . push ( genCellValueByElement ( getTypeByCellElement ( item ) , item ) ) ;
2024-01-16 10:56:55 +08:00
lastOriginCellElement = item ;
originCellIds . push ( item . dataset . id ) ;
2024-01-14 18:02:14 +08:00
}
} ) ;
const dragFillCellIndex = getPositionByCellElement ( lastOriginCellElement ) ;
const firstCellIndex = getPositionByCellElement ( nodeElement . querySelector ( ".av__cell--active" ) ) ;
2024-10-26 17:06:35 +08:00
let moveAVCellElement : HTMLElement ;
2024-01-14 18:02:14 +08:00
let lastCellElement : HTMLElement ;
documentSelf . onmousemove = ( moveEvent : MouseEvent ) = > {
const tempCellElement = hasClosestByClassName ( moveEvent . target as HTMLElement , "av__cell" ) as HTMLElement ;
2025-07-23 12:21:59 +08:00
if ( moveAVCellElement && tempCellElement && ( tempCellElement === moveAVCellElement ) ) {
2024-01-14 18:02:14 +08:00
return ;
}
2024-10-26 17:06:35 +08:00
moveAVCellElement = tempCellElement ;
if ( moveAVCellElement && moveAVCellElement . dataset . id ) {
const newIndex = getPositionByCellElement ( moveAVCellElement ) ;
2024-01-14 18:02:14 +08:00
nodeElement . querySelectorAll ( ".av__cell--active" ) . forEach ( ( item : HTMLElement ) = > {
2024-01-14 22:06:03 +08:00
if ( ! originCellIds . includes ( item . dataset . id ) ) {
2024-01-14 18:02:14 +08:00
item . classList . remove ( "av__cell--active" ) ;
}
} ) ;
2024-10-26 17:06:35 +08:00
if ( newIndex . celIndex !== dragFillCellIndex . celIndex ) {
2024-01-16 10:56:55 +08:00
lastCellElement = undefined ;
2024-01-14 18:02:14 +08:00
return ;
}
nodeElement . querySelectorAll ( ".av__row" ) . forEach ( ( rowElement : HTMLElement , index : number ) = > {
2024-10-26 17:06:35 +08:00
if ( ( newIndex . rowIndex < firstCellIndex . rowIndex && index >= newIndex . rowIndex && index < firstCellIndex . rowIndex ) ||
2024-10-29 12:08:37 +08:00
( newIndex . rowIndex > dragFillCellIndex . rowIndex && index <= newIndex . rowIndex && index > dragFillCellIndex . rowIndex ) ) {
2024-01-14 18:02:14 +08:00
rowElement . querySelectorAll ( ".av__cell" ) . forEach ( ( cellElement : HTMLElement , cellIndex : number ) = > {
if ( cellIndex >= firstCellIndex . celIndex && cellIndex <= newIndex . celIndex ) {
cellElement . classList . add ( "av__cell--active" ) ;
lastCellElement = cellElement ;
}
} ) ;
}
} ) ;
}
} ;
documentSelf . onmouseup = ( ) = > {
documentSelf . onmousemove = null ;
documentSelf . onmouseup = null ;
documentSelf . ondragstart = null ;
documentSelf . onselectstart = null ;
documentSelf . onselect = null ;
if ( lastCellElement ) {
2025-08-12 01:23:47 +08:00
dragFillCellsValue ( protyle , nodeElement , originData , originCellIds , lastOriginCellElement ) ;
2024-10-30 23:00:52 +08:00
const allActiveCellsElement = nodeElement . querySelectorAll ( ".av__cell--active" ) ;
2024-10-30 23:00:06 +08:00
addDragFill ( allActiveCellsElement [ allActiveCellsElement . length - 1 ] ) ;
2024-01-14 18:02:14 +08:00
}
return false ;
} ;
2024-01-14 22:06:03 +08:00
this . preventClick = true ;
2024-01-14 18:02:14 +08:00
return false ;
}
2024-01-13 22:34:46 +08:00
// av cell select
const avCellElement = hasClosestByClassName ( target , "av__cell" ) ;
2024-07-25 17:55:25 +08:00
if ( ! protyle . disabled && avCellElement && avCellElement . dataset . id && ! isInEmbedBlock ( avCellElement ) ) {
2025-06-18 18:13:38 +08:00
if ( ! nodeElement || nodeElement . dataset . avType !== "table" ) {
2024-01-13 22:34:46 +08:00
return ;
}
nodeElement . querySelectorAll ( ".av__cell--select" ) . forEach ( item = > {
item . classList . remove ( "av__cell--select" ) ;
2024-01-14 16:38:45 +08:00
} ) ;
2024-01-13 22:34:46 +08:00
nodeElement . querySelectorAll ( ".av__drag-fill" ) . forEach ( item = > {
item . remove ( ) ;
2024-01-14 16:38:45 +08:00
} ) ;
2024-01-13 22:34:46 +08:00
avCellElement . classList . add ( "av__cell--select" ) ;
const originIndex = getPositionByCellElement ( avCellElement ) ;
2024-10-26 17:06:35 +08:00
let moveSelectCellElement : HTMLElement ;
2024-01-13 22:34:46 +08:00
let lastCellElement : HTMLElement ;
2024-01-27 20:16:54 +08:00
const nodeRect = nodeElement . getBoundingClientRect ( ) ;
const scrollElement = nodeElement . querySelector ( ".av__scroll" ) ;
2025-08-13 12:08:46 +08:00
const bodyElement = hasClosestByClassName ( avCellElement , "av__body" ) as HTMLElement ;
2024-01-13 22:34:46 +08:00
documentSelf . onmousemove = ( moveEvent : MouseEvent ) = > {
const tempCellElement = hasClosestByClassName ( moveEvent . target as HTMLElement , "av__cell" ) as HTMLElement ;
2024-01-27 20:16:54 +08:00
if ( scrollElement . scrollWidth > scrollElement . clientWidth + 2 ) {
if ( moveEvent . clientX > nodeRect . right - 10 ) {
scrollElement . scrollLeft += 10 ;
} else if ( moveEvent . clientX < nodeRect . left + 34 ) {
scrollElement . scrollLeft -= 10 ;
}
if ( moveEvent . clientY < contentRect . top + 48 ) {
protyle . contentElement . scrollTop -= 5 ;
} else if ( moveEvent . clientY > contentRect . bottom - 48 ) {
protyle . contentElement . scrollTop += 5 ;
}
}
2025-08-13 12:08:46 +08:00
if ( bodyElement !== hasClosestByClassName ( tempCellElement , "av__body" ) ||
( moveSelectCellElement && tempCellElement && tempCellElement === moveSelectCellElement ) ) {
2024-01-13 22:34:46 +08:00
return ;
}
2024-01-20 11:23:54 +08:00
if ( tempCellElement && tempCellElement . dataset . id && ( event . clientX !== moveEvent . clientX || event . clientY !== moveEvent . clientY ) ) {
const newIndex = getPositionByCellElement ( tempCellElement ) ;
2024-01-13 22:34:46 +08:00
nodeElement . querySelectorAll ( ".av__cell--active" ) . forEach ( ( item : HTMLElement ) = > {
item . classList . remove ( "av__cell--active" ) ;
2024-01-14 16:38:45 +08:00
} ) ;
2025-08-13 12:08:46 +08:00
bodyElement . querySelectorAll ( ".av__row" ) . forEach ( ( rowElement : HTMLElement , index : number ) = > {
2024-01-13 22:34:46 +08:00
if ( index >= Math . min ( originIndex . rowIndex , newIndex . rowIndex ) && index <= Math . max ( originIndex . rowIndex , newIndex . rowIndex ) ) {
rowElement . querySelectorAll ( ".av__cell" ) . forEach ( ( cellElement : HTMLElement , cellIndex : number ) = > {
if ( cellIndex >= Math . min ( originIndex . celIndex , newIndex . celIndex ) && cellIndex <= Math . max ( originIndex . celIndex , newIndex . celIndex ) ) {
cellElement . classList . add ( "av__cell--active" ) ;
lastCellElement = cellElement ;
}
} ) ;
}
2024-01-14 16:38:45 +08:00
} ) ;
2024-10-26 17:06:35 +08:00
moveSelectCellElement = tempCellElement ;
2024-01-13 22:34:46 +08:00
}
} ;
documentSelf . onmouseup = ( ) = > {
documentSelf . onmousemove = null ;
documentSelf . onmouseup = null ;
documentSelf . ondragstart = null ;
documentSelf . onselectstart = null ;
documentSelf . onselect = null ;
2024-01-13 22:36:58 +08:00
if ( lastCellElement ) {
2024-01-14 18:02:14 +08:00
selectRow ( nodeElement . querySelector ( ".av__firstcol" ) , "unselectAll" ) ;
2024-01-13 22:36:58 +08:00
focusBlock ( nodeElement ) ;
2024-03-22 11:08:21 +08:00
addDragFill ( lastCellElement ) ;
2024-01-13 22:36:58 +08:00
this . preventClick = true ;
}
2024-01-13 22:34:46 +08:00
return false ;
} ;
return false ;
}
2022-08-31 22:29:52 +08:00
// 图片、iframe、video 缩放
if ( ! protyle . disabled && target . classList . contains ( "protyle-action__drag" ) ) {
if ( ! nodeElement ) {
return ;
2022-07-21 23:25:39 +08:00
}
2022-08-31 22:29:52 +08:00
let isCenter = true ;
if ( [ "NodeIFrame" , "NodeWidget" , "NodeVideo" ] . includes ( nodeElement . getAttribute ( "data-type" ) ) ) {
nodeElement . classList . add ( "iframe--drag" ) ;
if ( nodeElement . style . textAlign === "left" || nodeElement . style . textAlign === "right" ) {
isCenter = false ;
2022-05-26 15:18:53 +08:00
}
2022-08-31 22:29:52 +08:00
} else if ( target . parentElement . parentElement . getAttribute ( "data-type" ) === "img" ) {
target . parentElement . parentElement . classList . add ( "img--drag" ) ;
2022-05-26 15:18:53 +08:00
}
2022-08-31 22:29:52 +08:00
const id = nodeElement . getAttribute ( "data-node-id" ) ;
const html = nodeElement . outerHTML ;
const x = event . clientX ;
const dragElement = target . previousElementSibling as HTMLElement ;
const dragWidth = dragElement . clientWidth ;
const dragHeight = dragElement . clientHeight ;
2024-12-20 12:23:49 +08:00
2024-12-24 10:45:10 +08:00
const imgElement = dragElement . parentElement . parentElement ;
2024-12-20 12:23:49 +08:00
if ( dragElement . tagName === "IMG" ) {
2024-12-28 19:05:24 +08:00
img3115 ( imgElement ) ;
2024-12-20 12:23:49 +08:00
}
2022-08-31 22:29:52 +08:00
documentSelf . onmousemove = ( moveEvent : MouseEvent ) = > {
if ( dragElement . tagName === "IMG" ) {
2024-09-19 17:51:33 +08:00
dragElement . style . height = "" ;
2022-05-26 15:18:53 +08:00
}
2022-08-31 22:29:52 +08:00
if ( moveEvent . clientX > x - dragWidth + 8 && moveEvent . clientX < mostRight ) {
2024-12-20 12:23:49 +08:00
const multiple = ( ( dragElement . tagName === "IMG" && ! imgElement . style . minWidth && nodeElement . style . textAlign !== "center" ) || ! isCenter ) ? 1 : 2 ;
2024-12-04 18:03:58 +08:00
if ( dragElement . tagName === "IMG" ) {
dragElement . parentElement . style . width = Math . max ( 17 , dragWidth + ( moveEvent . clientX - x ) * multiple ) + "px" ;
2022-05-26 15:18:53 +08:00
} else {
2024-12-04 18:03:58 +08:00
dragElement . style . width = Math . max ( 17 , dragWidth + ( moveEvent . clientX - x ) * multiple ) + "px" ;
2022-05-26 15:18:53 +08:00
}
}
2022-08-31 22:29:52 +08:00
if ( dragElement . tagName !== "IMG" ) {
if ( moveEvent . clientY > y - dragHeight + 8 && moveEvent . clientY < mostBottom ) {
dragElement . style . height = ( dragHeight + ( moveEvent . clientY - y ) ) + "px" ;
}
2022-05-26 15:18:53 +08:00
}
2022-08-31 22:29:52 +08:00
} ;
2022-05-26 15:18:53 +08:00
2022-08-31 22:29:52 +08:00
documentSelf . onmouseup = ( ) = > {
documentSelf . onmousemove = null ;
documentSelf . onmouseup = null ;
documentSelf . ondragstart = null ;
documentSelf . onselectstart = null ;
documentSelf . onselect = null ;
if ( target . classList . contains ( "protyle-action__drag" ) && nodeElement ) {
updateTransaction ( protyle , id , nodeElement . outerHTML , html ) ;
}
nodeElement . classList . remove ( "iframe--drag" ) ;
target . parentElement . parentElement . classList . remove ( "img--drag" ) ;
} ;
2022-05-26 15:18:53 +08:00
return ;
}
2022-08-31 22:29:52 +08:00
// table cell select
let tableBlockElement : HTMLElement | false ;
2025-03-23 17:55:50 +08:00
const targetCellElement = hasClosestByTag ( target , "TH" ) || hasClosestByTag ( target , "TD" ) ;
if ( targetCellElement ) {
target = targetCellElement ;
2024-10-30 10:11:57 +08:00
}
2022-08-31 22:29:52 +08:00
if ( target . tagName === "TH" || target . tagName === "TD" || target . firstElementChild ? . tagName === "TABLE" || target . classList . contains ( "table__resize" ) || target . classList . contains ( "table__select" ) ) {
2025-06-20 13:35:12 +08:00
tableBlockElement = nodeElement ;
2022-08-31 22:29:52 +08:00
if ( tableBlockElement ) {
tableBlockElement . querySelector ( ".table__select" ) . removeAttribute ( "style" ) ;
window . siyuan . menus . menu . remove ( ) ;
2025-02-16 15:09:53 +08:00
hideElements ( [ "toolbar" ] , protyle ) ;
2025-03-23 17:55:50 +08:00
if ( target . classList . contains ( "table__select" ) ) {
target = document . elementFromPoint ( event . clientX , event . clientY ) as HTMLElement ;
2025-06-21 10:13:44 +08:00
nodeElement = hasClosestBlock ( target ) as HTMLElement ;
2025-03-23 17:55:50 +08:00
}
2022-08-31 22:29:52 +08:00
event . stopPropagation ( ) ;
2022-05-26 15:18:53 +08:00
}
// 后续拖拽操作写在多选节点中
}
// table col resize
2022-08-31 22:29:52 +08:00
if ( ! protyle . disabled && target . classList . contains ( "table__resize" ) ) {
2022-05-26 15:18:53 +08:00
if ( ! nodeElement ) {
return ;
}
const html = nodeElement . outerHTML ;
// https://github.com/siyuan-note/siyuan/issues/4455
if ( getSelection ( ) . rangeCount > 0 ) {
getSelection ( ) . getRangeAt ( 0 ) . collapse ( false ) ;
}
// @ts-ignore
nodeElement . firstElementChild . style . webkitUserModify = "read-only" ;
nodeElement . style . cursor = "col-resize" ;
target . removeAttribute ( "style" ) ;
const id = nodeElement . getAttribute ( "data-node-id" ) ;
const x = event . clientX ;
2023-03-28 10:22:32 +08:00
const colIndex = parseInt ( target . getAttribute ( "data-col-index" ) ) ;
2023-03-28 10:12:30 +08:00
const colElement = nodeElement . querySelectorAll ( "table col" ) [ colIndex ] as HTMLElement ;
2022-12-29 15:13:37 +08:00
// 清空初始化 table 时的最小宽度
if ( colElement . style . minWidth ) {
2023-03-28 10:12:30 +08:00
colElement . style . width = ( nodeElement . querySelectorAll ( "table td, table th" ) [ colIndex ] as HTMLElement ) . offsetWidth + "px" ;
2022-12-29 15:13:37 +08:00
colElement . style . minWidth = "" ;
}
2023-03-28 10:12:30 +08:00
// 移除 cell 上的宽度限制 https://github.com/siyuan-note/siyuan/issues/7795
nodeElement . querySelectorAll ( "tr" ) . forEach ( ( trItem : HTMLTableRowElement ) = > {
trItem . cells [ colIndex ] . style . width = "" ;
} ) ;
2022-05-26 15:18:53 +08:00
const oldWidth = colElement . clientWidth ;
const hasScroll = nodeElement . firstElementChild . clientWidth < nodeElement . firstElementChild . scrollWidth ;
documentSelf . onmousemove = ( moveEvent : MouseEvent ) = > {
if ( nodeElement . style . textAlign === "center" && ! hasScroll ) {
colElement . style . width = ( oldWidth + ( moveEvent . clientX - x ) * 2 ) + "px" ;
} else {
colElement . style . width = ( oldWidth + ( moveEvent . clientX - x ) ) + "px" ;
}
} ;
documentSelf . onmouseup = ( ) = > {
// @ts-ignore
nodeElement . firstElementChild . style . webkitUserModify = "" ;
nodeElement . style . cursor = "" ;
documentSelf . onmousemove = null ;
documentSelf . onmouseup = null ;
documentSelf . ondragstart = null ;
documentSelf . onselectstart = null ;
documentSelf . onselect = null ;
if ( nodeElement ) {
updateTransaction ( protyle , id , nodeElement . outerHTML , html ) ;
}
} ;
return ;
}
// 多选节点
2024-11-28 11:14:26 +08:00
let clentX = event . clientX ;
2022-05-26 15:18:53 +08:00
if ( event . clientX > mostRight ) {
2024-11-28 11:14:26 +08:00
clentX = mostRight ;
2022-05-26 15:18:53 +08:00
} else if ( event . clientX < mostLeft ) {
2024-11-28 11:14:26 +08:00
clentX = mostLeft ;
2022-05-26 15:18:53 +08:00
}
2025-05-19 21:07:53 +08:00
const mostTop = protyleRect . top + ( protyle . options . render . breadcrumb ? protyle.breadcrumb.element.parentElement.clientHeight : 0 ) ;
2022-05-26 15:18:53 +08:00
let mouseElement : Element ;
let moveCellElement : HTMLElement ;
2023-02-03 15:18:54 +08:00
let startFirstElement : Element ;
let endLastElement : Element ;
2024-04-19 10:52:59 +08:00
this . element . querySelectorAll ( "iframe" ) . forEach ( item = > {
2024-04-19 11:20:11 +08:00
item . style . pointerEvents = "none" ;
} ) ;
2024-07-06 09:57:00 +08:00
const needScroll = [ "IMG" , "VIDEO" , "AUDIO" ] . includes ( target . tagName ) || target . classList . contains ( "img" ) ;
2022-05-26 15:18:53 +08:00
documentSelf . onmousemove = ( moveEvent : MouseEvent ) = > {
2024-10-30 10:22:14 +08:00
let moveTarget : boolean | HTMLElement = moveEvent . target as HTMLElement ;
2022-05-26 15:18:53 +08:00
// table cell select
2025-02-18 21:45:53 +08:00
if ( tableBlockElement && tableBlockElement . contains ( moveTarget ) &&
2024-11-02 14:23:36 +08:00
! hasClosestByClassName ( tableBlockElement , "protyle-wysiwyg__embed" ) ) {
2025-02-05 17:08:35 +08:00
if ( moveTarget . classList . contains ( "table__select" ) ) {
moveTarget . classList . add ( "fn__none" ) ;
const pointElement = document . elementFromPoint ( moveEvent . clientX , moveEvent . clientY ) ;
moveTarget . classList . remove ( "fn__none" ) ;
2025-02-09 12:04:40 +08:00
moveTarget = hasClosestByTag ( pointElement , "TH" ) || hasClosestByTag ( pointElement , "TD" ) ;
2025-02-05 17:08:35 +08:00
}
2025-07-23 13:08:38 +08:00
if ( moveTarget && moveTarget === target ) {
2024-11-02 14:23:36 +08:00
tableBlockElement . querySelector ( ".table__select" ) . removeAttribute ( "style" ) ;
2025-02-05 17:08:35 +08:00
protyle . wysiwyg . element . classList . remove ( "protyle-wysiwyg--hiderange" ) ;
2024-11-02 14:23:36 +08:00
moveCellElement = moveTarget ;
return false ;
}
if ( moveTarget && ( moveTarget . tagName === "TH" || moveTarget . tagName === "TD" ) &&
2025-07-23 13:08:38 +08:00
( ! moveCellElement || moveCellElement !== moveTarget ) ) {
2022-05-26 15:18:53 +08:00
// @ts-ignore
tableBlockElement . firstElementChild . style . webkitUserModify = "read-only" ;
let width = target . offsetLeft + target . clientWidth - moveTarget . offsetLeft ;
let left = moveTarget . offsetLeft ;
if ( target . offsetLeft === moveTarget . offsetLeft ) {
width = Math . max ( target . clientWidth , moveTarget . clientWidth ) ;
} else if ( target . offsetLeft < moveTarget . offsetLeft ) {
width = moveTarget . offsetLeft + moveTarget . clientWidth - target . offsetLeft ;
left = target . offsetLeft ;
}
let height = target . offsetTop + target . clientHeight - moveTarget . offsetTop ;
let top = moveTarget . offsetTop ;
if ( target . offsetTop === moveTarget . offsetTop ) {
height = Math . max ( target . clientHeight , moveTarget . clientHeight ) ;
} else if ( target . offsetTop < moveTarget . offsetTop ) {
height = moveTarget . offsetTop + moveTarget . clientHeight - target . offsetTop ;
top = target . offsetTop ;
}
// https://github.com/siyuan-note/insider/issues/1015
Array . from ( tableBlockElement . querySelectorAll ( "th, td" ) ) . find ( ( item : HTMLElement ) = > {
const updateWidth = item . offsetLeft < left + width && item . offsetLeft + item . clientWidth > left + width ;
const updateWidth2 = item . offsetLeft < left && item . offsetLeft + item . clientWidth > left ;
if ( item . offsetTop < top && item . offsetTop + item . clientHeight > top ) {
if ( ( item . offsetLeft + 6 > left && item . offsetLeft + item . clientWidth - 6 < left + width ) || updateWidth || updateWidth2 ) {
height = top + height - item . offsetTop ;
top = item . offsetTop ;
}
if ( updateWidth ) {
width = item . offsetLeft + item . clientWidth - left ;
}
if ( updateWidth2 ) {
width = left + width - item . offsetLeft ;
left = item . offsetLeft ;
}
} else if ( item . offsetTop < top + height && item . offsetTop + item . clientHeight > top + height ) {
if ( ( item . offsetLeft + 6 > left && item . offsetLeft + item . clientWidth - 6 < left + width ) || updateWidth || updateWidth2 ) {
height = item . clientHeight + item . offsetTop - top ;
}
if ( updateWidth ) {
width = item . offsetLeft + item . clientWidth - left ;
}
if ( updateWidth2 ) {
width = left + width - item . offsetLeft ;
left = item . offsetLeft ;
}
} else if ( updateWidth2 && item . offsetTop + 6 > top && item . offsetTop + item . clientHeight - 6 < top + height ) {
width = left + width - item . offsetLeft ;
left = item . offsetLeft ;
} else if ( updateWidth && item . offsetTop + 6 > top && item . offsetTop + item . clientHeight - 6 < top + height ) {
width = item . offsetLeft + item . clientWidth - left ;
}
} ) ;
2025-02-05 17:08:35 +08:00
protyle . wysiwyg . element . classList . add ( "protyle-wysiwyg--hiderange" ) ;
2025-05-08 21:48:51 +08:00
tableBlockElement . querySelector ( ".table__select" ) . setAttribute ( "style" , ` left: ${ left - tableBlockElement . firstElementChild . scrollLeft } px;top: ${ top - tableBlockElement . querySelector ( "table" ) . scrollTop } px;height: ${ height } px;width: ${ width + 1 } px; ` ) ;
2022-05-26 15:18:53 +08:00
moveCellElement = moveTarget ;
}
return ;
}
2024-06-19 22:41:14 +08:00
// 在包含 img, video, audio 的元素上划选后无法上下滚动 https://ld246.com/article/1681778773806
// 在包含 img, video, audio 的元素上拖拽无法划选 https://github.com/siyuan-note/siyuan/issues/11763
2024-06-19 22:56:34 +08:00
if ( needScroll ) {
if ( moveEvent . clientY < contentRect . top + Constants . SIZE_SCROLL_TB || moveEvent . clientY > contentRect . bottom - Constants . SIZE_SCROLL_TB ) {
protyle . contentElement . scroll ( {
top : protyle.contentElement.scrollTop + ( moveEvent . clientY < contentRect . top + Constants . SIZE_SCROLL_TB ? - Constants.SIZE_SCROLL_STEP : Constants.SIZE_SCROLL_STEP ) ,
behavior : "smooth"
} ) ;
}
2024-06-19 22:41:14 +08:00
}
2022-05-26 15:18:53 +08:00
protyle . selectElement . classList . remove ( "fn__none" ) ;
// 向左选择,遇到 gutter 就不会弹出 toolbar
hideElements ( [ "gutter" ] , protyle ) ;
let newTop = 0 ;
let newLeft = 0 ;
let newWidth = 0 ;
let newHeight = 0 ;
2024-11-28 11:14:26 +08:00
if ( moveEvent . clientX < clentX ) {
2022-05-26 15:18:53 +08:00
if ( moveEvent . clientX < mostLeft ) {
// 向左越界
newLeft = mostLeft ;
} else {
// 向左
newLeft = moveEvent . clientX ;
}
2024-11-28 11:14:26 +08:00
newWidth = clentX - newLeft ;
2022-05-26 15:18:53 +08:00
} else {
if ( moveEvent . clientX > mostRight ) {
// 向右越界
2024-11-28 11:14:26 +08:00
newLeft = clentX ;
2022-05-26 15:18:53 +08:00
newWidth = mostRight - newLeft ;
} else {
// 向右
2024-11-28 11:14:26 +08:00
newLeft = clentX ;
newWidth = moveEvent . clientX - clentX ;
2022-05-26 15:18:53 +08:00
}
}
if ( moveEvent . clientY > y ) {
if ( moveEvent . clientY > mostBottom ) {
// 向下越界
newTop = y ;
newHeight = mostBottom - y ;
} else {
// 向下
newTop = y ;
newHeight = moveEvent . clientY - y ;
}
} else {
if ( moveEvent . clientY < mostTop ) {
// 向上越界
newTop = mostTop ;
} else {
// 向上
newTop = moveEvent . clientY ;
}
newHeight = y - newTop ;
}
2023-02-09 12:21:02 +08:00
if ( newHeight < 4 ) {
return ;
}
2022-05-26 15:18:53 +08:00
protyle . selectElement . setAttribute ( "style" , ` background-color: ${ protyle . selectElement . style . backgroundColor } ;top: ${ newTop } px;height: ${ newHeight } px;left: ${ newLeft + 2 } px;width: ${ newWidth - 2 } px; ` ) ;
const newMouseElement = document . elementFromPoint ( moveEvent . clientX , moveEvent . clientY ) ;
2025-07-23 13:08:38 +08:00
if ( mouseElement && mouseElement === newMouseElement && ! mouseElement . classList . contains ( "protyle-wysiwyg" ) &&
2022-05-26 15:18:53 +08:00
! mouseElement . classList . contains ( "list" ) && ! mouseElement . classList . contains ( "bq" ) && ! mouseElement . classList . contains ( "sb" ) ) {
// 性能优化, 同一个p元素不进行选中计算
return ;
} else {
mouseElement = newMouseElement ;
}
hideElements ( [ "select" ] , protyle ) ;
2023-02-04 09:51:24 +08:00
let firstElement ;
2023-02-03 15:18:54 +08:00
if ( moveEvent . clientY > y ) {
2023-07-29 11:54:38 +08:00
firstElement = startFirstElement || document . elementFromPoint ( newLeft , newTop ) ;
2023-02-04 09:51:24 +08:00
endLastElement = undefined ;
2023-02-03 15:18:54 +08:00
} else {
2023-07-29 11:54:38 +08:00
firstElement = document . elementFromPoint ( newLeft , newTop ) ;
2023-02-03 15:18:54 +08:00
startFirstElement = undefined ;
}
2022-05-26 15:18:53 +08:00
if ( ! firstElement ) {
return ;
}
2024-11-23 17:45:43 +08:00
if ( firstElement . classList . contains ( "protyle-wysiwyg" ) || firstElement . classList . contains ( "list" ) ||
firstElement . classList . contains ( "li" ) || firstElement . classList . contains ( "sb" ) ||
firstElement . classList . contains ( "bq" ) ) {
2023-07-29 11:54:38 +08:00
firstElement = document . elementFromPoint ( newLeft , newTop + 16 ) ;
2022-05-26 15:18:53 +08:00
}
2023-02-03 17:32:46 +08:00
if ( ! firstElement ) {
2022-05-26 15:18:53 +08:00
return ;
}
2023-02-03 17:32:46 +08:00
let firstBlockElement = hasClosestBlock ( firstElement ) ;
2024-03-21 10:14:18 +08:00
if ( ! firstBlockElement && firstElement . classList . contains ( "protyle-breadcrumb__bar" ) ) {
firstBlockElement = firstElement . nextElementSibling as HTMLElement ;
}
2023-02-03 17:32:46 +08:00
if ( moveEvent . clientY > y ) {
if ( ! startFirstElement ) {
2024-03-04 23:54:31 +08:00
// 向上选择导致滚动条滚动到顶部再向下选择至 > y 时, firstBlockElement 为 undefined https://ld246.com/article/1705233964097
if ( ! firstBlockElement ) {
firstBlockElement = protyle . wysiwyg . element . firstElementChild as HTMLElement ;
2024-03-21 10:14:18 +08:00
if ( firstBlockElement . classList . contains ( "protyle-breadcrumb__bar" ) ) {
firstBlockElement = firstBlockElement . nextElementSibling as HTMLElement ;
}
2024-03-04 23:54:31 +08:00
}
startFirstElement = firstBlockElement ;
2023-02-03 17:32:46 +08:00
}
2023-03-07 18:42:13 +08:00
} else if ( ! firstBlockElement &&
// https://github.com/siyuan-note/siyuan/issues/7580
moveEvent . clientY < protyle . wysiwyg . element . lastElementChild . getBoundingClientRect ( ) . bottom ) {
2023-02-03 17:32:46 +08:00
firstBlockElement = protyle . wysiwyg . element . firstElementChild as HTMLElement ;
2024-03-21 10:14:18 +08:00
if ( firstBlockElement . classList . contains ( "protyle-breadcrumb__bar" ) ) {
firstBlockElement = firstBlockElement . nextElementSibling as HTMLElement ;
}
2022-05-26 15:18:53 +08:00
}
let selectElements : Element [ ] = [ ] ;
let currentElement : Element | boolean = firstBlockElement ;
2024-04-19 10:52:59 +08:00
if ( currentElement ) {
2024-04-19 11:20:11 +08:00
// 从下往上划选遇到嵌入块时,选中整个嵌入块
2024-07-25 17:55:25 +08:00
const embedElement = isInEmbedBlock ( currentElement ) ;
2024-04-19 10:52:59 +08:00
if ( embedElement ) {
2024-04-19 11:20:11 +08:00
currentElement = embedElement ;
2024-04-19 10:52:59 +08:00
}
}
2022-05-26 15:18:53 +08:00
let hasJump = false ;
2023-02-04 09:51:24 +08:00
const selectBottom = endLastElement ? endLastElement . getBoundingClientRect ( ) . bottom : ( newTop + newHeight ) ;
2022-05-26 15:18:53 +08:00
while ( currentElement ) {
if ( currentElement && ! currentElement . classList . contains ( "protyle-attr" ) ) {
const currentRect = currentElement . getBoundingClientRect ( ) ;
2023-02-03 15:18:54 +08:00
if ( currentRect . height > 0 && currentRect . top < selectBottom && currentRect . left < newLeft + newWidth ) {
2022-05-26 15:18:53 +08:00
if ( hasJump ) {
// 父节点的下个节点在选中范围内才可使用父节点作为选中节点
if ( currentElement . nextElementSibling && ! currentElement . nextElementSibling . classList . contains ( "protyle-attr" ) ) {
const nextRect = currentElement . nextElementSibling . getBoundingClientRect ( ) ;
2023-02-03 15:18:54 +08:00
if ( nextRect . top < selectBottom && nextRect . left < newLeft + newWidth ) {
2022-05-26 15:18:53 +08:00
selectElements = [ currentElement ] ;
currentElement = currentElement . nextElementSibling ;
hasJump = false ;
} else if ( currentElement . parentElement . classList . contains ( "sb" ) ) {
currentElement = hasClosestBlock ( currentElement . parentElement ) ;
hasJump = true ;
} else {
break ;
}
} else {
currentElement = hasClosestBlock ( currentElement . parentElement ) ;
hasJump = true ;
}
} else {
2025-06-02 22:12:51 +08:00
if ( ! currentElement . classList . contains ( "protyle-breadcrumb__bar" ) &&
! currentElement . classList . contains ( "protyle-breadcrumb__item" ) ) {
2024-03-21 10:14:18 +08:00
selectElements . push ( currentElement ) ;
}
2022-05-26 15:18:53 +08:00
currentElement = currentElement . nextElementSibling ;
}
} else if ( currentElement . parentElement . classList . contains ( "sb" ) ) {
// 跳出超级块横向排版中的未选中元素
currentElement = hasClosestBlock ( currentElement . parentElement ) ;
hasJump = true ;
} else if ( currentRect . height === 0 && currentRect . width === 0 && currentElement . parentElement . getAttribute ( "fold" ) === "1" ) {
currentElement = currentElement . parentElement ;
selectElements = [ ] ;
} else {
break ;
}
} else {
currentElement = hasClosestBlock ( currentElement . parentElement ) ;
hasJump = true ;
}
}
2023-02-03 15:27:27 +08:00
if ( moveEvent . clientY <= y && ! endLastElement ) {
2023-02-04 09:51:24 +08:00
endLastElement = selectElements [ selectElements . length - 1 ] ;
2023-02-03 15:27:27 +08:00
}
2025-02-05 17:08:35 +08:00
if ( selectElements . length === 1 && ! selectElements [ 0 ] . classList . contains ( "list" ) &&
! selectElements [ 0 ] . classList . contains ( "bq" ) && ! selectElements [ 0 ] . classList . contains ( "sb" ) ) {
2022-05-26 15:18:53 +08:00
// 只有一个 p 时不选中
protyle . selectElement . style . backgroundColor = "transparent" ;
2025-04-18 16:51:18 +08:00
protyle . wysiwyg . element . classList . remove ( "protyle-wysiwyg--hiderange" ) ;
2022-05-26 15:18:53 +08:00
} else {
2025-04-18 16:51:18 +08:00
protyle . wysiwyg . element . classList . add ( "protyle-wysiwyg--hiderange" ) ;
2022-05-26 15:18:53 +08:00
selectElements . forEach ( item = > {
if ( ! hasClosestByClassName ( item , "protyle-wysiwyg__embed" ) ) {
item . classList . add ( "protyle-wysiwyg--select" ) ;
}
} ) ;
protyle . selectElement . style . backgroundColor = "" ;
}
} ;
documentSelf . onmouseup = ( mouseUpEvent ) = > {
documentSelf . onmousemove = null ;
documentSelf . onmouseup = null ;
documentSelf . ondragstart = null ;
documentSelf . onselectstart = null ;
documentSelf . onselect = null ;
2023-02-03 15:18:54 +08:00
startFirstElement = undefined ;
endLastElement = undefined ;
2025-04-18 16:51:18 +08:00
// 多选表格单元格后,选择菜单中的居左,然后 shift+左 选中的文字无法显示选中背景,因此需移除
// 多选块后 shift+左 选中的文字无法显示选中背景,因此需移除
protyle . wysiwyg . element . classList . remove ( "protyle-wysiwyg--hiderange" ) ;
2024-04-19 10:52:59 +08:00
this . element . querySelectorAll ( "iframe" ) . forEach ( item = > {
item . style . pointerEvents = "" ;
2024-04-19 11:20:11 +08:00
} ) ;
2022-05-26 15:18:53 +08:00
protyle . selectElement . classList . add ( "fn__none" ) ;
protyle . selectElement . removeAttribute ( "style" ) ;
2025-02-18 21:45:53 +08:00
if ( tableBlockElement ) {
2022-05-26 15:18:53 +08:00
// @ts-ignore
tableBlockElement . firstElementChild . style . webkitUserModify = "" ;
const tableSelectElement = tableBlockElement . querySelector ( ".table__select" ) as HTMLElement ;
if ( tableSelectElement . getAttribute ( "style" ) ) {
if ( getSelection ( ) . rangeCount > 0 ) {
getSelection ( ) . getRangeAt ( 0 ) . collapse ( false ) ;
}
window . siyuan . menus . menu . remove ( ) ;
2025-02-18 21:45:53 +08:00
if ( ! protyle . disabled ) {
window . siyuan . menus . menu . append ( new MenuItem ( {
2025-05-24 11:30:16 +08:00
id : "mergeCell" ,
2025-02-18 21:45:53 +08:00
label : window.siyuan.languages.mergeCell ,
click : ( ) = > {
if ( tableBlockElement ) {
const selectCellElements : HTMLTableCellElement [ ] = [ ] ;
const colIndexList : number [ ] = [ ] ;
const colCount = tableBlockElement . querySelectorAll ( "th" ) . length ;
let fnNoneMax = 0 ;
const scrollLeft = tableBlockElement . firstElementChild . scrollLeft ;
2025-05-08 17:58:26 +08:00
const scrollTop = tableBlockElement . querySelector ( "table" ) . scrollTop ;
2025-02-18 21:45:53 +08:00
let isTHead = false ;
let isTBody = false ;
tableBlockElement . querySelectorAll ( "th, td" ) . forEach ( ( item : HTMLTableCellElement , index : number ) = > {
if ( item . classList . contains ( "fn__none" ) ) {
// 合并的元素中间有 fn__none 的元素
2025-07-23 13:08:38 +08:00
if ( item . previousElementSibling && item . previousElementSibling === selectCellElements [ selectCellElements . length - 1 ] ) {
2025-02-18 21:45:53 +08:00
selectCellElements . push ( item ) ;
if ( ! isTHead && item . parentElement . parentElement . tagName === "THEAD" ) {
isTHead = true ;
} else if ( ! isTBody && item . parentElement . parentElement . tagName === "TBODY" ) {
isTBody = true ;
}
} else {
if ( index < fnNoneMax && colIndexList . includes ( ( index + 1 ) % colCount ) ) {
selectCellElements . push ( item ) ;
if ( ! isTHead && item . parentElement . parentElement . tagName === "THEAD" ) {
isTHead = true ;
} else if ( ! isTBody && item . parentElement . parentElement . tagName === "TBODY" ) {
isTBody = true ;
}
}
2022-05-26 15:18:53 +08:00
}
} else {
2025-05-08 17:58:26 +08:00
if ( isIncludeCell ( {
tableSelectElement ,
scrollLeft ,
scrollTop ,
item ,
} ) ) {
2022-05-26 15:18:53 +08:00
selectCellElements . push ( item ) ;
if ( ! isTHead && item . parentElement . parentElement . tagName === "THEAD" ) {
isTHead = true ;
} else if ( ! isTBody && item . parentElement . parentElement . tagName === "TBODY" ) {
isTBody = true ;
}
2025-02-18 21:45:53 +08:00
colIndexList . push ( ( index + 1 ) % colCount ) ;
// https://github.com/siyuan-note/insider/issues/1014
fnNoneMax = Math . max ( ( item . rowSpan - 1 ) * colCount + index + 1 , fnNoneMax ) ;
2022-05-26 15:18:53 +08:00
}
}
2025-02-18 21:45:53 +08:00
} ) ;
tableSelectElement . removeAttribute ( "style" ) ;
const oldHTML = tableBlockElement . outerHTML ;
let cellElement = selectCellElements [ 0 ] ;
let colSpan = cellElement . colSpan ;
let index = 1 ;
2025-07-23 13:08:38 +08:00
while ( cellElement . nextElementSibling && cellElement . nextElementSibling === selectCellElements [ index ] ) {
2025-02-18 21:45:53 +08:00
cellElement = cellElement . nextElementSibling as HTMLTableCellElement ;
if ( ! cellElement . classList . contains ( "fn__none" ) ) { // https://github.com/siyuan-note/insider/issues/1007#issuecomment-1046195608
colSpan += cellElement . colSpan ;
2022-05-26 15:18:53 +08:00
}
2025-02-18 21:45:53 +08:00
index ++ ;
2022-05-26 15:18:53 +08:00
}
2025-02-18 21:45:53 +08:00
let html = "" ;
let rowElement : Element = selectCellElements [ 0 ] . parentElement ;
let rowSpan = selectCellElements [ 0 ] . rowSpan ;
selectCellElements . forEach ( ( item , index ) = > {
let cellHTML = item . innerHTML . trim ( ) ;
if ( cellHTML . endsWith ( "<br>" ) ) {
cellHTML = cellHTML . substr ( 0 , cellHTML . length - 4 ) ;
}
html += cellHTML + ( ( ! cellHTML || index === selectCellElements . length - 1 ) ? "" : "<br>" ) ;
if ( index !== 0 ) {
2025-07-23 12:21:59 +08:00
if ( rowElement !== item . parentElement ) {
2025-02-18 21:45:53 +08:00
if ( ! item . classList . contains ( "fn__none" ) ) { // https://github.com/siyuan-note/insider/issues/1011
rowSpan += item . rowSpan ;
}
rowElement = item . parentElement ;
if ( selectCellElements [ 0 ] . parentElement . parentElement . tagName === "THEAD" && item . parentElement . parentElement . tagName !== "THEAD" ) {
selectCellElements [ 0 ] . parentElement . parentElement . insertAdjacentElement ( "beforeend" , item . parentElement ) ;
}
2022-05-26 15:18:53 +08:00
}
2025-02-18 21:45:53 +08:00
item . classList . add ( "fn__none" ) ;
item . innerHTML = "" ;
2022-05-26 15:18:53 +08:00
}
2025-02-18 21:45:53 +08:00
} ) ;
2022-05-26 15:18:53 +08:00
2025-02-18 21:45:53 +08:00
// https://github.com/siyuan-note/insider/issues/1017
if ( isTHead && isTBody ) {
rowElement = rowElement . parentElement . nextElementSibling . firstElementChild ;
while ( rowElement && rowElement . parentElement . tagName !== "THEAD" ) {
let colSpanCount = 0 ;
let noneCount = 0 ;
Array . from ( rowElement . children ) . forEach ( ( item : HTMLTableCellElement ) = > {
colSpanCount += item . colSpan - 1 ;
if ( item . classList . contains ( "fn__none" ) ) {
noneCount ++ ;
}
} ) ;
if ( colSpanCount !== noneCount ) {
selectCellElements [ 0 ] . parentElement . parentElement . insertAdjacentElement ( "beforeend" , rowElement ) ;
rowElement = rowElement . parentElement . nextElementSibling . firstElementChild ;
} else {
break ;
2022-05-26 15:18:53 +08:00
}
}
}
2025-02-18 21:45:53 +08:00
// 合并背景色不会修改,需要等计算完毕
setTimeout ( ( ) = > {
if ( tableBlockElement ) {
selectCellElements [ 0 ] . innerHTML = html + "<wbr>" ;
selectCellElements [ 0 ] . colSpan = colSpan ;
selectCellElements [ 0 ] . rowSpan = rowSpan ;
focusByWbr ( selectCellElements [ 0 ] , document . createRange ( ) ) ;
updateTransaction ( protyle , tableBlockElement . getAttribute ( "data-node-id" ) , tableBlockElement . outerHTML , oldHTML ) ;
}
} ) ;
}
2022-05-26 15:18:53 +08:00
}
2025-02-18 21:45:53 +08:00
} ) . element ) ;
2025-06-02 22:12:51 +08:00
window . siyuan . menus . menu . append ( new MenuItem ( {
id : "separator_1" ,
type : "separator"
} ) . element ) ;
2025-02-18 21:45:53 +08:00
window . siyuan . menus . menu . append ( new MenuItem ( {
2025-05-24 11:30:16 +08:00
id : "alignLeft" ,
2025-02-18 21:45:53 +08:00
icon : "iconAlignLeft" ,
accelerator : window.siyuan.config.keymap.editor.general.alignLeft.custom ,
label : window.siyuan.languages.alignLeft ,
click : ( ) = > {
if ( tableBlockElement ) {
const selectCellElements : HTMLTableCellElement [ ] = [ ] ;
const scrollLeft = tableBlockElement . firstElementChild . scrollLeft ;
2025-05-08 17:58:26 +08:00
const scrollTop = tableBlockElement . querySelector ( "table" ) . scrollTop ;
2025-02-18 21:45:53 +08:00
tableBlockElement . querySelectorAll ( "th, td" ) . forEach ( ( item : HTMLTableCellElement ) = > {
if ( ! item . classList . contains ( "fn__none" ) &&
2025-05-08 17:58:26 +08:00
isIncludeCell ( {
tableSelectElement ,
scrollLeft ,
scrollTop ,
item ,
} ) && ( selectCellElements . length === 0 || ( selectCellElements . length > 0 && item . offsetTop === selectCellElements [ 0 ] . offsetTop ) ) ) {
2025-02-18 21:45:53 +08:00
selectCellElements . push ( item ) ;
}
} ) ;
tableSelectElement . removeAttribute ( "style" ) ;
setTableAlign ( protyle , selectCellElements , tableBlockElement , "left" , getEditorRange ( tableBlockElement ) ) ;
}
2022-06-12 22:06:19 +08:00
}
2025-02-18 21:45:53 +08:00
} ) . element ) ;
window . siyuan . menus . menu . append ( new MenuItem ( {
2025-05-24 11:30:16 +08:00
id : "alignCenter" ,
2025-02-18 21:45:53 +08:00
icon : "iconAlignCenter" ,
accelerator : window.siyuan.config.keymap.editor.general.alignCenter.custom ,
label : window.siyuan.languages.alignCenter ,
click : ( ) = > {
if ( tableBlockElement ) {
const selectCellElements : HTMLTableCellElement [ ] = [ ] ;
const scrollLeft = tableBlockElement . firstElementChild . scrollLeft ;
2025-05-08 17:58:26 +08:00
const scrollTop = tableBlockElement . querySelector ( "table" ) . scrollTop ;
2025-02-18 21:45:53 +08:00
tableBlockElement . querySelectorAll ( "th, td" ) . forEach ( ( item : HTMLTableCellElement ) = > {
2025-05-08 17:58:26 +08:00
if ( ! item . classList . contains ( "fn__none" ) && isIncludeCell ( {
tableSelectElement ,
scrollLeft ,
scrollTop ,
item ,
} ) &&
2025-02-18 21:45:53 +08:00
( selectCellElements . length === 0 || ( selectCellElements . length > 0 && item . offsetTop === selectCellElements [ 0 ] . offsetTop ) ) ) {
selectCellElements . push ( item ) ;
}
} ) ;
tableSelectElement . removeAttribute ( "style" ) ;
setTableAlign ( protyle , selectCellElements , tableBlockElement , "center" , getEditorRange ( tableBlockElement ) ) ;
}
2022-06-12 22:06:19 +08:00
}
2025-02-18 21:45:53 +08:00
} ) . element ) ;
window . siyuan . menus . menu . append ( new MenuItem ( {
2025-05-24 11:30:16 +08:00
id : "alignRight" ,
2025-02-18 21:45:53 +08:00
icon : "iconAlignRight" ,
accelerator : window.siyuan.config.keymap.editor.general.alignRight.custom ,
label : window.siyuan.languages.alignRight ,
click : ( ) = > {
if ( tableBlockElement ) {
const selectCellElements : HTMLTableCellElement [ ] = [ ] ;
const scrollLeft = tableBlockElement . firstElementChild . scrollLeft ;
2025-05-08 17:58:26 +08:00
const scrollTop = tableBlockElement . querySelector ( "table" ) . scrollTop ;
2025-02-18 21:45:53 +08:00
tableBlockElement . querySelectorAll ( "th, td" ) . forEach ( ( item : HTMLTableCellElement ) = > {
2025-05-08 17:58:26 +08:00
if ( ! item . classList . contains ( "fn__none" ) && isIncludeCell ( {
2025-06-02 22:12:51 +08:00
tableSelectElement ,
scrollLeft ,
scrollTop ,
item ,
} ) && ( selectCellElements . length === 0 || ( selectCellElements . length > 0 && item . offsetTop === selectCellElements [ 0 ] . offsetTop ) ) ) {
2025-02-18 21:45:53 +08:00
selectCellElements . push ( item ) ;
}
} ) ;
tableSelectElement . removeAttribute ( "style" ) ;
setTableAlign ( protyle , selectCellElements , tableBlockElement , "right" , getEditorRange ( tableBlockElement ) ) ;
}
2022-06-12 22:06:19 +08:00
}
2025-02-18 21:45:53 +08:00
} ) . element ) ;
window . siyuan . menus . menu . append ( new MenuItem ( {
2025-05-24 11:30:16 +08:00
id : "useDefaultAlign" ,
2025-02-18 21:45:53 +08:00
icon : "" ,
label : window.siyuan.languages.useDefaultAlign ,
click : ( ) = > {
if ( tableBlockElement ) {
const selectCellElements : HTMLTableCellElement [ ] = [ ] ;
const scrollLeft = tableBlockElement . firstElementChild . scrollLeft ;
2025-05-08 17:58:26 +08:00
const scrollTop = tableBlockElement . querySelector ( "table" ) . scrollTop ;
2025-02-18 21:45:53 +08:00
tableBlockElement . querySelectorAll ( "th, td" ) . forEach ( ( item : HTMLTableCellElement ) = > {
2025-05-08 17:58:26 +08:00
if ( ! item . classList . contains ( "fn__none" ) && isIncludeCell ( {
tableSelectElement ,
scrollLeft ,
scrollTop ,
item ,
} ) &&
2025-02-18 21:45:53 +08:00
( selectCellElements . length === 0 || ( selectCellElements . length > 0 && item . offsetTop === selectCellElements [ 0 ] . offsetTop ) ) ) {
selectCellElements . push ( item ) ;
}
} ) ;
tableSelectElement . removeAttribute ( "style" ) ;
setTableAlign ( protyle , selectCellElements , tableBlockElement , "" , getEditorRange ( tableBlockElement ) ) ;
}
2024-09-03 23:14:05 +08:00
}
2025-02-18 21:45:53 +08:00
} ) . element ) ;
2025-06-02 22:12:51 +08:00
window . siyuan . menus . menu . append ( new MenuItem ( {
id : "separator_2" ,
type : "separator"
} ) . element ) ;
2025-02-18 21:45:53 +08:00
}
2025-03-08 11:22:13 +08:00
window . siyuan . menus . menu . append ( new MenuItem ( {
id : "copyPlainText" ,
label : window.siyuan.languages.copyPlainText ,
click() {
if ( tableBlockElement ) {
const selectCellElements : HTMLTableCellElement [ ] = [ ] ;
const scrollLeft = tableBlockElement . firstElementChild . scrollLeft ;
2025-05-08 17:58:26 +08:00
const scrollTop = tableBlockElement . querySelector ( "table" ) . scrollTop ;
2025-03-08 11:22:13 +08:00
const tableSelectElement = tableBlockElement . querySelector ( ".table__select" ) as HTMLElement ;
tableBlockElement . querySelectorAll ( "th, td" ) . forEach ( ( item : HTMLTableCellElement ) = > {
2025-05-08 17:58:26 +08:00
if ( ! item . classList . contains ( "fn__none" ) && isIncludeCell ( {
tableSelectElement ,
scrollLeft ,
scrollTop ,
item ,
} ) ) {
2025-03-08 11:22:13 +08:00
selectCellElements . push ( item ) ;
}
} ) ;
let textPlain = "" ;
selectCellElements . forEach ( ( item , index ) = > {
textPlain += item . textContent . trim ( ) + "\t" ;
if ( ! item . nextElementSibling || ! selectCellElements [ index + 1 ] ||
2025-07-23 13:08:38 +08:00
item . nextElementSibling !== selectCellElements [ index + 1 ] ) {
2025-03-08 11:22:13 +08:00
textPlain = textPlain . slice ( 0 , - 1 ) + "\n" ;
}
} ) ;
2025-03-08 17:47:55 +08:00
copyPlainText ( textPlain . slice ( 0 , - 1 ) ) ;
2025-03-08 11:22:13 +08:00
focusBlock ( tableBlockElement ) ;
}
}
} ) . element ) ;
2024-10-29 23:00:39 +08:00
window . siyuan . menus . menu . append ( new MenuItem ( {
2025-05-24 11:30:16 +08:00
id : "copy" ,
2024-10-29 23:00:39 +08:00
icon : "iconCopy" ,
accelerator : "⌘C" ,
label : window.siyuan.languages.copy ,
click() {
if ( tableBlockElement ) {
focusByRange ( getEditorRange ( tableBlockElement ) ) ;
document . execCommand ( "copy" ) ;
}
}
} ) . element ) ;
2025-02-18 21:45:53 +08:00
if ( ! protyle . disabled ) {
window . siyuan . menus . menu . append ( new MenuItem ( {
2025-05-24 11:30:16 +08:00
id : "cut" ,
2025-02-18 21:45:53 +08:00
icon : "iconCut" ,
accelerator : "⌘X" ,
label : window.siyuan.languages.cut ,
click() {
if ( tableBlockElement ) {
focusByRange ( getEditorRange ( tableBlockElement ) ) ;
document . execCommand ( "cut" ) ;
}
2024-10-29 23:00:39 +08:00
}
2025-02-18 21:45:53 +08:00
} ) . element ) ;
window . siyuan . menus . menu . append ( new MenuItem ( {
2025-05-24 11:30:16 +08:00
id : "clear" ,
2025-02-18 21:45:53 +08:00
label : window.siyuan.languages.clear ,
icon : "iconTrashcan" ,
accelerator : "⌦" ,
click() {
clearTableCell ( protyle , tableBlockElement as HTMLElement ) ;
}
} ) . element ) ;
window . siyuan . menus . menu . append ( new MenuItem ( {
2025-05-24 11:30:16 +08:00
id : "paste" ,
2025-02-18 21:45:53 +08:00
label : window.siyuan.languages.paste ,
icon : "iconPaste" ,
accelerator : "⌘V" ,
async click() {
if ( document . queryCommandSupported ( "paste" ) ) {
document . execCommand ( "paste" ) ;
} else if ( tableBlockElement ) {
try {
const text = await readClipboard ( ) ;
paste ( protyle , Object . assign ( text , { target : tableBlockElement as HTMLElement } ) ) ;
} catch ( e ) {
console . log ( e ) ;
}
2024-10-29 23:00:39 +08:00
}
}
2025-02-18 21:45:53 +08:00
} ) . element ) ;
}
2022-08-02 11:15:18 +08:00
window . siyuan . menus . menu . popup ( { x : mouseUpEvent.clientX - 8 , y : mouseUpEvent.clientY - 16 } ) ;
2022-05-26 15:18:53 +08:00
}
}
2022-06-28 18:39:24 +08:00
const ids : string [ ] = [ ] ;
2022-06-29 09:10:03 +08:00
const selectElement = protyle . wysiwyg . element . querySelectorAll ( ".protyle-wysiwyg--select" ) ;
2022-06-28 18:39:24 +08:00
selectElement . forEach ( item = > {
ids . push ( item . getAttribute ( "data-node-id" ) ) ;
} ) ;
countBlockWord ( ids ) ;
2022-05-26 15:18:53 +08:00
// 划选后不能存在跨块的 range https://github.com/siyuan-note/siyuan/issues/4473
if ( getSelection ( ) . rangeCount > 0 ) {
const range = getSelection ( ) . getRangeAt ( 0 ) ;
if ( range . toString ( ) === "" ||
window . siyuan . shiftIsPressed // https://ld246.com/article/1650096678723
) {
2023-12-07 23:41:26 +08:00
if ( event . detail > 2 ) {
2022-09-19 22:48:47 +08:00
// table 前或最后一个 cell 三击状态不对
let cursorElement = hasClosestBlock ( range . startContainer ) as Element ;
if ( cursorElement ) {
if ( cursorElement . nextElementSibling ? . classList . contains ( "table" ) ) {
2022-09-19 23:56:47 +08:00
setLastNodeRange ( getContenteditableElement ( cursorElement ) , range , false ) ;
2022-09-19 22:48:47 +08:00
} else if ( cursorElement . classList . contains ( "table" ) ) {
const cellElements = cursorElement . querySelectorAll ( "th, td" ) ;
cursorElement = cellElements [ cellElements . length - 1 ] ;
if ( cursorElement . contains ( range . startContainer ) ) {
2022-09-19 23:56:47 +08:00
setLastNodeRange ( cursorElement , range , false ) ;
2022-09-19 22:48:47 +08:00
}
}
}
2023-09-25 09:43:31 +08:00
return ;
2022-09-19 22:48:47 +08:00
}
2022-05-26 15:18:53 +08:00
}
2022-06-28 18:39:24 +08:00
if ( selectElement . length > 0 ) {
2022-05-26 15:18:53 +08:00
range . collapse ( true ) ;
2025-07-15 20:46:04 +08:00
if ( range . commonAncestorContainer . nodeType === 1 &&
range . startContainer . childNodes [ range . startOffset ] &&
range . startContainer . childNodes [ range . startOffset ] . nodeType === 1 &&
( range . commonAncestorContainer as HTMLElement ) . classList . contains ( "protyle-wysiwyg" ) ) {
focusBlock ( range . startContainer . childNodes [ range . startOffset ] as Element ) ;
}
2022-05-26 15:18:53 +08:00
return ;
}
const startBlockElement = hasClosestBlock ( range . startContainer ) ;
let endBlockElement : false | HTMLElement ;
2025-04-18 01:06:21 +08:00
if ( mouseUpEvent . detail > 2 && range . endContainer . nodeType !== 3 && [ "DIV" , "TD" , "TH" ] . includes ( ( range . endContainer as HTMLElement ) . tagName ) && range . endOffset === 0 ) {
2022-05-26 15:18:53 +08:00
// 三击选中段落块时, rangeEnd 会在下一个块
2023-11-22 17:14:27 +08:00
if ( ( range . endContainer as HTMLElement ) . classList . contains ( "protyle-attr" ) && startBlockElement ) {
2022-05-26 15:18:53 +08:00
// 三击在悬浮层中会选择到 attr https://github.com/siyuan-note/siyuan/issues/4636
2023-11-22 17:14:27 +08:00
// 需要获取可编辑元素,使用 previousElementSibling 的话会 https://github.com/siyuan-note/siyuan/issues/9714
setLastNodeRange ( getContenteditableElement ( startBlockElement ) , range , false ) ;
2025-04-18 01:06:21 +08:00
} else if ( [ "TD" , "TH" ] . includes ( ( range . endContainer as HTMLElement ) . tagName ) ) {
2025-04-18 16:58:56 +08:00
const cellElement = hasClosestByTag ( range . startContainer , "TH" ) || hasClosestByTag ( range . startContainer , "TD" ) ;
2025-04-18 01:06:21 +08:00
if ( cellElement ) {
setLastNodeRange ( cellElement , range , false ) ;
}
2022-05-26 15:18:53 +08:00
}
} else {
endBlockElement = hasClosestBlock ( range . endContainer ) ;
}
2025-07-23 13:08:38 +08:00
if ( startBlockElement && endBlockElement && endBlockElement !== startBlockElement ) {
2024-05-09 09:14:32 +08:00
if ( ( range . startContainer . nodeType === 1 && ( range . startContainer as HTMLElement ) . tagName === "DIV" && ( range . startContainer as HTMLElement ) . classList . contains ( "protyle-attr" ) ) ||
event . clientY > mouseUpEvent . clientY ) {
2024-05-09 09:07:13 +08:00
setFirstNodeRange ( getContenteditableElement ( endBlockElement ) , range ) ;
} else if ( range . endOffset === 0 && range . endContainer . nodeType === 1 && ( range . endContainer as HTMLElement ) . tagName === "DIV" ) {
setLastNodeRange ( getContenteditableElement ( startBlockElement ) , range , false ) ;
} else {
range . collapse ( true ) ;
}
2022-05-26 15:18:53 +08:00
}
}
} ;
} ) ;
2022-08-31 22:29:52 +08:00
}
private bindEvent ( protyle : IProtyle ) {
2023-12-12 00:53:15 +08:00
// 删除块时, av 头尾需重新计算位置
2023-12-12 20:53:42 +08:00
protyle . observer = new ResizeObserver ( ( ) = > {
2023-12-12 00:53:15 +08:00
const contentRect = protyle . contentElement . getBoundingClientRect ( ) ;
protyle . wysiwyg . element . querySelectorAll ( ".av" ) . forEach ( ( item : HTMLElement ) = > {
2025-07-11 10:32:30 +08:00
if ( item . querySelector ( ".av__scroll" ) ) {
2023-12-12 00:53:15 +08:00
stickyRow ( item , contentRect , "all" ) ;
}
} ) ;
} ) ;
2022-08-31 22:29:52 +08:00
this . element . addEventListener ( "focusout" , ( ) = > {
2023-07-11 21:05:18 +08:00
if ( getSelection ( ) . rangeCount === 0 ) {
return ;
}
const range = getSelection ( ) . getRangeAt ( 0 ) ;
2025-07-23 13:08:38 +08:00
if ( this . element === range . startContainer || this . element . contains ( range . startContainer ) ) {
2025-09-11 22:27:51 +08:00
protyle . toolbar . range = range . cloneRange ( ) ;
2022-08-31 22:29:52 +08:00
}
} ) ;
2025-09-06 18:02:18 +08:00
this . element . addEventListener ( "cut" , async ( event : ClipboardEvent & { target : HTMLElement } ) = > {
2022-10-27 23:10:29 +08:00
window . siyuan . ctrlIsPressed = false ; // https://github.com/siyuan-note/siyuan/issues/6373
2022-11-11 23:35:03 +08:00
if ( protyle . disabled ) {
return ;
}
2024-03-13 15:41:10 +08:00
if ( event . target . tagName === "PROTYLE-HTML" || event . target . localName === "input" ) {
2022-08-31 22:29:52 +08:00
event . stopPropagation ( ) ;
return ;
}
if ( protyle . options . render . breadcrumb ) {
protyle . breadcrumb . hide ( ) ;
}
const range = getEditorRange ( protyle . wysiwyg . element ) ;
let nodeElement = hasClosestBlock ( range . startContainer ) ;
if ( ! nodeElement ) {
2023-07-03 23:02:19 +08:00
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
return ;
}
2024-06-23 12:01:06 +08:00
// https://github.com/siyuan-note/siyuan/issues/11793
2024-07-25 17:55:25 +08:00
const embedElement = isInEmbedBlock ( nodeElement ) ;
2024-06-23 12:01:06 +08:00
if ( embedElement ) {
nodeElement = embedElement ;
}
2023-07-03 23:02:19 +08:00
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
2022-08-31 22:29:52 +08:00
const selectImgElement = nodeElement . querySelector ( ".img--select" ) ;
2023-12-15 12:04:18 +08:00
const selectAVElement = nodeElement . querySelector ( ".av__row--select, .av__cell--select" ) ;
2024-10-29 23:00:39 +08:00
const selectTableElement = nodeElement . querySelector ( ".table__select" ) ? . clientWidth > 0 ;
2022-08-31 22:29:52 +08:00
let selectElements = Array . from ( protyle . wysiwyg . element . querySelectorAll ( ".protyle-wysiwyg--select" ) ) ;
if ( selectElements . length === 0 && range . toString ( ) === "" && ! range . cloneContents ( ) . querySelector ( "img" ) &&
2024-10-29 23:00:39 +08:00
! selectImgElement && ! selectAVElement && ! selectTableElement ) {
2022-08-31 22:29:52 +08:00
nodeElement . classList . add ( "protyle-wysiwyg--select" ) ;
selectElements = [ nodeElement ] ;
}
let html = "" ;
2024-01-27 22:06:22 +08:00
let textPlain = "" ;
2025-09-05 12:01:46 +08:00
let isInCodeBlock = false ;
2025-09-06 18:02:18 +08:00
let needClipboardWrite = false ;
2022-08-31 22:29:52 +08:00
if ( selectElements . length > 0 ) {
2022-11-12 00:36:53 +08:00
if ( selectElements [ 0 ] . getAttribute ( "data-type" ) === "NodeListItem" &&
selectElements [ 0 ] . parentElement . classList . contains ( "list" ) && // 反链复制列表项 https://github.com/siyuan-note/siyuan/issues/6555
selectElements [ 0 ] . parentElement . childElementCount - 1 === selectElements . length ) {
2025-10-10 13:06:31 +08:00
const hasNoLiElement = selectElements . find ( item = > {
2025-10-17 22:57:52 +08:00
if ( ! selectElements [ 0 ] . parentElement . contains ( item ) ) {
2025-10-10 13:06:31 +08:00
return true ;
2022-08-31 22:29:52 +08:00
}
2025-10-10 13:06:31 +08:00
} ) ;
if ( ! hasNoLiElement ) {
selectElements = [ selectElements [ 0 ] . parentElement ] ;
2025-09-06 18:02:18 +08:00
}
2025-10-10 13:06:31 +08:00
}
let listHTML = "" ;
for ( let i = 0 ; i < selectElements . length ; i ++ ) {
const item = getTopAloneElement ( selectElements [ i ] ) ;
let itemHTML = "" ;
if ( item . getAttribute ( "data-type" ) === "NodeHeading" && item . getAttribute ( "fold" ) === "1" ) {
needClipboardWrite = true ;
const response = await fetchSyncPost ( "/api/block/getHeadingChildrenDOM" , {
id : item.getAttribute ( "data-node-id" ) ,
removeFoldAttr : false
} ) ;
itemHTML = response . data ;
} else if ( item . getAttribute ( "data-type" ) !== "NodeBlockQueryEmbed" && item . querySelector ( '[data-type="NodeHeading"][fold="1"]' ) ) {
needClipboardWrite = true ;
const response = await fetchSyncPost ( "/api/block/getBlockDOM" , {
id : item.getAttribute ( "data-node-id" ) ,
} ) ;
itemHTML = response . data . dom ;
} else {
itemHTML = removeEmbed ( item ) ;
}
2025-10-18 20:47:01 +08:00
if ( item . getAttribute ( "data-type" ) === "NodeListItem" ) {
if ( ! listHTML ) {
listHTML = ` <div data-subtype=" ${ item . getAttribute ( "data-subtype" ) } " data-node-id=" ${ Lute . NewNodeID ( ) } " data-type="NodeList" class="list"> ` ;
}
listHTML += itemHTML ;
if ( i === selectElements . length - 1 ||
selectElements [ i + 1 ] . getAttribute ( "data-type" ) !== "NodeListItem" ||
selectElements [ i + 1 ] . getAttribute ( "data-subtype" ) !== item . getAttribute ( "data-subtype" )
) {
html += ` ${ listHTML } <div class="protyle-attr" contenteditable="false"> ${ Constants . ZWSP } </div></div> ` ;
listHTML = "" ;
}
2025-10-10 13:06:31 +08:00
} else {
html += itemHTML ;
2022-08-31 22:29:52 +08:00
}
}
const nextElement = getNextBlock ( selectElements [ selectElements . length - 1 ] ) ;
2024-02-26 11:34:19 +08:00
removeBlock ( protyle , nodeElement , range , "remove" ) ;
2022-08-31 22:29:52 +08:00
if ( nextElement ) {
// Ctrl+X 剪切后光标应跳到下一行行首 https://github.com/siyuan-note/siyuan/issues/5485
focusBlock ( nextElement ) ;
}
2023-12-15 12:04:18 +08:00
} else if ( selectAVElement ) {
2025-10-10 13:06:31 +08:00
needClipboardWrite = true ;
const cellsValue = await updateCellsValue ( protyle , nodeElement ) ;
2024-01-29 15:44:18 +08:00
html = JSON . stringify ( cellsValue . json ) ;
2024-01-27 22:06:22 +08:00
textPlain = cellsValue . text ;
2024-10-29 23:00:39 +08:00
} else if ( selectTableElement ) {
const selectCellElements : HTMLTableCellElement [ ] = [ ] ;
const scrollLeft = nodeElement . firstElementChild . scrollLeft ;
2025-05-08 17:58:26 +08:00
const scrollTop = nodeElement . querySelector ( "table" ) . scrollTop ;
2024-10-29 23:00:39 +08:00
const tableSelectElement = nodeElement . querySelector ( ".table__select" ) as HTMLElement ;
2024-10-29 23:01:31 +08:00
html = "<table>" ;
2024-10-29 23:00:39 +08:00
nodeElement . querySelectorAll ( "th, td" ) . forEach ( ( item : HTMLTableCellElement ) = > {
2025-05-08 17:58:26 +08:00
if ( ! item . classList . contains ( "fn__none" ) && isIncludeCell ( {
tableSelectElement ,
scrollLeft ,
scrollTop ,
item ,
} ) ) {
2024-10-29 23:00:39 +08:00
selectCellElements . push ( item ) ;
}
} ) ;
2024-12-04 21:27:18 +08:00
tableSelectElement . removeAttribute ( "style" ) ;
2024-12-20 12:23:49 +08:00
if ( getSelection ( ) . rangeCount > 0 ) {
2024-12-13 21:56:06 +08:00
const range = getSelection ( ) . getRangeAt ( 0 ) ;
2024-12-12 10:48:57 +08:00
if ( nodeElement . contains ( range . startContainer ) ) {
2024-12-13 21:56:06 +08:00
range . insertNode ( document . createElement ( "wbr" ) ) ;
2024-12-12 10:48:57 +08:00
}
}
2024-12-04 21:27:18 +08:00
const oldHTML = nodeElement . outerHTML ;
2024-12-12 10:48:57 +08:00
nodeElement . querySelector ( "wbr" ) ? . remove ( ) ;
2024-12-04 21:27:18 +08:00
nodeElement . setAttribute ( "updated" , dayjs ( ) . format ( "YYYYMMDDHHmmss" ) ) ;
2024-10-29 23:00:39 +08:00
selectCellElements . forEach ( ( item , index ) = > {
if ( index === 0 || ! item . previousElementSibling ||
2025-07-23 13:08:38 +08:00
item . previousElementSibling !== selectCellElements [ index - 1 ] ) {
2024-10-29 23:01:31 +08:00
html += "<tr>" ;
2024-10-29 23:00:39 +08:00
}
html += item . outerHTML ;
if ( ! item . nextElementSibling || ! selectCellElements [ index + 1 ] ||
2025-07-23 13:08:38 +08:00
item . nextElementSibling !== selectCellElements [ index + 1 ] ) {
2024-10-29 23:00:39 +08:00
html += "</tr>" ;
}
item . innerHTML = "" ;
2024-10-29 23:01:31 +08:00
} ) ;
html += "</table>" ;
2024-10-29 23:00:39 +08:00
textPlain = protyle . lute . HTML2Md ( html ) ;
updateTransaction ( protyle , nodeElement . getAttribute ( "data-node-id" ) , nodeElement . outerHTML , oldHTML ) ;
2022-08-31 22:29:52 +08:00
} else {
const id = nodeElement . getAttribute ( "data-node-id" ) ;
2024-12-30 22:08:41 +08:00
setInsertWbrHTML ( nodeElement , range , protyle ) ;
const oldHTML = protyle . wysiwyg . lastHTMLs [ id ] || nodeElement . outerHTML ;
2022-08-31 22:29:52 +08:00
const tempElement = document . createElement ( "div" ) ;
// 首次选中标题时, range.startContainer 会为空
let startContainer = range . startContainer ;
if ( startContainer . nodeType === 3 && startContainer . textContent === "" ) {
const nextSibling = hasNextSibling ( range . startContainer ) ;
if ( nextSibling ) {
startContainer = nextSibling ;
}
}
const headElement = hasClosestByAttribute ( startContainer , "data-type" , "NodeHeading" ) ;
if ( headElement && range . toString ( ) === headElement . firstElementChild . textContent ) {
2025-02-23 18:34:38 +08:00
tempElement . insertAdjacentHTML ( "afterbegin" , headElement . firstElementChild . innerHTML ) ;
headElement . firstElementChild . innerHTML = "" ;
2025-07-23 13:08:38 +08:00
} else if ( range . toString ( ) !== "" && startContainer === range . endContainer &&
2025-03-12 09:21:32 +08:00
range . startContainer . nodeType === 3 &&
// 需使用 wholeText https://github.com/siyuan-note/siyuan/issues/14339
2025-03-15 18:13:01 +08:00
range . endOffset === ( range . endContainer as Text ) . wholeText . length &&
2025-03-12 09:21:32 +08:00
range . startOffset === 0 &&
2022-09-19 16:47:38 +08:00
! [ "DIV" , "TD" , "TH" , "TR" ] . includes ( range . startContainer . parentElement . tagName ) ) {
2022-08-31 22:29:52 +08:00
// 选中整个内联元素
tempElement . append ( range . startContainer . parentElement ) ;
} else if ( selectImgElement ) {
tempElement . append ( selectImgElement ) ;
2022-09-30 22:25:35 +08:00
} else if ( range . startContainer . nodeType === 3 && range . startContainer . parentElement . tagName === "SPAN" &&
2022-10-31 15:45:45 +08:00
range . startContainer . parentElement . getAttribute ( "data-type" ) &&
2025-07-23 13:08:38 +08:00
range . startContainer . parentElement === range . endContainer . parentElement ) {
2022-09-30 22:25:35 +08:00
// 剪切粗体等字体中的一部分
2022-10-03 16:22:18 +08:00
const spanElement = range . startContainer . parentElement ;
2022-09-30 22:25:35 +08:00
const attributes = spanElement . attributes ;
const newSpanElement = document . createElement ( "span" ) ;
for ( let i = 0 ; i < attributes . length ; i ++ ) {
newSpanElement . setAttribute ( attributes [ i ] . name , attributes [ i ] . value ) ;
}
if ( spanElement . getAttribute ( "data-type" ) . indexOf ( "block-ref" ) > - 1 &&
spanElement . getAttribute ( "data-subtype" ) === "d" ) {
// 引用被剪切后需变为静态锚文本
newSpanElement . setAttribute ( "data-subtype" , "s" ) ;
spanElement . setAttribute ( "data-subtype" , "s" ) ;
}
newSpanElement . textContent = range . toString ( ) ;
range . deleteContents ( ) ;
tempElement . append ( newSpanElement ) ;
2022-08-31 22:29:52 +08:00
} else {
if ( range . cloneContents ( ) . querySelectorAll ( "td, th" ) . length > 0 ) {
// 表格内多格子 cut https://github.com/siyuan-note/insider/issues/564
const wbrElement = document . createElement ( "wbr" ) ;
range . insertNode ( wbrElement ) ;
range . setStartAfter ( wbrElement ) ;
tempElement . append ( range . extractContents ( ) ) ;
nodeElement . outerHTML = protyle . lute . SpinBlockDOM ( nodeElement . outerHTML ) ;
nodeElement = protyle . wysiwyg . element . querySelector ( ` [data-node-id=" ${ id } "] ` ) as HTMLElement ;
2024-05-09 16:35:04 +08:00
mathRender ( nodeElement ) ;
2022-08-31 22:29:52 +08:00
focusByWbr ( nodeElement , range ) ;
} else {
const inlineMathElement = hasClosestByAttribute ( range . commonAncestorContainer , "data-type" , "inline-math" ) ;
if ( inlineMathElement ) {
// 表格内剪切数学公式 https://ld246.com/article/1631708573504
tempElement . append ( inlineMathElement ) ;
} else {
tempElement . append ( range . extractContents ( ) ) ;
let parentElement : Element | false ;
// https://ld246.com/article/1647689760545
2023-12-15 12:04:18 +08:00
if ( nodeElement . classList . contains ( "av" ) ) {
updateAVName ( protyle , nodeElement ) ;
} else if ( nodeElement . classList . contains ( "table" ) ) {
2025-02-09 12:04:40 +08:00
parentElement = hasClosestByTag ( range . startContainer , "TD" ) || hasClosestByTag ( range . startContainer , "TH" ) ;
2022-08-31 22:29:52 +08:00
} else {
parentElement = getContenteditableElement ( nodeElement ) ;
}
if ( parentElement ) {
// 引用文本剪切 https://ld246.com/article/1647689760545
// 表格多行剪切 https://ld246.com/article/1652603836350
2022-11-20 00:21:51 +08:00
// 自定义表情的段落剪切后表情丢失 https://ld246.com/article/1668781478724
2022-08-31 22:29:52 +08:00
Array . from ( parentElement . children ) . forEach ( item = > {
2022-11-20 00:21:51 +08:00
if ( item . textContent === "" && ( item . nodeType === 1 && ! [ "BR" , "IMG" ] . includes ( item . tagName ) ) ) {
2022-08-31 22:29:52 +08:00
item . remove ( ) ;
}
} ) ;
}
}
}
}
2023-04-14 22:26:34 +08:00
this . emojiToMd ( tempElement ) ;
2022-08-31 22:29:52 +08:00
html = tempElement . innerHTML ;
2024-03-24 23:46:11 +08:00
// https://github.com/siyuan-note/siyuan/issues/10722
if ( hasClosestByAttribute ( range . startContainer , "data-type" , "NodeCodeBlock" ) ||
2025-02-09 12:04:40 +08:00
hasClosestByTag ( range . startContainer , "CODE" ) ) {
2024-03-24 23:46:11 +08:00
textPlain = tempElement . textContent . replace ( Constants . ZWSP , "" ) ;
2025-09-05 12:01:46 +08:00
isInCodeBlock = true ;
2024-03-24 23:46:11 +08:00
}
2022-08-31 22:29:52 +08:00
// https://github.com/siyuan-note/siyuan/issues/4321
if ( ! nodeElement . classList . contains ( "table" ) ) {
const editableElement = getContenteditableElement ( nodeElement ) ;
if ( editableElement && editableElement . textContent === "" ) {
editableElement . innerHTML = "" ;
}
}
nodeElement . setAttribute ( "updated" , dayjs ( ) . format ( "YYYYMMDDHHmmss" ) ) ;
if ( nodeElement . getAttribute ( "data-type" ) === "NodeCodeBlock" ) {
range . insertNode ( document . createElement ( "wbr" ) ) ;
2024-08-15 12:00:16 +08:00
nodeElement . querySelector ( '[data-render="true"]' ) ? . removeAttribute ( "data-render" ) ;
2022-08-31 22:29:52 +08:00
highlightRender ( nodeElement ) ;
}
2025-02-23 18:34:38 +08:00
if ( nodeElement . parentElement . parentElement && ! nodeElement . classList . contains ( "av" ) ) {
2022-08-31 22:29:52 +08:00
// 选中 heading 时,使用删除的 transaction
2024-12-30 22:08:41 +08:00
setInsertWbrHTML ( nodeElement , range , protyle ) ;
updateTransaction ( protyle , id , protyle . wysiwyg . lastHTMLs [ id ] || nodeElement . outerHTML , oldHTML ) ;
2022-08-31 22:29:52 +08:00
}
}
protyle . hint . render ( protyle ) ;
2024-01-27 22:06:22 +08:00
if ( ! selectAVElement ) {
2024-03-24 23:46:11 +08:00
textPlain = textPlain || protyle . lute . BlockDOM2StdMd ( html ) . trimEnd ( ) ; // 需要 trimEnd, 否则 \n 会导致 https://github.com/siyuan-note/siyuan/issues/6218
2025-04-16 20:57:17 +08:00
if ( nodeElement . classList . contains ( "table" ) ) {
textPlain = textPlain . replace ( /<br>/g , "\n" ) . replace ( /<br\/>/g , "\n" ) ;
2025-04-17 23:33:04 +08:00
textPlain = textPlain . endsWith ( "\n" ) ? textPlain . replace ( /\n$/ , "" ) : textPlain ;
2025-04-16 20:57:17 +08:00
}
2024-01-27 22:06:22 +08:00
}
2023-10-09 21:45:36 +08:00
textPlain = textPlain . replace ( /\u00A0/g , " " ) ; // Replace non-breaking spaces with normal spaces when copying https://github.com/siyuan-note/siyuan/issues/9382
event . clipboardData . setData ( "text/plain" , textPlain ) ;
2025-07-17 17:26:58 +08:00
2025-09-06 18:02:18 +08:00
if ( ! isInCodeBlock ) {
enableLuteMarkdownSyntax ( protyle ) ;
const textSiyuan = selectTableElement ? protyle . lute . HTML2BlockDOM ( html ) : html ;
restoreLuteMarkdownSyntax ( protyle ) ;
event . clipboardData . setData ( "text/siyuan" , textSiyuan ) ;
// 在 text/html 中插入注释节点,用于右键菜单粘贴时获取 text/siyuan 数据
const textHTML = ` <!--data-siyuan=' ${ encodeBase64 ( textSiyuan ) } '--> ` + ( selectTableElement ? html : protyle.lute.BlockDOM2HTML ( selectAVElement ? textPlain : html ) ) ;
event . clipboardData . setData ( "text/html" , textHTML ) ;
if ( needClipboardWrite ) {
try {
await navigator . clipboard . write ( [ new ClipboardItem ( {
[ "text/plain" ] : textPlain ,
[ "text/html" ] : textHTML ,
} ) ] ) ;
} catch ( e ) {
console . log ( "Cut write clipboard error:" , e ) ;
}
}
2025-09-05 12:01:46 +08:00
}
2022-08-31 22:29:52 +08:00
} ) ;
let beforeContextmenuRange : Range ;
2022-10-03 18:01:03 +08:00
this . element . addEventListener ( "contextmenu" , ( event : MouseEvent & { detail : any } ) = > {
2023-08-02 21:35:49 +08:00
if ( event . shiftKey ) {
return ;
}
2022-08-31 22:29:52 +08:00
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
2022-10-05 21:29:56 +08:00
const x = event . clientX || event . detail . x ;
const y = event . clientY || event . detail . y ;
2022-08-31 22:29:52 +08:00
const selectElements = protyle . wysiwyg . element . querySelectorAll ( ".protyle-wysiwyg--select" ) ;
if ( selectElements . length > 1 ) {
// 多选块
hideElements ( [ "util" ] , protyle ) ;
protyle . gutter . renderMenu ( protyle , selectElements [ 0 ] ) ;
2022-10-03 18:01:03 +08:00
window . siyuan . menus . menu . popup ( { x , y } ) ;
2022-08-31 22:29:52 +08:00
return ;
}
2022-10-05 21:29:56 +08:00
const target = event . detail . target || event . target as HTMLElement ;
2024-07-25 17:55:25 +08:00
const embedElement = isInEmbedBlock ( target ) ;
2022-08-31 22:29:52 +08:00
if ( embedElement ) {
if ( getSelection ( ) . rangeCount === 0 ) {
focusSideBlock ( embedElement ) ;
}
protyle . gutter . renderMenu ( protyle , embedElement ) ;
2023-04-10 18:00:36 +08:00
/// #if MOBILE
window . siyuan . menus . menu . fullscreen ( ) ;
/// #else
2022-10-03 18:01:03 +08:00
window . siyuan . menus . menu . popup ( { x , y } ) ;
2023-04-10 18:00:36 +08:00
/// #endif
2022-08-31 22:29:52 +08:00
return false ;
}
2024-05-16 22:58:51 +08:00
const nodeElement = hasClosestBlock ( target ) ;
if ( ! nodeElement ) {
return false ;
}
2025-06-19 18:00:52 +08:00
const avGalleryItemElement = hasClosestByClassName ( target , "av__gallery-item" ) ;
if ( avGalleryItemElement ) {
2025-07-03 21:42:00 +08:00
openGalleryItemMenu ( {
2025-06-20 10:58:34 +08:00
target : avGalleryItemElement.querySelector ( ".protyle-icon--last" ) ,
2025-06-19 18:00:52 +08:00
protyle ,
2025-07-03 21:42:00 +08:00
position : {
x : event.clientX ,
y : event.clientY
}
2025-06-19 18:00:52 +08:00
} ) ;
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
2025-07-03 16:39:49 +08:00
return false ;
2025-06-19 18:00:52 +08:00
}
2024-05-15 12:14:26 +08:00
const avCellElement = hasClosestByClassName ( target , "av__cell" ) ;
if ( avCellElement ) {
2024-05-16 22:58:51 +08:00
if ( avCellElement . classList . contains ( "av__cell--header" ) ) {
if ( ! protyle . disabled ) {
showColMenu ( protyle , nodeElement , avCellElement ) ;
}
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
return ;
}
2024-05-15 12:14:26 +08:00
if ( getTypeByCellElement ( avCellElement ) === "mAsset" ) {
const assetImgElement = hasClosestByClassName ( target , "av__cellassetimg" ) || hasClosestByClassName ( target , "av__celltext--url" ) ;
if ( assetImgElement ) {
let index = 0 ;
Array . from ( avCellElement . children ) . find ( ( item , i ) = > {
if ( item === assetImgElement ) {
index = i ;
return true ;
}
2024-05-15 12:25:03 +08:00
} ) ;
2024-05-15 12:14:26 +08:00
editAssetItem ( {
protyle ,
cellElements : [ avCellElement ] ,
blockElement : hasClosestBlock ( assetImgElement ) as HTMLElement ,
content : target.tagName === "IMG" ? target . getAttribute ( "src" ) : target . getAttribute ( "data-url" ) ,
type : target . tagName === "IMG" ? "image" : "file" ,
2024-05-20 11:56:19 +08:00
name : target.tagName === "IMG" ? "" : target . getAttribute ( "data-name" ) ,
2024-05-15 12:14:26 +08:00
index ,
rect : target.getBoundingClientRect ( )
2024-05-15 12:25:03 +08:00
} ) ;
2024-05-15 12:14:26 +08:00
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
return ;
}
}
}
2024-01-10 11:33:49 +08:00
// 在 span 前面,防止单元格哪 block-ref 被修改
const avRowElement = hasClosestByClassName ( target , "av__row" ) ;
if ( avRowElement && avContextmenu ( protyle , avRowElement , {
x : event.clientX ,
y : avRowElement.getBoundingClientRect ( ) . bottom ,
h : avRowElement.clientHeight
} ) ) {
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
return ;
}
2024-05-16 22:58:51 +08:00
const avTabHeaderElement = hasClosestByClassName ( target , "item" ) ;
if ( nodeElement . classList . contains ( "av" ) && avTabHeaderElement ) {
if ( avTabHeaderElement . classList . contains ( "item--focus" ) ) {
openViewMenu ( { protyle , blockElement : nodeElement , element : avTabHeaderElement } ) ;
} else {
2025-06-16 23:32:48 +08:00
transaction ( protyle , [ {
action : "setAttrViewBlockView" ,
blockID : nodeElement.getAttribute ( "data-node-id" ) ,
id : avTabHeaderElement.dataset.id ,
avID : nodeElement.getAttribute ( "data-av-id" ) ,
2025-10-28 20:05:45 +08:00
} ] , [ {
action : "setAttrViewBlockView" ,
blockID : nodeElement.getAttribute ( "data-node-id" ) ,
id : avTabHeaderElement.parentElement.querySelector ( ".item--focus" ) . getAttribute ( "data-id" ) ,
avID : nodeElement.getAttribute ( "data-av-id" ) ,
2025-06-16 23:32:48 +08:00
} ] ) ;
window . siyuan . menus . menu . remove ( ) ;
openViewMenu ( {
protyle ,
blockElement : nodeElement ,
element : avTabHeaderElement
} ) ;
2024-05-16 22:58:51 +08:00
}
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
return ;
}
2022-08-31 22:29:52 +08:00
protyle . toolbar . range = getEditorRange ( protyle . element ) ;
2024-09-09 23:23:03 +08:00
if ( target . tagName === "SPAN" && ! isNotEditBlock ( nodeElement ) ) { // https://ld246.com/article/1665141518103
2024-11-13 15:29:09 +08:00
let types = target . getAttribute ( "data-type" ) ? . split ( " " ) || [ ] ;
2023-08-18 09:44:26 +08:00
if ( types . length === 0 ) {
// https://github.com/siyuan-note/siyuan/issues/8960
types = ( target . dataset . type || "" ) . split ( " " ) ;
}
2023-03-07 10:42:20 +08:00
if ( types . length > 0 ) {
removeSearchMark ( target ) ;
}
2023-10-14 12:14:12 +08:00
if ( types . includes ( "block-ref" ) ) {
2023-06-01 20:50:49 +08:00
refMenu ( protyle , target ) ;
2022-08-31 22:29:52 +08:00
// 阻止 popover
target . setAttribute ( "prevent-popover" , "true" ) ;
setTimeout ( ( ) = > {
target . removeAttribute ( "prevent-popover" ) ;
} , 620 ) ;
2022-10-07 23:01:03 +08:00
return false ;
2023-04-18 11:57:19 +08:00
} else if ( types . includes ( "file-annotation-ref" ) && ! protyle . disabled ) {
2023-06-29 22:07:41 +08:00
fileAnnotationRefMenu ( protyle , target ) ;
2022-10-07 23:01:03 +08:00
return false ;
2023-04-18 11:57:19 +08:00
} else if ( types . includes ( "tag" ) && ! protyle . disabled ) {
2023-06-01 20:50:49 +08:00
tagMenu ( protyle , target ) ;
2023-01-15 22:44:05 +08:00
return false ;
2023-03-07 10:42:20 +08:00
} else if ( types . includes ( "inline-memo" ) ) {
2022-10-07 23:01:03 +08:00
protyle . toolbar . showRender ( protyle , target ) ;
return false ;
2024-04-28 12:27:34 +08:00
} else if ( types . includes ( "a" ) ) {
2023-06-01 20:50:49 +08:00
linkMenu ( protyle , target ) ;
2023-01-15 22:44:05 +08:00
if ( window . siyuan . config . editor . floatWindowMode === 0 &&
target . getAttribute ( "data-href" ) ? . startsWith ( "siyuan://blocks" ) ) {
2022-10-07 23:01:03 +08:00
// 阻止 popover
target . setAttribute ( "prevent-popover" , "true" ) ;
setTimeout ( ( ) = > {
target . removeAttribute ( "prevent-popover" ) ;
} , 620 ) ;
}
return false ;
2022-08-31 22:29:52 +08:00
}
}
2024-06-28 22:42:28 +08:00
const inlineMathElement = hasClosestByAttribute ( target , "data-type" , "inline-math" ) ;
if ( inlineMathElement ) {
inlineMathMenu ( protyle , inlineMathElement ) ;
return false ;
}
2023-10-30 10:29:53 +08:00
if ( target . tagName === "IMG" && hasClosestByClassName ( target , "img" ) ) {
2023-06-01 20:50:49 +08:00
imgMenu ( protyle , protyle . toolbar . range , target . parentElement . parentElement , {
2022-10-03 18:01:03 +08:00
clientX : x + 4 ,
clientY : y
2022-08-31 22:29:52 +08:00
} ) ;
return false ;
}
2022-11-28 18:30:01 +08:00
if ( ! isNotEditBlock ( nodeElement ) && ! nodeElement . classList . contains ( "protyle-wysiwyg--select" ) &&
2023-08-16 17:16:23 +08:00
! hasClosestByClassName ( target , "protyle-action" ) && // https://github.com/siyuan-note/siyuan/issues/8983
2022-10-03 18:24:22 +08:00
( isMobile ( ) || event . detail . target || ( beforeContextmenuRange && nodeElement . contains ( beforeContextmenuRange . startContainer ) ) )
) {
2023-07-03 23:02:19 +08:00
if ( ( ! isMobile ( ) || protyle . toolbar ? . element . classList . contains ( "fn__none" ) ) && ! nodeElement . classList . contains ( "av" ) ) {
2022-08-31 22:29:52 +08:00
contentMenu ( protyle , nodeElement ) ;
2022-10-03 18:01:03 +08:00
window . siyuan . menus . menu . popup ( { x , y : y + 13 , h : 26 } ) ;
2022-08-31 22:29:52 +08:00
protyle . toolbar ? . element . classList . add ( "fn__none" ) ;
if ( nodeElement . classList . contains ( "table" ) ) {
nodeElement . querySelector ( ".table__select" ) . removeAttribute ( "style" ) ;
}
}
} else if ( protyle . toolbar . range . toString ( ) === "" ) {
hideElements ( [ "util" ] , protyle ) ;
2022-09-30 00:12:35 +08:00
if ( protyle . gutter ) {
protyle . gutter . renderMenu ( protyle , nodeElement ) ;
}
2023-04-10 18:00:36 +08:00
/// #if MOBILE
window . siyuan . menus . menu . fullscreen ( ) ;
/// #else
2022-10-03 18:01:03 +08:00
window . siyuan . menus . menu . popup ( { x , y } ) ;
2023-04-10 18:00:36 +08:00
/// #endif
2022-08-31 22:29:52 +08:00
protyle . toolbar ? . element . classList . add ( "fn__none" ) ;
}
} ) ;
this . element . addEventListener ( "pointerdown" , ( ) = > {
if ( getSelection ( ) . rangeCount > 0 ) {
beforeContextmenuRange = getSelection ( ) . getRangeAt ( 0 ) ;
} else {
beforeContextmenuRange = undefined ;
}
2024-02-03 10:44:01 +08:00
/// #if BROWSER && !MOBILE
if ( protyle . breadcrumb ) {
2024-02-03 12:32:56 +08:00
const indentElement = protyle . breadcrumb . element . parentElement . querySelector ( '[data-type="indent"]' ) ;
2024-02-03 10:44:01 +08:00
if ( indentElement && getSelection ( ) . rangeCount > 0 ) {
setTimeout ( ( ) = > {
const newRange = getSelection ( ) . getRangeAt ( 0 ) ;
const blockElement = hasClosestBlock ( newRange . startContainer ) ;
if ( ! blockElement ) {
2024-02-03 12:32:56 +08:00
return ;
2024-02-03 10:44:01 +08:00
}
const outdentElement = protyle . breadcrumb . element . parentElement . querySelector ( '[data-type="outdent"]' ) ;
if ( blockElement . parentElement . classList . contains ( "li" ) ) {
indentElement . removeAttribute ( "disabled" ) ;
outdentElement . removeAttribute ( "disabled" ) ;
} else {
indentElement . setAttribute ( "disabled" , "true" ) ;
outdentElement . setAttribute ( "disabled" , "true" ) ;
}
} , 520 ) ;
}
}
/// #endif
2022-08-31 22:29:52 +08:00
} ) ;
2022-09-17 00:53:44 +08:00
let preventGetTopHTML = false ;
2022-08-31 22:29:52 +08:00
this . element . addEventListener ( "mousewheel" , ( event : WheelEvent ) = > {
2025-02-05 17:26:32 +08:00
hideTooltip ( ) ;
2022-08-31 22:29:52 +08:00
// https://ld246.com/article/1648865235549
2022-09-17 00:53:44 +08:00
// 不能使用上一版本的 timeStamp, 否则一直滚动将导致间隔不够 https://ld246.com/article/1662852664926
if ( ! preventGetTopHTML &&
2022-08-31 22:29:52 +08:00
event . deltaY < 0 && ! protyle . scroll . element . classList . contains ( "fn__none" ) &&
protyle . contentElement . clientHeight === protyle . contentElement . scrollHeight &&
2023-05-10 22:32:08 +08:00
protyle . wysiwyg . element . firstElementChild . getAttribute ( "data-eof" ) !== "1" ) {
2022-08-31 22:29:52 +08:00
fetchPost ( "/api/filetree/getDoc" , {
id : protyle.wysiwyg.element.firstElementChild.getAttribute ( "data-node-id" ) ,
mode : 1 ,
2022-10-30 23:13:41 +08:00
size : window.siyuan.config.editor.dynamicLoadBlocks ,
2022-08-31 22:29:52 +08:00
} , getResponse = > {
2022-09-17 00:53:44 +08:00
preventGetTopHTML = false ;
2023-06-01 14:56:21 +08:00
onGet ( {
data : getResponse ,
protyle ,
action : [ Constants . CB_GET_BEFORE , Constants . CB_GET_UNCHANGEID ] ,
} ) ;
2022-08-31 22:29:52 +08:00
} ) ;
2022-09-17 00:53:44 +08:00
preventGetTopHTML = true ;
2022-08-31 22:29:52 +08:00
}
if ( event . deltaX === 0 ) {
return ;
}
// https://github.com/siyuan-note/siyuan/issues/4099
const tableElement = hasClosestByClassName ( event . target as HTMLElement , "table" ) ;
if ( tableElement ) {
const tableSelectElement = tableElement . querySelector ( ".table__select" ) as HTMLElement ;
if ( tableSelectElement ? . style . width ) {
tableSelectElement . removeAttribute ( "style" ) ;
window . siyuan . menus . menu . remove ( ) ;
}
}
} , { passive : true } ) ;
2022-05-26 15:18:53 +08:00
this . element . addEventListener ( "paste" , ( event : ClipboardEvent & { target : HTMLElement } ) = > {
2024-05-04 15:20:24 +08:00
// https://github.com/siyuan-note/siyuan/issues/11241
if ( event . target . localName === "input" && event . target . getAttribute ( "data-type" ) === "av-search" ) {
return ;
}
2022-10-31 15:52:01 +08:00
if ( protyle . disabled ) {
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
return ;
}
2022-10-27 23:10:29 +08:00
window . siyuan . ctrlIsPressed = false ; // https://github.com/siyuan-note/siyuan/issues/6373
2022-05-26 15:18:53 +08:00
// https://github.com/siyuan-note/siyuan/issues/4600
2024-03-13 15:41:10 +08:00
if ( event . target . tagName === "PROTYLE-HTML" || event . target . localName === "input" ) {
2022-05-26 15:18:53 +08:00
event . stopPropagation ( ) ;
return ;
}
2023-12-25 21:31:00 +08:00
if ( ! hasClosestByAttribute ( event . target , "contenteditable" , "true" ) ) {
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
return ;
}
2023-11-16 23:37:13 +08:00
const blockElement = hasClosestBlock ( event . target ) ;
2024-04-21 12:14:18 +08:00
if ( blockElement && ! getContenteditableElement ( blockElement ) ) {
2023-11-15 11:00:40 +08:00
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
return ;
}
2024-05-29 21:23:52 +08:00
if ( ! blockElement ) {
return ;
}
// 链接, 备注, 样式, 引用, pdf标注粘贴 https://github.com/siyuan-note/siyuan/issues/11572
const range = getSelection ( ) . getRangeAt ( 0 ) ;
2025-05-26 21:49:21 +08:00
protyle . toolbar . range = range ;
2024-05-29 21:23:52 +08:00
const inlineElement = range . startContainer . parentElement ;
if ( range . toString ( ) === "" && inlineElement . tagName === "SPAN" ) {
const currentTypes = ( inlineElement . getAttribute ( "data-type" ) || "" ) . split ( " " ) ;
if ( currentTypes . includes ( "inline-memo" ) || currentTypes . includes ( "text" ) ||
currentTypes . includes ( "block-ref" ) || currentTypes . includes ( "file-annotation-ref" ) ||
currentTypes . includes ( "a" ) ) {
const offset = getSelectionOffset ( inlineElement , blockElement , range ) ;
if ( offset . start === 0 ) {
range . setStartBefore ( inlineElement ) ;
range . collapse ( true ) ;
} else if ( offset . start === inlineElement . textContent . length ) {
range . setEndAfter ( inlineElement ) ;
range . collapse ( false ) ;
}
}
}
2022-05-26 15:18:53 +08:00
paste ( protyle , event ) ;
} ) ;
// 输入法测试点 https://github.com/siyuan-note/siyuan/issues/3027
let isComposition = false ; // for iPhone
2024-11-26 01:00:23 +08:00
this . element . addEventListener ( "compositionstart" , ( event ) = > {
isComposition = true ;
2024-11-27 10:43:54 +08:00
// 微软双拼由于 focusByRange 导致无法输入文字,因此不再 keydown 中记录了,但 keyup 会记录拼音字符,因此使用 isComposition 阻止 keyup 记录。
// 但搜狗输入法选中后继续输入不走 keydown, isComposition 阻止了 keyup 记录,因此需在此记录。
const range = getEditorRange ( protyle . wysiwyg . element ) ;
const nodeElement = hasClosestBlock ( range . startContainer ) ;
if ( ! isMac ( ) && nodeElement ) {
2024-12-28 19:05:24 +08:00
setInsertWbrHTML ( nodeElement , range , protyle ) ;
2024-11-27 10:43:54 +08:00
}
2022-05-26 15:18:53 +08:00
event . stopPropagation ( ) ;
} ) ;
this . element . addEventListener ( "compositionend" , ( event : InputEvent ) = > {
event . stopPropagation ( ) ;
isComposition = false ;
const range = getEditorRange ( this . element ) ;
const blockElement = hasClosestBlock ( range . startContainer ) ;
if ( ! blockElement ) {
return ;
}
if ( "" !== event . data ) {
this . escapeInline ( protyle , range , event ) ;
// 小鹤音形 ;k 不能使用 setTimeout;
// wysiwyg.element contenteditable 为 false 时,连拼 needRender 必须为 false
// hr 渲染;任务列表、粗体、数学公示结尾 needRender 必须为 true
input ( protyle , blockElement , range , true ) ;
} else {
const id = blockElement . getAttribute ( "data-node-id" ) ;
if ( protyle . wysiwyg . lastHTMLs [ id ] ) {
updateTransaction ( protyle , id , blockElement . outerHTML , protyle . wysiwyg . lastHTMLs [ id ] ) ;
}
}
} ) ;
2023-09-17 22:27:55 +08:00
let timeout : number ;
2022-05-26 15:18:53 +08:00
this . element . addEventListener ( "input" , ( event : InputEvent ) = > {
const target = event . target as HTMLElement ;
2023-07-03 23:02:19 +08:00
if ( target . tagName === "VIDEO" || target . tagName === "AUDIO" || event . inputType === "historyRedo" ) {
2022-05-26 15:18:53 +08:00
return ;
}
2022-08-15 17:30:35 +08:00
if ( event . inputType === "historyUndo" ) {
2022-11-09 00:19:47 +08:00
/// #if !BROWSER
2023-10-09 01:24:30 +08:00
ipcRenderer . send ( Constants . SIYUAN_CMD , "redo" ) ;
2022-11-09 00:19:47 +08:00
/// #endif
2022-08-15 17:30:35 +08:00
window . siyuan . menus . menu . remove ( ) ;
return ;
}
2023-09-14 00:01:31 +08:00
const range = getEditorRange ( this . element ) ;
2022-05-26 15:18:53 +08:00
const blockElement = hasClosestBlock ( range . startContainer ) ;
if ( ! blockElement ) {
return ;
}
2023-09-27 15:26:22 +08:00
if ( [ ":" , "(" , "【" , "( " , "[" , "{" , "「" , "『" , "#" , "/" , "、" ] . includes ( event . data ) ) {
2023-02-09 12:37:04 +08:00
protyle . hint . enableExtend = true ;
}
2022-05-26 15:18:53 +08:00
if ( event . isComposing || isComposition ||
// https://github.com/siyuan-note/siyuan/issues/337 编辑器内容拖拽问题
event . inputType === "deleteByDrag" || event . inputType === "insertFromDrop"
) {
return ;
}
this . escapeInline ( protyle , range , event ) ;
2023-11-25 09:32:41 +08:00
2023-11-25 10:05:59 +08:00
if ( ( /^\d{1}$/ . test ( event . data ) || event . data === "‘ " || event . data === "“" ||
// 百度输入法中文反双引号 https://github.com/siyuan-note/siyuan/issues/9686
event . data === "”" ||
event . data === "「" ) ) {
2023-09-14 00:01:31 +08:00
clearTimeout ( timeout ) ; // https://github.com/siyuan-note/siyuan/issues/9179
timeout = window . setTimeout ( ( ) = > {
2022-05-26 15:18:53 +08:00
input ( protyle , blockElement , range , true ) ; // 搜狗拼音数字后面句号变为点; Mac 反向双引号无法输入
2023-11-25 10:05:59 +08:00
} ) ;
2022-05-26 15:18:53 +08:00
} else {
2024-04-11 19:21:51 +08:00
input ( protyle , blockElement , range , true , event ) ;
2022-05-26 15:18:53 +08:00
}
event . stopPropagation ( ) ;
} ) ;
this . element . addEventListener ( "keyup" , ( event ) = > {
const range = getEditorRange ( this . element ) . cloneRange ( ) ;
2023-08-08 16:09:42 +08:00
const nodeElement = hasClosestBlock ( range . startContainer ) ;
2024-11-25 22:52:02 +08:00
2024-11-26 01:00:23 +08:00
if ( event . key !== "PageUp" && event . key !== "PageDown" && event . key !== "Home" && event . key !== "End" &&
2024-11-25 22:52:02 +08:00
event . key . indexOf ( "Arrow" ) === - 1 && event . key !== "Escape" && event . key !== "Shift" &&
event . key !== "Meta" && event . key !== "Alt" && event . key !== "Control" && event . key !== "CapsLock" &&
2024-11-25 23:01:36 +08:00
! event . ctrlKey && ! event . shiftKey && ! event . metaKey && ! event . altKey &&
2024-11-26 01:00:23 +08:00
! /^F\d{1,2}$/ . test ( event . key ) ) {
2024-11-27 10:43:54 +08:00
// 搜狗输入法不走 keydown, 没有选中字符后不走 compositionstart, 需重新记录历史状态
2024-11-26 01:00:23 +08:00
if ( ! isMac ( ) && nodeElement &&
2024-11-27 10:43:54 +08:00
// 微软双拼 keyup 会记录拼音字符,因此在 compositionstart 记录
! isComposition &&
2024-11-26 01:00:23 +08:00
( typeof protyle . wysiwyg . lastHTMLs [ nodeElement . getAttribute ( "data-node-id" ) ] === "undefined" || range . toString ( ) !== "" || ! this . preventKeyup ) ) {
2024-12-28 19:05:24 +08:00
setInsertWbrHTML ( nodeElement , range , protyle ) ;
2022-05-26 15:18:53 +08:00
}
2024-11-25 22:52:02 +08:00
this . preventKeyup = false ;
2022-05-26 15:18:53 +08:00
return ;
}
// 需放在 lastHTMLs 后,否则 https://github.com/siyuan-note/siyuan/issues/4388
if ( this . preventKeyup ) {
2024-11-25 22:52:02 +08:00
this . preventKeyup = false ;
2022-05-26 15:18:53 +08:00
return ;
}
2023-11-12 12:28:20 +08:00
if ( ( event . shiftKey || isOnlyMeta ( event ) ) && ! event . isComposing && range . toString ( ) !== "" ) {
2022-05-26 15:18:53 +08:00
// 工具栏
protyle . toolbar . render ( protyle , range , event ) ;
2022-06-27 23:48:24 +08:00
countSelectWord ( range ) ;
2022-05-26 15:18:53 +08:00
}
if ( event . eventPhase !== 3 && ! event . shiftKey && ( event . key . indexOf ( "Arrow" ) > - 1 || event . key === "Home" || event . key === "End" || event . key === "PageUp" || event . key === "PageDown" ) && ! event . isComposing ) {
if ( nodeElement ) {
2025-06-20 13:35:12 +08:00
clearSelect ( [ "img" , "av" ] , protyle . wysiwyg . element ) ;
2022-05-26 15:18:53 +08:00
this . setEmptyOutline ( protyle , nodeElement ) ;
2025-01-09 12:03:37 +08:00
if ( range . toString ( ) === "" && ! nodeElement . classList . contains ( "protyle-wysiwyg--select" ) ) {
2022-10-07 20:56:19 +08:00
countSelectWord ( range , protyle . block . rootID ) ;
}
2024-02-02 11:52:53 +08:00
if ( protyle . breadcrumb ) {
2024-02-03 12:32:56 +08:00
const indentElement = protyle . breadcrumb . element . parentElement . querySelector ( '[data-type="indent"]' ) ;
2024-02-02 11:52:53 +08:00
if ( indentElement ) {
const outdentElement = protyle . breadcrumb . element . parentElement . querySelector ( '[data-type="outdent"]' ) ;
if ( nodeElement . parentElement . classList . contains ( "li" ) ) {
indentElement . removeAttribute ( "disabled" ) ;
outdentElement . removeAttribute ( "disabled" ) ;
} else {
indentElement . setAttribute ( "disabled" , "true" ) ;
outdentElement . setAttribute ( "disabled" , "true" ) ;
}
}
}
2022-05-26 15:18:53 +08:00
}
event . stopPropagation ( ) ;
}
2023-08-08 16:09:42 +08:00
2025-02-06 10:27:39 +08:00
// 按下方向键后块高亮跟随光标移动 https://github.com/siyuan-note/siyuan/issues/8918
2025-02-08 09:25:14 +08:00
if ( ( event . key === "ArrowLeft" || event . key === "ArrowRight" ) &&
2023-08-08 16:09:42 +08:00
nodeElement && ! nodeElement . classList . contains ( "protyle-wysiwyg--select" ) ) {
2023-08-08 22:14:34 +08:00
const selectElements = Array . from ( protyle . wysiwyg . element . querySelectorAll ( ".protyle-wysiwyg--select" ) ) ;
let containRange = false ;
2023-08-08 16:09:42 +08:00
selectElements . find ( item = > {
if ( item . contains ( range . startContainer ) ) {
2023-08-08 22:14:34 +08:00
containRange = true ;
return true ;
2023-08-08 16:09:42 +08:00
}
2023-08-08 22:14:34 +08:00
} ) ;
2023-08-08 16:09:42 +08:00
if ( ! containRange && selectElements . length > 0 ) {
selectElements . forEach ( item = > {
2023-08-08 22:14:34 +08:00
item . classList . remove ( "protyle-wysiwyg--select" ) ;
} ) ;
nodeElement . classList . add ( "protyle-wysiwyg--select" ) ;
2023-08-08 16:09:42 +08:00
}
}
2022-05-26 15:18:53 +08:00
} ) ;
this . element . addEventListener ( "dblclick" , ( event : MouseEvent & { target : HTMLElement } ) = > {
2022-11-20 22:26:55 +08:00
if ( event . target . tagName === "IMG" && ! event . target . classList . contains ( "emoji" ) ) {
2024-12-07 10:27:19 +08:00
previewDocImage ( ( event . target as HTMLElement ) . getAttribute ( "src" ) , protyle . block . rootID ) ;
2022-05-26 15:18:53 +08:00
return ;
}
} ) ;
2025-04-17 10:12:22 +08:00
let mobileBlur = false ;
2022-05-26 15:18:53 +08:00
this . element . addEventListener ( "click" , ( event : MouseEvent & { target : HTMLElement } ) = > {
2024-01-13 22:34:46 +08:00
if ( this . preventClick ) {
this . preventClick = false ;
return ;
}
2023-06-01 20:50:49 +08:00
protyle . app . plugins . forEach ( item = > {
2023-05-19 09:21:59 +08:00
item . eventBus . emit ( "click-editorcontent" , {
protyle ,
event
} ) ;
2023-05-18 21:10:25 +08:00
} ) ;
2023-09-27 23:13:32 +08:00
hideElements ( [ "hint" , "util" ] , protyle ) ;
2023-11-12 12:28:20 +08:00
const ctrlIsPressed = isOnlyMeta ( event ) ;
2022-09-30 18:05:24 +08:00
const backlinkBreadcrumbItemElement = hasClosestByClassName ( event . target , "protyle-breadcrumb__item" ) ;
2022-09-30 00:12:35 +08:00
if ( backlinkBreadcrumbItemElement ) {
2022-10-30 00:16:48 +08:00
const breadcrumbId = backlinkBreadcrumbItemElement . getAttribute ( "data-id" ) ;
2025-03-22 23:42:20 +08:00
/// #if !MOBILE
2022-10-28 11:46:44 +08:00
if ( breadcrumbId ) {
2024-11-15 12:05:35 +08:00
if ( ctrlIsPressed && ! event . shiftKey && ! event . altKey ) {
2023-12-09 23:32:33 +08:00
checkFold ( breadcrumbId , ( zoomIn ) = > {
2023-04-12 15:06:57 +08:00
openFileById ( {
2023-06-01 20:50:49 +08:00
app : protyle.app ,
2023-04-12 15:06:57 +08:00
id : breadcrumbId ,
2023-12-09 23:32:33 +08:00
action : zoomIn ? [ Constants . CB_GET_FOCUS , Constants . CB_GET_ALL ] : [ Constants . CB_GET_FOCUS , Constants . CB_GET_CONTEXT ] ,
zoomIn
2023-04-12 15:06:57 +08:00
} ) ;
2023-02-02 15:33:34 +08:00
} ) ;
2022-10-28 11:46:44 +08:00
} else {
loadBreadcrumb ( protyle , backlinkBreadcrumbItemElement ) ;
}
2022-09-30 18:05:24 +08:00
} else {
// 引用标题时的更多加载
2022-10-03 16:22:18 +08:00
getBacklinkHeadingMore ( backlinkBreadcrumbItemElement ) ;
2022-09-30 18:05:24 +08:00
}
2025-03-22 23:42:20 +08:00
/// #else
if ( breadcrumbId ) {
loadBreadcrumb ( protyle , backlinkBreadcrumbItemElement ) ;
}
/// #endif
2022-09-30 00:13:07 +08:00
event . stopPropagation ( ) ;
2022-09-30 00:12:35 +08:00
return ;
}
2025-03-22 23:42:20 +08:00
2022-05-26 15:18:53 +08:00
this . setEmptyOutline ( protyle , event . target ) ;
const tableElement = hasClosestByClassName ( event . target , "table" ) ;
this . element . querySelectorAll ( ".table" ) . forEach ( item = > {
2025-03-27 23:24:11 +08:00
if ( item . tagName !== "DIV" ) {
return ;
}
2025-07-23 13:08:38 +08:00
if ( ! tableElement || item !== tableElement ) {
2022-05-26 15:18:53 +08:00
item . querySelector ( ".table__select" ) . removeAttribute ( "style" ) ;
}
2025-07-23 13:08:38 +08:00
if ( tableElement && tableElement === item && item . querySelector ( ".table__select" ) . getAttribute ( "style" ) ) {
2022-05-26 15:18:53 +08:00
// 防止合并单元格的菜单消失
event . stopPropagation ( ) ;
}
} ) ;
2023-02-23 14:06:25 +08:00
// 面包屑定位,需至于前,否则 return 的元素就无法进行面包屑定位
if ( protyle . options . render . breadcrumb ) {
2024-04-05 22:46:57 +08:00
protyle . breadcrumb . render ( protyle , false , hasClosestBlock ( event . target ) ) ;
2023-02-23 14:06:25 +08:00
}
2022-05-26 15:18:53 +08:00
const range = getEditorRange ( this . element ) ;
2024-08-28 17:30:30 +08:00
// https://github.com/siyuan-note/siyuan/issues/12317
if ( range . startContainer . nodeType !== 3 &&
( range . startContainer as Element ) . classList . contains ( "protyle-action" ) &&
range . startContainer . parentElement . classList . contains ( "code-block" ) ) {
setFirstNodeRange ( range . startContainer . parentElement . querySelector ( ".hljs" ) . lastElementChild , range ) ;
}
2022-05-26 15:18:53 +08:00
// 需放在嵌入块之前, 否则嵌入块内的引用、链接、pdf 双链无法点击打开 https://ld246.com/article/1630479789513
2024-04-17 20:26:56 +08:00
const aElement = hasClosestByAttribute ( event . target , "data-type" , "a" ) ||
hasClosestByClassName ( event . target , "av__celltext--url" ) ; // 数据库中资源文件、链接、电话、邮箱单元格
2024-04-23 23:28:41 +08:00
let aLink = aElement ? ( aElement . getAttribute ( "data-href" ) || "" ) : "" ;
2024-04-17 20:26:56 +08:00
if ( aElement && ! aLink && aElement . classList . contains ( "av__celltext--url" ) ) {
aLink = aElement . textContent . trim ( ) ;
if ( aElement . dataset . type === "phone" ) {
aLink = "tel:" + aLink ;
} else if ( aElement . dataset . type === "email" ) {
aLink = "mailto:" + aLink ;
} else if ( aElement . classList . contains ( "b3-chip" ) ) {
aLink = aElement . dataset . url ;
}
}
2024-05-27 10:47:23 +08:00
const blockRefElement = hasClosestByAttribute ( event . target , "data-type" , "block-ref" ) ;
2023-08-09 22:55:34 +08:00
if ( blockRefElement || aLink . startsWith ( "siyuan://blocks/" ) ) {
2022-05-26 15:18:53 +08:00
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
hideElements ( [ "dialog" , "toolbar" ] , protyle ) ;
2024-10-25 23:17:43 +08:00
if ( range . toString ( ) === "" || event . shiftKey ) {
let refBlockId : string ;
2022-05-26 15:18:53 +08:00
if ( blockRefElement ) {
2024-10-25 23:17:43 +08:00
refBlockId = blockRefElement . getAttribute ( "data-id" ) ;
2022-05-26 15:18:53 +08:00
} else if ( aElement ) {
2024-10-25 23:17:43 +08:00
refBlockId = aLink . substring ( 16 , 38 ) ;
2022-05-26 15:18:53 +08:00
}
2024-10-25 23:17:43 +08:00
checkFold ( refBlockId , ( zoomIn , action , isRoot ) = > {
// 块引用跳转后需要短暂高亮目标块 https://github.com/siyuan-note/siyuan/issues/11542
if ( ! isRoot ) {
action . push ( Constants . CB_GET_HL ) ;
}
/// #if MOBILE
2025-04-17 10:12:22 +08:00
mobileBlur = true ;
2024-10-25 23:17:43 +08:00
activeBlur ( ) ;
2025-04-16 11:06:47 +08:00
openMobileFileById ( protyle . app , refBlockId , zoomIn ? [ Constants . CB_GET_ALL ] : [ Constants . CB_GET_HL , Constants . CB_GET_CONTEXT , Constants . CB_GET_ROOTSCROLL ] ) ;
2024-10-25 23:17:43 +08:00
/// #else
if ( event . shiftKey ) {
openFileById ( {
app : protyle.app ,
id : refBlockId ,
position : "bottom" ,
action ,
zoomIn
} ) ;
window . dispatchEvent ( new KeyboardEvent ( "keydown" , { key : "Escape" } ) ) ;
} else if ( event . altKey ) {
openFileById ( {
app : protyle.app ,
id : refBlockId ,
position : "right" ,
action ,
zoomIn
} ) ;
} else if ( ctrlIsPressed ) {
openFileById ( {
app : protyle.app ,
id : refBlockId ,
keepCursor : true ,
action : zoomIn ? [ Constants . CB_GET_HL , Constants . CB_GET_ALL ] : [ Constants . CB_GET_HL , Constants . CB_GET_CONTEXT , Constants . CB_GET_ROOTSCROLL ] ,
zoomIn
} ) ;
} else {
openFileById ( {
app : protyle.app ,
id : refBlockId ,
action ,
zoomIn
} ) ;
}
/// #endif
} ) ;
/// #if !MOBILE
if ( protyle . model ) {
// 打开双链需记录到后退中 https://github.com/siyuan-note/insider/issues/801
let blockElement : HTMLElement | false ;
if ( blockRefElement ) {
blockElement = hasClosestBlock ( blockRefElement ) ;
} else if ( aElement ) {
blockElement = hasClosestBlock ( aElement ) ;
}
if ( blockElement ) {
pushBack ( protyle , getEditorRange ( this . element ) , blockElement ) ;
}
2022-05-26 15:18:53 +08:00
}
2024-10-25 23:17:43 +08:00
/// #endif
return ;
2022-05-26 15:18:53 +08:00
}
}
2024-03-05 23:07:56 +08:00
/// #if MOBILE
// https://github.com/siyuan-note/siyuan/issues/10513
const virtualRefElement = hasClosestByAttribute ( event . target , "data-type" , "virtual-block-ref" ) ;
2025-04-19 10:00:10 +08:00
if ( virtualRefElement && range . toString ( ) === "" ) {
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
2024-03-05 23:18:11 +08:00
const blockElement = hasClosestBlock ( virtualRefElement ) ;
2024-03-05 23:07:56 +08:00
if ( blockElement ) {
fetchPost ( "/api/block/getBlockDefIDsByRefText" , {
anchor : virtualRefElement.textContent ,
excludeIDs : [ blockElement . getAttribute ( "data-node-id" ) ]
} , ( response ) = > {
2025-04-13 16:53:10 +08:00
checkFold ( response . data . refDefs [ 0 ] . refID , ( zoomIn ) = > {
2025-04-17 10:12:22 +08:00
mobileBlur = true ;
2024-03-05 23:07:56 +08:00
activeBlur ( ) ;
2025-04-16 11:06:47 +08:00
openMobileFileById ( protyle . app , response . data . refDefs [ 0 ] . refID , zoomIn ? [ Constants . CB_GET_ALL ] : [ Constants . CB_GET_HL , Constants . CB_GET_CONTEXT , Constants . CB_GET_ROOTSCROLL ] ) ;
2024-03-05 23:07:56 +08:00
} ) ;
} ) ;
}
return ;
}
/// #endif
2022-05-26 15:18:53 +08:00
const fileElement = hasClosestByAttribute ( event . target , "data-type" , "file-annotation-ref" ) ;
if ( fileElement && range . toString ( ) === "" ) {
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
2024-04-30 23:27:18 +08:00
openLink ( protyle , fileElement . getAttribute ( "data-id" ) , event , ctrlIsPressed ) ;
2022-05-26 15:18:53 +08:00
return ;
}
2024-09-21 10:29:11 +08:00
if ( aElement &&
// https://github.com/siyuan-note/siyuan/issues/11980
( event . shiftKey || range . toString ( ) === "" ) &&
// 如果aLink 为空时,当 data-type="a inline-math" 可继续后续操作
aLink ) {
2022-05-26 15:18:53 +08:00
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
2024-04-26 22:50:52 +08:00
openLink ( protyle , aLink , event , ctrlIsPressed ) ;
2022-05-26 15:18:53 +08:00
return ;
}
2024-09-09 23:02:40 +08:00
if ( aElement && aElement . classList . contains ( "av__celltext--url" ) && ! aLink ) {
let index = 0 ;
Array . from ( aElement . parentElement . children ) . find ( ( item , i ) = > {
if ( item === aElement ) {
index = i ;
return true ;
}
} ) ;
editAssetItem ( {
protyle ,
cellElements : [ aElement . parentElement ] ,
blockElement : hasClosestBlock ( aElement ) as HTMLElement ,
content : aElement.getAttribute ( "data-url" ) ,
type : "file" ,
name : aElement.getAttribute ( "data-name" ) ,
index ,
rect : aElement.getBoundingClientRect ( )
} ) ;
return ;
}
2022-05-26 15:18:53 +08:00
const tagElement = hasClosestByAttribute ( event . target , "data-type" , "tag" ) ;
2025-02-03 17:27:06 +08:00
if ( tagElement && ! event . altKey && ! event . shiftKey && range . toString ( ) === "" ) {
2023-04-21 14:54:21 +08:00
/// #if !MOBILE
2024-11-12 17:29:50 +08:00
openGlobalSearch ( protyle . app , ` # ${ tagElement . textContent } # ` , ! ctrlIsPressed , { method : 0 } ) ;
2022-05-26 15:18:53 +08:00
hideElements ( [ "dialog" ] ) ;
2023-04-21 14:54:21 +08:00
/// #else
2023-06-01 20:50:49 +08:00
popSearch ( protyle . app , {
2023-04-21 14:54:21 +08:00
hasReplace : false ,
method : 0 ,
hPath : "" ,
idPath : [ ] ,
k : ` # ${ tagElement . textContent } # ` ,
r : "" ,
page : 1 ,
} ) ;
/// #endif
2022-05-26 15:18:53 +08:00
return ;
}
const embedItemElement = hasClosestByClassName ( event . target , "protyle-wysiwyg__embed" ) ;
2024-09-26 16:18:11 +08:00
if ( embedItemElement ) {
2022-06-23 01:19:04 +08:00
const embedId = embedItemElement . getAttribute ( "data-id" ) ;
2023-12-09 23:32:33 +08:00
checkFold ( embedId , ( zoomIn , action ) = > {
2023-09-25 09:17:24 +08:00
/// #if MOBILE
2025-04-17 10:12:22 +08:00
mobileBlur = true ;
2023-09-25 09:17:24 +08:00
activeBlur ( ) ;
2025-04-16 11:06:47 +08:00
openMobileFileById ( protyle . app , embedId , zoomIn ? [ Constants . CB_GET_ALL ] : [ Constants . CB_GET_HL , Constants . CB_GET_CONTEXT , Constants . CB_GET_ROOTSCROLL ] ) ;
2023-09-25 09:17:24 +08:00
/// #else
if ( event . shiftKey ) {
openFileById ( {
app : protyle.app ,
id : embedId ,
position : "bottom" ,
2023-12-09 23:32:33 +08:00
action ,
zoomIn
2023-09-25 09:17:24 +08:00
} ) ;
} else if ( event . altKey ) {
openFileById ( {
app : protyle.app ,
id : embedId ,
position : "right" ,
2023-12-09 23:32:33 +08:00
action ,
zoomIn
2023-09-25 09:17:24 +08:00
} ) ;
} else if ( ctrlIsPressed ) {
openFileById ( {
app : protyle.app ,
id : embedId ,
2023-12-09 23:32:33 +08:00
action : zoomIn ? [ Constants . CB_GET_HL , Constants . CB_GET_ALL ] : [ Constants . CB_GET_HL , Constants . CB_GET_CONTEXT ] ,
zoomIn ,
2023-09-25 09:17:24 +08:00
keepCursor : true ,
} ) ;
} else if ( ! protyle . disabled ) {
window . siyuan . blockPanels . push ( new BlockPanel ( {
app : protyle.app ,
targetElement : embedItemElement ,
isBacklink : false ,
2025-01-13 12:47:28 +08:00
refDefs : [ { refID : embedId } ]
2023-09-25 09:17:24 +08:00
} ) ) ;
}
/// #endif
2023-12-09 23:33:06 +08:00
} ) ;
2024-09-26 16:18:11 +08:00
// https://github.com/siyuan-note/siyuan/issues/12585
if ( ! ctrlIsPressed ) {
event . stopPropagation ( ) ;
return ;
}
2022-05-26 15:18:53 +08:00
}
2023-06-01 20:50:49 +08:00
if ( commonClick ( event , protyle ) ) {
2022-05-26 15:18:53 +08:00
return ;
}
if ( hasTopClosestByClassName ( event . target , "protyle-action__copy" ) ) {
return ;
}
const editElement = hasClosestByClassName ( event . target , "protyle-action__edit" ) ;
if ( editElement && ! protyle . disabled ) {
protyle . toolbar . showRender ( protyle , editElement . parentElement . parentElement ) ;
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
return ;
}
const menuElement = hasClosestByClassName ( event . target , "protyle-action__menu" ) ;
2022-10-17 09:49:59 +08:00
if ( menuElement ) {
2022-05-26 15:18:53 +08:00
protyle . gutter . renderMenu ( protyle , menuElement . parentElement . parentElement ) ;
2023-04-10 18:00:36 +08:00
/// #if MOBILE
window . siyuan . menus . menu . fullscreen ( ) ;
/// #else
2022-05-26 15:18:53 +08:00
const rect = menuElement . getBoundingClientRect ( ) ;
2022-08-02 11:13:11 +08:00
window . siyuan . menus . menu . popup ( {
x : rect.left ,
2023-10-04 21:35:40 +08:00
y : rect.top ,
isLeft : true
} ) ;
2023-04-10 18:00:36 +08:00
/// #endif
2022-05-26 15:18:53 +08:00
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
return ;
}
const reloadElement = hasClosestByClassName ( event . target , "protyle-action__reload" ) ;
if ( reloadElement ) {
2024-07-25 17:55:25 +08:00
const embedReloadElement = isInEmbedBlock ( reloadElement ) ;
2022-05-26 15:18:53 +08:00
if ( embedReloadElement ) {
embedReloadElement . removeAttribute ( "data-render" ) ;
blockRender ( protyle , embedReloadElement ) ;
2025-09-28 19:56:04 +08:00
} else {
const blockElement = hasClosestBlock ( reloadElement ) ;
if ( blockElement && blockElement . getAttribute ( "data-subtype" ) === "echarts" ) {
blockElement . removeAttribute ( "data-render" ) ;
chartRender ( blockElement ) ;
}
2022-05-26 15:18:53 +08:00
}
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
return ;
}
const languageElement = hasClosestByClassName ( event . target , "protyle-action__language" ) ;
2024-04-18 11:15:52 +08:00
if ( languageElement && ! protyle . disabled && ! ctrlIsPressed ) {
2025-04-22 21:39:01 +08:00
protyle . toolbar . showCodeLanguage ( protyle , [ languageElement ] ) ;
2022-05-26 15:18:53 +08:00
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
return ;
}
// 需放在属性后,否则数学公式无法点击属性;需放在 action 后,否则嵌入块的的 action 无法打开;需放在嵌入块后,否则嵌入块中的数学公式会被打开
const mathElement = hasClosestByAttribute ( event . target , "data-subtype" , "math" ) ;
2023-02-09 11:16:34 +08:00
if ( ! event . shiftKey && ! ctrlIsPressed && mathElement && ! protyle . disabled ) {
2022-05-26 15:18:53 +08:00
protyle . toolbar . showRender ( protyle , mathElement ) ;
event . stopPropagation ( ) ;
return ;
}
const actionElement = hasClosestByClassName ( event . target , "protyle-action" ) ;
if ( actionElement ) {
const type = actionElement . parentElement . parentElement . getAttribute ( "data-type" ) ;
if ( type === "img" && ! protyle . disabled ) {
2023-06-01 20:50:49 +08:00
imgMenu ( protyle , range , actionElement . parentElement . parentElement , {
2022-06-09 11:28:35 +08:00
clientX : event.clientX + 4 ,
2022-05-26 15:18:53 +08:00
clientY : event.clientY
} ) ;
2024-04-18 11:15:52 +08:00
event . stopPropagation ( ) ;
return ;
2023-08-20 12:26:36 +08:00
} else if ( actionElement . parentElement . classList . contains ( "li" ) ) {
2023-08-28 23:26:47 +08:00
const actionId = actionElement . parentElement . getAttribute ( "data-node-id" ) ;
2023-08-20 12:26:36 +08:00
if ( event . altKey && ! protyle . disabled ) {
2022-05-26 15:18:53 +08:00
// 展开/折叠当前层级的所有列表项
if ( actionElement . parentElement . parentElement . classList . contains ( "protyle-wysiwyg" ) ) {
// 缩放列表项 https://ld246.com/article/1653123034794
setFold ( protyle , actionElement . parentElement ) ;
} else {
let hasFold = true ;
const oldHTML = actionElement . parentElement . parentElement . outerHTML ;
Array . from ( actionElement . parentElement . parentElement . children ) . find ( ( listItemElement ) = > {
if ( listItemElement . classList . contains ( "li" ) ) {
if ( listItemElement . getAttribute ( "fold" ) !== "1" && listItemElement . childElementCount > 3 ) {
hasFold = false ;
return true ;
}
}
} ) ;
Array . from ( actionElement . parentElement . parentElement . children ) . find ( ( listItemElement ) = > {
if ( listItemElement . classList . contains ( "li" ) ) {
if ( hasFold ) {
listItemElement . removeAttribute ( "fold" ) ;
} else if ( listItemElement . childElementCount > 3 ) {
listItemElement . setAttribute ( "fold" , "1" ) ;
}
}
} ) ;
updateTransaction ( protyle , actionElement . parentElement . parentElement . getAttribute ( "data-node-id" ) , actionElement . parentElement . parentElement . outerHTML , oldHTML ) ;
}
hideElements ( [ "gutter" ] , protyle ) ;
2023-08-20 12:26:36 +08:00
} else if ( event . shiftKey && ! protyle . disabled ) {
2023-09-24 17:53:28 +08:00
openAttr ( actionElement . parentElement , "bookmark" , protyle ) ;
2023-02-09 11:16:34 +08:00
} else if ( ctrlIsPressed ) {
2023-08-20 12:41:00 +08:00
zoomOut ( { protyle , id : actionId } ) ;
2022-05-26 15:18:53 +08:00
} else {
if ( actionElement . classList . contains ( "protyle-action--task" ) ) {
2023-08-20 12:26:36 +08:00
if ( ! protyle . disabled ) {
const html = actionElement . parentElement . outerHTML ;
if ( actionElement . parentElement . classList . contains ( "protyle-task--done" ) ) {
actionElement . querySelector ( "use" ) . setAttribute ( "xlink:href" , "#iconUncheck" ) ;
actionElement . parentElement . classList . remove ( "protyle-task--done" ) ;
} else {
actionElement . querySelector ( "use" ) . setAttribute ( "xlink:href" , "#iconCheck" ) ;
actionElement . parentElement . classList . add ( "protyle-task--done" ) ;
}
actionElement . parentElement . setAttribute ( "updated" , dayjs ( ) . format ( "YYYYMMDDHHmmss" ) ) ;
2023-08-20 12:41:00 +08:00
updateTransaction ( protyle , actionId , actionElement . parentElement . outerHTML , html ) ;
2022-05-26 15:18:53 +08:00
}
2024-02-25 23:21:03 +08:00
} else if ( window . siyuan . config . editor . listItemDotNumberClickFocus ) {
2023-09-08 09:32:06 +08:00
if ( protyle . block . showAll && protyle . block . id === actionId ) {
2023-08-20 12:41:00 +08:00
enterBack ( protyle , actionId ) ;
} else {
zoomOut ( { protyle , id : actionId } ) ;
}
2022-05-26 15:18:53 +08:00
}
}
2024-04-18 11:15:52 +08:00
event . stopPropagation ( ) ;
return ;
2022-05-26 15:18:53 +08:00
}
}
const selectElement = hasClosestByClassName ( event . target , "hr" ) ||
hasClosestByClassName ( event . target , "iframe" ) ;
2023-02-09 11:16:34 +08:00
if ( ! event . shiftKey && ! ctrlIsPressed && selectElement ) {
2022-05-26 15:18:53 +08:00
selectElement . classList . add ( "protyle-wysiwyg--select" ) ;
2025-01-15 11:42:08 +08:00
globalClickHideMenu ( event . target ) ;
2022-05-26 15:18:53 +08:00
event . stopPropagation ( ) ;
return ;
}
const imgElement = hasTopClosestByClassName ( event . target , "img" ) ;
2023-02-09 11:16:34 +08:00
if ( ! event . shiftKey && ! ctrlIsPressed && imgElement ) {
2022-05-26 15:18:53 +08:00
imgElement . classList . add ( "img--select" ) ;
2024-06-03 23:57:30 +08:00
const nextSibling = hasNextSibling ( imgElement ) ;
2024-05-30 10:58:07 +08:00
if ( nextSibling ) {
if ( nextSibling . textContent . startsWith ( Constants . ZWSP ) ) {
range . setStart ( nextSibling , 1 ) ;
} else {
range . setStart ( nextSibling , 0 ) ;
}
range . collapse ( true ) ;
focusByRange ( range ) ;
// 需等待 range 更新再次进行渲染
if ( protyle . options . render . breadcrumb ) {
protyle . breadcrumb . render ( protyle ) ;
}
2023-02-23 14:06:25 +08:00
}
2022-05-26 15:18:53 +08:00
return ;
}
2024-02-02 21:57:47 +08:00
const emojiElement = hasTopClosestByClassName ( event . target , "emoji" ) ;
2025-01-12 16:58:14 +08:00
if ( ! protyle . disabled && ! event . shiftKey && ! ctrlIsPressed && emojiElement ) {
2024-02-02 21:57:47 +08:00
const nodeElement = hasClosestBlock ( emojiElement ) ;
if ( nodeElement ) {
2024-02-03 12:32:56 +08:00
const emojiRect = emojiElement . getBoundingClientRect ( ) ;
2024-02-02 21:57:47 +08:00
openEmojiPanel ( "" , "av" , {
x : emojiRect.left ,
y : emojiRect.bottom ,
h : emojiRect.height ,
w : emojiRect.width
} , ( unicode ) = > {
2024-02-03 12:32:56 +08:00
emojiElement . insertAdjacentHTML ( "afterend" , "<wbr>" ) ;
2024-02-02 21:57:47 +08:00
const oldHTML = nodeElement . outerHTML ;
let emojiHTML ;
2024-11-13 00:51:30 +08:00
if ( unicode . startsWith ( "api/icon/getDynamicIcon" ) ) {
emojiHTML = ` <img class="emoji" src=" ${ unicode } "/> ` ;
} else if ( unicode . indexOf ( "." ) > - 1 ) {
2024-02-02 21:57:47 +08:00
const emojiList = unicode . split ( "." ) ;
emojiHTML = ` <img alt=" ${ emojiList [ 0 ] } " class="emoji" src="/emojis/ ${ unicode } " title=" ${ emojiList [ 0 ] } "> ` ;
} else {
emojiHTML = unicode2Emoji ( unicode ) ;
}
emojiElement . outerHTML = emojiHTML ;
hideElements ( [ "dialog" ] ) ;
updateTransaction ( protyle , nodeElement . getAttribute ( "data-node-id" ) , nodeElement . outerHTML , oldHTML ) ;
focusByWbr ( nodeElement , range ) ;
2024-11-13 00:51:30 +08:00
} , emojiElement ) ;
2024-02-02 21:57:47 +08:00
}
return ;
}
2023-06-08 19:21:33 +08:00
if ( avClick ( protyle , event ) ) {
return ;
}
2022-05-26 15:18:53 +08:00
setTimeout ( ( ) = > {
// 选中后,在选中的文字上点击需等待 range 更新
2024-02-16 13:07:21 +08:00
let newRange = getEditorRange ( this . element ) ;
2025-10-25 00:15:58 +08:00
// 点击两侧或间隙导致光标跳转到开头 https://github.com/siyuan-note/siyuan/issues/16179
if ( hasClosestBlock ( event . target ) !== hasClosestBlock ( newRange . startContainer ) &&
2025-10-27 17:36:29 +08:00
this . element . querySelector ( "[data-node-id]" ) ? . contains ( newRange . startContainer ) ) {
2025-10-25 00:15:58 +08:00
const rect = this . element . getBoundingClientRect ( ) ;
let rangeElement = document . elementFromPoint ( rect . left + rect . width / 2 , event . clientY ) ;
if ( rangeElement === this . element ) {
rangeElement = document . elementFromPoint ( rect . left + rect . width / 2 , event . clientY + 8 ) ;
}
2025-10-25 22:24:39 +08:00
let blockElement = hasClosestBlock ( rangeElement ) ;
2025-10-25 00:15:58 +08:00
if ( blockElement ) {
2025-10-27 17:35:20 +08:00
const embedElement = isInEmbedBlock ( blockElement ) ;
2025-10-25 22:24:39 +08:00
if ( embedElement ) {
blockElement = embedElement ;
}
2025-10-25 00:15:58 +08:00
newRange = focusBlock ( blockElement , undefined , event . clientX < rect . left + parseInt ( this . element . style . paddingLeft ) ) || newRange ;
2025-10-25 11:02:11 +08:00
if ( protyle . options . render . breadcrumb ) {
protyle . breadcrumb . render ( protyle , false , blockElement ) ;
}
2025-10-25 00:15:58 +08:00
}
}
2024-02-16 13:07:21 +08:00
// https://github.com/siyuan-note/siyuan/issues/10357
2024-02-16 22:30:17 +08:00
const attrElement = hasClosestByClassName ( newRange . endContainer , "protyle-attr" ) ;
2024-02-16 13:07:21 +08:00
if ( attrElement ) {
newRange = setLastNodeRange ( attrElement . previousElementSibling , newRange , false ) ;
}
2025-03-31 22:47:25 +08:00
// https://github.com/siyuan-note/siyuan/issues/14481
const inlineMathElement = hasClosestByAttribute ( newRange . startContainer , "data-type" , "inline-math" ) ;
if ( inlineMathElement ) {
newRange . setEndAfter ( inlineMathElement ) ;
newRange . collapse ( false ) ;
focusByRange ( newRange ) ;
}
2023-03-01 18:58:39 +08:00
/// #if !MOBILE
2022-05-26 15:18:53 +08:00
if ( newRange . toString ( ) . replace ( Constants . ZWSP , "" ) !== "" ) {
protyle . toolbar . render ( protyle , newRange ) ;
} else {
2023-11-29 22:55:53 +08:00
// https://github.com/siyuan-note/siyuan/issues/9785
protyle . toolbar . range = newRange ;
2022-05-26 15:18:53 +08:00
}
2023-03-01 18:58:39 +08:00
/// #endif
2022-06-28 17:51:51 +08:00
if ( ! protyle . wysiwyg . element . querySelector ( ".protyle-wysiwyg--select" ) ) {
2022-10-07 20:56:19 +08:00
countSelectWord ( newRange , protyle . block . rootID ) ;
2022-06-28 17:51:51 +08:00
}
2025-04-17 10:12:22 +08:00
if ( getSelection ( ) . rangeCount === 0 && ! mobileBlur ) {
2025-10-27 17:35:20 +08:00
// https://github.com/siyuan-note/siyuan/issues/14589
// https://github.com/siyuan-note/siyuan/issues/14569
2022-09-17 22:04:07 +08:00
// https://github.com/siyuan-note/siyuan/issues/5901
2022-09-18 17:51:19 +08:00
focusByRange ( newRange ) ;
2022-09-17 22:04:07 +08:00
}
2022-06-29 20:25:30 +08:00
/// #if !MOBILE
2022-05-26 15:18:53 +08:00
pushBack ( protyle , newRange ) ;
2022-06-29 20:25:30 +08:00
/// #endif
2025-04-17 10:12:57 +08:00
mobileBlur = false ;
2023-09-05 16:12:29 +08:00
} , ( isMobile ( ) || isInIOS ( ) ) ? 520 : 0 ) ; // Android/iPad 双击慢了出不来
2025-06-20 13:35:12 +08:00
2023-02-08 17:33:49 +08:00
protyle . hint . enableExtend = false ;
2022-05-26 15:18:53 +08:00
if ( this . element . querySelector ( ".protyle-wysiwyg--select" ) && range . toString ( ) !== "" ) {
// 选中块后,文字不能被选中。需在 shift click 之后, 防止shift点击单个块出现文字选中
range . collapse ( false ) ;
focusByRange ( range ) ;
}
} ) ;
}
}