package.src.input.TextareaInput.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of codemirror Show documentation
Show all versions of codemirror Show documentation
Basic configuration for the CodeMirror code editor
The newest version!
import { operation, runInOp } from "../display/operations.js"
import { prepareSelection } from "../display/selection.js"
import { applyTextInput, copyableRanges, handlePaste, hiddenTextarea, setLastCopied } from "./input.js"
import { cursorCoords, posFromMouse } from "../measurement/position_measurement.js"
import { eventInWidget } from "../measurement/widgets.js"
import { simpleSelection } from "../model/selection.js"
import { selectAll, setSelection } from "../model/selection_updates.js"
import { captureRightClick, ie, ie_version, ios, mac, mobile, presto, webkit } from "../util/browser.js"
import { activeElt, removeChildrenAndAdd, selectInput } from "../util/dom.js"
import { e_preventDefault, e_stop, off, on, signalDOMEvent } from "../util/event.js"
import { hasSelection } from "../util/feature_detection.js"
import { Delayed, sel_dontScroll } from "../util/misc.js"
// TEXTAREA INPUT STYLE
export default class TextareaInput {
constructor(cm) {
this.cm = cm
// See input.poll and input.reset
this.prevInput = ""
// Flag that indicates whether we expect input to appear real soon
// now (after some event like 'keypress' or 'input') and are
// polling intensively.
this.pollingFast = false
// Self-resetting timeout for the poller
this.polling = new Delayed()
// Used to work around IE issue with selection being forgotten when focus moves away from textarea
this.hasSelection = false
this.composing = null
}
init(display) {
let input = this, cm = this.cm
this.createField(display)
const te = this.textarea
display.wrapper.insertBefore(this.wrapper, display.wrapper.firstChild)
// Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore)
if (ios) te.style.width = "0px"
on(te, "input", () => {
if (ie && ie_version >= 9 && this.hasSelection) this.hasSelection = null
input.poll()
})
on(te, "paste", e => {
if (signalDOMEvent(cm, e) || handlePaste(e, cm)) return
cm.state.pasteIncoming = +new Date
input.fastPoll()
})
function prepareCopyCut(e) {
if (signalDOMEvent(cm, e)) return
if (cm.somethingSelected()) {
setLastCopied({lineWise: false, text: cm.getSelections()})
} else if (!cm.options.lineWiseCopyCut) {
return
} else {
let ranges = copyableRanges(cm)
setLastCopied({lineWise: true, text: ranges.text})
if (e.type == "cut") {
cm.setSelections(ranges.ranges, null, sel_dontScroll)
} else {
input.prevInput = ""
te.value = ranges.text.join("\n")
selectInput(te)
}
}
if (e.type == "cut") cm.state.cutIncoming = +new Date
}
on(te, "cut", prepareCopyCut)
on(te, "copy", prepareCopyCut)
on(display.scroller, "paste", e => {
if (eventInWidget(display, e) || signalDOMEvent(cm, e)) return
if (!te.dispatchEvent) {
cm.state.pasteIncoming = +new Date
input.focus()
return
}
// Pass the `paste` event to the textarea so it's handled by its event listener.
const event = new Event("paste")
event.clipboardData = e.clipboardData
te.dispatchEvent(event)
})
// Prevent normal selection in the editor (we handle our own)
on(display.lineSpace, "selectstart", e => {
if (!eventInWidget(display, e)) e_preventDefault(e)
})
on(te, "compositionstart", () => {
let start = cm.getCursor("from")
if (input.composing) input.composing.range.clear()
input.composing = {
start: start,
range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"})
}
})
on(te, "compositionend", () => {
if (input.composing) {
input.poll()
input.composing.range.clear()
input.composing = null
}
})
}
createField(_display) {
// Wraps and hides input textarea
this.wrapper = hiddenTextarea()
// The semihidden textarea that is focused when the editor is
// focused, and receives input.
this.textarea = this.wrapper.firstChild
}
screenReaderLabelChanged(label) {
// Label for screenreaders, accessibility
if(label) {
this.textarea.setAttribute('aria-label', label)
} else {
this.textarea.removeAttribute('aria-label')
}
}
prepareSelection() {
// Redraw the selection and/or cursor
let cm = this.cm, display = cm.display, doc = cm.doc
let result = prepareSelection(cm)
// Move the hidden textarea near the cursor to prevent scrolling artifacts
if (cm.options.moveInputWithCursor) {
let headPos = cursorCoords(cm, doc.sel.primary().head, "div")
let wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect()
result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
headPos.top + lineOff.top - wrapOff.top))
result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
headPos.left + lineOff.left - wrapOff.left))
}
return result
}
showSelection(drawn) {
let cm = this.cm, display = cm.display
removeChildrenAndAdd(display.cursorDiv, drawn.cursors)
removeChildrenAndAdd(display.selectionDiv, drawn.selection)
if (drawn.teTop != null) {
this.wrapper.style.top = drawn.teTop + "px"
this.wrapper.style.left = drawn.teLeft + "px"
}
}
// Reset the input to correspond to the selection (or to be empty,
// when not typing and nothing is selected)
reset(typing) {
if (this.contextMenuPending || this.composing) return
let cm = this.cm
if (cm.somethingSelected()) {
this.prevInput = ""
let content = cm.getSelection()
this.textarea.value = content
if (cm.state.focused) selectInput(this.textarea)
if (ie && ie_version >= 9) this.hasSelection = content
} else if (!typing) {
this.prevInput = this.textarea.value = ""
if (ie && ie_version >= 9) this.hasSelection = null
}
}
getField() { return this.textarea }
supportsTouch() { return false }
focus() {
if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt(this.textarea.ownerDocument) != this.textarea)) {
try { this.textarea.focus() }
catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM
}
}
blur() { this.textarea.blur() }
resetPosition() {
this.wrapper.style.top = this.wrapper.style.left = 0
}
receivedFocus() { this.slowPoll() }
// Poll for input changes, using the normal rate of polling. This
// runs as long as the editor is focused.
slowPoll() {
if (this.pollingFast) return
this.polling.set(this.cm.options.pollInterval, () => {
this.poll()
if (this.cm.state.focused) this.slowPoll()
})
}
// When an event has just come in that is likely to add or change
// something in the input textarea, we poll faster, to ensure that
// the change appears on the screen quickly.
fastPoll() {
let missed = false, input = this
input.pollingFast = true
function p() {
let changed = input.poll()
if (!changed && !missed) {missed = true; input.polling.set(60, p)}
else {input.pollingFast = false; input.slowPoll()}
}
input.polling.set(20, p)
}
// Read input from the textarea, and update the document to match.
// When something is selected, it is present in the textarea, and
// selected (unless it is huge, in which case a placeholder is
// used). When nothing is selected, the cursor sits after previously
// seen text (can be empty), which is stored in prevInput (we must
// not reset the textarea when typing, because that breaks IME).
poll() {
let cm = this.cm, input = this.textarea, prevInput = this.prevInput
// Since this is called a *lot*, try to bail out as cheaply as
// possible when it is clear that nothing happened. hasSelection
// will be the case when there is a lot of text in the textarea,
// in which case reading its value would be expensive.
if (this.contextMenuPending || !cm.state.focused ||
(hasSelection(input) && !prevInput && !this.composing) ||
cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq)
return false
let text = input.value
// If nothing changed, bail.
if (text == prevInput && !cm.somethingSelected()) return false
// Work around nonsensical selection resetting in IE9/10, and
// inexplicable appearance of private area unicode characters on
// some key combos in Mac (#2689).
if (ie && ie_version >= 9 && this.hasSelection === text ||
mac && /[\uf700-\uf7ff]/.test(text)) {
cm.display.input.reset()
return false
}
if (cm.doc.sel == cm.display.selForContextMenu) {
let first = text.charCodeAt(0)
if (first == 0x200b && !prevInput) prevInput = "\u200b"
if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo") }
}
// Find the part of the input that is actually new
let same = 0, l = Math.min(prevInput.length, text.length)
while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same
runInOp(cm, () => {
applyTextInput(cm, text.slice(same), prevInput.length - same,
null, this.composing ? "*compose" : null)
// Don't leave long text in the textarea, since it makes further polling slow
if (text.length > 1000 || text.indexOf("\n") > -1) input.value = this.prevInput = ""
else this.prevInput = text
if (this.composing) {
this.composing.range.clear()
this.composing.range = cm.markText(this.composing.start, cm.getCursor("to"),
{className: "CodeMirror-composing"})
}
})
return true
}
ensurePolled() {
if (this.pollingFast && this.poll()) this.pollingFast = false
}
onKeyPress() {
if (ie && ie_version >= 9) this.hasSelection = null
this.fastPoll()
}
onContextMenu(e) {
let input = this, cm = input.cm, display = cm.display, te = input.textarea
if (input.contextMenuPending) input.contextMenuPending()
let pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop
if (!pos || presto) return // Opera is difficult.
// Reset the current text selection only if the click is done outside of the selection
// and 'resetSelectionOnContextMenu' option is true.
let reset = cm.options.resetSelectionOnContextMenu
if (reset && cm.doc.sel.contains(pos) == -1)
operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll)
let oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText
let wrapperBox = input.wrapper.offsetParent.getBoundingClientRect()
input.wrapper.style.cssText = "position: static"
te.style.cssText = `position: absolute; width: 30px; height: 30px;
top: ${e.clientY - wrapperBox.top - 5}px; left: ${e.clientX - wrapperBox.left - 5}px;
z-index: 1000; background: ${ie ? "rgba(255, 255, 255, .05)" : "transparent"};
outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);`
let oldScrollY
if (webkit) oldScrollY = te.ownerDocument.defaultView.scrollY // Work around Chrome issue (#2712)
display.input.focus()
if (webkit) te.ownerDocument.defaultView.scrollTo(null, oldScrollY)
display.input.reset()
// Adds "Select all" to context menu in FF
if (!cm.somethingSelected()) te.value = input.prevInput = " "
input.contextMenuPending = rehide
display.selForContextMenu = cm.doc.sel
clearTimeout(display.detectingSelectAll)
// Select-all will be greyed out if there's nothing to select, so
// this adds a zero-width space so that we can later check whether
// it got selected.
function prepareSelectAllHack() {
if (te.selectionStart != null) {
let selected = cm.somethingSelected()
let extval = "\u200b" + (selected ? te.value : "")
te.value = "\u21da" // Used to catch context-menu undo
te.value = extval
input.prevInput = selected ? "" : "\u200b"
te.selectionStart = 1; te.selectionEnd = extval.length
// Re-set this, in case some other handler touched the
// selection in the meantime.
display.selForContextMenu = cm.doc.sel
}
}
function rehide() {
if (input.contextMenuPending != rehide) return
input.contextMenuPending = false
input.wrapper.style.cssText = oldWrapperCSS
te.style.cssText = oldCSS
if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos)
// Try to detect the user choosing select-all
if (te.selectionStart != null) {
if (!ie || (ie && ie_version < 9)) prepareSelectAllHack()
let i = 0, poll = () => {
if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 &&
te.selectionEnd > 0 && input.prevInput == "\u200b") {
operation(cm, selectAll)(cm)
} else if (i++ < 10) {
display.detectingSelectAll = setTimeout(poll, 500)
} else {
display.selForContextMenu = null
display.input.reset()
}
}
display.detectingSelectAll = setTimeout(poll, 200)
}
}
if (ie && ie_version >= 9) prepareSelectAllHack()
if (captureRightClick) {
e_stop(e)
let mouseup = () => {
off(window, "mouseup", mouseup)
setTimeout(rehide, 20)
}
on(window, "mouseup", mouseup)
} else {
setTimeout(rehide, 50)
}
}
readOnlyChanged(val) {
if (!val) this.reset()
this.textarea.disabled = val == "nocursor"
this.textarea.readOnly = !!val
}
setUneditable() {}
}
TextareaInput.prototype.needsContentAttribute = false
© 2015 - 2025 Weber Informatics LLC | Privacy Policy