mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-19 06:58:26 +03:00
Merge pull request #3943 from atom/ns-text-editor-shadow-dom
Render text editor contents inside shadow DOM
This commit is contained in:
commit
2c83c3fe17
@ -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'
|
||||
|
||||
|
@ -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",
|
||||
@ -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",
|
||||
|
1
spec/fixtures/packages/package-with-stylesheets/stylesheets/4.test-context.css
vendored
Normal file
1
spec/fixtures/packages/package-with-stylesheets/stylesheets/4.test-context.css
vendored
Normal file
@ -0,0 +1 @@
|
||||
a { color: red }
|
@ -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,11 +305,22 @@ describe "PackageManager", ->
|
||||
expect(atom.themes.stylesheetElementForId(two)).toBeNull()
|
||||
expect(atom.themes.stylesheetElementForId(three)).toBeNull()
|
||||
|
||||
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", ->
|
||||
waitsForPromise ->
|
||||
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'
|
||||
|
||||
runs ->
|
||||
element = atom.styles.getStyleElements().find (element) -> element.context is 'test-context'
|
||||
expect(element).toBeDefined()
|
||||
|
||||
describe "grammar loading", ->
|
||||
it "loads the package's grammars", ->
|
||||
|
@ -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", ->
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
||||
@ -219,7 +220,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 ""
|
||||
@ -338,12 +339,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)
|
||||
|
@ -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;}"
|
||||
|
@ -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')
|
||||
@ -1537,8 +1537,9 @@ describe "TextEditorComponent", ->
|
||||
|
||||
it "transfers focus to the hidden input", ->
|
||||
expect(document.activeElement).toBe document.body
|
||||
componentNode.focus()
|
||||
expect(document.activeElement).toBe inputNode
|
||||
wrapperNode.focus()
|
||||
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
|
||||
@ -1667,12 +1668,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')
|
||||
|
@ -19,18 +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(element.querySelector('input')).toBe document.activeElement
|
||||
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
|
||||
|
@ -85,7 +85,16 @@ 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
|
||||
|
||||
runs ->
|
||||
reloadHandler.reset()
|
||||
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 ->
|
||||
reloadHandler.callCount == 1
|
||||
@ -93,17 +102,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'])
|
||||
|
||||
waitsFor ->
|
||||
reloadHandler.callCount == 1
|
||||
|
||||
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 ->
|
||||
@ -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'])
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -21,5 +21,11 @@ HighlightsComponent = React.createClass
|
||||
|
||||
highlightComponents
|
||||
|
||||
componentDidMount: ->
|
||||
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')
|
||||
|
@ -7,16 +7,17 @@ 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: ''}
|
||||
|
||||
componentDidMount: ->
|
||||
@getDOMNode().addEventListener 'paste', @onPaste
|
||||
@getDOMNode().addEventListener 'compositionupdate', @onCompositionUpdate
|
||||
node = @getDOMNode()
|
||||
node.addEventListener 'paste', @onPaste
|
||||
node.addEventListener 'compositionupdate', @onCompositionUpdate
|
||||
|
||||
# Don't let text accumulate in the input forever, but avoid excessive reflows
|
||||
componentDidUpdate: ->
|
||||
@ -34,11 +35,5 @@ InputComponent = React.createClass
|
||||
onPaste: (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
onFocus: ->
|
||||
@props.onFocus?()
|
||||
|
||||
onBlur: ->
|
||||
@props.onBlur?()
|
||||
|
||||
focus: ->
|
||||
@getDOMNode().focus()
|
||||
|
@ -57,6 +57,12 @@ LinesComponent = React.createClass
|
||||
@lineIdsByScreenRow = {}
|
||||
@renderedDecorationsByLineId = {}
|
||||
|
||||
componentDidMount: ->
|
||||
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,
|
||||
'renderedRowRange', 'lineDecorations', 'highlightDecorations', 'lineHeightInPixels', 'defaultCharWidth',
|
||||
|
@ -50,6 +50,7 @@ class Package
|
||||
keymaps: null
|
||||
menus: null
|
||||
stylesheets: null
|
||||
stylesheetDisposables: null
|
||||
grammars: null
|
||||
scopedProperties: null
|
||||
mainModulePath: null
|
||||
@ -175,9 +176,16 @@ class Package
|
||||
activateStylesheets: ->
|
||||
return if @stylesheetsActivated
|
||||
|
||||
type = @getStylesheetType()
|
||||
for [stylesheetPath, content] in @stylesheets
|
||||
atom.themes.applyStylesheet(stylesheetPath, content, type)
|
||||
|
||||
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
|
||||
|
||||
activateResources: ->
|
||||
@ -320,7 +328,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
|
||||
@ -329,11 +337,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?
|
||||
|
@ -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)
|
||||
|
@ -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,11 @@ 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
|
||||
|
||||
componentWillUnmount: ->
|
||||
@getDOMNode().removeEventListener 'scroll', @onScroll
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
return true if newProps.visible isnt @props.visible
|
||||
|
||||
|
@ -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
|
||||
@ -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
|
||||
|
@ -81,6 +81,18 @@ jQuery.event.remove = (elem, types, originalHandler, selector, mappedTypes) ->
|
||||
handler = HandlersByOriginalHandler.get(originalHandler) ? originalHandler
|
||||
JQueryEventRemove(elem, types, handler, selector, mappedTypes, RemoveEventListener if atom?.commands?)
|
||||
|
||||
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:
|
||||
show: 1000
|
||||
|
@ -25,6 +25,7 @@ class StyleManager
|
||||
|
||||
addStyleSheet: (source, params) ->
|
||||
sourcePath = params?.sourcePath
|
||||
context = params?.context
|
||||
group = params?.group
|
||||
|
||||
if sourcePath? and styleElement = @styleElementsBySourcePath[sourcePath]
|
||||
|
@ -1,8 +1,8 @@
|
||||
{Emitter, CompositeDisposable} = require 'event-kit'
|
||||
|
||||
class StylesElement extends HTMLElement
|
||||
createdCallback: ->
|
||||
@emitter = new Emitter
|
||||
subscriptions: null
|
||||
context: null
|
||||
|
||||
onDidAddStyleElement: (callback) ->
|
||||
@emitter.on 'did-add-style-element', callback
|
||||
@ -13,15 +13,42 @@ class StylesElement extends HTMLElement
|
||||
onDidUpdateStyleElement: (callback) ->
|
||||
@emitter.on 'did-update-style-element', callback
|
||||
|
||||
attachedCallback: ->
|
||||
@subscriptions = new CompositeDisposable
|
||||
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?
|
||||
|
||||
@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))
|
||||
|
||||
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
|
||||
|
||||
styleElementClone = styleElement.cloneNode(true)
|
||||
styleElementClone.context = styleElement.context
|
||||
@styleElementClonesByOriginalElement.set(styleElement, styleElementClone)
|
||||
|
||||
group = styleElement.getAttribute('group')
|
||||
@ -35,16 +62,17 @@ 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
|
||||
|
||||
detachedCallback: ->
|
||||
@subscriptions.dispose()
|
||||
|
||||
module.exports = StylesElement = document.registerElement 'atom-styles', prototype: StylesElement.prototype
|
||||
|
@ -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,
|
||||
@ -102,13 +102,11 @@ 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'
|
||||
style: hiddenInputStyle
|
||||
onFocus: @onInputFocused
|
||||
onBlur: @onInputBlurred
|
||||
|
||||
LinesComponent {
|
||||
ref: 'lines',
|
||||
@ -175,14 +173,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
|
||||
@ -193,9 +191,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 +213,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 +259,7 @@ TextEditorComponent = React.createClass
|
||||
@forceUpdate()
|
||||
|
||||
getTopmostDOMNode: ->
|
||||
@props.parentView
|
||||
@props.hostElement
|
||||
|
||||
getRenderedRowRange: ->
|
||||
{editor, lineOverdrawMargin} = @props
|
||||
@ -378,8 +376,8 @@ 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
|
||||
|
||||
scrollViewNode = @refs.scrollView.getDOMNode()
|
||||
scrollViewNode.addEventListener 'scroll', @onScrollViewScroll
|
||||
@ -415,6 +413,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
|
||||
@ -428,8 +429,14 @@ 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: ->
|
||||
if @isMounted()
|
||||
@setState(focused: false)
|
||||
|
||||
onTextInput: (event) ->
|
||||
event.stopPropagation()
|
||||
@ -452,12 +459,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
|
||||
|
||||
@ -621,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.sheet? or @containsScrollbarSelector(styleElement.sheet)
|
||||
@sampleFontStyling()
|
||||
@sampleBackgroundColors()
|
||||
@remeasureCharacterWidths()
|
||||
@ -771,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
|
||||
@ -806,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
|
||||
@ -907,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: ->
|
||||
@ -977,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'
|
||||
|
@ -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
|
||||
@ -18,13 +20,34 @@ class TextEditorElement extends HTMLElement
|
||||
@initializeContent()
|
||||
@createSpacePenShim()
|
||||
@addEventListener 'focus', @focused.bind(this)
|
||||
@addEventListener 'focusout', @focusedOut.bind(this)
|
||||
@addEventListener 'blur', @blurred.bind(this)
|
||||
|
||||
initializeContent: (attributes) ->
|
||||
@classList.add('editor', 'react', 'editor-colors')
|
||||
@classList.add('editor')
|
||||
@setAttribute('tabindex', -1)
|
||||
|
||||
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'
|
||||
@__spacePenView = new TextEditorView(this)
|
||||
@ -64,12 +87,19 @@ class TextEditorElement extends HTMLElement
|
||||
|
||||
mountComponent: ->
|
||||
@componentDescriptor ?= TextEditorComponent(
|
||||
parentView: this
|
||||
hostElement: this
|
||||
rootElement: @rootElement
|
||||
stylesElement: @stylesElement
|
||||
editor: @model
|
||||
mini: @model.mini
|
||||
lineOverdrawMargin: @lineOverdrawMargin
|
||||
)
|
||||
@component = React.renderComponent(@componentDescriptor, this)
|
||||
@component = React.renderComponent(@componentDescriptor, @rootElement)
|
||||
|
||||
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()
|
||||
@ -79,15 +109,17 @@ class TextEditorElement extends HTMLElement
|
||||
|
||||
focused: ->
|
||||
if @component?
|
||||
@component.onFocus()
|
||||
@component.focused()
|
||||
else
|
||||
@focusOnAttach = true
|
||||
|
||||
focusedOut: (event) ->
|
||||
event.stopImmediatePropagation() if @contains(event.relatedTarget)
|
||||
|
||||
blurred: (event) ->
|
||||
event.stopImmediatePropagation() if @contains(event.relatedTarget)
|
||||
unless atom.config.get('editor.useShadowDOM')
|
||||
if event.relatedTarget is @component?.refs.input?.getDOMNode()
|
||||
event.stopImmediatePropagation()
|
||||
return
|
||||
|
||||
@component?.blurred()
|
||||
|
||||
addGrammarScopeAttribute: ->
|
||||
grammarScope = @model.getGrammar()?.scopeName?.replace(/\./g, ' ')
|
||||
|
@ -73,13 +73,26 @@ 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')
|
||||
|
||||
|
||||
if atom.config.get('editor.useShadowDOM')
|
||||
@underlayer = $("<div class='underlayer'></div>").appendTo(this)
|
||||
@overlayer = $("<div class='overlayer'></div>").appendTo(this)
|
||||
else
|
||||
@underlayer = @find('.highlights').addClass('underlayer')
|
||||
@overlayer = @find('.lines').addClass('overlayer')
|
||||
|
||||
@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 = @find('.gutter')
|
||||
@gutter = @root.find('.gutter')
|
||||
|
||||
@gutter.removeClassFromAllLines = (klass) =>
|
||||
deprecate('Use decorations instead: http://blog.atom.io/2014/07/24/decorations.html')
|
||||
@ -95,6 +108,13 @@ class TextEditorView extends View
|
||||
lines.addClass(klass)
|
||||
lines.length > 0
|
||||
|
||||
find: ->
|
||||
shadowResult = @root.find.apply(@root, arguments)
|
||||
if shadowResult.length > 0
|
||||
shadowResult
|
||||
else
|
||||
super
|
||||
|
||||
# Public: Get the underlying editor model for this view.
|
||||
#
|
||||
# Returns an {TextEditor}
|
||||
@ -107,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: -> @component?.state.focused
|
||||
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
|
||||
|
||||
@ -164,7 +184,7 @@ class TextEditorView extends View
|
||||
appendToLinesView: (view) ->
|
||||
view.css('position', 'absolute')
|
||||
view.css('z-index', 1)
|
||||
@find('.lines').prepend(view)
|
||||
@overlayer.append(view)
|
||||
|
||||
unmountComponent: ->
|
||||
React.unmountComponentAtNode(@element) if @component.isMounted()
|
||||
|
@ -249,6 +249,9 @@ class ThemeManager
|
||||
if nativeStylesheetPath = fs.resolveOnLoadPath(process.platform, ['css', 'less'])
|
||||
@requireStylesheet(nativeStylesheetPath)
|
||||
|
||||
textEditorStylesPath = path.join(@resourcePath, 'static', 'text-editor-shadow.less')
|
||||
atom.styles.addStyleSheet(@loadLessStylesheet(textEditorStylesPath), sourcePath: textEditorStylesPath, context: 'atom-text-editor')
|
||||
|
||||
stylesheetElementForId: (id) ->
|
||||
document.head.querySelector("atom-styles style[source-path=\"#{id}\"]")
|
||||
|
||||
|
@ -21,7 +21,7 @@
|
||||
@import "popover-list";
|
||||
@import "messages";
|
||||
@import "markdown";
|
||||
@import "editor";
|
||||
@import "text-editor-light";
|
||||
@import "select-list";
|
||||
@import "syntax";
|
||||
@import "utilities";
|
||||
|
@ -1,310 +0,0 @@
|
||||
@import "ui-variables";
|
||||
@import "octicon-utf-codes";
|
||||
@import "octicon-mixins";
|
||||
|
||||
atom-text-editor.react {
|
||||
.editor-contents {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.underlayer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
.lines {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.editor-contents.is-focused .cursor {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.cursors.blink-off .cursor {
|
||||
opacity: 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;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
atom-text-editor.mini {
|
||||
font-size: @input-font-size;
|
||||
line-height: @component-line-height;
|
||||
max-height: @component-line-height + 2; // +2 for borders
|
||||
|
||||
.placeholder-text {
|
||||
position: absolute;
|
||||
color: @text-color-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
atom-text-editor {
|
||||
z-index: 0;
|
||||
font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
atom-text-editor, .editor-contents {
|
||||
overflow: hidden;
|
||||
cursor: text;
|
||||
display: -webkit-flex;
|
||||
-webkit-user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
atom-text-editor .gutter .line-number.cursor-line {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
atom-text-editor .gutter {
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
cursor: default;
|
||||
min-width: 1em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
atom-text-editor .gutter .line-number {
|
||||
padding-left: .5em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
atom-text-editor .gutter .line-numbers {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
atom-text-editor .gutter .line-number.folded.cursor-line {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
atom-text-editor .gutter .line-number .icon-right {
|
||||
.octicon(chevron-down, 0.8em);
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
padding-left: .1em;
|
||||
padding-right: .5em;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
atom-text-editor .gutter:hover .line-number.foldable .icon-right {
|
||||
visibility: visible;
|
||||
|
||||
&:before {
|
||||
content: @chevron-down;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
atom-text-editor .gutter, atom-text-editor .gutter:hover {
|
||||
.line-number.folded .icon-right {
|
||||
.octicon(chevron-right, 0.8em);
|
||||
visibility: visible;
|
||||
|
||||
&:before { // chevron-right renders too far right compared to chevron-down
|
||||
position: relative;
|
||||
left: -.1em;
|
||||
content: @chevron-right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
atom-text-editor .fold-marker {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
atom-text-editor .fold-marker:after {
|
||||
.icon(0.8em, inline);
|
||||
content: @ellipsis;
|
||||
padding-left: 0.2em;
|
||||
}
|
||||
|
||||
atom-text-editor .line.cursor-line .fold-marker:after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
atom-text-editor.is-blurred .line.cursor-line {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
atom-text-editor .invisible-character {
|
||||
font-weight: normal !important;
|
||||
font-style: normal !important;
|
||||
}
|
||||
|
||||
atom-text-editor .indent-guide {
|
||||
display: inline-block;
|
||||
box-shadow: inset 1px 0;
|
||||
}
|
||||
|
||||
atom-text-editor .vertical-scrollbar,
|
||||
atom-text-editor .horizontal-scrollbar {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
atom-text-editor .vertical-scrollbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 15px;
|
||||
overflow-y: auto;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
atom-text-editor .scroll-view {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
atom-text-editor.soft-wrap .scroll-view {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
atom-text-editor .underlayer {
|
||||
z-index: 0;
|
||||
position: absolute;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
atom-text-editor .lines {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
atom-text-editor .overlayer {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
atom-text-editor .line {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
atom-text-editor .line span {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
atom-text-editor .cursor {
|
||||
position: absolute;
|
||||
border-left: 1px solid;
|
||||
}
|
||||
|
||||
atom-text-editor .cursor,
|
||||
atom-text-editor.is-focused .cursor.blink-off {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
atom-text-editor.is-focused .cursor {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.cursor.hidden-cursor {
|
||||
display: none;
|
||||
}
|
||||
|
||||
atom-text-editor .hidden-input {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
atom-text-editor .highlight {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
atom-text-editor .highlight .region,
|
||||
atom-text-editor .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;
|
||||
}
|
||||
}
|
13
static/text-editor-light.less
Normal file
13
static/text-editor-light.less
Normal file
@ -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
|
||||
}
|
282
static/text-editor-shadow.less
Normal file
282
static/text-editor-shadow.less
Normal file
@ -0,0 +1,282 @@
|
||||
@import "ui-variables";
|
||||
@import "octicon-utf-codes";
|
||||
@import "octicon-mixins";
|
||||
|
||||
.editor, .editor-contents {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.underlayer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
.lines {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cursors.blink-off .cursor {
|
||||
opacity: 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;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.editor {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.editor, .editor-contents {
|
||||
overflow: hidden;
|
||||
cursor: text;
|
||||
display: -webkit-flex;
|
||||
-webkit-user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gutter .line-number.cursor-line {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
cursor: default;
|
||||
min-width: 1em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.gutter .line-number {
|
||||
padding-left: .5em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.gutter .line-numbers {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gutter .line-number.folded.cursor-line {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.gutter .line-number .icon-right {
|
||||
.octicon(chevron-down, 0.8em);
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
padding-left: .1em;
|
||||
padding-right: .5em;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.gutter:hover .line-number.foldable .icon-right {
|
||||
visibility: visible;
|
||||
|
||||
&:before {
|
||||
content: @chevron-down;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.gutter, .gutter:hover {
|
||||
.line-number.folded .icon-right {
|
||||
.octicon(chevron-right, 0.8em);
|
||||
visibility: visible;
|
||||
|
||||
&:before { // chevron-right renders too far right compared to chevron-down
|
||||
position: relative;
|
||||
left: -.1em;
|
||||
content: @chevron-right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fold-marker {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.fold-marker:after {
|
||||
.icon(0.8em, inline);
|
||||
content: @ellipsis;
|
||||
padding-left: 0.2em;
|
||||
}
|
||||
|
||||
.line.cursor-line .fold-marker:after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
atom-text-editor::shadow.is-blurred .line.cursor-line {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.invisible-character {
|
||||
font-weight: normal !important;
|
||||
font-style: normal !important;
|
||||
}
|
||||
|
||||
.indent-guide {
|
||||
display: inline-block;
|
||||
box-shadow: inset 1px 0;
|
||||
}
|
||||
|
||||
.vertical-scrollbar,
|
||||
.horizontal-scrollbar {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.vertical-scrollbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 15px;
|
||||
overflow-y: auto;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.scroll-view {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
atom-text-editor::shadow.soft-wrap .scroll-view {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.underlayer {
|
||||
z-index: 0;
|
||||
position: absolute;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.lines {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.overlayer {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.line {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.line span {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
position: absolute;
|
||||
border-left: 1px solid;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.is-focused .cursor {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.is-focused .cursor.blink-off {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.cursor.hidden-cursor {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hidden-input {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.highlight .region,
|
||||
.selection .region {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
Loading…
Reference in New Issue
Block a user