11import * as p from "@bokehjs/core/properties"
22import { div } from "@bokehjs/core/dom"
33
4- import { HTMLBox , HTMLBoxView } from "./layout"
5-
6- const normalizeNative = ( nativeRange : any ) => {
7-
8- // document.getSelection model has properties startContainer and endContainer
9- // shadow.getSelection model has baseNode and focusNode
10- // Unify formats to always look like document.getSelection
11-
12- if ( nativeRange ) {
13-
14- const range = nativeRange ;
15-
16- // // HACK: To allow pasting
17- if ( range . baseNode ?. classList ?. value === 'ql-clipboard' ) {
18- return null
19- }
20-
21- if ( range . baseNode ) {
22- range . startContainer = nativeRange . baseNode ;
23- range . endContainer = nativeRange . focusNode ;
24- range . startOffset = nativeRange . baseOffset ;
25- range . endOffset = nativeRange . focusOffset ;
26-
27- if ( range . endOffset < range . startOffset ) {
28- range . startContainer = nativeRange . focusNode ;
29- range . endContainer = nativeRange . baseNode ;
30- range . startOffset = nativeRange . focusOffset ;
31- range . endOffset = nativeRange . baseOffset ;
32- }
33- }
34-
35- if ( range . startContainer ) {
36-
37- return {
38- start : { node : range . startContainer , offset : range . startOffset } ,
39- end : { node : range . endContainer , offset : range . endOffset } ,
40- native : range
41- } ;
42- }
43- }
44-
45- return null
46- } ;
4+ import { HTMLBox , HTMLBoxView } from "./layout"
475
486export class QuillInputView extends HTMLBoxView {
497 override model : QuillInput
@@ -59,7 +17,7 @@ export class QuillInputView extends HTMLBoxView {
5917 this . connect ( this . model . properties . disabled . change , ( ) => this . quill . enable ( ! this . model . disabled ) )
6018 this . connect ( this . model . properties . visible . change , ( ) => {
6119 if ( this . model . visible )
62- this . container . style . visibility = 'visible' ;
20+ this . container . style . visibility = 'visible' ;
6321 } )
6422 this . connect ( this . model . properties . text . change , ( ) => {
6523 if ( this . _editing )
@@ -71,7 +29,7 @@ export class QuillInputView extends HTMLBoxView {
7129 this . quill . enable ( ! this . model . disabled )
7230 this . _editing = false
7331 } )
74- const { mode, toolbar, placeholder} = this . model . properties
32+ const { mode, toolbar, placeholder } = this . model . properties
7533 this . on_change ( [ placeholder ] , ( ) => {
7634 this . quill . root . setAttribute ( 'data-placeholder' , this . model . placeholder )
7735 } )
@@ -93,7 +51,7 @@ export class QuillInputView extends HTMLBoxView {
9351
9452 render ( ) : void {
9553 super . render ( )
96- this . container = div ( { style : "visibility: hidden;" } )
54+ this . container = div ( { style : "visibility: hidden;" } )
9755 this . shadow_el . appendChild ( this . container )
9856 const theme = ( this . model . mode === 'bubble' ) ? 'bubble' : 'snow'
9957 this . watch_stylesheets ( )
@@ -106,16 +64,82 @@ export class QuillInputView extends HTMLBoxView {
10664 theme : theme
10765 } ) ;
10866
109- // Apply only with getSelection() is defined (e.g. undefined on Firefox)
110- if ( typeof this . quill . root . getRootNode ( ) . getSelection !== 'undefined' ) {
111- // Hack Quill and replace document.getSelection with shadow.getSelection
112- // see https://stackoverflow.com/questions/67914657/quill-editor-inside-shadow-dom/67944380#67944380
113- this . quill . selection . getNativeRange = ( ) => {
67+ // Apply ShadowDOM patch found at:
68+ // https://github.com/quilljs/quill/issues/2961#issuecomment-1775999845
69+
70+ const hasShadowRootSelection = ! ! ( ( document . createElement ( 'div' ) . attachShadow ( { mode : 'open' } ) as any ) . getSelection ) ;
71+ // Each browser engine has a different implementation for retrieving the Range
72+ const getNativeRange = ( rootNode : any ) => {
73+ try {
74+ if ( hasShadowRootSelection ) {
75+ // In Chromium, the shadow root has a getSelection function which returns the range
76+ return rootNode . getSelection ( ) . getRangeAt ( 0 ) ;
77+ } else {
78+ const selection = window . getSelection ( ) ;
79+ if ( ( selection as any ) . getComposedRanges ) {
80+ // Webkit range retrieval is done with getComposedRanges (see: https://bugs.webkit.org/show_bug.cgi?id=163921)
81+ return ( selection as any ) . getComposedRanges ( rootNode ) [ 0 ] ;
82+ } else {
83+ // Gecko implements the range API properly in Native Shadow: https://developer.mozilla.org/en-US/docs/Web/API/Selection/getRangeAt
84+ return ( selection as any ) . getRangeAt ( 0 ) ;
85+ }
86+ }
87+ } catch {
88+ return null ;
89+ }
90+ }
91+
92+ /**
93+ * Original implementation uses document.active element which does not work in Native Shadow.
94+ * Replace document.activeElement with shadowRoot.activeElement
95+ **/
96+ this . quill . selection . hasFocus = ( ) => {
97+ const rootNode = ( this . quill . root . getRootNode ( ) as ShadowRoot ) ;
98+ return rootNode . activeElement === this . quill . root ;
99+ }
114100
115- const selection = ( this . shadow_el as any ) . getSelection ( ) ;
116- const range = normalizeNative ( selection ) ;
117- return range ;
118- } ;
101+ /**
102+ * Original implementation uses document.getSelection which does not work in Native Shadow.
103+ * Replace document.getSelection with shadow dom equivalent (different for each browser)
104+ **/
105+ this . quill . selection . getNativeRange = ( ) => {
106+ const rootNode = ( this . quill . root . getRootNode ( ) as ShadowRoot ) ;
107+ const nativeRange = getNativeRange ( rootNode ) ;
108+ return ! ! nativeRange ? this . quill . selection . normalizeNative ( nativeRange ) : null ;
109+ } ;
110+
111+ /**
112+ * Original implementation relies on Selection.addRange to programmatically set the range, which does not work
113+ * in Webkit with Native Shadow. Selection.addRange works fine in Chromium and Gecko.
114+ **/
115+ this . quill . selection . setNativeRange = ( startNode : any , startOffset : any ) => {
116+ var endNode = arguments . length > 2 && arguments [ 2 ] !== undefined ? arguments [ 2 ] : startNode ;
117+ var endOffset = arguments . length > 3 && arguments [ 3 ] !== undefined ? arguments [ 3 ] : startOffset ;
118+ var force = arguments . length > 4 && arguments [ 4 ] !== undefined ? arguments [ 4 ] : false ;
119+ if ( startNode != null && ( this . quill . selection . root . parentNode == null || startNode . parentNode == null || endNode . parentNode == null ) ) {
120+ return ;
121+ }
122+ var selection = document . getSelection ( ) ;
123+ if ( selection == null ) return ;
124+ if ( startNode != null ) {
125+ if ( ! this . quill . selection . hasFocus ( ) ) this . quill . selection . root . focus ( ) ;
126+ var native = ( this . quill . selection . getNativeRange ( ) || { } ) . native ;
127+ if ( native == null || force || startNode !== native . startContainer || startOffset !== native . startOffset || endNode !== native . endContainer || endOffset !== native . endOffset ) {
128+ if ( startNode . tagName == "BR" ) {
129+ startOffset = [ ] . indexOf . call ( startNode . parentNode . childNodes , startNode ) ;
130+ startNode = startNode . parentNode ;
131+ }
132+ if ( endNode . tagName == "BR" ) {
133+ endOffset = [ ] . indexOf . call ( endNode . parentNode . childNodes , endNode ) ;
134+ endNode = endNode . parentNode ;
135+ }
136+ selection . setBaseAndExtent ( startNode , startOffset , endNode , endOffset ) ;
137+ }
138+ } else {
139+ selection . removeAllRanges ( ) ;
140+ this . quill . selection . root . blur ( ) ;
141+ document . body . focus ( ) ;
142+ }
119143 }
120144
121145 this . _editor = ( this . shadow_el . querySelector ( '.ql-editor' ) as HTMLDivElement )
@@ -167,7 +191,7 @@ export namespace QuillInput {
167191 }
168192}
169193
170- export interface QuillInput extends QuillInput . Attrs { }
194+ export interface QuillInput extends QuillInput . Attrs { }
171195
172196export class QuillInput extends HTMLBox {
173197 properties : QuillInput . Props
0 commit comments