mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-24 09:16:07 +08:00
73e7c1c52e
comment Adding test harness Using key strokes in main window test Tests work now Clean up argument variables Rename list manager and get rid of old spec-helper methods Extract out time overrides from spec-helper Spectron test for contenteditable fix spec exit codes and boot mode fix(spec): cleanup N1.sh and make specs fail with exit code 1 Revert tests and get it working in window Move to spec_integration and add window load tester Specs pass. Console logs still in Remove console logs Extract N1 Launcher ready method Make integrated unit test runner feat(tests): adding integration tests Summary: The /spectron folder got moved to /spec_integration There are now unit tests (the old ones) run via the renamed `script/grunt run-unit-tests` There are now integration tests run via the command `script/grunt run-integration-tests`. There are two types of integration tests: 1. Tests that operate on the whole app via Selenium/Chromedriver. These tests have access to Spectron APIs but do NOT have access to any JS object running inside the application. See the `app-boot-spec.es6` for an example of these tests. This is tricky because we want to test the Main window, but Spectron may latch onto any other of our loading windows. Code in `integration-helper` give us an API that finds and loads the main window so we can test it 2. Tests that run in the unit test suite that need Spectron to perform integration-like behavior. These are the contentedtiable specs. The Spectron server is accessed from the app and can be used to trigger actions on the running app, from the app. These tests use the windowed-test runner so Spectron can identify whether the tests have completed, passed, or failed. Unfortunately Spectron can't access the logs , nor the exit code of the test script thereby forcing us to parse the HTML DOM. (Note this is still a WIP) I also revamped the `N1.sh` file when getting the launch arguments to work properly. It's much cleaner. We didn't need most of the data. Test Plan: new tests Reviewers: juan, bengotow Differential Revision: https://phab.nylas.com/D2289 Fix composer specs Tests can properly detect when Spectron is in the environment Report plain text output in specs fixing contenteditable specs Testing slow keymaps on contenteditable specs Move to DOm mutation Spell as `subtree` not `subTree`
526 lines
20 KiB
CoffeeScript
526 lines
20 KiB
CoffeeScript
_ = require 'underscore'
|
|
_s = require 'underscore.string'
|
|
|
|
DOMUtils =
|
|
Mutating:
|
|
replaceFirstListItem: (li, replaceWith) ->
|
|
list = DOMUtils.closest(li, "ul, ol")
|
|
|
|
if replaceWith.length is 0
|
|
replaceWith = replaceWith.replace /\s/g, " "
|
|
text = document.createElement("div")
|
|
text.innerHTML = "<br>"
|
|
else
|
|
replaceWith = replaceWith.replace /\s/g, " "
|
|
text = document.createElement("span")
|
|
text.innerHTML = "#{replaceWith}"
|
|
|
|
if list.querySelectorAll('li').length <= 1
|
|
# Delete the whole list and replace with text
|
|
list.parentNode.replaceChild(text, list)
|
|
else
|
|
# Delete the list item and prepend the text before the rest of the
|
|
# list
|
|
li.parentNode.removeChild(li)
|
|
list.parentNode.insertBefore(text, list)
|
|
|
|
child = text.childNodes[0] ? text
|
|
index = Math.max(replaceWith.length - 1, 0)
|
|
selection = document.getSelection()
|
|
selection.setBaseAndExtent(child, index, child, index)
|
|
|
|
removeEmptyNodes: (node) ->
|
|
Array::slice.call(node.childNodes).forEach (child) ->
|
|
if child.textContent is ''
|
|
node.removeChild(child)
|
|
else
|
|
DOMUtils.Mutating.removeEmptyNodes(child)
|
|
|
|
# Given a bunch of elements, it will go through and find all elements
|
|
# that are adjacent to that one of the same type. For each set of
|
|
# adjacent elements, it will put all children of those elements into
|
|
# the first one and delete the remaining elements.
|
|
collapseAdjacentElements: (els=[]) ->
|
|
return if els.length is 0
|
|
els = Array::slice.call(els)
|
|
|
|
seenEls = []
|
|
toMerge = []
|
|
|
|
for el in els
|
|
continue if el in seenEls
|
|
adjacent = DOMUtils.collectAdjacent(el)
|
|
seenEls = seenEls.concat(adjacent)
|
|
continue if adjacent.length <= 1
|
|
toMerge.push(adjacent)
|
|
|
|
anchors = []
|
|
for mergeSet in toMerge
|
|
anchor = mergeSet[0]
|
|
remaining = mergeSet[1..-1]
|
|
for el in remaining
|
|
while (el.childNodes.length > 0)
|
|
anchor.appendChild(el.childNodes[0])
|
|
DOMUtils.Mutating.removeElements(remaining)
|
|
anchors.push(anchor)
|
|
|
|
return anchors
|
|
|
|
removeElements: (elements=[]) ->
|
|
for el in elements
|
|
try
|
|
if el.parentNode then el.parentNode.removeChild(el)
|
|
catch
|
|
# This can happen if we've already removed ourselves from the
|
|
# node or it no longer exists
|
|
continue
|
|
return elements
|
|
|
|
applyTextInRange: (range, selection, newText) ->
|
|
range.deleteContents()
|
|
node = document.createTextNode(newText)
|
|
range.insertNode(node)
|
|
range.selectNode(node)
|
|
selection.removeAllRanges()
|
|
selection.addRange(range)
|
|
|
|
getRangeAtAndSelectWord: (selection, index) ->
|
|
range = selection.getRangeAt(index)
|
|
|
|
# On Windows, right-clicking a word does not select it at the OS-level.
|
|
if range.collapsed
|
|
DOMUtils.Mutating.selectWordContainingRange(range)
|
|
range = selection.getRangeAt(index)
|
|
return range
|
|
|
|
# This method finds the bounding points of the word that the range
|
|
# is currently within and selects that word.
|
|
selectWordContainingRange: (range) ->
|
|
selection = document.getSelection()
|
|
node = selection.focusNode
|
|
text = node.textContent
|
|
wordStart = _s.reverse(text.substring(0, selection.focusOffset)).search(/\s/)
|
|
if wordStart is -1
|
|
wordStart = 0
|
|
else
|
|
wordStart = selection.focusOffset - wordStart
|
|
wordEnd = text.substring(selection.focusOffset).search(/\s/)
|
|
if wordEnd is -1
|
|
wordEnd = text.length
|
|
else
|
|
wordEnd += selection.focusOffset
|
|
|
|
selection.removeAllRanges()
|
|
range = new Range()
|
|
range.setStart(node, wordStart)
|
|
range.setEnd(node, wordEnd)
|
|
selection.addRange(range)
|
|
|
|
moveSelectionToIndexInAnchorNode: (selection, index) ->
|
|
return unless selection.isCollapsed
|
|
node = selection.anchorNode
|
|
selection.setBaseAndExtent(node, index, node, index)
|
|
|
|
moveSelectionToEnd: (selection) ->
|
|
return unless selection.isCollapsed
|
|
node = DOMUtils.findLastTextNode(selection.anchorNode)
|
|
index = node.length
|
|
selection.setBaseAndExtent(node, index, node, index)
|
|
|
|
getSelectionRectFromDOM: (selection) ->
|
|
selection ?= document.getSelection()
|
|
node = selection.anchorNode
|
|
if node.nodeType is Node.TEXT_NODE
|
|
r = document.createRange()
|
|
r.selectNodeContents(node)
|
|
return r.getBoundingClientRect()
|
|
else if node.nodeType is Node.ELEMENT_NODE
|
|
return node.getBoundingClientRect()
|
|
else
|
|
return null
|
|
|
|
isSelectionInTextNode: (selection) ->
|
|
selection ?= document.getSelection()
|
|
return false unless selection
|
|
return selection.isCollapsed and selection.anchorNode.nodeType is Node.TEXT_NODE and selection.anchorOffset > 0
|
|
|
|
isAtTabChar: (selection) ->
|
|
selection ?= document.getSelection()
|
|
if DOMUtils.isSelectionInTextNode(selection)
|
|
return selection.anchorNode.textContent[selection.anchorOffset - 1] is "\t"
|
|
else return false
|
|
|
|
isAtBeginningOfDocument: (dom, selection) ->
|
|
selection ?= document.getSelection()
|
|
return false if not selection.isCollapsed
|
|
return false if selection.anchorOffset > 0
|
|
return true if dom.childNodes.length is 0
|
|
return true if selection.anchorNode is dom
|
|
firstChild = dom.childNodes[0]
|
|
return selection.anchorNode is firstChild
|
|
|
|
atStartOfList: ->
|
|
selection = document.getSelection()
|
|
anchor = selection.anchorNode
|
|
return false if not selection.isCollapsed
|
|
return true if anchor?.nodeName is "LI"
|
|
return false if selection.anchorOffset > 0
|
|
li = DOMUtils.closest(anchor, "li")
|
|
return unless li
|
|
return DOMUtils.isFirstChild(li, anchor)
|
|
|
|
# https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
|
|
# Only Elements (not Text nodes) have the `closest` method
|
|
closest: (node, selector) ->
|
|
if node instanceof HTMLElement
|
|
return node.closest(selector)
|
|
else if node?.parentNode
|
|
return DOMUtils.closest(node.parentNode, selector)
|
|
else return null
|
|
|
|
closestAtCursor: (selector) ->
|
|
selection = document.getSelection()
|
|
return unless selection?.isCollapsed
|
|
return DOMUtils.closest(selection.anchorNode, selector)
|
|
|
|
isInList: ->
|
|
li = DOMUtils.closestAtCursor("li")
|
|
list = DOMUtils.closestAtCursor("ul, ol")
|
|
return li and list
|
|
|
|
# Returns an array of all immediately adjacent nodes of a particular
|
|
# nodeName relative to the root. Includes the root if it has the correct
|
|
# nodeName.
|
|
#
|
|
# nodName is optional. if left blank it'll be the nodeName of the root
|
|
collectAdjacent: (root, nodeName) ->
|
|
nodeName ?= root.nodeName
|
|
adjacent = []
|
|
|
|
node = root
|
|
while node.nextSibling?.nodeName is nodeName
|
|
adjacent.push(node.nextSibling)
|
|
node = node.nextSibling
|
|
|
|
if root.nodeName is nodeName
|
|
adjacent.unshift(root)
|
|
|
|
node = root
|
|
while node.previousSibling?.nodeName is nodeName
|
|
adjacent.unshift(node.previousSibling)
|
|
node = node.previousSibling
|
|
|
|
return adjacent
|
|
|
|
getNodeIndex: (context, nodeToFind) =>
|
|
DOMUtils.findSimilarNodes(context, nodeToFind).indexOf nodeToFind
|
|
|
|
# We need to break each node apart and cache since the `selection`
|
|
# object will mutate underneath us.
|
|
isSameSelection: (newSelection, oldSelection, context) =>
|
|
return true if not newSelection?
|
|
return false if not oldSelection
|
|
return false if not newSelection.anchorNode? or not newSelection.focusNode?
|
|
|
|
anchorIndex = DOMUtils.getNodeIndex(context, newSelection.anchorNode)
|
|
focusIndex = DOMUtils.getNodeIndex(context, newSelection.focusNode)
|
|
|
|
anchorEqual = newSelection.anchorNode.isEqualNode oldSelection.startNode
|
|
anchorIndexEqual = anchorIndex is oldSelection.startNodeIndex
|
|
focusEqual = newSelection.focusNode.isEqualNode oldSelection.endNode
|
|
focusIndexEqual = focusIndex is oldSelection.endNodeIndex
|
|
if not anchorEqual and not focusEqual
|
|
# This means the newSelection is the same, but just from the opposite
|
|
# direction. We don't care in this case, so check the reciprocal as
|
|
# well.
|
|
anchorEqual = newSelection.anchorNode.isEqualNode oldSelection.endNode
|
|
anchorIndexEqual = anchorIndex is oldSelection.endNodeIndex
|
|
focusEqual = newSelection.focusNode.isEqualNode oldSelection.startNode
|
|
focusIndexEqual = focusIndex is oldSelection.startndNodeIndex
|
|
|
|
anchorOffsetEqual = newSelection.anchorOffset == oldSelection.startOffset
|
|
focusOffsetEqual = newSelection.focusOffset == oldSelection.endOffset
|
|
if not anchorOffsetEqual and not focusOffsetEqual
|
|
# This means the newSelection is the same, but just from the opposite
|
|
# direction. We don't care in this case, so check the reciprocal as
|
|
# well.
|
|
anchorOffsetEqual = newSelection.anchorOffset == oldSelection.focusOffset
|
|
focusOffsetEqual = newSelection.focusOffset == oldSelection.anchorOffset
|
|
|
|
if (anchorEqual and
|
|
anchorIndexEqual and
|
|
anchorOffsetEqual and
|
|
focusEqual and
|
|
focusIndexEqual and
|
|
focusOffsetEqual)
|
|
return true
|
|
else
|
|
return false
|
|
|
|
|
|
getRangeInScope: (scope) =>
|
|
selection = document.getSelection()
|
|
return null if not DOMUtils.selectionInScope(selection, scope)
|
|
try
|
|
range = selection.getRangeAt(0)
|
|
catch
|
|
console.warn "Selection is not returning a range"
|
|
return document.createRange()
|
|
range
|
|
|
|
selectionInScope: (selection, scope) ->
|
|
return false if not selection?
|
|
return false if not scope?
|
|
return (scope.contains(selection.anchorNode) and
|
|
scope.contains(selection.focusNode))
|
|
|
|
isEmptyBoudingRect: (rect) ->
|
|
rect.top is 0 and rect.bottom is 0 and rect.left is 0 and rect.right is 0
|
|
|
|
atEndOfContent: (selection, rootScope, containerScope) ->
|
|
containerScope ?= rootScope
|
|
if selection.isCollapsed
|
|
|
|
# We need to use `lastChild` instead of `lastElementChild` because
|
|
# we need to eventually check if the `selection.focusNode`, which is
|
|
# usually a TEXT node, is equal to the returned `lastChild`.
|
|
# `lastElementChild` will not return TEXT nodes.
|
|
#
|
|
# Unfortunately, `lastChild` can sometime return COMMENT nodes and
|
|
# other blank TEXT nodes that we don't want to compare to.
|
|
#
|
|
# For example, if you have the structure:
|
|
# <div>
|
|
# <p>Foo</p>
|
|
# </div>
|
|
#
|
|
# The div may have 2 childNodes and 1 childElementNode. The 2nd
|
|
# hidden childNode is a TEXT node with a data of "\n". I actually
|
|
# want to return the <p></p>.
|
|
#
|
|
# However, The <p> element may have 1 childNode and 0
|
|
# childElementNodes. In that case I DO want to return the TEXT node
|
|
# that has the data of "foo"
|
|
lastChild = DOMUtils.lastNonBlankChildNode(containerScope)
|
|
|
|
# Special case for a completely empty contenteditable.
|
|
# In this case `lastChild` will be null, but we are definitely at
|
|
# the end of the content.
|
|
if containerScope is rootScope
|
|
return true if containerScope.childNodes.length is 0
|
|
|
|
return false unless lastChild
|
|
|
|
# NOTE: `.contains` returns true if `lastChild` is equal to
|
|
# `selection.focusNode`
|
|
#
|
|
# See: http://ejohn.org/blog/comparing-document-position/
|
|
inLastChild = lastChild.contains(selection.focusNode)
|
|
|
|
# We should do true object identity here instead of `.isEqualNode`
|
|
isLastChild = lastChild is selection.focusNode
|
|
|
|
if isLastChild
|
|
if selection.focusNode?.length
|
|
atEndIndex = selection.focusOffset is selection.focusNode.length
|
|
else
|
|
atEndIndex = selection.focusOffset is 0
|
|
return atEndIndex
|
|
else if inLastChild
|
|
DOMUtils.atEndOfContent(selection, rootScope, lastChild)
|
|
else return false
|
|
|
|
else return false
|
|
|
|
lastNonBlankChildNode: (node) ->
|
|
lastNode = null
|
|
for childNode in node.childNodes by -1
|
|
if childNode.nodeType is Node.TEXT_NODE
|
|
if DOMUtils.isBlankTextNode(childNode)
|
|
continue
|
|
else
|
|
return childNode
|
|
else if childNode.nodeType is Node.ELEMENT_NODE
|
|
return childNode
|
|
else continue
|
|
return lastNode
|
|
|
|
findLastTextNode: (node) ->
|
|
return null unless node
|
|
return node if node.nodeType is Node.TEXT_NODE
|
|
for childNode in node.childNodes by -1
|
|
if childNode.nodeType is Node.TEXT_NODE
|
|
return childNode
|
|
else if childNode.nodeType is Node.ELEMENT_NODE
|
|
return DOMUtils.findLastTextNode(childNode)
|
|
else continue
|
|
return null
|
|
|
|
findFirstTextNode: (node) ->
|
|
return null unless node
|
|
return node if node.nodeType is Node.TEXT_NODE
|
|
for childNode in node.childNodes
|
|
if childNode.nodeType is Node.TEXT_NODE
|
|
return childNode
|
|
else if childNode.nodeType is Node.ELEMENT_NODE
|
|
return DOMUtils.findFirstTextNode(childNode)
|
|
else continue
|
|
return null
|
|
|
|
isBlankTextNode: (node) ->
|
|
return if not node?.data
|
|
# \u00a0 is
|
|
node.data.replace(/\u00a0/g, "x").trim().length is 0
|
|
|
|
findSimilarNodes: (context, nodeToFind) ->
|
|
nodeList = []
|
|
if nodeToFind.isEqualNode(context)
|
|
nodeList.push(context)
|
|
return nodeList
|
|
treeWalker = document.createTreeWalker context
|
|
while treeWalker.nextNode()
|
|
if treeWalker.currentNode.isEqualNode nodeToFind
|
|
nodeList.push(treeWalker.currentNode)
|
|
|
|
return nodeList
|
|
|
|
escapeHTMLCharacters: (text) ->
|
|
map =
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
text.replace /[&<>"']/g, (m) -> map[m]
|
|
|
|
# Checks to see if a particular node is visible and any of its parents
|
|
# are visible.
|
|
#
|
|
# WARNING. This is a fairly expensive operation and should be used
|
|
# sparingly.
|
|
nodeIsVisible: (node) ->
|
|
while node and node isnt window.document
|
|
style = window.getComputedStyle(node)
|
|
node = node.parentNode
|
|
continue unless style?
|
|
# NOTE: opacity must be soft ==
|
|
if style.opacity is 0 or style.opacity is "0" or style.visibility is "hidden" or style.display is "none"
|
|
return false
|
|
return true
|
|
|
|
# Finds all of the non blank node in a {Document} object or HTML string.
|
|
#
|
|
# - `elementOrHTML` a dom element or an HTML string. If passed a
|
|
# string, it will use `DOMParser` to convert it into a DOM object.
|
|
#
|
|
# "Non blank" is defined as any node whose `textContent` returns a
|
|
# whitespace string.
|
|
#
|
|
# It will also reject nodes we see are invisible due to basic CSS
|
|
# properties.
|
|
#
|
|
# Returns an array of DOM Nodes
|
|
nodesWithContent: (elementOrHTML) ->
|
|
nodes = []
|
|
if _.isString(elementOrHTML)
|
|
domParser = new DOMParser()
|
|
doc = domParser.parseFromString(elementOrHTML, "text/html")
|
|
allNodes = doc.body.childNodes
|
|
else if elementOrHTML?.childNodes
|
|
allNodes = elementOrHTML.childNodes
|
|
else return nodes
|
|
|
|
# We need to check `childNodes` instead of `children` to look for
|
|
# plain Text nodes.
|
|
for node in allNodes by -1
|
|
if node.nodeName is "IMG"
|
|
nodes.unshift node
|
|
|
|
# It's important to use `textContent` and NOT `innerText`.
|
|
# `innerText` causes a full reflow on every call because it
|
|
# calcaultes CSS styles to determine if the text is truly visible or
|
|
# not. This utility method must NOT cause a reflow. We instead will
|
|
# check for basic cases ourselves.
|
|
if (node.textContent ? "").trim().length is 0
|
|
continue
|
|
|
|
if node.style?.opacity is 0 or node.style?.opacity is "0" or node.style?.visibility is "hidden" or node.style?.display is "none"
|
|
continue
|
|
|
|
nodes.unshift node
|
|
|
|
# No nodes with content found!
|
|
return nodes
|
|
|
|
parents: (node) ->
|
|
nodes = []
|
|
nodes.unshift(node) while node = node.parentNode
|
|
return nodes
|
|
|
|
# Returns true if the node is the first child of the root, is the root,
|
|
# or is the first child of the first child of the root, etc.
|
|
isFirstChild: (root, node) ->
|
|
return false unless root and node
|
|
return true if root is node
|
|
return false unless root.childNodes[0]
|
|
return true if root.childNodes[0] is node
|
|
return DOMUtils.isFirstChild(root.childNodes[0], node)
|
|
|
|
commonAncestor: (nodes=[]) ->
|
|
nodes = Array::slice.call(nodes)
|
|
|
|
minDepth = Number.MAX_VALUE
|
|
# Sometimes we can potentially have tons of REALLY deeply nested
|
|
# nodes. Since we're looking for a common ancestor we can really speed
|
|
# this up by keeping track of the min depth reached. We know that we
|
|
# won't need to check past that.
|
|
parents = ->
|
|
nodes = []
|
|
depth = 0
|
|
while node = node.parentNode
|
|
nodes.unshift(node)
|
|
depth += 1
|
|
if depth > minDepth then break
|
|
minDepth = Math.min(minDepth, depth)
|
|
return nodes
|
|
|
|
# _.intersection will preserve the ordering of the parent node arrays.
|
|
# parents are ordered top to bottom, so the last node is the most
|
|
# specific common ancenstor
|
|
_.last(_.intersection.apply(null, nodes.map(DOMUtils.parents)))
|
|
|
|
scrollAdjustmentToMakeNodeVisibleInContainer: (node, container) ->
|
|
return unless node
|
|
nodeRect = node.getBoundingClientRect()
|
|
containerRect = container.getBoundingClientRect()
|
|
return @scrollAdjustmentToMakeRectVisibleInRect(nodeRect, containerRect)
|
|
|
|
scrollAdjustmentToMakeRectVisibleInRect: (nodeRect, containerRect) ->
|
|
distanceBelowBottom = (nodeRect.top + nodeRect.height) - (containerRect.top + containerRect.height)
|
|
if distanceBelowBottom >= 0
|
|
return distanceBelowBottom
|
|
|
|
distanceAboveTop = containerRect.top - nodeRect.top
|
|
if distanceAboveTop >= 0
|
|
return -distanceAboveTop
|
|
|
|
return 0
|
|
|
|
# This allows pretty much everything except:
|
|
# script, embed, head, html, iframe, link, style, base
|
|
# Comes form React's support HTML elements: https://facebook.github.io/react/docs/tags-and-attributes.html
|
|
permissiveTags: -> ["a", "abbr", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blockquote", "body", "br", "button", "canvas", "caption", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "div", "dl", "dt", "em", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "i", "img", "input", "ins", "kbd", "keygen", "label", "legend", "li", "main", "map", "mark", "menu", "menuitem", "meta", "meter", "nav", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "small", "source", "span", "strong", "sub", "summary", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "u", "ul", "var", "video", "wbr"]
|
|
|
|
# Comes form React's support HTML elements: https://facebook.github.io/react/docs/tags-and-attributes.html
|
|
# Removed: class
|
|
allAttributes: [ 'abbr', 'accept', 'acceptcharset', 'accesskey', 'action', 'align', 'alt', 'async', 'autocomplete', 'axis', 'border', 'bgcolor', 'cellpadding', 'cellspacing', 'char', 'charoff', 'charset', 'checked', 'classid', 'classname', 'colspan', 'cols', 'content', 'contenteditable', 'contextmenu', 'controls', 'coords', 'data', 'datetime', 'defer', 'dir', 'disabled', 'download', 'draggable', 'enctype', 'form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'frame', 'frameborder', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'htmlfor', 'httpequiv', 'icon', 'id', 'label', 'lang', 'list', 'loop', 'low', 'manifest', 'marginheight', 'marginwidth', 'max', 'maxlength', 'media', 'mediagroup', 'method', 'min', 'multiple', 'muted', 'name', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'poster', 'preload', 'radiogroup', 'readonly', 'rel', 'required', 'role', 'rowspan', 'rows', 'rules', 'sandbox', 'scope', 'scoped', 'scrolling', 'seamless', 'selected', 'shape', 'size', 'sizes', 'sortable', 'sorted', 'span', 'spellcheck', 'src', 'srcdoc', 'srcset', 'start', 'step', 'style', 'summary', 'tabindex', 'target', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wmode' ]
|
|
|
|
# Allows any attribute on any tag.
|
|
permissiveAttributes: ->
|
|
allAttrMap = {}
|
|
for tag in DOMUtils.permissiveTags()
|
|
allAttrMap[tag] = DOMUtils.allAttributes
|
|
return allAttrMap
|
|
|
|
module.exports = DOMUtils
|