-
Notifications
You must be signed in to change notification settings - Fork 63
Configuration API #504
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
Configuration API #504
Conversation
samuelpecher
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. I like that configuration can still be done on the element directly. Just a couple comments on attachment tag name and a potential listener issue.
Overall I think we should build a way to provide the config from Rails as well.
Lexical Extensions! Should we consider refactoring parts of Lexxy into a set of extensions?
This feels like the right direction! I was going to try it with the next feature to make sure we can still keep the nice OO-patterns established in Lexxy.
|
|
||
| createDOM() { | ||
| const figure = createElement("action-text-attachment", { "content-type": this.contentType, "data-lexxy-decorator": true }) | ||
| const figure = createElement(lexxyConfig.get("global.attachmentTagName"), { "content-type": this.contentType, "data-lexxy-decorator": true }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about passing the tag name to the Node constructor and keeping as internal node state rather than hooking into the config at every step?
Same would apply to ActionTextAttachmentNode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like it! I noticed you did the same for custom-upload-events, and considered including it here. But I also wanted to keep this minimal.
If you are expecting to merge custom-upload-events soonish I'd rather not introduce it here, otherwise I'm ok to include it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm splitting that branch into separate PRs back to main, so I'll handle that then 👌
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be a quite edge case but if we kept it in the internal node state, it would be copied/pasted carrying the tag, which assumes that the editor where you are pasting the node has the same override. I would keep it simple with a global access I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jorgemanrubia: the exchange format will be text/html unless the editors share the same namespace. The rendered HTML will include the rendered tag name and I think we should match that behavior.
Though I do propose that differently configured Lexxy editors shouldn't share a namespace and we should set it depending on the loaded config:
// ...
namespace: `Lexxy.${configName}`,
jorgemanrubia
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good spike @zachasme!
We need to add tests here. So far, Capybara has served us well. But here, to test the configuration interfaces and such, I think we could add jest to Lexxy. I would still rely on Capybara as our main testing mechanism, but it is nice to have more traditional unit testing available too. We could configure in the local CI script and in GH actions too. That would let us test the Configuration objects properly here.
I would still add some high level system tests to make sure the configurations work as expected. I would be fine with just testing the overrides in capybara, assuming the system is tested through jest. WDYT?
|
Thank you for the feedback @samuelpecher and @jorgemanrubia! 👌
Agreed. I wanted to hear your thoughts on the general approach before writing new tests.
Yes, I think adding jest is justified at this point. Though I'd rather tackle that in a separate PR. Until we introduce more granular options for toolbar/etc, I believe I can cover most of the configuration system with some high-level system tests. |
|
To me, the motivation of jest would be to test the system we are bootstrapping here, more than the granular options themselves. I think those tests belong to this PR. |
Makes sense. Alright, I'll set up jest here. @samuelpecher I'll give |
| return this.isEmpty || this.toString().match(/^\s*$/g) !== null | ||
| } | ||
|
|
||
| get preset() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you configure the element with config= it makes sense to me that this is get config instead of using a new name. We do that with other attributes lke name. Feels more consistent. Any reason to use a different name here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No reason, they should definitely be consistent. I've been back and forth on config/preset. I prefer preset, it feels more descriptive than the generic config.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But config is what we expose as the public API to be used by the editor users. What is confusing to me is using different terms here:
<lexxy-editor config="whatever"I would expect that if you do editorElement.config returns whatever. Now, we want to make that, if config is not set, then its default value is default, so that you can query editorElement.config consistently in any scenario. See what I mean?
We could also change the public api method to preset, but I think it is important that things remain consistent here. I like config slightly better because we are talking about configurations when defining these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could go with presets indeed at the configuration level. So the lexxy configuration could expose two different structures:
{
global: new Configuration(...)
presets: new Configuration({
default: {
}
...
})
}So that we use preset at the public API (setter) and as a getter in the element.
And we make global a explicit configuration object to query like config.global.get("tagName") or whatever. And same for the presets: config.presets.get("default.whatever") or just config.presets.default to grab the properties we want for a preset.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, for creating a editor-specific config we could do:
this.#config = new Configuration(lexxyConfig.presets.default)
this.#config.merge(this.#overrides)Thinking we could even extend the configuration constructor to admit a list of configurations (...configurations) that it will merge to avoid the explicit merge:
this.#config = new Configuration(lexxyConfig.presets.default, this.#overrides)I think that is pretty clean. The code expresses what we are trying to do in a single line.
322cbb7 to
c9d12d8
Compare
|
Thanks for the thorough feedback @jorgemanrubia! The PR might have been a bit too WIP, but the responses here are really clearing things up for me. It's slowly coming together nicely. I think this is an important discussion: { toolbar: true }
// or
{
toolbar: {
bold: true
}
}
I agree we should assume everything is on by default, fully defined by the default preset. But I also think we should allow users to configure({ default: { toolbar: false }})In cases where a bool is overriding a hash, I think it would be valuable to recursively write the bool into nested hashes. It will complicate the merge logic, but it will make Alternatively, we can keep the config class completely generic and rely on the rest of the codebase to handle these differences.
I think they both have merit. In isolation
I've also changed our workflow .yml to run on all pushes. Was there a specific reason we previously had: on:
pull_request:
push:
branches: [ main ] |
jorgemanrubia
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking tighter @zachasme 👏
src/test/dom_helper.js
Outdated
| @@ -0,0 +1,5 @@ | |||
| import { JSDOM } from "jsdom" | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I would prefer to keep test/ as a first level folder, parallel to src/. I think it is cleaner, so that it doesn't look like another system inside the source. I see that in trix it was inside, but in trix we also had a top level src/trix folder that we don't have here.
Also, I would move the dom_helper.js to a folder test/helpers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved them into test/javascript/. 👍
My thinking was we wanted them separate from the rails tests, but I agree this is cleaner.
|
|
||
| test("uses defaults", () => { | ||
| const element = createElement("<lexxy-editor></lexxy-editor>") | ||
| const config = new EditorConfiguration(element, "default") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the new implementation, we don't have the problem I mentioned of having tests altering the same global structure over and over. I would remove the second param (preset) in the editor configuration constructor, since we don't need to override the config for testing purposes anymore. In the tests, I would insert that preset at the element level createElement("<lexxy-editor preset="simple"></lexxy-editor>") or just omit it there if you want to pass the default.
const element = createElement("<lexxy-editor></lexxy-editor>")
const config = new EditorConfiguration(element)
// or, for a specific one:
const element = createElement("<lexxy-editor preset="simple"></lexxy-editor>")
const config = new EditorConfiguration(element)That way we would be testing the real thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. I kept the param because I had trouble getting jsdom to play nice with custom elements. I found a solution, though, and the param is gone.
src/config/lexxy.js
Outdated
| presets: new Configuration({ default: DEFAULT_PRESET }) | ||
| } | ||
|
|
||
| export function configure(changes) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a function to import, maybe configureLexxy instead of just configure would be more convenient.
The thing is that I would prefer an OO interface, so that you would do:
Lexxy.configure(...)
Lexxy.config.global
Lexxy.config.presets.get(...)I think that feels more consistent and offers a proper namespace for other stuff we may add down the road. This way also you don't have to figure out how to rename lexxy functions and identifiers: lexxyConfig, lexxyConfigure... they are properly namespaced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're talking public interface, we could keep the existing named exports and also provide a convenience default export, so the following would work the same:
import * as Lexxy from "lexxy"
// same as
import Lexxy from "lexxy"
// usage
Lexxy.configure(...)
Lexxy.highlightAll()We could also assign the same object to a global window.Lexxy. What do you think?
In terms of internal usage, how about exporting the following from config/lexxy.js?
export default {
global,
presets,
configure(changes) {
return presets.merge(changes)
}
}
// usage
import Lexxy from "./config/lexxy"
Lexxy.global.get(..)And then re-export Lexxy.configure for the public interface.
I would wait to see if we need the extra complication. If someone sets that the toolbar is |
|
On @zachasme and we also need to document the new configuration API in the README 🙏 |
Good point, let's keep it simple.
Almost there! |
eaa7673 to
a7eb6ed
Compare
fc66146 to
623b785
Compare
|
Further refinements:
|
| } else if (this.editorElement.supportsMarkdown) { | ||
| this.#pasteMarkdown(text) | ||
| } else { | ||
| this.#pasteRichText(clipboardData) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice 👏
jorgemanrubia
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking tight @zachasme 👏! Couple of minor comments.
src/elements/editor.js
Outdated
| } | ||
|
|
||
| #createDefaultToolbar() { | ||
| #createDefaultToolbar(container = this) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this new parameter? #findOrCreateDefaultToolbar was tighter before. Why expanding the container and pass it as a param now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm using it to optionally construct the default toolbar inside an external container, if provided:
Previously the
toolbarelement attribute accepted dom id for external toolbars. I've renamed it totoolbartarget(inspired bypopovertarget), such thattoolbarconfig is always boolean.I've created a test for external toolbars. I had to change the implementation to construct a full
<lexxy-toolbar>inside the provided target. Is this how you intended it to work?
I wanted to add a test for external toolbars, and couldn't make it work without building the full default toolbar. But I'm possibly misunderstanding how external toolbars should work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zachasme if a toolbar ID is provided, then Lexical should just grab that toolbar. So we just need to keep the previous behavior/code there.
I see that you changed toolbar to toolbarTarget. That is a breaking change here, I would just stick to toolbar.
I think the semantic should be:
- If
false: no toolbar - If
true: default toolbar - If other string: reuse that toolbar directly (existing behavior)
Does that make sense to you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes total sense. Splitting toolbar into toolbar/toolbartarget was just personal preference, I'll revert that part.
That is unrelated to the new parameter mentioned above, though. I added that to make the following pattern work, which currently fails on main:
<div id="external"></div>
<lexxy-editor toolbar="external">
// Uncaught TypeError: this.toolbarElement.setEditor is not a functionI assumed it a bug that we weren't constructing a full toolbar inside the external element (README says "or pass an element ID to render the toolbar in an external element").
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh but the toolbar should be a lexxy-toolbar element, not a regular div
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW good point that this is confusing. We can mention in the README that the expected element is a lexxy-toolbar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried that as well but got another error.
<lexxy-toolbar id="external"></lexxy-toolbar>
<lexxy-editor toolbar="external">
// Uncaught TypeError: Cannot read properties of null (reading 'children')I could only make it work by providing a full toolbar directly in html:
<lexxy-toolbar id="external">
<button class="lexxy-editor__toolbar-button" type="button" name="bold" data-command="bold" title="Bold">
<svg ...
...
</lexxy-toolbar>
<lexxy-editor toolbar="external">Is this how it's meant to be used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes: it is meant to let you define a fully customizable toolbar as an external element and reference it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see! Thanks for clearing that up 👍 I'll revert, update the README and test the intended usage instead.
| // you can override specific options with attributes on editor elements | ||
| <lexxy-editor preset="simple" attachments="true"></lexxy-editor> | ||
| ``` | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should include a reference to Lexxy.global and the option to configure tag names, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right! ✅
What do you think of the public interface? Right now I only expose Lexxy.configure which re-routes it's input to global/presets.
configure({ global: newGlobal, ...newPresets }) {
if (newGlobal) {
global.merge(newGlobal)
}
presets.merge(newPresets)
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is good 👍
01dbfed to
221aeb4
Compare
Introduce `Lexxy.configure()` to customize editor behavior through presets and global options. Users can configure per-preset or override via element attributes.
Test for Configuration class and EditorConfiguration. Update GitHub Actions and local CI to run JS tests.
Based off suggestions from @jorgemanrubia, this PR allows configuring lexxy instances in three layers:
I took into consideration the work on custom-upload-events, which will require:
importDOMAPI.Note that I'm only exposing the
configurefunction as public API, but we could include a getter if end-users need to read current config values.As for next steps:
{toolbar: true}and{toolbar:{bold:false}}.