Skip to content
4 changes: 4 additions & 0 deletions src/test/test_helpers/test_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const testGroup = function (name, options, callback) {
}

const beforeEach = async () => {
window.onbeforeunload = () => {} // Inhibit Chrome page reload errors in testing

// Ensure window is active on CI so focus and blur events are natively dispatched
window.focus()

Expand All @@ -33,6 +35,8 @@ export const testGroup = function (name, options, callback) {
}

const afterEach = () => {
window.onbeforeunload = null

if (template != null) setFixtureHTML("")
return teardown?.()
}
Expand Down
10 changes: 10 additions & 0 deletions src/trix/config/text_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ export default {
}
},
},
target: {
groupTagName: "a",
parser(element) {
const matchingSelector = `a:not(${attachmentSelector})`
const link = element.closest(matchingSelector)
if (link) {
return link.getAttribute("target")
}
},
},
strike: {
tagName: "del",
inheritable: true,
Expand Down
80 changes: 57 additions & 23 deletions src/trix/controllers/toolbar_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ const dialogSelector = "[data-trix-dialog]"
const activeDialogSelector = `${dialogSelector}[data-trix-active]`
const dialogButtonSelector = `${dialogSelector} [data-trix-method]`
const dialogInputSelector = `${dialogSelector} [data-trix-input]`
const getInputForDialog = (element, attributeName) => {
if (!attributeName) { attributeName = getAttributeName(element) }
return element.querySelector(`[data-trix-input][name='${attributeName}']`)
const getInputForDialog = (element, dialogName, attributeName) => {
const name = dialogName === attributeName ? dialogName : `${dialogName}[${attributeName}]`
return element.querySelector(`[data-trix-input][name='${name}']`)
}
const getActionName = (element) => element.getAttribute("data-trix-action")
const getAttributeName = (element) => {
return element.getAttribute("data-trix-attribute") || element.getAttribute("data-trix-dialog-attribute")
return element.getAttribute("data-trix-attribute")
}
const getDialogName = (element) => element.getAttribute("data-trix-dialog")
const getDialogAttributeNames = (element) => {
return (element.getAttribute("data-trix-dialog-attribute") ||
element.getAttribute("data-trix-dialog-attributes")).split(" ")
}

export default class ToolbarController extends BasicObject {
constructor(element) {
Expand Down Expand Up @@ -94,7 +98,7 @@ export default class ToolbarController extends BasicObject {
event.preventDefault()
const attribute = element.getAttribute("name")
const dialog = this.getDialog(attribute)
this.setAttribute(dialog)
this.setAttributes(dialog)
}
if (event.keyCode === 27) {
// Escape key
Expand Down Expand Up @@ -190,37 +194,67 @@ export default class ToolbarController extends BasicObject {
disabledInput.removeAttribute("disabled")
})

const attributeName = getAttributeName(element)
if (attributeName) {
const input = getInputForDialog(element, dialogName)
const attributeNames = getDialogAttributeNames(element)
for (const attributeName of attributeNames) {
const input = getInputForDialog(element, dialogName, attributeName)
if (input) {
input.value = this.attributes[attributeName] || ""
input.select()
switch (input.type) {
case "checkbox":
input.checked = this.attributes[attributeName] === input.value
break
default:
input.value = this.attributes[attributeName] || ""
input.select()
}
}
}

return this.delegate?.toolbarDidShowDialog(dialogName)
}

setAttribute(dialogElement) {
const attributeName = getAttributeName(dialogElement)
const input = getInputForDialog(dialogElement, attributeName)
if (input.willValidate && !input.checkValidity()) {
input.setAttribute("data-trix-validate", "")
input.classList.add("trix-validate")
return input.focus()
} else {
this.delegate?.toolbarDidUpdateAttribute(attributeName, input.value)
return this.hideDialog()
setAttributes(dialogElement) {
const dialogName = getDialogName(dialogElement)
const attributeNames = getDialogAttributeNames(dialogElement)

for (const attributeName of attributeNames) {
const input = getInputForDialog(dialogElement, dialogName, attributeName)

if (input.willValidate && !input.checkValidity()) {
input.setAttribute("data-trix-validate", "")
input.classList.add("trix-validate")
input.focus()
} else {
switch (input.type) {
case "checkbox":
if (input.checked) {
this.delegate?.toolbarDidUpdateAttribute(attributeName, input.value)
} else {
this.delegate?.toolbarDidRemoveAttribute(attributeName)
}
break
default:
this.delegate?.toolbarDidUpdateAttribute(attributeName, input.value)
}
}
}

this.hideDialog()
}

removeAttribute(dialogElement) {
const attributeName = getAttributeName(dialogElement)
this.delegate?.toolbarDidRemoveAttribute(attributeName)
setAttribute(dialogElement) { this.setAttributes(dialogElement) }

removeAttributes(dialogElement) {
const attributeNames = getDialogAttributeNames(dialogElement)

for (const attributeName of attributeNames) {
this.delegate?.toolbarDidRemoveAttribute(attributeName)
}

return this.hideDialog()
}

removeAttribute(dialogElement) { this.removeAttributes(dialogElement) }

hideDialog() {
const element = this.element.querySelector(activeDialogSelector)
if (element) {
Expand Down
2 changes: 1 addition & 1 deletion src/trix/models/html_sanitizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import BasicObject from "trix/core/basic_object"

import { nodeIsAttachmentElement, removeNode, tagName, walkTree } from "trix/core/helpers"

const DEFAULT_ALLOWED_ATTRIBUTES = "style href src width height language class".split(" ")
const DEFAULT_ALLOWED_ATTRIBUTES = "style href target src width height language class".split(" ")
const DEFAULT_FORBIDDEN_PROTOCOLS = "javascript:".split(" ")
const DEFAULT_FORBIDDEN_ELEMENTS = "script iframe form noscript".split(" ")

Expand Down
13 changes: 10 additions & 3 deletions src/trix/views/piece_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,24 @@ export default class PieceView extends ObjectView {
}

createContainerElement() {
const attributes = {}
let groupTagName

for (const key in this.attributes) {
const value = this.attributes[key]
const config = getTextConfig(key)
if (config) {
if (config.groupTagName) {
const attributes = {}
attributes[key] = value
return makeElement(config.groupTagName, attributes)
groupTagName = groupTagName || config.groupTagName

if (config.groupTagName === groupTagName) {
attributes[key] = value
}
}
}
}

if (groupTagName) { return makeElement(groupTagName, attributes) }
}

preserveSpaces(string) {
Expand Down