Skip to content

Instantly share code, notes, and snippets.

@putzwasser
Created April 22, 2020 21:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save putzwasser/289cdc3fece578912756003c187ead68 to your computer and use it in GitHub Desktop.
Save putzwasser/289cdc3fece578912756003c187ead68 to your computer and use it in GitHub Desktop.
LinkEditor rel attribute extension
export default function findLinkRange(position, value, model) {
return model.createRange(
_findBound(position, value, true, model),
_findBound(position, value, false, model)
);
}
function _findBound(position, value, lookBack, model) {
let node =
position.textNode ||
(lookBack ? position.nodeBefore : position.nodeAfter);
let lastNode = null;
while (node && node.getAttribute("linkHref") == value) {
lastNode = node;
node = lookBack ? node.previousSibling : node.nextSibling;
}
return lastNode
? model.createPositionAt(lastNode, lookBack ? "before" : "after")
: position;
}
require('./manifest');
import {Command} from 'ckeditor5-exports';
import findLinkRange from './findLinkRange';
import toMap from './toMap';
export default class LinkAttributeCommand extends Command {
constructor(editor, attributeKey) {
super(editor);
this.attributeKey = attributeKey;
}
refresh() {
const model = this.editor.model;
const doc = model.document;
this.value = doc.selection.getAttribute(this.attributeKey);
this.isEnabled = model.schema.checkAttributeInSelection(doc.selection, this.attributeKey);
}
execute(value) {
const model = this.editor.model;
const doc = model.document;
const selection = doc.selection;
const toggleMode = value === undefined;
value = toggleMode ? !this.value : value;
model.change(writer => {
if (toggleMode && !value) {
const rangesToUnset = selection.isCollapsed ?
[findLinkRange(selection.getFirstPosition(), selection.getAttribute('linkHref'), model)] : selection.getRanges();
for (const range of rangesToUnset) {
writer.removeAttribute(this.attributeKey, range);
}
} else if (selection.isCollapsed) {
const position = selection.getFirstPosition();
if (selection.hasAttribute('linkHref')) {
const linkRange = findLinkRange(selection.getFirstPosition(), selection.getAttribute('linkHref'), model);
if (value === false) {
writer.removeAttribute(this.attributeKey, linkRange);
} else {
writer.setAttribute(this.attributeKey, value, linkRange);
writer.setSelection(linkRange);
}
} else if (value !== '') {
const attributes = toMap(selection.getAttributes());
attributes.set(this.attributeKey, value);
const node = writer.createText(value, attributes);
writer.insert(node, position);
writer.setSelection(Range.createOn(node));
}
} else {
const ranges = model.schema.getValidRanges(selection.getRanges(), this.attributeKey);
for (const range of ranges) {
if (value === false) {
writer.removeAttribute(this.attributeKey, range);
} else {
writer.setAttribute(this.attributeKey, value, range);
}
}
}
});
}
}
import React, { PureComponent } from "react";
import PropTypes from "prop-types";
import { $get, $transform } from "plow-js";
import {connect} from "react-redux";
import {selectors} from "@neos-project/neos-ui-redux-store";
import { executeCommand } from "@neos-project/neos-ui-ckeditor5-bindings";
import { SelectBox } from '@neos-project/neos-ui-editors';
const optionValues = {
nofollow: {
label: 'nofollow',
group: 'Annotation'
},
noopener: {
label: 'noopener',
group: 'Annotation'
},
noreferrer: {
label: 'noreferrer',
group: 'Annotation'
},
sponsored: {
label: 'sponsored',
group: 'Annotation'
},
ugc: {
label: 'user generated content',
group: 'Annotation'
},
alternate: {
label: 'alternate',
group: 'Relation'
},
author: {
label: 'author',
group: 'Relation'
},
bookmark: {
label: 'bookmark',
group: 'Relation'
},
help: {
label: 'help document',
group: 'Relation'
},
license: {
label: 'copyright information',
group: 'Relation'
},
prev: {
label: 'previous document',
group: 'Relation'
},
next: {
label: 'next document',
group: 'Relation'
},
tag: {
label: 'keyword/tag',
group: 'Relation'
}
};
@connect(
$transform({
formattingUnderCursor: selectors.UI.ContentCanvas.formattingUnderCursor
})
)
export default class LinkEditorOptions extends PureComponent {
static propTypes = {
formattingUnderCursor: PropTypes.objectOf(PropTypes.oneOfType([
PropTypes.array,
PropTypes.string,
PropTypes.object
])),
linkingOptions: PropTypes.object
};
getLinkRelAttributeValue() {
return $get("linkRelAttribute", this.props.formattingUnderCursor) || "";
}
handleLinkRelChange = value => {
executeCommand('linkRelAttribute', value, false);
}
render() {
return $get('linkRelAttribute', this.props.linkingOptions) ? (
<div style={{ flexGrow: 1 }}>
<div style={{ padding: 8 }}>
Rel attribute
<SelectBox
options={{ values: optionValues, allowEmpty: true, multiple: true, placeholder: 'Set link relation...' }}
commit={value => {
executeCommand('linkRelAttribute', value, false);
}}
value={this.getLinkRelAttributeValue()}
/>
</div>
</div>
) : null;
}
}
import { Plugin } from "ckeditor5-exports";
import LinkAttributeCommand from "./LinkAttributeCommand";
import { downcastAttributeToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
import { upcastAttributeToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
const REL = 'linkRelAttribute';
export default class LinkRelAttributePlugin extends Plugin {
static get pluginName() {
return "LinkRelAttribute";
}
init() {
const editor = this.editor;
editor.model.schema.extend('$text', { allowAttributes: REL });
this.editor.commands.get("unlink").on("execute", (evt, args) => {
editor.execute("removeRelAttribute");
});
editor.conversion.for("downcast").add(downcastAttributeToElement({
model: REL,
view: (attributeValue, writer) => {
// the priority has got to be the same as here so the elements would get merged:
// https://github.com/ckeditor/ckeditor5-link/blob/20e96361014fd13bfb93620f5eb5f528e6b1fe6d/src/utils.js#L33
const linkElement = writer.createAttributeElement(
"a",
attributeValue ? { rel: attributeValue.join(' ') } : {},
{ priority: 5 }
);
return linkElement;
},
converterPriority: 'low'
}));
editor.conversion.for('upcast').add(upcastAttributeToAttribute({
view: {
name: 'a',
key: 'rel'
},
model: 'linkTarget',
converterPriority: 'low'
}));
editor.commands.add(REL, new LinkAttributeCommand(this.editor, REL));
}
}
import manifest from "@neos-project/neos-ui-extensibility";
import LinkRelAttributePlugin from "./LinkRelAttributePlugin";
import LinkEditorOptions from "./LinkEditorOptions";
import {$add, $get} from "plow-js";
const addPlugin = (Plugin, isEnabled) => (ckEditorConfiguration, options) => {
if (!isEnabled || isEnabled(options.editorOptions, options)) {
ckEditorConfiguration.plugins = ckEditorConfiguration.plugins || [];
return $add("plugins", Plugin, ckEditorConfiguration);
}
return ckEditorConfiguration;
};
manifest('Oevre.EditorPlugins:LinkEditor', {}, globalRegistry => {
const ckEditorRegistry = globalRegistry.get('ckEditor5');
const config = ckEditorRegistry.get("config");
config.set(
"linkRelAttribute",
addPlugin(LinkRelAttributePlugin, a => $get("linking.linkRelAttribute", a))
);
const containerRegistry = globalRegistry.get("containers");
containerRegistry.set(
"LinkInput/OptionsPanel/LinkEditorRelAttribute",
LinkEditorOptions
);
});
export default function objectToMap(obj) {
const map = new Map();
for (const key in obj) {
map.set(key, obj[key]);
}
return map;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment