Jump to content

Matt Finger

Invision Community Team
  • Posts

    110
  • Joined

  • Last visited

  • Days Won

    6

Matt Finger last won the day on June 23

Matt Finger had the most liked content!

About Matt Finger

Recent Profile Visitors

The recent visitors block is disabled and is not being shown to other users.

Matt Finger's Achievements

  1. Several questions have arisen regarding the v5 Editor Extension system. While we have yet to create a fully fleshed out dev docs center, I wanted to share a tentative, high-level rundown of what's possible. What you need Before continuing, I want to state this material is intended for developers. It is expected you have prerequisite knowledge of JavaScript, and have also read through and understand the tiptap core concepts. Lastly, I want to restate the extension framework is a work in progress subject to change, and therefore the specifics in this post are all tentative, whereas the final v5 docs will be posted elsewhere. Introduction It is possible to add 3rd party plugins which have direct access to the Tiptap APIs from the following editor methods ips.ui.editorv5.registerExtension(name, definition) ips.ui.editorv5.registerNode(name, definition) ips.ui.editorv5.registerMark(name, definition) The definition is forwarded to the corresponding Tiptap extension generator Extension.create(definition), Node.create(definition) or Mark.create(definition). Several important concepts and differences between the Tiptap definitions and the ones in ips.ui.editorv5 are noted below Add Buttons Perhaps the most important addition to the underlying Tiptap definition is the ability to specify an addButtons() method which adds buttons to the toolbar The method should return an object mapping strings to ButtonDefinitions, for example const buttonDef = { addButtons() { return { test1: { html: '<div>Red background</div>', command: ({commands}) => commands.toggleMark(this.name), locations: [ { after: 'bold'} ], isAvailable: editor => editor.can().setMark(this.name), isActive: editor => editor.isActive('bold') } } } } Button Lang Strings Make sure to register a lang string for the button either in the jslang.php file of the app or by calling ips.setString(key, value) directly. This string is used for the tooltip on the button, set it to an empty string to remove the tooltip. The button lang key is generated as follows ipsCustom{ExtensionType}__{ExtensionName}_{ButtonKey} where ExtensionType is Extension, Node or Mark and is determined by the type of plugin ExtensionName is the name you passed to registerExtension, registerNode or registerMark ButtonKey is the key in the object returned by addButtons() Button Content There are 2 ways to add content to a button: ButtonDefinition.html - String - This string is parsed, sanitized then set as the innerHTML of the button in the toolbar. All attributes except 'class', 'style', 'id', 'src', 'title', 'rel', 'alt' are removed All the following elements are removed: 'script', 'link', 'style', 'pre', 'code', 'iframe', 'object', 'embed', 'noscript', 'video' Only the first element is preserved. For ex, <div>some content></div><img /> would have to be wrapped in another element ButtonDefinition.element - HTMLElement - Alternatively, you can use a HTMLElement instance. It is sanitized in the same way as the html Also, note that, even if you specify an HTMLElement, only a copy of its outerHTML is inserted in the toolbar, so things like event listeners or references to the element will not return the element in the toolbar ButtonDefinition.command - function({...CommandProps, event?: MouseEvent}):void Called whenever the button is clicked. This function is passed the arguments object provided by editor.commands.command(props), with the additional property event which is the click event. ButtonDefinition.isActive - function(Editor):boolean|boolean|undefined This is called to determine if the button should have an 'active' class applied. It's passed the editor instance. Alternatively, you can pass a boolean to make it always active or inactive. If omitted, the button will never have the 'active' styling ButtonDefinition.isAvailable - function(editor):boolean|boolean|undefined This is called to determine if the button should be available in the toolbar. Like the isActive property, it gets passed the editor and returns a boolean ButtonDefinition.locations - (LocationDef|Location|null)[] The locations determine where the button will appear. Each element should be either the Location directly or a LocationDef. You null is interpreted as "put this at the end of the toolbar" Omit the locations array to add the button to the end of the toolbar Location - string - A location is a string representing a button key of another button in the toolbar. You can also put the button in dropdown toolbars using the following strings as a Location align__nested - the text align options format__nested - the extra options that exist inside the ... dropdown insert__nested - the + insert button (where the boxes and quotes natively live) LocationDef - Object LocationDef.before - Location|undefined - Optional: The key of another item in that toolbar that this button should be placed before. If there is no button with the provided key, the button is added to the end of the toolbar. LocationDef.after - Location|undefined - Optional: Same as before, except this button will appear after the provided button. If you define both LocationDef.before and LocationDef.after, the after rule will apply unless that item is nonexistent See the [data-toolbar-item] attribute of items in the toolbar to get the key of items. Use traditional JS functions All the root-level methods of the definition object get bound to the built extension, so it is very important you use traditional functions instead of arrow functions for the root-level methods. For example, the following would not work ips.ui.editorv5.registerNode('myextension', { addButtons: () => ({ makeRed: { html: "<div>make red</div>", isActive: editor => editor.isActive(this.name), ... } }) }) But this would ips.ui.editorv5.registerNode('myextension', { addButtons() { return { makeRed: { html: "<div>make red</div>", isActive: editor => editor.isActive(this.name), ... } } } }) Conversely, it is recommended to use arrow functions instead of traditional functions for any closures (functions created inside the root-level methods). Doing it this way ensures the this keyword is preserved. For example ips.ui.editorv5.registerNode('myextension', { addKeyboardShortcuts() { return { // BAD Enter({editor}) { return editor.commands.toggleMark(this.name) // `this` may not be defined as expected }, // GOOD Escape: ({editor}) => { return editor.commands.unsetMark(this.name) // `this` refers to the extension definition itself } } } }) The plugin name Unlike the Tiptap API, you don't need to pass a name in the definition itself. The editor method will automatically add the name depending on the extension type (Node, Mark, or Extension) and the name argument of the register<Node|Mark|Extension>() method. That being said, keep in mind You should be especially careful to make sure your name is unique. There can only be one extension of a given type with the same name. Use this.name in the definition methods rather than the string name you give. All the root-level methods of the definition are bound to the generated extension so no worries about this not being defined Be careful adding __ (double underscores) to the name, or to the restrictions (see below) for that matter. This can potentially lead to conflicting lang strings... for example if the extension is my__extension, and someone creates another extension named my with a restriction extension, the restriction's lang string will likely conflict with this extensions' lang string. Restrictions The Editor has 3 possible states: Minimal, Standard and Advanced. Community Admins can configure which user groups use which state, and can add restrictions to a given state level. For example, the feature "convert links to iframes" can be restricted to the Advanced Editor; in this case, the raw_embed restriction will be present unless the Advanced Editor is used. From the perspective of apps, however, each restriction is a string, and you can see if a restriction is active by checking editor.options.restrictions.has(restriction) (editor.options.restrictions is of the type Set<string>). When a restriction is present the functionality corresponding to it should be disabled. The editor has no knowledge whether it's in the Minimal, Standard or Advanced state, but only whether the restriction string is present. You can define restrictions for your extension by adding an array of strings as the property restrictions in your definition. Every restriction Must contain only letters, numbers dashes and underscores. Gets prefixed before passing to internal handlers with ipsCustom(Node|Mark|Extension)__{extension_name}__, where Node|Mark|Extension corresponds to the extension type, and extension_name is the name arg passed when registering the extension. It is a lot to type but this ensures there won't be conflicts with other restrictions. This prefixed name is also what gets added to the set editor.options.restrictions Must have a lang string containing the human-readable name registered. The lang string tells admins what the feature does, and a warning is raised when there is not one. This can be added either to jslang.php in an app before building it, or by calling ips.setString(restrictionKey, 'Human Readable Name') For example, in the js: ips.ui.editorv5.registerNode('myNode', { restrictions: [ "can_add" ], addButtons() { return { addSomething: { isAvailable: editor => !editor.options.restrictions.has('ipsCustomNode__myNode__can_add'), html: '+', } } } }) To add the lang string in the JS, you'd add this before or immediately after registering the node ips.setString("ipsCustomNode__myCustomNode__can_add", "Can add myNode nodes?"); Or, ideally, in jslang.php, add $lang = array( // ... other keys ... "ipsCustomNode__myCustomNode__can_add" => "Can add myNode nodes?", ); A note on Privileges vs Restrictions A privilege-based system assumes a feature is disabled by default, whereas a restriction-based system assumes it's enabled by default. The Editor Restriction System is, well, restriction-based. Therefore, though it is technically possible to use a restriction as a privilege, where a feature is disabled unless the restriction is present, it is strongly discouraged because it creates a counter-intuitive UI for Admins: it will disappear when a user is less restricted. The Empty Restriction You can also define a restriction with an empty string as the unprefixed key. When this restriction is present, the extension isn't added to the editor instance. For every other key, the onus is on the developer to ensure the restriction is enforced accordingly. ips.ui.editorv5.registerExtension('restrictibleExtension', { restrictions: [ "" ] // "" disables the extension altogether }) ips.setString('ipsCustomExtension__restrictibleExtension__', "Can use the restrictible third party extension") Descriptions For any given extension restriction lang string, you can add a more detailed restriction by creating a lang string with the same key suffixed by :description. This description appears in the AdminCP Restriction Configuration Form. For example, if the restriction itself after prefixing is ipsCustomNode__mynode__, the description would be stored in the lang string ipsCustomNode__mynode__:description. HTML Escaping Lastly, it is also worth noting, for security purposes, any html inside the restriction name or description will be sanitized. For example, the lang definition "<code>" is inserted in the AdminCP Restriction Form's source HTML as &lt;code&gt; IPS Plugins are Sandboxed All the IPS plugin definitions are sandboxed, meaning any properties in the definition that are not whitelisted will be removed. The biggest property that is removed is addProseMirrorPlugins(), but any property not in the following keys will be removed [ 'name', 'group', 'isInline', 'isBlock', 'isText', 'selectable', 'exitable', 'atom', 'addOptions', 'addKeyboardShortcuts', 'addAttributes', 'addGlobalAttributes', 'renderHTML', 'parseHTML', 'onCreate', 'onUpdate', 'onBeforeCreate', 'onFocus', 'onBlur', 'onBeforeCreate', 'onSelectionUpdate', 'onDestroy', 'onTransaction', 'priority', 'defaultOptions', 'addInputRules', 'marks', 'addNodeView', 'whitespace', 'defining', 'isolating', 'renderText' ] Priority The priority key's default value is higher than the Tiptap default. It is recommended to not specify this in the definition unless you know for sure you need to override other custom plugin commands, keyboard shortcuts, or HTML converters, there is no need to set this. How to load the code? Method 1: Using an App (best practice) In most cases, it's best to add your extension inside a custom app. In most cases, the extension will define a new type of content that is not whitelisted by the backend Parser, so it is lost during sanitization. This is why it is beneficial to use an app which has both the extension and a parser rule to whitelist the element/attributes. It also adds organization to your customization so that you won't forget where or how it's setup. To do this, simply put your plugins in *.js files in <your app dir>/dev/editor/. When the app is built and installed it is all compiled and loaded seamlessly on the front end Method 2: Using the ips:editorBeforeInit event For simpler plugins that do not require any new content types (for example a <span> with a style attribute), you can indeed add them inside a <script> tag anywhere in the page, with either inline or remote JS code. To ensure that your extension is registered after ips.ui.editorv5 is created but before the editor is actually created, hook onto the event ips:editorBeforeInit which is fired on the document. For example <script type="application/ecmascript"> document.addEventListener('ips:editorBeforeInit', () => { ips.ui.editorv5.registerMark('redBackground', { parseHTML() { return [{tag: 'span', getAttrs: node => node.style.backgroundColor === "red" && null}] }, renderHTML() { return ['span', {style: 'background-color: red'}, 0] }, ... }) }, {once: true}) // the once:true flag is very useful because it prevents the same code from needlessly running and replacing the extension over and over </script> This code can go anywhere, but directly in the theme is a good place to start. Note it gets cumbersome very quickly this way The actual Event is a CustomEvent instance that has the following in its detail property elem : HTMLElement - The element that has the data-ipseditorv5 attribute textarea: HTMLTextAreaElement - The actual textarea that is being replaced with an editor in a form content: string - The starting content of the editor. options: Object - The config that is about to be passed to the editor. You can modify this object to change the default configuration, but note that this is stateful - you'd have to read/update the options each time the event fires instead of just once like for registering extensions. Font family support It is possible, when listening to this event, to add or change supported font families. At the core, a font family consists of A main property. This is either a global family default (see below) or a string to be added as the first entry in the font-family CSS property An array of fallbacks. The entries, like main, are either a string or a global font family default The editor option ipsFontFamily.additionalFontDefinitions defines additional fonts as an object mapping the name to the font definition. For example, to add fonts from the ips:editorBeforeInit event document.addEventListener('ips:editorBeforeInit', e => { e.detail.options.ipsFontFamily = { additionalFontDefinitions: { "Comic Sans": { // the user will see "comic sans" in the dropdown main: "Comic Sans MS", // this is added to the CSS property fallbacks: [7] // 7 corresponds to 'sans-serif'. The compiled font family property will be `font-family: "Comic Sans MS", sans-serif; } } }; }) Extending Fonts For any builtin fonts, you can define them in your plugin to change their behavior. For example, if you have MS-specific versions of Helvetica installed on your community, you could set fallbacks: ['Helvetica MS']. This will make Helvetica MS the first fallback, while preserving the default fallbacks after it. Global Family Constants In css, certain keywords are accepted as the font family which will translate to dynamic CSS behavior or OS preset fonts. The following numbers can be used in lieu of a font string to embed these keywords. Note the type number MUST be used in the js, not the stringified object property shown below { "6": "serif", "7": "sans-serif", "8": "monospace", "9": "cursive", "10": "fantasy", "16": "emoji", "17": "math", "18": "fangsong" } Builtin support for unknown fonts The option ipsFontFamily.allowUnknownFonts works as follows. When processing HTML, if there exists a span[style] tag in the HTML that defines a font-family and the font is unknown, the editor will construct a temporary font family definition and preserve its value. That being said, the custom font will be forgotten should the user change the font to a builtin choice; to ensure an option remains visible, you'll need to define it in your extension. It is generally good practice to not allow these fonts as they will have varying support across devices. Example Plugin This is an example of an Extension which will register a Mark that creates <kbd> elements to denote keyboard shortcuts ips.ui.editorv5.registerMark('myKbdElement', { exitable: true, parseHTML() { return [{ tag: 'kbd' }] }, renderHTML() { return [ 'kbd', { style: ` border-color: var(--i-color_hard); background-color: var(--i-background_2)` }, 0 ] }, addButtons() { return { test1: { html: '<span><i class="fa-solid fa-keyboard"></i></span>', command: ({chain}) => chain().toggleMark(this.name).focus().run(), // This puts the button before bold, after the 'link' button, inside the `...` menu and at the end of the toolbar locations: [ { before: 'bold' }, 'link', 'format__nested', null ], isAvailable: editor => editor.can().setMark(this.name), isActive: editor => editor.isActive(this.name) } } }, addKeyboardShortcuts() { return { // "Mod" is command or ctrl, depending on system defaults; "Mod-Shift-K": ({editor}) => editor.chain().toggleMark(this.name).focus().run() } }, restrictions: [ "" ] }) // This sets the restriction string. Ideally, this should go in the jslang.php of your app but it is ok to do this here ips.setString("ipsCustomMark__myKbdElement__", "Can apply keyboard styles"); ips.setString("ipsCustomMark__myKbdElement_test1", "Keyboard Command"); Note that, in this case of <kbd> elements, you'd also have to add a parser rule via the AdminCP Dev Center to allow them to be saved.
  2. I see... what's going on is there is a keyboard shortcut that auto-inserts closing characters for code blocks, such as a closing ` or ]. It's getting confused because the keycodes which are usually a backtick or opening-square-bracket are being used for other characters, so I have to update that feature to use the character rather than the button's key code.
  3. I'm not sure what you mean as v5 will still embed youtube and the like immediately when you paste a link as Youtube runs on the OEmbed system. Additionally, like you mentioned, when enabled OG Embeds are used to replace a URL with a preview automatically. The only case where you need the link panel button is when you are creating a raw iframe... e.g. a basic <iframe> tag that just has a src and no wrappers or anything. The reason we didn't add an auto-embed feature for that type of iframe is it could catch a lot of links, and doesn't really reflect the intent behind pasting a URL. It's also a feature that is not really intended for regular members to use.
  4. Yeah, so in case anyone else is confused, the custom iframe feature starts from the link panel directly. Also worth noting, for security purposes, beyond whitelisting the domain, the url needs to use HTTPS protocol to work iframe-embed.mp4
  5. Yeah, there are whitespace preservation options in the editor that we need to adjust to support CKEditor4-created content. Hopefully this will be fixed in the next release.
  6. Would you happen to have any links to content behaving this way? You can PM me if you don't want to share publicly.
  7. So the "Insert image from URL" button has been removed in 5, and there is a global setting which can prevent this behavior. And, yes, the "embed external content" restriction also prevents external image URIs from being converted to images.
  8. These are on our list but will likely not make it in the initial Community 5 release. Tables sound simple enough, but when you factor everything that goes into delivering a powerful table system - background color, border color, border width, what type of content can go in a cell, what to do about overflow, actions applying to the entire row/column etc - and make it not only powerful but easy to use and reliable, the dev time really adds up. 🤔 😉
  9. You currently have to manually click the embed button in the link menu, but this only applies to the new custom iframes. For the predefined embed types, e.g. other topics, youtube, X, og embeds, it will automatically convert to an iframe.
  10. We recently announced the new Invision Community 5 editor which adds many new exciting features such as semantically correct header tags, custom boxes and more. As the new editor is a leap forward in technology, some legacy features had to be left behind. We received a lot of messages about these changes, and have created new tools based on that feedback to ensure you still have the tools you need. The new features are based around restricting some high level editor functionality for specific member groups and enabling an easy way to add custom embeds. Permission Levels Invision Community 5 puts a lot of new tools in the editor, including header tags, boxes and positioning tools. These are useful features, but perhaps you do not want your members changing the semantic structure of the page by adding H1 tags. Or maybe you don't want them being able to add custom boxes with colors. Based on this feedback, we have introduced a permission levels system. At the heart of the system lies three editor permission levels: Minimal, Standard and Advanced. Specific editor features are assigned to one or more levels. For example, you may only want header tags and content boxes to be for the 'advanced' permission level which only administrators can use. These permission levels are configurable via the Admin Control Panel. When is Each Restriction Level Used? Now that we have set up the permission levels, we need to apply them to member groups. We do this by simplying heading over to the Member Groups section of the Admin Control Panel. In the "Content" section of that form, there are two new options: Default Editor Restriction Level: This is the restriction level the group uses by default, for example in Forum Topics and Blog Posts. Editor Restriction Level for Comments: This is the level used for Comments (including Topic Replies) throughout the Community. When a member has multiple groups, they will use the most permissible editor setting out of all groups. Custom Embeds In response to news that the ability to toggle into 'source mode' and directly edit the underlying structure of the editor document was not implemented because editor technology has moved on, many people told us they used that feature to add custom iframes from specific services they use. We understood the need for custom embeds, and we've added the option to create iframe elements with any whitelisted URL from a link. CleanShot 2024-06-20 at 15.49.43.mp4 Additionally, iframes created this way have configurable height and width so you can resize to your liking This feature has two editor permissions: "Can Embed External Content," and "Can Convert Links to iframes". Adding iframes into a post can potentially be a security issue, so strong controls are needed to ensure there isn't abuse of this system. The editor will only allow links to be converted to iframes if the domain has been whitelisted. The whitelist exists in the new tab, Admin Control Panel > System > Posting & Editor > Embeds. The feature can also be entirely disabled from here. That wraps up this round of changes based on your comments. We hope that you enjoy this update to our Invision Community 5 editor and we always appreciate your feedback. View full blog entry
  11. We recently announced the new Invision Community 5 editor which adds many new exciting features such as semantically correct header tags, custom boxes and more. As the new editor is a leap forward in technology, some legacy features had to be left behind. We received a lot of messages about these changes, and have created new tools based on that feedback to ensure you still have the tools you need. The new features are based around restricting some high level editor functionality for specific member groups and enabling an easy way to add custom embeds. Permission Levels Invision Community 5 puts a lot of new tools in the editor, including header tags, boxes and positioning tools. These are useful features, but perhaps you do not want your members changing the semantic structure of the page by adding H1 tags. Or maybe you don't want them being able to add custom boxes with colors. Based on this feedback, we have introduced a permission levels system. At the heart of the system lies three editor permission levels: Minimal, Standard and Advanced. Specific editor features are assigned to one or more levels. For example, you may only want header tags and content boxes to be for the 'advanced' permission level which only administrators can use. These permission levels are configurable via the Admin Control Panel. When is Each Restriction Level Used? Now that we have set up the permission levels, we need to apply them to member groups. We do this by simplying heading over to the Member Groups section of the Admin Control Panel. In the "Content" section of that form, there are two new options: Default Editor Restriction Level: This is the restriction level the group uses by default, for example in Forum Topics and Blog Posts. Editor Restriction Level for Comments: This is the level used for Comments (including Topic Replies) throughout the Community. When a member has multiple groups, they will use the most permissible editor setting out of all groups. Custom Embeds In response to news that the ability to toggle into 'source mode' and directly edit the underlying structure of the editor document was not implemented because editor technology has moved on, many people told us they used that feature to add custom iframes from specific services they use. We understood the need for custom embeds, and we've added the option to create iframe elements with any whitelisted URL from a link. CleanShot 2024-06-20 at 15.49.43.mp4 Additionally, iframes created this way have configurable height and width so you can resize to your liking This feature has two editor permissions: "Can Embed External Content," and "Can Convert Links to iframes". Adding iframes into a post can potentially be a security issue, so strong controls are needed to ensure there isn't abuse of this system. The editor will only allow links to be converted to iframes if the domain has been whitelisted. The whitelist exists in the new tab, Admin Control Panel > System > Posting & Editor > Embeds. The feature can also be entirely disabled from here. That wraps up this round of changes based on your comments. We hope that you enjoy this update to our Invision Community 5 editor and we always appreciate your feedback.
  12. I concur. Again, if this can be reproduced let us know so hopefully we can sort it out but otherwise unfortunately there isn't anything we can do.
  13. Have you tried pasting the same URL as the users experiencing the issue, ideally using a test account that has the same groups/permissions? We do some very light processing on pasted URLs to automatically convert them into links or, when applicable, trigger an embed or attachment upload. If a JS error is thrown, or the embed validation somehow returns an empty embed it could cause this, though unfortunately without being able to reproduce there isn't much we can do. It's also possible a third party IC App or CKEditor Plugin could be interfering.
×
×
  • Create New...