diff --git a/src/test/test_helpers/test_helpers.js b/src/test/test_helpers/test_helpers.js index f51dc3566..cd28afc72 100644 --- a/src/test/test_helpers/test_helpers.js +++ b/src/test/test_helpers/test_helpers.js @@ -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() @@ -33,6 +35,8 @@ export const testGroup = function (name, options, callback) { } const afterEach = () => { + window.onbeforeunload = null + if (template != null) setFixtureHTML("") return teardown?.() } diff --git a/src/trix/config/text_attributes.js b/src/trix/config/text_attributes.js index 8ac432a73..c727912fa 100644 --- a/src/trix/config/text_attributes.js +++ b/src/trix/config/text_attributes.js @@ -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, diff --git a/src/trix/controllers/toolbar_controller.js b/src/trix/controllers/toolbar_controller.js index a1178fea7..3d62b8003 100644 --- a/src/trix/controllers/toolbar_controller.js +++ b/src/trix/controllers/toolbar_controller.js @@ -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) { @@ -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 @@ -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) { diff --git a/src/trix/models/html_sanitizer.js b/src/trix/models/html_sanitizer.js index 3dd0b3e68..19df347c5 100644 --- a/src/trix/models/html_sanitizer.js +++ b/src/trix/models/html_sanitizer.js @@ -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(" ") diff --git a/src/trix/views/piece_view.js b/src/trix/views/piece_view.js index c03d6c266..6d7a70cc4 100644 --- a/src/trix/views/piece_view.js +++ b/src/trix/views/piece_view.js @@ -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) {