From 22212be90d5e59590148130492f7ee228f7d06a0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 9 Oct 2014 16:25:06 -0700 Subject: [PATCH 01/68] Give atom-text-editor elements a shadow root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Themes aren’t applying correctly and there are issues with mini editors but this basically works. I’m leaving the .editor node in the shadow DOM for theme compatibility and because React still wants to render into a wrapper element. --- src/text-editor-element.coffee | 8 ++- static/editor.less | 94 ++++++++++++++-------------------- 2 files changed, 44 insertions(+), 58 deletions(-) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 8416bb285..bc830c7bd 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -22,8 +22,12 @@ class TextEditorElement extends HTMLElement @addEventListener 'blur', @blurred.bind(this) initializeContent: (attributes) -> - @classList.add('editor', 'react', 'editor-colors') + @classList.add('editor') @setAttribute('tabindex', -1) + @shadowRoot = @createShadowRoot() + @root = document.createElement('div') + @root.classList.add('editor', 'editor-colors') + @shadowRoot.appendChild(@root) createSpacePenShim: -> TextEditorView ?= require './text-editor-view' @@ -69,7 +73,7 @@ class TextEditorElement extends HTMLElement mini: @model.mini lineOverdrawMargin: @lineOverdrawMargin ) - @component = React.renderComponent(@componentDescriptor, this) + @component = React.renderComponent(@componentDescriptor, @root) unmountComponent: -> return unless @component?.isMounted() diff --git a/static/editor.less b/static/editor.less index cc8e1be65..57592d9b9 100644 --- a/static/editor.less +++ b/static/editor.less @@ -2,8 +2,9 @@ @import "octicon-utf-codes"; @import "octicon-mixins"; -atom-text-editor.react { - .editor-contents { +atom-text-editor::shadow { + .editor, .editor-contents { + height: 100%; width: 100%; } @@ -85,7 +86,7 @@ atom-text-editor.react { } } -atom-text-editor.mini { +atom-text-editor.mini::shadow { font-size: @input-font-size; line-height: @component-line-height; max-height: @component-line-height + 2; // +2 for borders @@ -96,13 +97,13 @@ atom-text-editor.mini { } } -atom-text-editor { +atom-text-editor::shadow { z-index: 0; font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier; line-height: 1.3; } -atom-text-editor, .editor-contents { +atom-text-editor::shadow, atom-text-editor::shadow .editor-contents { overflow: hidden; cursor: text; display: -webkit-flex; @@ -110,11 +111,11 @@ atom-text-editor, .editor-contents { position: relative; } -atom-text-editor .gutter .line-number.cursor-line { +atom-text-editor::shadow .gutter .line-number.cursor-line { opacity: 1; } -atom-text-editor .gutter { +atom-text-editor::shadow .gutter { overflow: hidden; text-align: right; cursor: default; @@ -122,20 +123,20 @@ atom-text-editor .gutter { box-sizing: border-box; } -atom-text-editor .gutter .line-number { +atom-text-editor::shadow .gutter .line-number { padding-left: .5em; opacity: 0.6; } -atom-text-editor .gutter .line-numbers { +atom-text-editor::shadow .gutter .line-numbers { position: relative; } -atom-text-editor .gutter .line-number.folded.cursor-line { +atom-text-editor::shadow .gutter .line-number.folded.cursor-line { opacity: 1; } -atom-text-editor .gutter .line-number .icon-right { +atom-text-editor::shadow .gutter .line-number .icon-right { .octicon(chevron-down, 0.8em); display: inline-block; visibility: hidden; @@ -144,7 +145,7 @@ atom-text-editor .gutter .line-number .icon-right { opacity: .6; } -atom-text-editor .gutter:hover .line-number.foldable .icon-right { +atom-text-editor::shadow .gutter:hover .line-number.foldable .icon-right { visibility: visible; &:before { @@ -156,7 +157,7 @@ atom-text-editor .gutter:hover .line-number.foldable .icon-right { } } -atom-text-editor .gutter, atom-text-editor .gutter:hover { +atom-text-editor::shadow .gutter, atom-text-editor::shadow .gutter:hover { .line-number.folded .icon-right { .octicon(chevron-right, 0.8em); visibility: visible; @@ -169,40 +170,40 @@ atom-text-editor .gutter, atom-text-editor .gutter:hover { } } -atom-text-editor .fold-marker { +atom-text-editor::shadow .fold-marker { cursor: default; } -atom-text-editor .fold-marker:after { +atom-text-editor::shadow .fold-marker:after { .icon(0.8em, inline); content: @ellipsis; padding-left: 0.2em; } -atom-text-editor .line.cursor-line .fold-marker:after { +atom-text-editor::shadow .line.cursor-line .fold-marker:after { opacity: 1; } -atom-text-editor.is-blurred .line.cursor-line { +atom-text-editor::shadow.is-blurred .line.cursor-line { background: rgba(0, 0, 0, 0); } -atom-text-editor .invisible-character { +atom-text-editor::shadow .invisible-character { font-weight: normal !important; font-style: normal !important; } -atom-text-editor .indent-guide { +atom-text-editor::shadow .indent-guide { display: inline-block; box-shadow: inset 1px 0; } -atom-text-editor .vertical-scrollbar, -atom-text-editor .horizontal-scrollbar { +atom-text-editor::shadow .vertical-scrollbar, +atom-text-editor::shadow .horizontal-scrollbar { cursor: default; } -atom-text-editor .vertical-scrollbar { +atom-text-editor::shadow .vertical-scrollbar { position: absolute; top: 0; right: 0; @@ -213,7 +214,7 @@ atom-text-editor .vertical-scrollbar { z-index: 3; } -atom-text-editor .scroll-view { +atom-text-editor::shadow .scroll-view { overflow-x: auto; overflow-y: hidden; -webkit-flex: 1; @@ -221,45 +222,45 @@ atom-text-editor .scroll-view { position: relative; } -atom-text-editor.soft-wrap .scroll-view { +atom-text-editor::shadow.soft-wrap .scroll-view { overflow-x: hidden; } -atom-text-editor .underlayer { +atom-text-editor::shadow .underlayer { z-index: 0; position: absolute; min-height: 100%; } -atom-text-editor .lines { +atom-text-editor::shadow .lines { position: relative; z-index: 1; } -atom-text-editor .overlayer { +atom-text-editor::shadow .overlayer { z-index: 2; position: absolute; } -atom-text-editor .line { +atom-text-editor::shadow .line { white-space: pre; } -atom-text-editor .line span { +atom-text-editor::shadow .line span { vertical-align: top; } -atom-text-editor .cursor { +atom-text-editor::shadow .cursor { position: absolute; border-left: 1px solid; } -atom-text-editor .cursor, -atom-text-editor.is-focused .cursor.blink-off { +atom-text-editor::shadow .cursor, +atom-text-editor::shadow.is-focused .cursor.blink-off { visibility: hidden; } -atom-text-editor.is-focused .cursor { +atom-text-editor::shadow.is-focused .cursor { visibility: visible; } @@ -267,7 +268,7 @@ atom-text-editor.is-focused .cursor { display: none; } -atom-text-editor .hidden-input { +atom-text-editor::shadow .hidden-input { padding: 0; border: 0; position: absolute; @@ -278,33 +279,14 @@ atom-text-editor .hidden-input { width: 1px; } -atom-text-editor .highlight { +atom-text-editor::shadow .highlight { background: none; padding: 0; } -atom-text-editor .highlight .region, -atom-text-editor .selection .region { +atom-text-editor::shadow .highlight .region, +atom-text-editor::shadow .selection .region { position: absolute; pointer-events: none; z-index: -1; } - -atom-text-editor.mini:not(.react) { - height: auto; - line-height: 25px; - - .cursor { - width: 2px; - line-height: 20px; - margin-top: 2px; - } - - .gutter { - display: none; - } - - .scroll-view { - overflow: hidden; - } -} From 963c92eb4e916329be64d7781824f787ee80b303 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 10 Oct 2014 14:38:13 -0700 Subject: [PATCH 02/68] Hack: Add editor stylesheets to atom-text-editor shadow root --- src/text-editor-element.coffee | 7 +++++++ src/theme-package.coffee | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index bc830c7bd..97a822c47 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -25,6 +25,13 @@ class TextEditorElement extends HTMLElement @classList.add('editor') @setAttribute('tabindex', -1) @shadowRoot = @createShadowRoot() + + for styleElement in document.querySelectorAll('head style.syntax-theme') + @shadowRoot.appendChild(styleElement.cloneNode(true)) + + atom.themes.onDidAddStylesheet (sheet) => + @shadowRoot.appendChild(sheet.ownerNode.cloneNode(true)) + @root = document.createElement('div') @root.classList.add('editor', 'editor-colors') @shadowRoot.appendChild(@root) diff --git a/src/theme-package.coffee b/src/theme-package.coffee index 3c03d818f..8904ee43f 100644 --- a/src/theme-package.coffee +++ b/src/theme-package.coffee @@ -5,7 +5,7 @@ module.exports = class ThemePackage extends Package getType: -> 'theme' - getStylesheetType: -> 'theme' + getStylesheetType: -> "#{@metadata.theme}-theme" enable: -> atom.config.unshiftAtKeyPath('core.themes', @name) From 769c6c52bb7bd9172821b5d75f2b0d71dc789b35 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 11 Oct 2014 08:39:24 -0700 Subject: [PATCH 03/68] =?UTF-8?q?Make=20atom-text-editor=20have=20?= =?UTF-8?q?=E2=80=9Cdisplay:=20block=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/editor.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/editor.less b/static/editor.less index 57592d9b9..e92b3d560 100644 --- a/static/editor.less +++ b/static/editor.less @@ -2,6 +2,10 @@ @import "octicon-utf-codes"; @import "octicon-mixins"; +atom-text-editor { + display: block; +} + atom-text-editor::shadow { .editor, .editor-contents { height: 100%; From 087387e633a2b2c0e100997c2d6b256947dd651a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 11 Oct 2014 08:46:32 -0700 Subject: [PATCH 04/68] Style mini editor font sizes on atom-text-editor host element MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …instead of via the shadow DOM. We always honor the computed font styles of the host element. --- static/editor.less | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/static/editor.less b/static/editor.less index e92b3d560..1ce82ed3e 100644 --- a/static/editor.less +++ b/static/editor.less @@ -88,12 +88,6 @@ atom-text-editor::shadow { } } } -} - -atom-text-editor.mini::shadow { - font-size: @input-font-size; - line-height: @component-line-height; - max-height: @component-line-height + 2; // +2 for borders .placeholder-text { position: absolute; @@ -101,6 +95,12 @@ atom-text-editor.mini::shadow { } } +atom-text-editor.mini { + font-size: @input-font-size; + line-height: @component-line-height; + max-height: @component-line-height + 2; // +2 for borders +} + atom-text-editor::shadow { z-index: 0; font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier; From 4e8e5a84c41b913c3563f8354fdca4ceebd93d58 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Oct 2014 15:25:40 -0600 Subject: [PATCH 05/68] Support context attribute in --- spec/styles-element-spec.coffee | 21 +++++++++++++++++++++ src/style-manager.coffee | 1 + src/styles-element.coffee | 18 +++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/spec/styles-element-spec.coffee b/spec/styles-element-spec.coffee index 0cfb1a185..7b1fa1873 100644 --- a/spec/styles-element-spec.coffee +++ b/spec/styles-element-spec.coffee @@ -52,3 +52,24 @@ describe "StylesElement", -> expect(element.children.length).toBe initialChildCount + 1 expect(element.children[initialChildCount].textContent).toBe "a {color: blue;}" expect(updatedStyleElements).toEqual [element.children[initialChildCount]] + + it "only includes style elements matching the 'context' attribute", -> + initialChildCount = element.children.length + + atom.styles.addStyleSheet("a {color: red;}", context: 'test-context') + atom.styles.addStyleSheet("a {color: green;}") + + expect(element.children.length).toBe initialChildCount + 1 + expect(element.children[initialChildCount].textContent).toBe "a {color: green;}" + + element.setAttribute('context', 'test-context') + + expect(element.children.length).toBe 1 + expect(element.children[0].textContent).toBe "a {color: red;}" + + atom.styles.addStyleSheet("a {color: blue;}", context: 'test-context') + atom.styles.addStyleSheet("a {color: yellow;}") + + expect(element.children.length).toBe 2 + expect(element.children[0].textContent).toBe "a {color: red;}" + expect(element.children[1].textContent).toBe "a {color: blue;}" diff --git a/src/style-manager.coffee b/src/style-manager.coffee index 9f6b912fd..7b9a786c7 100644 --- a/src/style-manager.coffee +++ b/src/style-manager.coffee @@ -25,6 +25,7 @@ class StyleManager addStyleSheet: (source, params) -> sourcePath = params?.sourcePath + context = params?.context group = params?.group if sourcePath? and styleElement = @styleElementsBySourcePath[sourcePath] diff --git a/src/styles-element.coffee b/src/styles-element.coffee index 069c3f8c7..311cb1633 100644 --- a/src/styles-element.coffee +++ b/src/styles-element.coffee @@ -3,6 +3,10 @@ class StylesElement extends HTMLElement createdCallback: -> @emitter = new Emitter + @context = @getAttribute('context') ? undefined + + attributeChangedCallback: (attrName, oldVal, newVal) -> + @contextChanged() if attrName is 'context' onDidAddStyleElement: (callback) -> @emitter.on 'did-add-style-element', callback @@ -21,7 +25,10 @@ class StylesElement extends HTMLElement @subscriptions.add atom.styles.onDidUpdateStyleElement(@styleElementUpdated.bind(this)) styleElementAdded: (styleElement) -> + return unless styleElement.context is @context + styleElementClone = styleElement.cloneNode(true) + styleElementClone.context = styleElement.context @styleElementClonesByOriginalElement.set(styleElement, styleElementClone) group = styleElement.getAttribute('group') @@ -35,15 +42,24 @@ class StylesElement extends HTMLElement @emitter.emit 'did-add-style-element', styleElementClone styleElementRemoved: (styleElement) -> - styleElementClone = @styleElementClonesByOriginalElement.get(styleElement) + return unless styleElement.context is @context + + styleElementClone = @styleElementClonesByOriginalElement.get(styleElement) ? styleElement styleElementClone.remove() @emitter.emit 'did-remove-style-element', styleElementClone styleElementUpdated: (styleElement) -> + return unless styleElement.context is @context + styleElementClone = @styleElementClonesByOriginalElement.get(styleElement) styleElementClone.textContent = styleElement.textContent @emitter.emit 'did-update-style-element', styleElementClone + contextChanged: -> + @styleElementRemoved(child) for child in Array::slice.call(@children) + @context = @getAttribute('context') + @styleElementAdded(styleElement) for styleElement in atom.styles.getStyleElements() + detachedCallback: -> @subscriptions.dispose() From 3b6189e94b04d28610dcd19cae70c0d05d102522 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Oct 2014 17:04:10 -0600 Subject: [PATCH 06/68] =?UTF-8?q?Create=20WeakMap=20on=20element=20creatio?= =?UTF-8?q?n=20to=20support=20=E2=80=98context=E2=80=99=20attribute=20chan?= =?UTF-8?q?ge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles-element.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles-element.coffee b/src/styles-element.coffee index 311cb1633..8c97f2d44 100644 --- a/src/styles-element.coffee +++ b/src/styles-element.coffee @@ -3,6 +3,7 @@ class StylesElement extends HTMLElement createdCallback: -> @emitter = new Emitter + @styleElementClonesByOriginalElement = new WeakMap @context = @getAttribute('context') ? undefined attributeChangedCallback: (attrName, oldVal, newVal) -> @@ -19,7 +20,6 @@ class StylesElement extends HTMLElement attachedCallback: -> @subscriptions = new CompositeDisposable - @styleElementClonesByOriginalElement = new WeakMap @subscriptions.add atom.styles.observeStyleElements(@styleElementAdded.bind(this)) @subscriptions.add atom.styles.onDidRemoveStyleElement(@styleElementRemoved.bind(this)) @subscriptions.add atom.styles.onDidUpdateStyleElement(@styleElementUpdated.bind(this)) From 2affff30ffa594d3f962c3c5405155058d767f9e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Oct 2014 17:04:45 -0600 Subject: [PATCH 07/68] Handle events with native handlers to avoid shadow DOM issues with React --- src/input-component.coffee | 6 ++++-- src/text-editor-component.coffee | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/input-component.coffee b/src/input-component.coffee index 230c4be37..5c893815d 100644 --- a/src/input-component.coffee +++ b/src/input-component.coffee @@ -7,9 +7,9 @@ InputComponent = React.createClass displayName: 'InputComponent' render: -> - {className, style, onFocus, onBlur} = @props + {className, style} = @props - input {className, style, onFocus, onBlur, 'data-react-skip-selection-restoration': true} + input {className, style, 'data-react-skip-selection-restoration': true} getInitialState: -> {lastChar: ''} @@ -17,6 +17,8 @@ InputComponent = React.createClass componentDidMount: -> @getDOMNode().addEventListener 'paste', @onPaste @getDOMNode().addEventListener 'compositionupdate', @onCompositionUpdate + @getDOMNode().addEventListener 'focus', @onFocus + @getDOMNode().addEventListener 'blur', @onBlur # Don't let text accumulate in the input forever, but avoid excessive reflows componentDidUpdate: -> diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 6dc4700d0..fdda54f03 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -102,7 +102,7 @@ TextEditorComponent = React.createClass @useHardwareAcceleration, @performedInitialMeasurement, @backgroundColor, @gutterBackgroundColor } - div ref: 'scrollView', className: 'scroll-view', onMouseDown: @onMouseDown, + div ref: 'scrollView', className: 'scroll-view', InputComponent ref: 'input' className: 'hidden-input' @@ -380,6 +380,7 @@ TextEditorComponent = React.createClass node.addEventListener 'mousewheel', @onMouseWheel node.addEventListener 'focus', @onFocus # For some reason, React's built in focus events seem to bubble node.addEventListener 'textInput', @onTextInput + @refs.scrollView.getDOMNode().addEventListener 'mousedown', @onMouseDown scrollViewNode = @refs.scrollView.getDOMNode() scrollViewNode.addEventListener 'scroll', @onScrollViewScroll From c2d0b6d4f54ff0e7c62d068033d0ffcfafea0360 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Oct 2014 17:05:37 -0600 Subject: [PATCH 08/68] Load editor stylesheet in shadow root with style manager context param --- src/text-editor-element.coffee | 4 + src/theme-manager.coffee | 3 + static/atom.less | 1 - static/editor.less | 221 ++++++++++++++++----------------- 4 files changed, 116 insertions(+), 113 deletions(-) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 97a822c47..3c277d6a3 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -26,6 +26,10 @@ class TextEditorElement extends HTMLElement @setAttribute('tabindex', -1) @shadowRoot = @createShadowRoot() + stylesElement = document.createElement('atom-styles') + stylesElement.setAttribute('context', 'atom-text-editor') + @shadowRoot.appendChild(stylesElement) + for styleElement in document.querySelectorAll('head style.syntax-theme') @shadowRoot.appendChild(styleElement.cloneNode(true)) diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index e87028ae3..b161c6279 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -249,6 +249,9 @@ class ThemeManager if nativeStylesheetPath = fs.resolveOnLoadPath(process.platform, ['css', 'less']) @requireStylesheet(nativeStylesheetPath) + textEditorStylesPath = path.join(@resourcePath, 'static', 'editor.less') + atom.styles.addStyleSheet(@loadLessStylesheet(textEditorStylesPath), sourcePath: 'textEditorStylesPath', context: 'atom-text-editor') + stylesheetElementForId: (id) -> document.head.querySelector("atom-styles style[source-path=\"#{id}\"]") diff --git a/static/atom.less b/static/atom.less index cd62ab8a7..975905ad1 100644 --- a/static/atom.less +++ b/static/atom.less @@ -22,7 +22,6 @@ @import "popover-list"; @import "messages"; @import "markdown"; -@import "editor"; @import "select-list"; @import "syntax"; @import "utilities"; diff --git a/static/editor.less b/static/editor.less index 1ce82ed3e..df8bd7e3c 100644 --- a/static/editor.less +++ b/static/editor.less @@ -2,112 +2,106 @@ @import "octicon-utf-codes"; @import "octicon-mixins"; -atom-text-editor { +:host { display: block; } -atom-text-editor::shadow { - .editor, .editor-contents { - height: 100%; - width: 100%; - } +.editor, .editor-contents { + height: 100%; + width: 100%; +} - .underlayer { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: -2; - } +.underlayer { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: -2; +} - .lines { - min-width: 100%; - } +.lines { + min-width: 100%; +} - .cursor { - z-index: 4; - pointer-events: none; - } +.cursor { + z-index: 4; + pointer-events: none; +} - .editor-contents.is-focused .cursor { - visibility: visible; - } +.cursors.blink-off .cursor { + opacity: 0; +} - .cursors.blink-off .cursor { - opacity: 0; - } +.horizontal-scrollbar { + position: absolute; + left: 0; + right: 0; + bottom: 0; - .horizontal-scrollbar { - position: absolute; - left: 0; - right: 0; - bottom: 0; + height: 15px; + overflow-x: auto; + overflow-y: hidden; + z-index: 3; + .scrollbar-content { height: 15px; - overflow-x: auto; - overflow-y: hidden; - z-index: 3; - - .scrollbar-content { - height: 15px; - } - } - - .vertical-scrollbar { - overflow-x: hidden; - } - - .scrollbar-corner { - position: absolute; - overflow: auto; - bottom: 0; - right: 0; - } - - .scroll-view { - overflow: hidden; - z-index: 0; - } - - .scroll-view-content { - position: relative; - width: 100%; - } - - .gutter { - .line-number { - white-space: nowrap; - padding-left: .5em; - - .icon-right { - padding: 0 .4em; - &:before { - text-align: center; - } - } - } - } - - .placeholder-text { - position: absolute; - color: @text-color-subtle; } } -atom-text-editor.mini { +.vertical-scrollbar { + overflow-x: hidden; +} + +.scrollbar-corner { + position: absolute; + overflow: auto; + bottom: 0; + right: 0; +} + +.scroll-view { + overflow: hidden; + z-index: 0; +} + +.scroll-view-content { + position: relative; + width: 100%; +} + +.gutter { + .line-number { + white-space: nowrap; + padding-left: .5em; + + .icon-right { + padding: 0 .4em; + &:before { + text-align: center; + } + } + } +} + +.placeholder-text { + position: absolute; + color: @text-color-subtle; +} + +:host(.mini) { font-size: @input-font-size; line-height: @component-line-height; max-height: @component-line-height + 2; // +2 for borders } -atom-text-editor::shadow { +.editor { z-index: 0; font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier; line-height: 1.3; } -atom-text-editor::shadow, atom-text-editor::shadow .editor-contents { +.editor, .editor-contents { overflow: hidden; cursor: text; display: -webkit-flex; @@ -115,11 +109,11 @@ atom-text-editor::shadow, atom-text-editor::shadow .editor-contents { position: relative; } -atom-text-editor::shadow .gutter .line-number.cursor-line { +.gutter .line-number.cursor-line { opacity: 1; } -atom-text-editor::shadow .gutter { +.gutter { overflow: hidden; text-align: right; cursor: default; @@ -127,20 +121,20 @@ atom-text-editor::shadow .gutter { box-sizing: border-box; } -atom-text-editor::shadow .gutter .line-number { +.gutter .line-number { padding-left: .5em; opacity: 0.6; } -atom-text-editor::shadow .gutter .line-numbers { +.gutter .line-numbers { position: relative; } -atom-text-editor::shadow .gutter .line-number.folded.cursor-line { +.gutter .line-number.folded.cursor-line { opacity: 1; } -atom-text-editor::shadow .gutter .line-number .icon-right { +.gutter .line-number .icon-right { .octicon(chevron-down, 0.8em); display: inline-block; visibility: hidden; @@ -149,7 +143,7 @@ atom-text-editor::shadow .gutter .line-number .icon-right { opacity: .6; } -atom-text-editor::shadow .gutter:hover .line-number.foldable .icon-right { +.gutter:hover .line-number.foldable .icon-right { visibility: visible; &:before { @@ -161,7 +155,7 @@ atom-text-editor::shadow .gutter:hover .line-number.foldable .icon-right { } } -atom-text-editor::shadow .gutter, atom-text-editor::shadow .gutter:hover { +.gutter, .gutter:hover { .line-number.folded .icon-right { .octicon(chevron-right, 0.8em); visibility: visible; @@ -174,17 +168,17 @@ atom-text-editor::shadow .gutter, atom-text-editor::shadow .gutter:hover { } } -atom-text-editor::shadow .fold-marker { +.fold-marker { cursor: default; } -atom-text-editor::shadow .fold-marker:after { +.fold-marker:after { .icon(0.8em, inline); content: @ellipsis; padding-left: 0.2em; } -atom-text-editor::shadow .line.cursor-line .fold-marker:after { +.line.cursor-line .fold-marker:after { opacity: 1; } @@ -192,22 +186,22 @@ atom-text-editor::shadow.is-blurred .line.cursor-line { background: rgba(0, 0, 0, 0); } -atom-text-editor::shadow .invisible-character { +.invisible-character { font-weight: normal !important; font-style: normal !important; } -atom-text-editor::shadow .indent-guide { +.indent-guide { display: inline-block; box-shadow: inset 1px 0; } -atom-text-editor::shadow .vertical-scrollbar, -atom-text-editor::shadow .horizontal-scrollbar { +.vertical-scrollbar, +.horizontal-scrollbar { cursor: default; } -atom-text-editor::shadow .vertical-scrollbar { +.vertical-scrollbar { position: absolute; top: 0; right: 0; @@ -218,7 +212,7 @@ atom-text-editor::shadow .vertical-scrollbar { z-index: 3; } -atom-text-editor::shadow .scroll-view { +.scroll-view { overflow-x: auto; overflow-y: hidden; -webkit-flex: 1; @@ -230,49 +224,52 @@ atom-text-editor::shadow.soft-wrap .scroll-view { overflow-x: hidden; } -atom-text-editor::shadow .underlayer { +.underlayer { z-index: 0; position: absolute; min-height: 100%; } -atom-text-editor::shadow .lines { +.lines { position: relative; z-index: 1; } -atom-text-editor::shadow .overlayer { +.overlayer { z-index: 2; position: absolute; } -atom-text-editor::shadow .line { +.line { white-space: pre; } -atom-text-editor::shadow .line span { +.line span { vertical-align: top; } -atom-text-editor::shadow .cursor { +.cursor { position: absolute; border-left: 1px solid; } -atom-text-editor::shadow .cursor, -atom-text-editor::shadow.is-focused .cursor.blink-off { +.cursor { visibility: hidden; } -atom-text-editor::shadow.is-focused .cursor { +.is-focused .cursor { visibility: visible; } +.is-focused .cursor.blink-off { + visibility: hidden; +} + .cursor.hidden-cursor { display: none; } -atom-text-editor::shadow .hidden-input { +.hidden-input { padding: 0; border: 0; position: absolute; @@ -283,13 +280,13 @@ atom-text-editor::shadow .hidden-input { width: 1px; } -atom-text-editor::shadow .highlight { +.highlight { background: none; padding: 0; } -atom-text-editor::shadow .highlight .region, -atom-text-editor::shadow .selection .region { +.highlight .region, +.selection .region { position: absolute; pointer-events: none; z-index: -1; From 1a98cb70708d3d3e118f75c68868338ea1a70144 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Oct 2014 11:55:54 -0600 Subject: [PATCH 09/68] Use atom.styles to activate stylesheets in packages --- src/package.coffee | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/package.coffee b/src/package.coffee index 76024a3c3..efdb1c2a1 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -62,6 +62,7 @@ class Package constructor: (@path, @metadata) -> @emitter = new Emitter + @stylesheetDisposables = new CompositeDisposable @metadata ?= Package.loadMetadata(@path) @bundledPackage = Package.isBundledPackagePath(@path) @name = @metadata?.name ? path.basename(@path) @@ -175,9 +176,9 @@ class Package activateStylesheets: -> return if @stylesheetsActivated - type = @getStylesheetType() - for [stylesheetPath, content] in @stylesheets - atom.themes.applyStylesheet(stylesheetPath, content, type) + group = @getStylesheetType() + for [sourcePath, source] in @stylesheets + @stylesheetDisposables.add(atom.styles.addStyleSheet(source, {sourcePath, sourcePath})) @stylesheetsActivated = true activateResources: -> @@ -329,11 +330,10 @@ class Package reloadStylesheets: -> oldSheets = _.clone(@stylesheets) @loadStylesheets() - atom.themes.removeStylesheet(stylesheetPath) for [stylesheetPath] in oldSheets - @reloadStylesheet(stylesheetPath, content) for [stylesheetPath, content] in @stylesheets - - reloadStylesheet: (stylesheetPath, content) -> - atom.themes.applyStylesheet(stylesheetPath, content, @getStylesheetType()) + @stylesheetDisposables.dispose() + @stylesheetDisposables = new CompositeDisposable + @stylesheetsActivated = false + @activateStylesheets() requireMainModule: -> return @mainModule if @mainModule? From 582066915bc4852fad7e6737c378cf31f9a1cf7f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Oct 2014 12:30:17 -0600 Subject: [PATCH 10/68] Apply syntax theme stylesheets in text editor shadow DOM via atom.styles --- src/package.coffee | 3 ++- src/text-editor-element.coffee | 6 ------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/package.coffee b/src/package.coffee index efdb1c2a1..77a8a775f 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -176,9 +176,10 @@ class Package activateStylesheets: -> return if @stylesheetsActivated + context = 'atom-text-editor' if @metadata.theme is 'syntax' group = @getStylesheetType() for [sourcePath, source] in @stylesheets - @stylesheetDisposables.add(atom.styles.addStyleSheet(source, {sourcePath, sourcePath})) + @stylesheetDisposables.add(atom.styles.addStyleSheet(source, {sourcePath, sourcePath, context})) @stylesheetsActivated = true activateResources: -> diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 3c277d6a3..2e91558da 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -30,12 +30,6 @@ class TextEditorElement extends HTMLElement stylesElement.setAttribute('context', 'atom-text-editor') @shadowRoot.appendChild(stylesElement) - for styleElement in document.querySelectorAll('head style.syntax-theme') - @shadowRoot.appendChild(styleElement.cloneNode(true)) - - atom.themes.onDidAddStylesheet (sheet) => - @shadowRoot.appendChild(sheet.ownerNode.cloneNode(true)) - @root = document.createElement('div') @root.classList.add('editor', 'editor-colors') @shadowRoot.appendChild(@root) From 2b218d2e01ea608132dbd871cb4b00dcd5c2def2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Oct 2014 12:30:53 -0600 Subject: [PATCH 11/68] Only update atom-styles children on context attribute change if attached --- src/styles-element.coffee | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/styles-element.coffee b/src/styles-element.coffee index 8c97f2d44..309ac5065 100644 --- a/src/styles-element.coffee +++ b/src/styles-element.coffee @@ -1,6 +1,8 @@ {Emitter, CompositeDisposable} = require 'event-kit' class StylesElement extends HTMLElement + attached: false + createdCallback: -> @emitter = new Emitter @styleElementClonesByOriginalElement = new WeakMap @@ -19,11 +21,17 @@ class StylesElement extends HTMLElement @emitter.on 'did-update-style-element', callback attachedCallback: -> + @attached = true + @subscriptions = new CompositeDisposable @subscriptions.add atom.styles.observeStyleElements(@styleElementAdded.bind(this)) @subscriptions.add atom.styles.onDidRemoveStyleElement(@styleElementRemoved.bind(this)) @subscriptions.add atom.styles.onDidUpdateStyleElement(@styleElementUpdated.bind(this)) + detachedCallback: -> + @attached = false + @subscriptions.dispose() + styleElementAdded: (styleElement) -> return unless styleElement.context is @context @@ -56,11 +64,9 @@ class StylesElement extends HTMLElement @emitter.emit 'did-update-style-element', styleElementClone contextChanged: -> - @styleElementRemoved(child) for child in Array::slice.call(@children) @context = @getAttribute('context') - @styleElementAdded(styleElement) for styleElement in atom.styles.getStyleElements() - - detachedCallback: -> - @subscriptions.dispose() + if @attached + @styleElementRemoved(child) for child in Array::slice.call(@children) + @styleElementAdded(styleElement) for styleElement in atom.styles.getStyleElements() module.exports = StylesElement = document.registerElement 'atom-styles', prototype: StylesElement.prototype From 596987fbce4cd047706b2b34aa9df815dd6311ad Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Oct 2014 13:43:12 -0600 Subject: [PATCH 12/68] Fix sourcePath on text editor stylesheet loading --- src/theme-manager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index b161c6279..4919fbef2 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -250,7 +250,7 @@ class ThemeManager @requireStylesheet(nativeStylesheetPath) textEditorStylesPath = path.join(@resourcePath, 'static', 'editor.less') - atom.styles.addStyleSheet(@loadLessStylesheet(textEditorStylesPath), sourcePath: 'textEditorStylesPath', context: 'atom-text-editor') + atom.styles.addStyleSheet(@loadLessStylesheet(textEditorStylesPath), sourcePath: textEditorStylesPath, context: 'atom-text-editor') stylesheetElementForId: (id) -> document.head.querySelector("atom-styles style[source-path=\"#{id}\"]") From 65f40d6f7bd214f759e4b876c4df90ca34c76758 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Oct 2014 13:43:25 -0600 Subject: [PATCH 13/68] Move font styling to host element so font preferences work --- static/editor.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/editor.less b/static/editor.less index df8bd7e3c..ab6ad2179 100644 --- a/static/editor.less +++ b/static/editor.less @@ -4,6 +4,8 @@ :host { display: block; + font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier; + line-height: 1.3; } .editor, .editor-contents { @@ -97,8 +99,6 @@ .editor { z-index: 0; - font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier; - line-height: 1.3; } .editor, .editor-contents { From 22f62681166a23371f832a81a48e3961d58b3931 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Oct 2014 14:27:35 -0600 Subject: [PATCH 14/68] Assign StylesElement::context on attachment --- src/styles-element.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/styles-element.coffee b/src/styles-element.coffee index 309ac5065..84fd901d3 100644 --- a/src/styles-element.coffee +++ b/src/styles-element.coffee @@ -1,12 +1,12 @@ {Emitter, CompositeDisposable} = require 'event-kit' class StylesElement extends HTMLElement + context: null attached: false createdCallback: -> @emitter = new Emitter @styleElementClonesByOriginalElement = new WeakMap - @context = @getAttribute('context') ? undefined attributeChangedCallback: (attrName, oldVal, newVal) -> @contextChanged() if attrName is 'context' @@ -23,6 +23,7 @@ class StylesElement extends HTMLElement attachedCallback: -> @attached = true + @context = @getAttribute('context') ? undefined @subscriptions = new CompositeDisposable @subscriptions.add atom.styles.observeStyleElements(@styleElementAdded.bind(this)) @subscriptions.add atom.styles.onDidRemoveStyleElement(@styleElementRemoved.bind(this)) From 5f4fb230579401102f2c153f5b8d68c0e68b032d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Oct 2014 14:45:32 -0600 Subject: [PATCH 15/68] Initialize atom-styles element in editor shadow dom before measuring --- src/styles-element.coffee | 45 +++++++++++++++++++--------------- src/text-editor-element.coffee | 1 + 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/styles-element.coffee b/src/styles-element.coffee index 84fd901d3..8510fe594 100644 --- a/src/styles-element.coffee +++ b/src/styles-element.coffee @@ -1,15 +1,8 @@ {Emitter, CompositeDisposable} = require 'event-kit' class StylesElement extends HTMLElement + subscriptions: null context: null - attached: false - - createdCallback: -> - @emitter = new Emitter - @styleElementClonesByOriginalElement = new WeakMap - - attributeChangedCallback: (attrName, oldVal, newVal) -> - @contextChanged() if attrName is 'context' onDidAddStyleElement: (callback) -> @emitter.on 'did-add-style-element', callback @@ -20,18 +13,36 @@ class StylesElement extends HTMLElement onDidUpdateStyleElement: (callback) -> @emitter.on 'did-update-style-element', callback - attachedCallback: -> - @attached = true + createdCallback: -> + @emitter = new Emitter + @styleElementClonesByOriginalElement = new WeakMap + + attachedCallback: -> + @initialize() + + detachedCallback: -> + @subscriptions.dispose() + @subscriptions = null + + attributeChangedCallback: (attrName, oldVal, newVal) -> + @contextChanged() if attrName is 'context' + + initialize: -> + return if @subscriptions? - @context = @getAttribute('context') ? undefined @subscriptions = new CompositeDisposable + @context = @getAttribute('context') ? undefined + @subscriptions.add atom.styles.observeStyleElements(@styleElementAdded.bind(this)) @subscriptions.add atom.styles.onDidRemoveStyleElement(@styleElementRemoved.bind(this)) @subscriptions.add atom.styles.onDidUpdateStyleElement(@styleElementUpdated.bind(this)) - detachedCallback: -> - @attached = false - @subscriptions.dispose() + contextChanged: -> + return unless @subscriptions? + + @styleElementRemoved(child) for child in Array::slice.call(@children) + @context = @getAttribute('context') + @styleElementAdded(styleElement) for styleElement in atom.styles.getStyleElements() styleElementAdded: (styleElement) -> return unless styleElement.context is @context @@ -64,10 +75,4 @@ class StylesElement extends HTMLElement styleElementClone.textContent = styleElement.textContent @emitter.emit 'did-update-style-element', styleElementClone - contextChanged: -> - @context = @getAttribute('context') - if @attached - @styleElementRemoved(child) for child in Array::slice.call(@children) - @styleElementAdded(styleElement) for styleElement in atom.styles.getStyleElements() - module.exports = StylesElement = document.registerElement 'atom-styles', prototype: StylesElement.prototype diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 2e91558da..b31084693 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -28,6 +28,7 @@ class TextEditorElement extends HTMLElement stylesElement = document.createElement('atom-styles') stylesElement.setAttribute('context', 'atom-text-editor') + stylesElement.initialize() @shadowRoot.appendChild(stylesElement) @root = document.createElement('div') From 2ab5fa405cac569109fdf1e6f8562873a04eae57 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Oct 2014 14:46:52 -0600 Subject: [PATCH 16/68] Apply mini and is-focused class to both editor host element and root This preserves existing theming behavior --- src/text-editor-component.coffee | 28 +++++++++++++++------------- src/text-editor-element.coffee | 11 ++++++----- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index fdda54f03..d43e499b3 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -193,9 +193,9 @@ TextEditorComponent = React.createClass @checkForVisibilityChange() componentWillUnmount: -> - {editor, parentView} = @props + {editor, hostElement} = @props - parentView.__spacePenView.trigger 'editor:will-be-removed', [parentView.__spacePenView] + hostElement.__spacePenView.trigger 'editor:will-be-removed', [hostElement.__spacePenView] @unsubscribe() @scopedConfigSubscriptions.dispose() window.removeEventListener 'resize', @requestHeightAndWidthMeasurement @@ -215,9 +215,9 @@ TextEditorComponent = React.createClass if @props.editor.isAlive() @updateParentViewFocusedClassIfNeeded(prevState) @updateParentViewMiniClassIfNeeded(prevState) - @props.parentView.__spacePenView.trigger 'cursor:moved' if cursorMoved - @props.parentView.__spacePenView.trigger 'selection:changed' if selectionChanged - @props.parentView.__spacePenView.trigger 'editor:display-updated' + @props.hostElement.__spacePenView.trigger 'cursor:moved' if cursorMoved + @props.hostElement.__spacePenView.trigger 'selection:changed' if selectionChanged + @props.hostElement.__spacePenView.trigger 'editor:display-updated' becameVisible: -> @updatesPaused = true @@ -261,7 +261,7 @@ TextEditorComponent = React.createClass @forceUpdate() getTopmostDOMNode: -> - @props.parentView + @props.hostElement getRenderedRowRange: -> {editor, lineOverdrawMargin} = @props @@ -772,10 +772,10 @@ TextEditorComponent = React.createClass measureHeightAndWidth: -> return unless @isMounted() - {editor, parentView} = @props + {editor, hostElement} = @props scrollViewNode = @refs.scrollView.getDOMNode() - {position} = getComputedStyle(parentView) - {height} = parentView.style + {position} = getComputedStyle(hostElement) + {height} = hostElement.style if position is 'absolute' or height if @autoHeight @@ -807,9 +807,9 @@ TextEditorComponent = React.createClass @remeasureCharacterWidths() sampleBackgroundColors: (suppressUpdate) -> - {parentView} = @props + {hostElement} = @props {showLineNumbers} = @state - {backgroundColor} = getComputedStyle(parentView) + {backgroundColor} = getComputedStyle(hostElement) if backgroundColor isnt @backgroundColor @backgroundColor = backgroundColor @@ -978,11 +978,13 @@ TextEditorComponent = React.createClass updateParentViewFocusedClassIfNeeded: (prevState) -> if prevState.focused isnt @state.focused - @props.parentView.classList.toggle('is-focused', @state.focused) + @props.hostElement.classList.toggle('is-focused', @state.focused) + @props.rootElement.classList.toggle('is-focused', @state.focused) updateParentViewMiniClassIfNeeded: (prevProps) -> if prevProps.mini isnt @props.mini - @props.parentView.classList.toggle('mini', @props.mini) + @props.hostElement.classList.toggle('mini', @props.mini) + @props.rootElement.classList.toggle('mini', @props.mini) runScrollBenchmark: -> unless process.env.NODE_ENV is 'production' diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index b31084693..d91345eee 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -31,9 +31,9 @@ class TextEditorElement extends HTMLElement stylesElement.initialize() @shadowRoot.appendChild(stylesElement) - @root = document.createElement('div') - @root.classList.add('editor', 'editor-colors') - @shadowRoot.appendChild(@root) + @rootElement = document.createElement('div') + @rootElement.classList.add('editor', 'editor-colors') + @shadowRoot.appendChild(@rootElement) createSpacePenShim: -> TextEditorView ?= require './text-editor-view' @@ -74,12 +74,13 @@ class TextEditorElement extends HTMLElement mountComponent: -> @componentDescriptor ?= TextEditorComponent( - parentView: this + hostElement: this + rootElement: @rootElement editor: @model mini: @model.mini lineOverdrawMargin: @lineOverdrawMargin ) - @component = React.renderComponent(@componentDescriptor, @root) + @component = React.renderComponent(@componentDescriptor, @rootElement) unmountComponent: -> return unless @component?.isMounted() From b86f6870c5963f2a04c783853613690a13f329b9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Oct 2014 15:11:29 -0600 Subject: [PATCH 17/68] Use native event handlers instead of React MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React event handlers don’t work because of the shadow DOM --- src/gutter-component.coffee | 8 ++++++-- src/scrollbar-component.coffee | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index c6cf62490..798d81ac3 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -16,12 +16,12 @@ GutterComponent = React.createClass measuredWidth: null render: -> - {scrollHeight, scrollViewHeight, onMouseDown, backgroundColor, gutterBackgroundColor} = @props + {scrollHeight, scrollViewHeight, backgroundColor, gutterBackgroundColor} = @props if gutterBackgroundColor isnt 'rbga(0, 0, 0, 0)' backgroundColor = gutterBackgroundColor - div className: 'gutter', onClick: @onClick, onMouseDown: @onMouseDown, + div className: 'gutter', div className: 'line-numbers', ref: 'lineNumbers', style: height: Math.max(scrollHeight, scrollViewHeight) WebkitTransform: @getTransform() @@ -45,6 +45,10 @@ GutterComponent = React.createClass @appendDummyLineNumber() @updateLineNumbers() if @props.performedInitialMeasurement + node = @getDOMNode() + node.addEventListener 'click', @onClick + node.addEventListener 'mousedown', @onMouseDown + # Only update the gutter if the visible row range has changed or if a # non-zero-delta change to the screen lines has occurred within the current # visible row range. diff --git a/src/scrollbar-component.coffee b/src/scrollbar-component.coffee index 1e23d686c..a7645b277 100644 --- a/src/scrollbar-component.coffee +++ b/src/scrollbar-component.coffee @@ -23,7 +23,7 @@ ScrollbarComponent = React.createClass style.right = verticalScrollbarWidth if scrollableInOppositeDirection style.height = horizontalScrollbarHeight - div {className, style, @onScroll}, + div {className, style}, switch orientation when 'vertical' div className: 'scrollbar-content', style: {height: scrollHeight} @@ -36,6 +36,8 @@ ScrollbarComponent = React.createClass unless orientation is 'vertical' or orientation is 'horizontal' throw new Error("Must specify an orientation property of 'vertical' or 'horizontal'") + @getDOMNode().addEventListener 'scroll', @onScroll + shouldComponentUpdate: (newProps) -> return true if newProps.visible isnt @props.visible From 5d3602d37ba831330830e0144c340181d5d1275c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Oct 2014 15:12:09 -0600 Subject: [PATCH 18/68] Get node once to attach event handlers --- src/input-component.coffee | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/input-component.coffee b/src/input-component.coffee index 5c893815d..cacf16266 100644 --- a/src/input-component.coffee +++ b/src/input-component.coffee @@ -15,10 +15,11 @@ InputComponent = React.createClass {lastChar: ''} componentDidMount: -> - @getDOMNode().addEventListener 'paste', @onPaste - @getDOMNode().addEventListener 'compositionupdate', @onCompositionUpdate - @getDOMNode().addEventListener 'focus', @onFocus - @getDOMNode().addEventListener 'blur', @onBlur + node = @getDOMNode() + node.addEventListener 'paste', @onPaste + node.addEventListener 'compositionupdate', @onCompositionUpdate + node.addEventListener 'focus', @onFocus + node.addEventListener 'blur', @onBlur # Don't let text accumulate in the input forever, but avoid excessive reflows componentDidUpdate: -> From 5be21d6743812a699b9ed8995fa4f50746f9caaf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Oct 2014 19:42:30 -0600 Subject: [PATCH 19/68] Avoid traversing through shadow root on mousewheel events --- src/text-editor-component.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index d43e499b3..8b0f71a4c 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -908,10 +908,10 @@ TextEditorComponent = React.createClass lineNumberNodeForScreenRow: (screenRow) -> @refs.gutter.lineNumberNodeForScreenRow(screenRow) screenRowForNode: (node) -> - while node isnt document + while node? if screenRow = node.dataset.screenRow return parseInt(screenRow) - node = node.parentNode + node = node.parentElement null getFontSize: -> From 268fceb0738858393b361074a15428c6b2be2d5f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Oct 2014 19:42:56 -0600 Subject: [PATCH 20/68] Specify border-box sizing for the cursor to fix specs --- static/editor.less | 1 + 1 file changed, 1 insertion(+) diff --git a/static/editor.less b/static/editor.less index ab6ad2179..6cf487e9b 100644 --- a/static/editor.less +++ b/static/editor.less @@ -29,6 +29,7 @@ .cursor { z-index: 4; pointer-events: none; + box-sizing: border-box; } .cursors.blink-off .cursor { From b2bc09c13d81f69b58f2deb0c1c04d5c47feae08 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 12:42:29 -0600 Subject: [PATCH 21/68] Apply stylesheets with atom-editor-context in text editor specs --- spec/text-editor-component-spec.coffee | 9 +++++---- src/text-editor-component.coffee | 12 ++++++------ src/text-editor-element.coffee | 11 ++++++----- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 251a54111..9512f7fdb 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -721,11 +721,11 @@ describe "TextEditorComponent", -> editor.setCursorScreenPosition([0, 16]) nextAnimationFrame() - atom.themes.applyStylesheet 'test', """ + atom.styles.addStyleSheet """ .function.js { font-weight: bold; } - """ + """, context: 'atom-text-editor' nextAnimationFrame() # update based on new measurements cursor = componentNode.querySelector('.cursor') @@ -1667,12 +1667,13 @@ describe "TextEditorComponent", -> component.measureHeightAndWidth() nextAnimationFrame() - atom.themes.applyStylesheet "test", """ + atom.styles.addStyleSheet """ ::-webkit-scrollbar { width: 8px; height: 8px; } - """ + """, context: 'atom-text-editor' + nextAnimationFrame() scrollbarCornerNode = componentNode.querySelector('.scrollbar-corner') diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 8b0f71a4c..037c96e65 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -175,14 +175,14 @@ TextEditorComponent = React.createClass @setScrollSensitivity(atom.config.get('editor.scrollSensitivity')) componentDidMount: -> - {editor} = @props + {editor, stylesElement} = @props @observeEditor() @listenForDOMEvents() - @subscribe atom.themes.onDidAddStylesheet @onStylesheetsChanged - @subscribe atom.themes.onDidUpdateStylesheet @onStylesheetsChanged - @subscribe atom.themes.onDidRemoveStylesheet @onStylesheetsChanged + @subscribe stylesElement.onDidAddStyleElement @onStylesheetsChanged + @subscribe stylesElement.onDidUpdateStyleElement @onStylesheetsChanged + @subscribe stylesElement.onDidRemoveStyleElement @onStylesheetsChanged unless atom.themes.isInitialLoadComplete() @subscribe atom.themes.onDidReloadAll @onStylesheetsChanged @subscribe scrollbarStyle.changes, @refreshScrollbars @@ -622,11 +622,11 @@ TextEditorComponent = React.createClass else editor.setSelectedScreenRange([tailPosition, [dragRow + 1, 0]], preserveFolds: true) - onStylesheetsChanged: (stylesheet) -> + onStylesheetsChanged: (styleElement) -> return unless @performedInitialMeasurement return unless atom.themes.isInitialLoadComplete() - @refreshScrollbars() if not stylesheet? or @containsScrollbarSelector(stylesheet) + @refreshScrollbars() if not styleElement? or @containsScrollbarSelector(styleElement.sheet) @sampleFontStyling() @sampleBackgroundColors() @remeasureCharacterWidths() diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index d91345eee..7bfb89e67 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -24,12 +24,12 @@ class TextEditorElement extends HTMLElement initializeContent: (attributes) -> @classList.add('editor') @setAttribute('tabindex', -1) - @shadowRoot = @createShadowRoot() + @createShadowRoot() - stylesElement = document.createElement('atom-styles') - stylesElement.setAttribute('context', 'atom-text-editor') - stylesElement.initialize() - @shadowRoot.appendChild(stylesElement) + @stylesElement = document.createElement('atom-styles') + @stylesElement.setAttribute('context', 'atom-text-editor') + @stylesElement.initialize() + @shadowRoot.appendChild(@stylesElement) @rootElement = document.createElement('div') @rootElement.classList.add('editor', 'editor-colors') @@ -76,6 +76,7 @@ class TextEditorElement extends HTMLElement @componentDescriptor ?= TextEditorComponent( hostElement: this rootElement: @rootElement + stylesElement: @stylesElement editor: @model mini: @model.mini lineOverdrawMargin: @lineOverdrawMargin From 158bbef38fda35b82a45599e0eb885c0515de7e8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 12:42:42 -0600 Subject: [PATCH 22/68] Account for shadow dom when asserting active element --- spec/text-editor-component-spec.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 9512f7fdb..9d33c47da 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -1538,7 +1538,8 @@ describe "TextEditorComponent", -> it "transfers focus to the hidden input", -> expect(document.activeElement).toBe document.body componentNode.focus() - expect(document.activeElement).toBe inputNode + expect(document.activeElement).toBe wrapperNode + expect(wrapperNode.shadowRoot.activeElement).toBe inputNode it "adds the 'is-focused' class to the editor when the hidden input is focused", -> expect(document.activeElement).toBe document.body From fa733c85adf507be007a84c9a994c739ff09cab9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 12:43:41 -0600 Subject: [PATCH 23/68] =?UTF-8?q?Fix=20setEditorHeightInLines=20shim=20now?= =?UTF-8?q?=20that=20we=E2=80=99ve=20dropped=20.react=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/spec-helper.coffee | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 6f84c2eab..663b10ae1 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -338,12 +338,8 @@ window.setEditorWidthInChars = (editorView, widthInChars, charWidth=editorView.c $(window).trigger 'resize' # update width of editor view's on-screen lines window.setEditorHeightInLines = (editorView, heightInLines, lineHeight=editorView.lineHeight) -> - if editorView.hasClass('react') - editorView.height(editorView.getEditor().getLineHeightInPixels() * heightInLines) - editorView.component?.measureHeightAndWidth() - else - editorView.height(lineHeight * heightInLines + editorView.renderedLines.position().top) - $(window).trigger 'resize' # update editor view's on-screen lines + editorView.height(editorView.getEditor().getLineHeightInPixels() * heightInLines) + editorView.component?.measureHeightAndWidth() $.fn.resultOfTrigger = (type) -> event = $.Event(type) From 2321aa2bee8b59421c9caf2bffd28d825bb50d67 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 12:44:54 -0600 Subject: [PATCH 24/68] Get SpacePen outlet shims from inside shadow DOM --- src/text-editor-view.coffee | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index 2e7365622..0a36ffadf 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -73,13 +73,16 @@ class TextEditorView extends View setModel: (@model) -> @editor = @model - @scrollView = @find('.scroll-view') - @underlayer = @find('.highlights').addClass('underlayer') - @overlayer = @find('.lines').addClass('overlayer') - @hiddenInput = @.find('.hidden-input') + $root = $(@element.rootElement) + + @scrollView = $root.find('.scroll-view') + @underlayer = $root.find('.highlights').addClass('underlayer') + @overlayer = $root.find('.lines').addClass('overlayer') + @hiddenInput = $root.find('.hidden-input') + @lines = $root.find('.lines') @subscribe atom.config.observe 'editor.showLineNumbers', => - @gutter = @find('.gutter') + @gutter = $root.find('.gutter') @gutter.removeClassFromAllLines = (klass) => deprecate('Use decorations instead: http://blog.atom.io/2014/07/24/decorations.html') @@ -164,7 +167,7 @@ class TextEditorView extends View appendToLinesView: (view) -> view.css('position', 'absolute') view.css('z-index', 1) - @find('.lines').prepend(view) + @lines.prepend(view) unmountComponent: -> React.unmountComponentAtNode(@element) if @component.isMounted() From 2d3d64f3993634d2124b8dda6a621bcb75e857f9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 13:16:57 -0600 Subject: [PATCH 25/68] Call reloadStylesheets instead of reloadStylesheet in spec --- spec/package-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/package-spec.coffee b/spec/package-spec.coffee index 89b8225c0..970d9b64f 100644 --- a/spec/package-spec.coffee +++ b/spec/package-spec.coffee @@ -92,7 +92,7 @@ describe "Package", -> it "reloads without readding to the stylesheets list", -> expect(theme.getStylesheetPaths().length).toBe 3 - theme.reloadStylesheet(theme.getStylesheetPaths()[0]) + theme.reloadStylesheets() expect(theme.getStylesheetPaths().length).toBe 3 describe "events", -> From cdb62812d21128459795b812c14980b9c35cac8b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 13:17:32 -0600 Subject: [PATCH 26/68] =?UTF-8?q?Don=E2=80=99t=20use=20syntax=20themes=20i?= =?UTF-8?q?n=20spec=20because=20they=20are=20inserted=20in=20shadow=20DOM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/theme-manager-spec.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index b03984534..eee89b721 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -85,7 +85,7 @@ describe "ThemeManager", -> runs -> reloadHandler.reset() expect($('style.theme')).toHaveLength 0 - atom.config.set('core.themes', ['atom-dark-syntax']) + atom.config.set('core.themes', ['atom-dark-ui']) waitsFor -> reloadHandler.callCount == 1 @@ -93,8 +93,8 @@ describe "ThemeManager", -> runs -> reloadHandler.reset() expect($('style[group=theme]')).toHaveLength 2 - expect($('style[group=theme]:eq(1)').attr('source-path')).toMatch /atom-dark-syntax/ - atom.config.set('core.themes', ['atom-light-syntax', 'atom-dark-syntax']) + expect($('style[group=theme]:eq(1)').attr('source-path')).toMatch /atom-dark-ui/ + atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-ui']) waitsFor -> reloadHandler.callCount == 1 @@ -102,8 +102,8 @@ describe "ThemeManager", -> runs -> reloadHandler.reset() expect($('style[group=theme]')).toHaveLength 2 - expect($('style[group=theme]:eq(0)').attr('source-path')).toMatch /atom-dark-syntax/ - expect($('style[group=theme]:eq(1)').attr('source-path')).toMatch /atom-light-syntax/ + expect($('style[group=theme]:eq(0)').attr('source-path')).toMatch /atom-dark-ui/ + expect($('style[group=theme]:eq(1)').attr('source-path')).toMatch /atom-light-ui/ atom.config.set('core.themes', []) waitsFor -> From c11675dca12d91dfcb626d68844863ee1ccf0b8b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 13:17:53 -0600 Subject: [PATCH 27/68] =?UTF-8?q?Don=E2=80=99t=20recycle=20the=20same=20co?= =?UTF-8?q?mposite=20disposable=20for=20stylesheet=20activation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/package.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/package.coffee b/src/package.coffee index 77a8a775f..c465a3cb8 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -50,6 +50,7 @@ class Package keymaps: null menus: null stylesheets: null + stylesheetDisposables: null grammars: null scopedProperties: null mainModulePath: null @@ -62,7 +63,6 @@ class Package constructor: (@path, @metadata) -> @emitter = new Emitter - @stylesheetDisposables = new CompositeDisposable @metadata ?= Package.loadMetadata(@path) @bundledPackage = Package.isBundledPackagePath(@path) @name = @metadata?.name ? path.basename(@path) @@ -178,8 +178,9 @@ class Package context = 'atom-text-editor' if @metadata.theme is 'syntax' group = @getStylesheetType() + @stylesheetDisposables = new CompositeDisposable for [sourcePath, source] in @stylesheets - @stylesheetDisposables.add(atom.styles.addStyleSheet(source, {sourcePath, sourcePath, context})) + @stylesheetDisposables.add(atom.styles.addStyleSheet(source, {sourcePath, group, context})) @stylesheetsActivated = true activateResources: -> @@ -322,7 +323,7 @@ class Package deactivateResources: -> grammar.deactivate() for grammar in @grammars scopedProperties.deactivate() for scopedProperties in @scopedProperties - atom.themes.removeStylesheet(stylesheetPath) for [stylesheetPath] in @stylesheets + @stylesheetDisposables?.dispose() @activationDisposables?.dispose() @stylesheetsActivated = false @grammarsActivated = false From 42fc54f716138d4a9df49d28b35478874aa69e2e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 13:18:15 -0600 Subject: [PATCH 28/68] Protect against stylesheets changing while detached --- src/text-editor-component.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 037c96e65..7c852aa78 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -626,7 +626,7 @@ TextEditorComponent = React.createClass return unless @performedInitialMeasurement return unless atom.themes.isInitialLoadComplete() - @refreshScrollbars() if not styleElement? or @containsScrollbarSelector(styleElement.sheet) + @refreshScrollbars() if not styleElement.sheet? or @containsScrollbarSelector(styleElement.sheet) @sampleFontStyling() @sampleBackgroundColors() @remeasureCharacterWidths() From e8d70583837fb0525253e81e92d293cce1c3cf17 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 13:18:29 -0600 Subject: [PATCH 29/68] =?UTF-8?q?Go=20back=20to=20the=20plain=20=E2=80=9Ct?= =?UTF-8?q?heme=E2=80=9D=20group=20for=20theme=20stylesheets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/theme-package.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme-package.coffee b/src/theme-package.coffee index 8904ee43f..d0be0a0ef 100644 --- a/src/theme-package.coffee +++ b/src/theme-package.coffee @@ -5,7 +5,7 @@ module.exports = class ThemePackage extends Package getType: -> 'theme' - getStylesheetType: -> "#{@metadata.theme}-theme" + getStylesheetType: -> "theme" enable: -> atom.config.unshiftAtKeyPath('core.themes', @name) From 7badd9ba25ca3de7f5dd6972403425eb810e4a92 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 13:53:17 -0600 Subject: [PATCH 30/68] =?UTF-8?q?Don=E2=80=99t=20rely=20on=20:focus=20sele?= =?UTF-8?q?ctor=20for=20toHaveFocus=20matcher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :focus doesn’t work properly when focus is inside the shadow DOM of an element, but document.activeElement does. --- spec/spec-helper.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 663b10ae1..0f313433a 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -219,7 +219,7 @@ addCustomMatchers = (spec) -> @message = -> return "Expected element '" + @actual + "' or its descendants" + notText + " to have focus." element = @actual element = element.get(0) if element.jquery - element.webkitMatchesSelector(":focus") or element.querySelector(":focus") + element is document.activeElement or element.contains(document.activeElement) toShow: -> notText = if @isNot then " not" else "" From 58744f6b7b973adf1ff81051747d756a7ba0b5c1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 13:53:39 -0600 Subject: [PATCH 31/68] Account for shadow DOM when asserting on focus --- spec/text-editor-element-spec.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-element-spec.coffee b/spec/text-editor-element-spec.coffee index ae213bc01..4dd875527 100644 --- a/spec/text-editor-element-spec.coffee +++ b/spec/text-editor-element-spec.coffee @@ -33,4 +33,5 @@ describe "TextEditorElement", -> expect(focusoutCalled).toBe false expect(blurCalled).toBe false expect(element.hasFocus()).toBe true - expect(element.querySelector('input')).toBe document.activeElement + expect(document.activeElement).toBe element + expect(element.shadowRoot.activeElement).toBe element.shadowRoot.querySelector('input') From 1f777addd9243c94db185d293ca29347a9d81fe3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 13:55:52 -0600 Subject: [PATCH 32/68] Sample font styling when font config values change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We previously could do it whenever stylesheets changed, but these values end up getting assigned to the global stylesheet for cascading reasons and we’re only watching the local stylesheet. We poll the host elements DOM properties, but forcing a sync poll when the config values change makes behavior synchronous for specs and more responsive when changing these values. --- src/text-editor-component.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 7c852aa78..53c674713 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -416,6 +416,9 @@ TextEditorComponent = React.createClass observeConfig: -> @subscribe atom.config.observe 'editor.useHardwareAcceleration', @setUseHardwareAcceleration + @subscribe atom.config.onDidChange 'editor.fontSize', @sampleFontStyling + @subscribe atom.config.onDidChange 'editor.fontFamily', @sampleFontStyling + @subscribe atom.config.onDidChange 'editor.lineHeight', @sampleFontStyling onGrammarChanged: -> {editor} = @props From 3b455c00d31573bee5024a668a47906ad0d01883 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 13:56:14 -0600 Subject: [PATCH 33/68] Proxy TextEditorView::find calls to the root inside the shadow DOM --- src/text-editor-view.coffee | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index 0a36ffadf..40cc44735 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -73,16 +73,15 @@ class TextEditorView extends View setModel: (@model) -> @editor = @model - $root = $(@element.rootElement) + @root = $(@element.rootElement) - @scrollView = $root.find('.scroll-view') - @underlayer = $root.find('.highlights').addClass('underlayer') - @overlayer = $root.find('.lines').addClass('overlayer') - @hiddenInput = $root.find('.hidden-input') - @lines = $root.find('.lines') + @scrollView = @root.find('.scroll-view') + @underlayer = @root.find('.highlights').addClass('underlayer') + @overlayer = @root.find('.lines').addClass('overlayer') + @hiddenInput = @root.find('.hidden-input') @subscribe atom.config.observe 'editor.showLineNumbers', => - @gutter = $root.find('.gutter') + @gutter = @root.find('.gutter') @gutter.removeClassFromAllLines = (klass) => deprecate('Use decorations instead: http://blog.atom.io/2014/07/24/decorations.html') @@ -98,6 +97,9 @@ class TextEditorView extends View lines.addClass(klass) lines.length > 0 + find: -> + @root.find.apply(@root, arguments) + # Public: Get the underlying editor model for this view. # # Returns an {TextEditor} From 866f2d9a76c53d7775734d01fa5954bda85f370e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 13:57:08 -0600 Subject: [PATCH 34/68] Fix appendToLinesView --- src/text-editor-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index 40cc44735..84a757a97 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -169,7 +169,7 @@ class TextEditorView extends View appendToLinesView: (view) -> view.css('position', 'absolute') view.css('z-index', 1) - @lines.prepend(view) + @find('.lines').prepend(view) unmountComponent: -> React.unmountComponentAtNode(@element) if @component.isMounted() From c4cfac56154090ab92b966a0f2964025b90c4cf5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 15:03:37 -0600 Subject: [PATCH 35/68] Use event capture for pane focus/blur events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Focusin/focusout don’t seem to bubble properly across shadow DOM boundaries, so capturing is a more reliable alternative. --- src/pane-element.coffee | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pane-element.coffee b/src/pane-element.coffee index e44eaea6f..2c3708523 100644 --- a/src/pane-element.coffee +++ b/src/pane-element.coffee @@ -28,9 +28,17 @@ class PaneElement extends HTMLElement @itemViews.setAttribute 'class', 'item-views' subscribeToDOMEvents: -> - @addEventListener 'focusin', => @model.focus() - @addEventListener 'focusout', => @model.blur() - @addEventListener 'focus', => @getActiveView()?.focus() + handleFocus = (event) => + @model.focus() + if event.target is this and view = @getActiveView() + view.focus() + event.stopPropagation() + + handleBlur = (event) => + @model.blur() unless @contains(event.relatedTarget) + + @addEventListener 'focus', handleFocus, true + @addEventListener 'blur', handleBlur, true createSpacePenShim: -> @__spacePenView = new PaneView(this) From 720290878056f55284afe12d216aaa5dac2be2b9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 15:19:23 -0600 Subject: [PATCH 36/68] Split editor stylesheet into light and shadow DOM versions This prevents the need for a :host pseudo-class in the editor CSS which breaks linting. It also fits selectors targeting the host element in a more intuitive spot in the cascade. --- src/theme-manager.coffee | 2 +- static/atom.less | 1 + static/text-editor-light.less | 13 +++++++++++++ static/{editor.less => text-editor-shadow.less} | 12 ------------ 4 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 static/text-editor-light.less rename static/{editor.less => text-editor-shadow.less} (93%) diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 4919fbef2..a0293f2cf 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -249,7 +249,7 @@ class ThemeManager if nativeStylesheetPath = fs.resolveOnLoadPath(process.platform, ['css', 'less']) @requireStylesheet(nativeStylesheetPath) - textEditorStylesPath = path.join(@resourcePath, 'static', 'editor.less') + textEditorStylesPath = path.join(@resourcePath, 'static', 'text-editor-shadow.less') atom.styles.addStyleSheet(@loadLessStylesheet(textEditorStylesPath), sourcePath: textEditorStylesPath, context: 'atom-text-editor') stylesheetElementForId: (id) -> diff --git a/static/atom.less b/static/atom.less index 975905ad1..3f9aad4c9 100644 --- a/static/atom.less +++ b/static/atom.less @@ -22,6 +22,7 @@ @import "popover-list"; @import "messages"; @import "markdown"; +@import "text-editor-light"; @import "select-list"; @import "syntax"; @import "utilities"; diff --git a/static/text-editor-light.less b/static/text-editor-light.less new file mode 100644 index 000000000..7021c4978 --- /dev/null +++ b/static/text-editor-light.less @@ -0,0 +1,13 @@ +@import "ui-variables"; + +atom-text-editor { + display: block; + font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier; + line-height: 1.3; +} + +atom-text-editor.mini { + font-size: @input-font-size; + line-height: @component-line-height; + max-height: @component-line-height + 2; // +2 for borders +} diff --git a/static/editor.less b/static/text-editor-shadow.less similarity index 93% rename from static/editor.less rename to static/text-editor-shadow.less index 6cf487e9b..8187bdf93 100644 --- a/static/editor.less +++ b/static/text-editor-shadow.less @@ -2,12 +2,6 @@ @import "octicon-utf-codes"; @import "octicon-mixins"; -:host { - display: block; - font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier; - line-height: 1.3; -} - .editor, .editor-contents { height: 100%; width: 100%; @@ -92,12 +86,6 @@ color: @text-color-subtle; } -:host(.mini) { - font-size: @input-font-size; - line-height: @component-line-height; - max-height: @component-line-height + 2; // +2 for borders -} - .editor { z-index: 0; } From ab846a24958cfed5140cfb5e4a89cc5637b11856 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 17:31:15 -0600 Subject: [PATCH 37/68] Put views appended via appendToLinesView in the light DOM This adds an insertion point to the lines div via a tag, allowing immediate children of the editor tag to be positioned relative to the lines div but still be styled via global CSS. --- src/lines-component.coffee | 4 ++++ src/text-editor-view.coffee | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 865b4b19a..f75df5bc8 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -57,6 +57,10 @@ LinesComponent = React.createClass @lineIdsByScreenRow = {} @renderedDecorationsByLineId = {} + componentDidMount: -> + node = @getDOMNode() + node.appendChild(document.createElement('content')) + shouldComponentUpdate: (newProps) -> return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'lineDecorations', 'highlightDecorations', 'lineHeightInPixels', 'defaultCharWidth', diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index 84a757a97..7c2963517 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -169,7 +169,7 @@ class TextEditorView extends View appendToLinesView: (view) -> view.css('position', 'absolute') view.css('z-index', 1) - @find('.lines').prepend(view) + @append(view) unmountComponent: -> React.unmountComponentAtNode(@element) if @component.isMounted() From 8aeabe5fe5cae7c15fa1335bda1b2897c0e15695 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 18:40:45 -0600 Subject: [PATCH 38/68] =?UTF-8?q?Listen=20for=20=E2=80=98blur=E2=80=99=20o?= =?UTF-8?q?n=20mini=20editor=20of=20select=20list=20rather=20than=20?= =?UTF-8?q?=E2=80=98focusout=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The focusout handler on the mini editor’s hidden input wasn’t being triggered, but we can listen for blur directly on the editor now that the shadow DOM abstracts the focus. --- src/select-list-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/select-list-view.coffee b/src/select-list-view.coffee index 1fdc60e4a..ec56b88cf 100644 --- a/src/select-list-view.coffee +++ b/src/select-list-view.coffee @@ -57,7 +57,7 @@ class SelectListView extends View initialize: -> @filterEditorView.getEditor().getBuffer().onDidChange => @schedulePopulateList() - @filterEditorView.hiddenInput.on 'focusout', => + @filterEditorView.on 'blur', => @cancel() unless @cancelling # This prevents the focusout event from firing on the filter editor view From 62c0db11eeb8748e2f4ee483c940ef6f403c675b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 19:10:28 -0600 Subject: [PATCH 39/68] =?UTF-8?q?Define=20enter=20as=20=E2=80=98core:confi?= =?UTF-8?q?rm=E2=80=99=20in=20select-list=20mini=20editors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We were leaving this to packages to define, and they were getting it wrong by selecting into the ‘input’ which is now in shadow. --- keymaps/base.cson | 3 +++ 1 file changed, 3 insertions(+) diff --git a/keymaps/base.cson b/keymaps/base.cson index 11cbe5977..f187cf2ed 100644 --- a/keymaps/base.cson +++ b/keymaps/base.cson @@ -15,6 +15,9 @@ 'shift-tab': 'editor:outdent-selected-rows' 'ctrl-K': 'editor:delete-line' +'.select-list atom-text-editor.mini': + 'enter': 'core:confirm' + '.tool-panel.panel-left, .tool-panel.panel-right': 'escape': 'tool-panel:unfocus' From eb19989ecd2fd4918d41d9521e77991db9f6c71b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 19:55:42 -0600 Subject: [PATCH 40/68] Handle focus at the host element level Detecting focus and blur at the level of the input is creating problems when we blur and then immediately refocus. This is simpler. --- src/input-component.coffee | 8 -------- src/text-editor-component.coffee | 18 +++++++----------- src/text-editor-element.coffee | 3 ++- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/input-component.coffee b/src/input-component.coffee index cacf16266..776be6c14 100644 --- a/src/input-component.coffee +++ b/src/input-component.coffee @@ -18,8 +18,6 @@ InputComponent = React.createClass node = @getDOMNode() node.addEventListener 'paste', @onPaste node.addEventListener 'compositionupdate', @onCompositionUpdate - node.addEventListener 'focus', @onFocus - node.addEventListener 'blur', @onBlur # Don't let text accumulate in the input forever, but avoid excessive reflows componentDidUpdate: -> @@ -37,11 +35,5 @@ InputComponent = React.createClass onPaste: (e) -> e.preventDefault() - onFocus: -> - @props.onFocus?() - - onBlur: -> - @props.onBlur?() - focus: -> @getDOMNode().focus() diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 53c674713..022f5fbd5 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -107,8 +107,6 @@ TextEditorComponent = React.createClass ref: 'input' className: 'hidden-input' style: hiddenInputStyle - onFocus: @onInputFocused - onBlur: @onInputBlurred LinesComponent { ref: 'lines', @@ -378,7 +376,6 @@ TextEditorComponent = React.createClass listenForDOMEvents: -> node = @getDOMNode() node.addEventListener 'mousewheel', @onMouseWheel - node.addEventListener 'focus', @onFocus # For some reason, React's built in focus events seem to bubble node.addEventListener 'textInput', @onTextInput @refs.scrollView.getDOMNode().addEventListener 'mousedown', @onMouseDown @@ -432,8 +429,13 @@ TextEditorComponent = React.createClass subscriptions.add atom.config.observe scopeDescriptor, 'editor.showLineNumbers', @setShowLineNumbers subscriptions.add atom.config.observe scopeDescriptor, 'editor.scrollSensitivity', @setScrollSensitivity - onFocus: -> - @refs.input.focus() if @isMounted() + focused: -> + if @isMounted() + @setState(focused: true) + @refs.input.focus() + + blurred: -> + @setState(focused: false) onTextInput: (event) -> event.stopPropagation() @@ -456,12 +458,6 @@ TextEditorComponent = React.createClass inputNode.value = event.data if editor.insertText(event.data) - onInputFocused: -> - @setState(focused: true) - - onInputBlurred: -> - @setState(focused: false) - onVerticalScroll: (scrollTop) -> {editor} = @props diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 7bfb89e67..da9913944 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -91,7 +91,7 @@ class TextEditorElement extends HTMLElement focused: -> if @component? - @component.onFocus() + @component.focused() else @focusOnAttach = true @@ -99,6 +99,7 @@ class TextEditorElement extends HTMLElement event.stopImmediatePropagation() if @contains(event.relatedTarget) blurred: (event) -> + @component.blurred() event.stopImmediatePropagation() if @contains(event.relatedTarget) addGrammarScopeAttribute: -> From bda14292939f68ac83a982725e12d52bfe181801 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 20:04:47 -0600 Subject: [PATCH 41/68] =?UTF-8?q?Trigger=20=E2=80=98blur=E2=80=99=20on=20s?= =?UTF-8?q?elect=20list=20editor=20instead=20of=20=E2=80=98focusut?= =?UTF-8?q?=E2=80=99=20on=20its=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/select-list-view-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/select-list-view-spec.coffee b/spec/select-list-view-spec.coffee index b59f76faa..db6a1b8c8 100644 --- a/spec/select-list-view-spec.coffee +++ b/spec/select-list-view-spec.coffee @@ -184,7 +184,7 @@ describe "SelectListView", -> describe "when the mini editor loses focus", -> it "triggers the cancelled hook and detaches the select list", -> spyOn(selectList, 'detach') - filterEditorView.hiddenInput.trigger 'focusout' + filterEditorView.trigger 'blur' expect(selectList.cancelled).toHaveBeenCalled() expect(selectList.detach).toHaveBeenCalled() From c64a4b7ca96c6cc3075daaee4e85eab02d626337 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Oct 2014 20:19:28 -0600 Subject: [PATCH 42/68] Fallback to light DOM in TextEditorView::find if nothing found in shadow --- src/text-editor-view.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index 7c2963517..1ca437b71 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -98,7 +98,11 @@ class TextEditorView extends View lines.length > 0 find: -> - @root.find.apply(@root, arguments) + shadowResult = @root.find.apply(@root, arguments) + if shadowResult.length > 0 + shadowResult + else + super # Public: Get the underlying editor model for this view. # From 5e8655fa60626af3c83a8ed5699af5b60af31f7e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 17 Oct 2014 18:37:28 -0600 Subject: [PATCH 43/68] =?UTF-8?q?Don=E2=80=99t=20use=20:focus=20selector?= =?UTF-8?q?=20to=20store=20previously=20focused=20element?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/select-list-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/select-list-view.coffee b/src/select-list-view.coffee index ec56b88cf..dc494cb00 100644 --- a/src/select-list-view.coffee +++ b/src/select-list-view.coffee @@ -254,7 +254,7 @@ class SelectListView extends View # Extended: Store the currently focused element. This element will be given # back focus when {::cancel} is called. storeFocusedElement: -> - @previouslyFocusedElement = $(':focus') + @previouslyFocusedElement = $(document.activeElement) ### Section: Private From 2e46cf9b8db6f7a09de80968be4a69d9299ceec9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 20 Oct 2014 11:49:03 -0600 Subject: [PATCH 44/68] Refefine $.fn.position in terms of offsetTop/Left to work w/ shadow DOM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default implementation of position seems to barf when things are in the shadow DOM. This seems to be a suitable replacement that doesn’t. --- src/space-pen-extensions.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/space-pen-extensions.coffee b/src/space-pen-extensions.coffee index e295de872..08bb13064 100644 --- a/src/space-pen-extensions.coffee +++ b/src/space-pen-extensions.coffee @@ -78,6 +78,11 @@ jQuery.event.remove = (elem, types, originalHandler, selector, mappedTypes) -> handler = HandlersByOriginalHandler.get(originalHandler) ? originalHandler JQueryEventRemove(elem, types, handler, selector, mappedTypes, RemoveEventListener if atom?.commands?) +jQuery.fn.position = -> + top = @[0].offsetTop + left = @[0].offsetLeft + {top, left} + tooltipDefaults = delay: show: 1000 From 5cc243ec11c921f47626dfb65f0c84cbf1863965 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 20 Oct 2014 11:49:23 -0600 Subject: [PATCH 45/68] Inject both underlayer and overlayer via shadow DOM insertion points --- src/highlights-component.coffee | 5 +++++ src/lines-component.coffee | 5 +++-- src/text-editor-view.coffee | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/highlights-component.coffee b/src/highlights-component.coffee index dd0749ffd..cfd42d01e 100644 --- a/src/highlights-component.coffee +++ b/src/highlights-component.coffee @@ -21,5 +21,10 @@ HighlightsComponent = React.createClass highlightComponents + componentDidMount: -> + insertionPoint = document.createElement('content') + insertionPoint.setAttribute('select', '.underlayer') + @getDOMNode().appendChild(insertionPoint) + shouldComponentUpdate: (newProps) -> not isEqualForProperties(newProps, @props, 'highlightDecorations', 'lineHeightInPixels', 'defaultCharWidth', 'scopedCharacterWidthsChangeCount') diff --git a/src/lines-component.coffee b/src/lines-component.coffee index f75df5bc8..a217263e3 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -58,8 +58,9 @@ LinesComponent = React.createClass @renderedDecorationsByLineId = {} componentDidMount: -> - node = @getDOMNode() - node.appendChild(document.createElement('content')) + insertionPoint = document.createElement('content') + insertionPoint.setAttribute('select', '.overlayer') + @getDOMNode().appendChild(insertionPoint) shouldComponentUpdate: (newProps) -> return true unless isEqualForProperties(newProps, @props, diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index 1ca437b71..ff3ce6485 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -76,8 +76,8 @@ class TextEditorView extends View @root = $(@element.rootElement) @scrollView = @root.find('.scroll-view') - @underlayer = @root.find('.highlights').addClass('underlayer') - @overlayer = @root.find('.lines').addClass('overlayer') + @underlayer = $("
").appendTo(this) + @overlayer = $("
").appendTo(this) @hiddenInput = @root.find('.hidden-input') @subscribe atom.config.observe 'editor.showLineNumbers', => @@ -173,7 +173,7 @@ class TextEditorView extends View appendToLinesView: (view) -> view.css('position', 'absolute') view.css('z-index', 1) - @append(view) + @overlayer.append(view) unmountComponent: -> React.unmountComponentAtNode(@element) if @component.isMounted() From 7863db480ec2890dd6c76daaf928e0c4417ff1a4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 20 Oct 2014 17:03:16 -0600 Subject: [PATCH 46/68] Override jQuery.contains instead of jQuery.fn.position MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turns out the problems with position inside the shadow DOM are due to the fact that elements in the light DOM don’t claim to contain elements from a shadow DOM, causing jQuery.fn.offset to bail out early and misreport positions inside the editor. --- src/space-pen-extensions.coffee | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/space-pen-extensions.coffee b/src/space-pen-extensions.coffee index 08bb13064..5d72851ea 100644 --- a/src/space-pen-extensions.coffee +++ b/src/space-pen-extensions.coffee @@ -78,10 +78,17 @@ jQuery.event.remove = (elem, types, originalHandler, selector, mappedTypes) -> handler = HandlersByOriginalHandler.get(originalHandler) ? originalHandler JQueryEventRemove(elem, types, handler, selector, mappedTypes, RemoveEventListener if atom?.commands?) -jQuery.fn.position = -> - top = @[0].offsetTop - left = @[0].offsetLeft - {top, left} +JQueryContains = jQuery.contains + +jQuery.contains = (a, b) -> + shadowRoot = null + currentNode = b + while currentNode + if currentNode instanceof ShadowRoot and a.contains(currentNode.host) + return true + currentNode = currentNode.parentNode + + JQueryContains.call(this, a, b) tooltipDefaults = delay: From adaf1829da590aebd55922eea4299fa714f3b114 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 21 Oct 2014 09:43:24 -0600 Subject: [PATCH 47/68] Determine focus using document.activeElement instead of component state --- src/text-editor-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index ff3ce6485..b62535fed 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -116,7 +116,7 @@ class TextEditorView extends View Object.defineProperty @::, 'firstRenderedScreenRow', get: -> @component.getRenderedRowRange()[0] Object.defineProperty @::, 'lastRenderedScreenRow', get: -> @component.getRenderedRowRange()[1] Object.defineProperty @::, 'active', get: -> @is(@getPaneView()?.activeView) - Object.defineProperty @::, 'isFocused', get: -> @component?.state.focused + Object.defineProperty @::, 'isFocused', get: -> @element is document.activeElement Object.defineProperty @::, 'mini', get: -> @component?.props.mini Object.defineProperty @::, 'component', get: -> @element?.component From cf3f1aa2eba2cf754ec559765194ee963ea1cd82 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 21 Oct 2014 10:17:57 -0600 Subject: [PATCH 48/68] =?UTF-8?q?Don=E2=80=99t=20handle=20text=20editor=20?= =?UTF-8?q?focus=20when=20it=20already=20has=20focus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/text-editor-element.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index da9913944..df45c0455 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -89,7 +89,11 @@ class TextEditorElement extends HTMLElement React.unmountComponentAtNode(this) @component = null - focused: -> + focused: (event) -> + if @hasFocus() + event.stopPropagation() + return + if @component? @component.focused() else From dd17e8f01881b1356b04e48566f0026b60e4d0bc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 21 Oct 2014 10:19:41 -0600 Subject: [PATCH 49/68] Replace focusout event handlers on hiddenInput shim with blur handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The focusout event doesn’t seem to work for elements in the shadow DOM. Other people seem to share this experience: https://code.google.com/p/chromium/issues/detail?id=378163#c7 --- src/text-editor-view.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index b62535fed..89540bf9c 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -80,6 +80,10 @@ class TextEditorView extends View @overlayer = $("
").appendTo(this) @hiddenInput = @root.find('.hidden-input') + @hiddenInput.on = (args...) => + args[0] = 'blur' if args[0] is 'focusout' + $::on.apply(this, args) + @subscribe atom.config.observe 'editor.showLineNumbers', => @gutter = @root.find('.gutter') From 6f3c53a17aee5d266a1e7bfa63510d636d66e97e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 21 Oct 2014 10:30:39 -0600 Subject: [PATCH 50/68] Only cancel focus events if the editor is or contains the related target --- src/text-editor-element.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index df45c0455..8ad062ab8 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -90,7 +90,7 @@ class TextEditorElement extends HTMLElement @component = null focused: (event) -> - if @hasFocus() + if @contains(event.relatedTarget) or this is event.relatedTarget event.stopPropagation() return From 84d1101903ffeb2ca377467048e579cdaff9d2bc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 21 Oct 2014 13:43:36 -0600 Subject: [PATCH 51/68] Upgrade package-generator to fix specs with shadow DOM --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 154a53ca1..06fc5801b 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "markdown-preview": "0.107.0", "metrics": "0.36.0", "open-on-github": "0.30.0", - "package-generator": "0.31.0", + "package-generator": "0.32.0", "release-notes": "0.36.0", "settings-view": "0.154.0", "snippets": "0.56.0", From d060ecdc243d77c29da6e190e7d14e5af898b711 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 21 Oct 2014 15:24:35 -0600 Subject: [PATCH 52/68] Assign package stylesheet context based on double-extension in file name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If stylesheet files are named with 2 extensions, the first extension is used as the context argument when the package’s stylesheets are loaded. This allows people to target the text editor by naming their stylesheet `index.atom-text-editor.less`. --- .../stylesheets/4.test-context.css | 1 + spec/package-manager-spec.coffee | 5 +++++ src/package.coffee | 7 ++++++- 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 spec/fixtures/packages/package-with-stylesheets/stylesheets/4.test-context.css diff --git a/spec/fixtures/packages/package-with-stylesheets/stylesheets/4.test-context.css b/spec/fixtures/packages/package-with-stylesheets/stylesheets/4.test-context.css new file mode 100644 index 000000000..2cf54c986 --- /dev/null +++ b/spec/fixtures/packages/package-with-stylesheets/stylesheets/4.test-context.css @@ -0,0 +1 @@ +a { color: red } diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 2cacbfb1d..c79d36c18 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -298,6 +298,11 @@ describe "PackageManager", -> expect(atom.themes.stylesheetElementForId(three)).not.toBeNull() expect($('#jasmine-content').css('font-size')).toBe '3px' + it "assigns the stylesheet's context based on the filename", -> + atom.packages.activatePackage("package-with-stylesheets") + element = atom.styles.getStyleElements().find (element) -> element.context is 'test-context' + expect(element).toBeDefined() + describe "grammar loading", -> it "loads the package's grammars", -> waitsForPromise -> diff --git a/src/package.coffee b/src/package.coffee index c465a3cb8..79485da69 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -176,10 +176,15 @@ class Package activateStylesheets: -> return if @stylesheetsActivated - context = 'atom-text-editor' if @metadata.theme is 'syntax' + group = @getStylesheetType() @stylesheetDisposables = new CompositeDisposable for [sourcePath, source] in @stylesheets + if match = path.basename(sourcePath).match(/[^.]*\.([^.]*)\./) + context = match[1] + else if @metadata.theme is 'syntax' + context = 'atom-text-editor' + @stylesheetDisposables.add(atom.styles.addStyleSheet(source, {sourcePath, group, context})) @stylesheetsActivated = true From dd4e7d6921615f2654d9a0b713e4b3c3d466a62d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 22 Oct 2014 17:28:32 -0600 Subject: [PATCH 53/68] Wait for promise resolution on all calls to activatePackage This avoids a race condition where stylesheets would be added after all packages were deactivated and leak into the next spec. --- spec/package-manager-spec.coffee | 97 +++++++++++++++++++------------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index c79d36c18..d460591b7 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -48,9 +48,10 @@ describe "PackageManager", -> describe "when called multiple times", -> it "it only calls activate on the package once", -> spyOn(Package.prototype, 'activateNow').andCallThrough() - atom.packages.activatePackage('package-with-index') - atom.packages.activatePackage('package-with-index') - + waitsForPromise -> + atom.packages.activatePackage('package-with-index') + waitsForPromise -> + atom.packages.activatePackage('package-with-index') waitsForPromise -> atom.packages.activatePackage('package-with-index') @@ -182,8 +183,10 @@ describe "PackageManager", -> pack.mainModule.someNumber = 77 atom.packages.deactivatePackage("package-with-serialization") spyOn(pack.mainModule, 'activate').andCallThrough() - atom.packages.activatePackage("package-with-serialization") - expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77}) + waitsForPromise -> + atom.packages.activatePackage("package-with-serialization") + runs -> + expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77}) it "logs warning instead of throwing an exception if the package fails to load", -> atom.config.set("core.disabledPackages", []) @@ -202,11 +205,13 @@ describe "PackageManager", -> expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element2[0])).toHaveLength 0 expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element3[0])).toHaveLength 0 - atom.packages.activatePackage("package-with-keymaps") + waitsForPromise -> + atom.packages.activatePackage("package-with-keymaps") - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])[0].command).toBe "test-1" - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element2[0])[0].command).toBe "test-2" - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element3[0])).toHaveLength 0 + runs -> + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])[0].command).toBe "test-1" + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element2[0])[0].command).toBe "test-2" + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element3[0])).toHaveLength 0 describe "when the metadata contains a 'keymaps' manifest", -> it "loads only the keymaps specified by the manifest, in the specified order", -> @@ -215,11 +220,13 @@ describe "PackageManager", -> expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])).toHaveLength 0 - atom.packages.activatePackage("package-with-keymaps-manifest") + waitsForPromise -> + atom.packages.activatePackage("package-with-keymaps-manifest") - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])[0].command).toBe 'keymap-1' - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-n', target:element1[0])[0].command).toBe 'keymap-2' - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-y', target:element3[0])).toHaveLength 0 + runs -> + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])[0].command).toBe 'keymap-1' + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-n', target:element1[0])[0].command).toBe 'keymap-2' + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-y', target:element3[0])).toHaveLength 0 describe "menu loading", -> beforeEach -> @@ -232,14 +239,16 @@ describe "PackageManager", -> expect(atom.contextMenu.templateForElement(element)).toEqual [] - atom.packages.activatePackage("package-with-menus") + waitsForPromise -> + atom.packages.activatePackage("package-with-menus") - expect(atom.menu.template.length).toBe 2 - expect(atom.menu.template[0].label).toBe "Second to Last" - expect(atom.menu.template[1].label).toBe "Last" - expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 1" - expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 2" - expect(atom.contextMenu.templateForElement(element)[2].label).toBe "Menu item 3" + runs -> + expect(atom.menu.template.length).toBe 2 + expect(atom.menu.template[0].label).toBe "Second to Last" + expect(atom.menu.template[1].label).toBe "Last" + expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 1" + expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 2" + expect(atom.contextMenu.templateForElement(element)[2].label).toBe "Menu item 3" describe "when the metadata contains a 'menus' manifest", -> it "loads only the menus specified by the manifest, in the specified order", -> @@ -247,13 +256,15 @@ describe "PackageManager", -> expect(atom.contextMenu.templateForElement(element)).toEqual [] - atom.packages.activatePackage("package-with-menus-manifest") + waitsForPromise -> + atom.packages.activatePackage("package-with-menus-manifest") - expect(atom.menu.template[0].label).toBe "Second to Last" - expect(atom.menu.template[1].label).toBe "Last" - expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 2" - expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 1" - expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined() + runs -> + expect(atom.menu.template[0].label).toBe "Second to Last" + expect(atom.menu.template[1].label).toBe "Last" + expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 2" + expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 1" + expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined() describe "stylesheet loading", -> describe "when the metadata contains a 'stylesheets' manifest", -> @@ -270,12 +281,14 @@ describe "PackageManager", -> expect(atom.themes.stylesheetElementForId(two)).toBeNull() expect(atom.themes.stylesheetElementForId(three)).toBeNull() - atom.packages.activatePackage("package-with-stylesheets-manifest") + waitsForPromise -> + atom.packages.activatePackage("package-with-stylesheets-manifest") - expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(three)).toBeNull() - expect($('#jasmine-content').css('font-size')).toBe '1px' + runs -> + expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() + expect($('#jasmine-content').css('font-size')).toBe '1px' describe "when the metadata does not contain a 'stylesheets' manifest", -> it "loads all stylesheets from the stylesheets directory", -> @@ -292,16 +305,22 @@ describe "PackageManager", -> expect(atom.themes.stylesheetElementForId(two)).toBeNull() expect(atom.themes.stylesheetElementForId(three)).toBeNull() - atom.packages.activatePackage("package-with-stylesheets") - expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(three)).not.toBeNull() - expect($('#jasmine-content').css('font-size')).toBe '3px' + waitsForPromise -> + atom.packages.activatePackage("package-with-stylesheets") + + runs -> + expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(three)).not.toBeNull() + expect($('#jasmine-content').css('font-size')).toBe '3px' it "assigns the stylesheet's context based on the filename", -> - atom.packages.activatePackage("package-with-stylesheets") - element = atom.styles.getStyleElements().find (element) -> element.context is 'test-context' - expect(element).toBeDefined() + waitsForPromise -> + atom.packages.activatePackage("package-with-stylesheets") + + runs -> + element = atom.styles.getStyleElements().find (element) -> element.context is 'test-context' + expect(element).toBeDefined() describe "grammar loading", -> it "loads the package's grammars", -> From 497b4a4e24275c5f6bb7fa3c0cde997bcfaf268c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 23 Oct 2014 14:46:05 -0600 Subject: [PATCH 54/68] Toggle quotes back --- src/theme-package.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme-package.coffee b/src/theme-package.coffee index d0be0a0ef..3c03d818f 100644 --- a/src/theme-package.coffee +++ b/src/theme-package.coffee @@ -5,7 +5,7 @@ module.exports = class ThemePackage extends Package getType: -> 'theme' - getStylesheetType: -> "theme" + getStylesheetType: -> 'theme' enable: -> atom.config.unshiftAtKeyPath('core.themes', @name) From 160bb29034d96cf27c71d0bd13b2cacfbc38715c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 28 Oct 2014 19:28:22 -0600 Subject: [PATCH 55/68] Null-guard component in blur handler --- src/text-editor-element.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 8ad062ab8..c4a8148d9 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -103,7 +103,7 @@ class TextEditorElement extends HTMLElement event.stopImmediatePropagation() if @contains(event.relatedTarget) blurred: (event) -> - @component.blurred() + @component?.blurred() event.stopImmediatePropagation() if @contains(event.relatedTarget) addGrammarScopeAttribute: -> From 0488fc21daa0b673507e8befd91c4db793beef9f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 28 Oct 2014 19:32:22 -0600 Subject: [PATCH 56/68] :arrow_up: autocomplete for shadow DOM fix with auto-selecting 1 option --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 06fc5801b..bd82372db 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "solarized-dark-syntax": "0.22.0", "solarized-light-syntax": "0.12.0", "archive-view": "0.37.0", - "autocomplete": "0.32.0", + "autocomplete": "0.33.0", "autoflow": "0.18.0", "autosave": "0.18.0", "background-tips": "0.17.0", From 4537e9bd1a649b4d1209f2e1b6d46c8f1a065d31 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 30 Oct 2014 14:51:44 -0700 Subject: [PATCH 57/68] Fix specs --- spec/theme-manager-spec.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index eee89b721..334a0c9df 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -92,8 +92,8 @@ describe "ThemeManager", -> runs -> reloadHandler.reset() - expect($('style[group=theme]')).toHaveLength 2 - expect($('style[group=theme]:eq(1)').attr('source-path')).toMatch /atom-dark-ui/ + expect($('style[group=theme]')).toHaveLength 1 + expect($('style[group=theme]:eq(0)').attr('source-path')).toMatch /atom-dark-ui/ atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-ui']) waitsFor -> @@ -111,7 +111,7 @@ describe "ThemeManager", -> runs -> reloadHandler.reset() - expect($('style[group=theme]')).toHaveLength 2 + expect($('style[group=theme]')).toHaveLength 1 # atom-dark-ui has an directory path, the syntax one doesn't atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui']) From dd7335c30b43b9fd0b284e3997198b6a77d3ea90 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 4 Nov 2014 13:48:35 -0700 Subject: [PATCH 58/68] Simplify focus/blur handling Signed-off-by: Max Brunsfeld --- src/text-editor-element.coffee | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index c4a8148d9..05fa8437c 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -18,7 +18,6 @@ class TextEditorElement extends HTMLElement @initializeContent() @createSpacePenShim() @addEventListener 'focus', @focused.bind(this) - @addEventListener 'focusout', @focusedOut.bind(this) @addEventListener 'blur', @blurred.bind(this) initializeContent: (attributes) -> @@ -90,21 +89,13 @@ class TextEditorElement extends HTMLElement @component = null focused: (event) -> - if @contains(event.relatedTarget) or this is event.relatedTarget - event.stopPropagation() - return - if @component? @component.focused() else @focusOnAttach = true - focusedOut: (event) -> - event.stopImmediatePropagation() if @contains(event.relatedTarget) - blurred: (event) -> @component?.blurred() - event.stopImmediatePropagation() if @contains(event.relatedTarget) addGrammarScopeAttribute: -> grammarScope = @model.getGrammar()?.scopeName?.replace(/\./g, ' ') From 0e57ede712dec59c929a36213b5be5d750ecd1c0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 4 Nov 2014 10:47:59 -0700 Subject: [PATCH 59/68] Only create a shadow root if editor.useShadowDOM config setting is true Signed-off-by: Max Brunsfeld --- src/highlights-component.coffee | 7 ++++--- src/lines-component.coffee | 7 ++++--- src/text-editor-element.coffee | 31 +++++++++++++++++++++++-------- src/text-editor-view.coffee | 11 +++++++++-- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/highlights-component.coffee b/src/highlights-component.coffee index cfd42d01e..7020a93e4 100644 --- a/src/highlights-component.coffee +++ b/src/highlights-component.coffee @@ -22,9 +22,10 @@ HighlightsComponent = React.createClass highlightComponents componentDidMount: -> - insertionPoint = document.createElement('content') - insertionPoint.setAttribute('select', '.underlayer') - @getDOMNode().appendChild(insertionPoint) + if atom.config.get('editor.useShadowDOM') + insertionPoint = document.createElement('content') + insertionPoint.setAttribute('select', '.underlayer') + @getDOMNode().appendChild(insertionPoint) shouldComponentUpdate: (newProps) -> not isEqualForProperties(newProps, @props, 'highlightDecorations', 'lineHeightInPixels', 'defaultCharWidth', 'scopedCharacterWidthsChangeCount') diff --git a/src/lines-component.coffee b/src/lines-component.coffee index a217263e3..f94926c80 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -58,9 +58,10 @@ LinesComponent = React.createClass @renderedDecorationsByLineId = {} componentDidMount: -> - insertionPoint = document.createElement('content') - insertionPoint.setAttribute('select', '.overlayer') - @getDOMNode().appendChild(insertionPoint) + if atom.config.get('editor.useShadowDOM') + insertionPoint = document.createElement('content') + insertionPoint.setAttribute('select', '.overlayer') + @getDOMNode().appendChild(insertionPoint) shouldComponentUpdate: (newProps) -> return true unless isEqualForProperties(newProps, @props, diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 05fa8437c..b75001f8f 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -6,6 +6,8 @@ TextEditor = require './text-editor' TextEditorComponent = require './text-editor-component' TextEditorView = null +GlobalStylesElement = null + class TextEditorElement extends HTMLElement model: null componentDescriptor: null @@ -23,16 +25,29 @@ class TextEditorElement extends HTMLElement initializeContent: (attributes) -> @classList.add('editor') @setAttribute('tabindex', -1) - @createShadowRoot() - @stylesElement = document.createElement('atom-styles') - @stylesElement.setAttribute('context', 'atom-text-editor') - @stylesElement.initialize() - @shadowRoot.appendChild(@stylesElement) - @rootElement = document.createElement('div') - @rootElement.classList.add('editor', 'editor-colors') - @shadowRoot.appendChild(@rootElement) + if atom.config.get('editor.useShadowDOM') + @createShadowRoot() + + @stylesElement = document.createElement('atom-styles') + @stylesElement.setAttribute('context', 'atom-text-editor') + @stylesElement.initialize() + + @rootElement = document.createElement('div') + @rootElement.classList.add('editor', 'editor-colors') + + @shadowRoot.appendChild(@stylesElement) + @shadowRoot.appendChild(@rootElement) + else + unless GlobalStylesElement? + GlobalStylesElement = document.createElement('atom-styles') + GlobalStylesElement.setAttribute('context', 'atom-text-editor') + GlobalStylesElement.initialize() + document.head.appendChild(GlobalStylesElement) + + @stylesElement = GlobalStylesElement + @rootElement = this createSpacePenShim: -> TextEditorView ?= require './text-editor-view' diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index 89540bf9c..e84703067 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -76,8 +76,15 @@ class TextEditorView extends View @root = $(@element.rootElement) @scrollView = @root.find('.scroll-view') - @underlayer = $("
").appendTo(this) - @overlayer = $("
").appendTo(this) + + + if atom.config.get('editor.useShadowDOM') + @underlayer = $("
").appendTo(this) + @overlayer = $("
").appendTo(this) + else + @underlayer = @find('.highlights').addClass('underlayer') + @overlayer = @find('.lines').addClass('overlayer') + @hiddenInput = @root.find('.hidden-input') @hiddenInput.on = (args...) => From 9690e44ffe881100b5773aa05f8db4f227419c91 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 4 Nov 2014 11:12:25 -0700 Subject: [PATCH 60/68] Correctly handle focus when shadow DOM is disabled Signed-off-by: Max Brunsfeld --- src/text-editor-component.coffee | 2 +- src/text-editor-element.coffee | 5 +++++ src/text-editor-view.coffee | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 022f5fbd5..149c9194d 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -93,7 +93,7 @@ TextEditorComponent = React.createClass className += ' is-focused' if focused className += ' has-selection' if hasSelection - div {className, style, tabIndex: -1}, + div {className, style}, if @shouldRenderGutter() GutterComponent { ref: 'gutter', onMouseDown: @onGutterMouseDown, lineDecorations, diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index b75001f8f..b8e0e2fe3 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -110,6 +110,11 @@ class TextEditorElement extends HTMLElement @focusOnAttach = true blurred: (event) -> + unless atom.config.get('editor.useShadowDOM') + if event.relatedTarget is @component?.refs.input.getDOMNode() + event.stopImmediatePropagation() + return + @component?.blurred() addGrammarScopeAttribute: -> diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index e84703067..4fbe487d6 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -127,7 +127,7 @@ class TextEditorView extends View Object.defineProperty @::, 'firstRenderedScreenRow', get: -> @component.getRenderedRowRange()[0] Object.defineProperty @::, 'lastRenderedScreenRow', get: -> @component.getRenderedRowRange()[1] Object.defineProperty @::, 'active', get: -> @is(@getPaneView()?.activeView) - Object.defineProperty @::, 'isFocused', get: -> @element is document.activeElement + Object.defineProperty @::, 'isFocused', get: -> document.activeElement is @element or document.activeElement is @element.component?.refs.input.getDOMNode() Object.defineProperty @::, 'mini', get: -> @component?.props.mini Object.defineProperty @::, 'component', get: -> @element?.component From 7fe9c14772e23787b4744dd7c2c9d4dec23c5b24 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 4 Nov 2014 13:56:25 -0700 Subject: [PATCH 61/68] :lipstick: --- src/text-editor-element.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index b8e0e2fe3..12c4ee615 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -26,7 +26,6 @@ class TextEditorElement extends HTMLElement @classList.add('editor') @setAttribute('tabindex', -1) - if atom.config.get('editor.useShadowDOM') @createShadowRoot() From 9b70cf2044fa81ff7af7367770f1e5785e5c7358 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 4 Nov 2014 13:58:34 -0700 Subject: [PATCH 62/68] Make blur event on text editor element work with shadow DOM disabled When the shadow DOM is enabled, this happens organically because the focus is abstracted across the shadow boundary. Without that abstraction boundary, we need to pretend that a blur of the hidden input is actually a blur of the entire editor. Signed-off-by: Max Brunsfeld --- src/text-editor-element.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 12c4ee615..5a39f8348 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -95,6 +95,7 @@ class TextEditorElement extends HTMLElement lineOverdrawMargin: @lineOverdrawMargin ) @component = React.renderComponent(@componentDescriptor, @rootElement) + @component.refs.input.getDOMNode().addEventListener 'blur', => @dispatchEvent(new FocusEvent('blur', bubbles: false)) unmountComponent: -> return unless @component?.isMounted() From 100af7d27dce491244e46585935baeecb8774889 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 4 Nov 2014 14:35:53 -0700 Subject: [PATCH 63/68] Fix corner cases related to lifecycle state of EditorComponent on events Signed-off-by: Max Brunsfeld --- src/scrollbar-component.coffee | 3 +++ src/text-editor-component.coffee | 3 ++- src/text-editor-element.coffee | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/scrollbar-component.coffee b/src/scrollbar-component.coffee index a7645b277..9290835db 100644 --- a/src/scrollbar-component.coffee +++ b/src/scrollbar-component.coffee @@ -38,6 +38,9 @@ ScrollbarComponent = React.createClass @getDOMNode().addEventListener 'scroll', @onScroll + componentWillUnmount: -> + @getDOMNode().removeEventListener 'scroll', @onScroll + shouldComponentUpdate: (newProps) -> return true if newProps.visible isnt @props.visible diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 149c9194d..a57f2762d 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -435,7 +435,8 @@ TextEditorComponent = React.createClass @refs.input.focus() blurred: -> - @setState(focused: false) + if @isMounted() + @setState(focused: false) onTextInput: (event) -> event.stopPropagation() diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 5a39f8348..b83b4a11d 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -111,7 +111,7 @@ class TextEditorElement extends HTMLElement blurred: (event) -> unless atom.config.get('editor.useShadowDOM') - if event.relatedTarget is @component?.refs.input.getDOMNode() + if event.relatedTarget is @component?.refs.input?.getDOMNode() event.stopImmediatePropagation() return From dd1e5338c6cd05ee35bd29caed810dc4aa7e9be5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 4 Nov 2014 15:02:08 -0700 Subject: [PATCH 64/68] Focus the root TextEditorElement in spec instead of component node Signed-off-by: Max Brunsfeld --- spec/text-editor-component-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 9d33c47da..811681648 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -1537,7 +1537,7 @@ describe "TextEditorComponent", -> it "transfers focus to the hidden input", -> expect(document.activeElement).toBe document.body - componentNode.focus() + wrapperNode.focus() expect(document.activeElement).toBe wrapperNode expect(wrapperNode.shadowRoot.activeElement).toBe inputNode From 670a710753f65cbb8cd001d54a3755e4bcdfa23e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 4 Nov 2014 15:02:52 -0700 Subject: [PATCH 65/68] Test editor focus/blur handling with shadow DOM enabled/disabled Signed-off-by: Max Brunsfeld --- spec/text-editor-element-spec.coffee | 52 ++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/spec/text-editor-element-spec.coffee b/spec/text-editor-element-spec.coffee index 4dd875527..6de999618 100644 --- a/spec/text-editor-element-spec.coffee +++ b/spec/text-editor-element-spec.coffee @@ -19,19 +19,43 @@ describe "TextEditorElement", -> element = jasmineContent.firstChild expect(element.getModel().getPlaceholderText()).toBe 'testing' - describe "::focus()", -> - it "transfers focus to the hidden text area and does not emit 'focusout' or 'blur' events", -> - element = new TextEditorElement - jasmineContent.appendChild(element) + describe "focus and blur handling", -> + describe "when the editor.useShadowDOM config option is true", -> + it "proxies focus/blur events to/from the hidden input inside the shadow root", -> + atom.config.set('editor.useShadowDOM', true) - focusoutCalled = false - element.addEventListener 'focusout', -> focusoutCalled = true - blurCalled = false - element.addEventListener 'blur', -> blurCalled = true + element = new TextEditorElement + jasmineContent.appendChild(element) - element.focus() - expect(focusoutCalled).toBe false - expect(blurCalled).toBe false - expect(element.hasFocus()).toBe true - expect(document.activeElement).toBe element - expect(element.shadowRoot.activeElement).toBe element.shadowRoot.querySelector('input') + blurCalled = false + element.addEventListener 'blur', -> blurCalled = true + + element.focus() + expect(blurCalled).toBe false + expect(element.hasFocus()).toBe true + expect(document.activeElement).toBe element + expect(element.shadowRoot.activeElement).toBe element.shadowRoot.querySelector('input') + + document.body.focus() + expect(blurCalled).toBe true + + describe "when the editor.useShadowDOM config option is false", -> + afterEach -> + document.head.querySelector('atom-styles[context="atom-text-editor"]').remove() + + it "proxies focus/blur events to/from the hidden input", -> + atom.config.set('editor.useShadowDOM', false) + + element = new TextEditorElement + jasmineContent.appendChild(element) + + blurCalled = false + element.addEventListener 'blur', -> blurCalled = true + + element.focus() + expect(blurCalled).toBe false + expect(element.hasFocus()).toBe true + expect(document.activeElement).toBe element.querySelector('input') + + document.body.focus() + expect(blurCalled).toBe true From e1d6d55311a2ccf45b6422340943b8fbe1f69a2d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 4 Nov 2014 15:03:13 -0700 Subject: [PATCH 66/68] Enable editor.useShadowDOM in all specs --- spec/spec-helper.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 0f313433a..9eef239ee 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -106,6 +106,7 @@ beforeEach -> config.set "editor.autoIndent", false config.set "core.disabledPackages", ["package-that-throws-an-exception", "package-with-broken-package-json", "package-with-broken-keymap"] + config.set "editor.useShadowDOM", true config.load.reset() config.save.reset() From 2b2149bca137ad8399afc9121e68f1677ddd01a3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 4 Nov 2014 15:05:08 -0700 Subject: [PATCH 67/68] Add config schema for editor.useShadowDOM Signed-off-by: Max Brunsfeld --- src/config-schema.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/config-schema.coffee b/src/config-schema.coffee index 85bb5e194..afb1af4ce 100644 --- a/src/config-schema.coffee +++ b/src/config-schema.coffee @@ -109,6 +109,11 @@ module.exports = type: 'boolean' default: true description: 'Disabling will improve editor font rendering but reduce scrolling performance.' + useShadowDOM: + type: 'boolean' + default: false + title: 'Use Shadow DOM' + description: 'Enable to test out themes and packages with the new shadow DOM before it ships by default.' confirmCheckoutHeadRevision: type: 'boolean' default: true From badf1725fa8110072ca2f9b5c20871b27e1a5817 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 4 Nov 2014 16:34:11 -0700 Subject: [PATCH 68/68] Handle focus on hidden input when shadow DOM is disabled Signed-off-by: Max Brunsfeld --- src/text-editor-element.coffee | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index b83b4a11d..4f6f9c2d2 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -95,7 +95,11 @@ class TextEditorElement extends HTMLElement lineOverdrawMargin: @lineOverdrawMargin ) @component = React.renderComponent(@componentDescriptor, @rootElement) - @component.refs.input.getDOMNode().addEventListener 'blur', => @dispatchEvent(new FocusEvent('blur', bubbles: false)) + + unless atom.config.get('editor.useShadowDOM') + inputNode = @component.refs.input.getDOMNode() + inputNode.addEventListener 'focus', @focused.bind(this) + inputNode.addEventListener 'blur', => @dispatchEvent(new FocusEvent('blur', bubbles: false)) unmountComponent: -> return unless @component?.isMounted() @@ -103,7 +107,7 @@ class TextEditorElement extends HTMLElement React.unmountComponentAtNode(this) @component = null - focused: (event) -> + focused: -> if @component? @component.focused() else