Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow groups of attributes to be set via a toolbar dialog #1197

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
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