-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Table of contents (document outline) #966
Comments
Very interesting feature request. And, as a matter of fact, we made a proof-of-concept of it 2 years ago on a hackathon: https://github.com/ckeditor/ckeditor5-hackathon/tree/table-of-contents#editable-table-of-contents The cool thing about this feature was that the table of contents was editable as well. That was achieved by making it a second view of the same, shared model. I'm not 100% sure that it'd be easy to achieve today (the API has changed), but architecturally speaking this is supported. Anyway, what I can tell for sure is that:
This plugin is not on our radar yet. I hope, though, that it will get some 👍 :) |
I am pretty sure that it is possible (may need some small fixes to make it work with no converters for everything that is not a header), but I am not sure if it safe. There might be some bugs with some actions (enter, backspace, paste in the middle of the header etc.). And I am not sure if it is a proper UX: one expects to have links and to scroll the content to the proper place in the document, not to be able to edit. Also, it may happen that one wanted to edit only the table of content and is surprised that headers changed too. It is a very very cool feature as POC, but most probably not what users expect to have. |
Amazing 😍! That's a really great starting point. If the outline was directly editable, I would expect a different editing convention. Enter would create a heading of the same level and tab / shift+tab would move change the header size. I think I would just go with the conventions established by Google Drive and MS Word though. Ultimately I'd like to store the output of the ToC as JSON for a few reasons. That would make it easier to customize the display for different scenarios. Settings could automate numbering and allow things like roman numerals for a configurable number of headings. A publishing view could put the ToC first after a title page and some kind of paging integration could automate the page reference in the ToC. Baby steps though. I'll see if I can get it up to date with the current API first. |
👍 |
Yes, we must figure out what the feature is supposed to be:
Then, at the top of it, comes the aspect of editability. In each of the above cases, users may (or may not) expect the ToC to be editable:
But regardless of the editing strategy, keep in mind that we must come up with an UX which works with other features in the headings. ATM the headings can have Font Family/Size, Highlight, Basic Styles, Alignment, etc.. Would they be available (and rendered) in the ToC too?
See, there many issues that must be resolved to get the editing right. Provided it's possible to get it right (I wouldn't be so sure). That's why I'm for the navigational, plain text implementation in the MVP in we go with this feature. |
Since that would be a different view, with different converters registered, it would only render headings and text. However this view operates on the same model as the original view. This means that the position in the heading in that view would be accordingly mapped to the model. So anything typed at that position would go to a correct position in the model. Basically, the selection would also have all the attributes: highlght, styling, etc. but not rendered. So you'd "type with those styles". Apart of headings and text it might be wise to support also links there, especially since two-step caret movement. |
To clarify one thing – I don't think that it's wise to make the ToC as a second view to the model. It'd be a waste of time for too little benefit. It's a pretty tricky problem to enable all the features, the typing, etc. Not worth it. Especially that I also agree that an editable ToC is a nice PoC, but not something really necessary. |
I agree. That's why I didn't comment on this issue earlier. I remember this PoC from 2016 hackathon too. At first, I wanted to point it out but then I thought that maybe it's pointing in the wrong direction. Anyway, my comment was more an insight to how things would work if the editor would be configured that way. A curiosity, hint, someone might learn something from it. |
Well, I would certainly love an up-to-date and more intuitive version of a Table of Contents feature. I can pick up the torch myself, but it would be more efficient to get someone familiar with the CKEditor eco-system to build it. There's a very old blog post (10 years old!) talking about a call for sponsors. I'm not sure what relationship sponsors have with roadmap features, but is CKEditor open to new sponsors? |
@archonic we are always open for various initiatives, please get in touch with us through https://ckeditor.com/contact/ to discuss possible options. |
I came across this while implementing custom two-pane side-by-side editors. Here is my implementation of ToC/Outline plugin. (Uses stable branch of ck5editor) Pre-Reqs: Needs a DOM element with id import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import EditingController from '@ckeditor/ckeditor5-engine/src/controller/editingcontroller';
import { downcastElementToElement, insertElement, remove } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
import Conversion from '@ckeditor/ckeditor5-engine/src/conversion/conversion';
import '../../theme/toc.css';
export default class TableOfContentsPlugin extends Plugin {
/**
* Return the name of the plugin
* @return {String} The name of the plugin
*/
static get pluginName() {
return 'TableOfContentsPlugin';
}
/**
* Return the editor conversion configuration for a given model like heading1
* @param {String} model The name of the model
* @return {Object} The conversion config
*/
static getEditorConversionConfig( model ) {
return {
model,
view: {
name: 'p',
classes: `ck-toc-${ model }`
}
};
}
init() {
const editor = this.editor;
const tableOfContents = new EditingController( editor.model );
tableOfContents.view.attachDomRoot(document.getElementById( 'editor-toc' ));
const dispatcher = tableOfContents.downcastDispatcher;
downcastElementToElement( TableOfContentsPlugin.getEditorConversionConfig( 'heading1' ) )( dispatcher );
downcastElementToElement( TableOfContentsPlugin.getEditorConversionConfig( 'heading2' ) )( dispatcher );
downcastElementToElement( TableOfContentsPlugin.getEditorConversionConfig( 'heading3' ) )( dispatcher );
downcastElementToElement( {
model: 'paragraph',
view: {
name: 'span',
styles: {
display: 'none'
}
}
} )( dispatcher );
}
}
I have a few questions for the authors here:
My attempt: function customDownCastElementToElement(config) {
config.view = _normalizeToElementConfig(config.view, 'container');
return dispatcher => {
const debouncedEventHandler = debounce(insertElement(config.view), 200);
// We immediately return false and call debounced function
dispatcher.on('insert:' + config.model, (evt, ...args) => {
evt.stop();
debouncedEventHandler(evt, ...args);
return false;
}, { priority: 'highest' });
};
}
My attempt: function downcastElementToElement(config) {
....
return dispatcher => {
if (config.view) {
return dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.converterPriority || 'normal' } );
} else {
return dispatcher.on( 'insert:' + config.model, remove(), { priority: 'high' } );
}
};
} |
Hi, sorry for the late reply. You discuss some interesting problems in your post.
However, if your ToC is not editable, I have a different idea for you. You could add a callback to This solution isn't as nice and elegant as creating a new conversion pipeline but might actually work. I tested it a little manually and it seems nice. const tocDiv = document.getElementById( 'toc' );
class ToC extends Plugin {
init() {
const htmlDataProcessor = new HtmlDataProcessor();
const writer = new UpcastWriter();
const debouncedTocUpdate = debounce( () => {
const tocView = getFilteredView( this.editor.editing.view.document.getRoot() );
tocDiv.innerHTML = htmlDataProcessor.toData( tocView );
}, 500 );
this.editor.editing.view.on( 'render', () => { debouncedTocUpdate() }, { priority: 'low' } );
// Filtering should be more sophisticated if we expect headings inside block quotes or other elements (not on the first level).
function getFilteredView( viewRoot ) {
const viewFragment = writer.createDocumentFragment();
for ( const child of viewRoot.getChildren() ) {
if ( /^h[1-6]$/.test( child.name ) ) {
// Clone so the original `child` is not removed from `viewRoot` when appended to `viewFragment`.
const clonedChild = writer.clone( child, true );
writer.appendChild( clonedChild, viewFragment );
}
}
return viewFragment;
}
}
} |
If you want to have different conversion to the ToC section, you could try mixing those approaches, also you could try using (extending) |
Thank you for the detailed reply. I understand why you would not want to debounce the updates, and not have a NO-OP element. As for your solution , if I am not wrong, you are filtering out view elements, and setting innerHTML. As noted in your comment about iterating in a nested fashion, I think it will be faster to do it via the DOM itself. To maintain a 1-1 mapping from model to view that does not go wrong, I think having a filter on the model data layer is probably better. Something along the lines of
And yes, my ToC is non-editable so I do not need upcasting. If this is fast, debouncing would not be required at all. The problems I outlined before occur because I am attaching a new controller on the same model. If I can filter out and return a clone of the model for my second view, that should do the trick. Anyway, I realize that this is not a priority for you guys right now. Thank you for the help and hard work. I have a subscription, so if I run into issues, I will raise them via support requests. |
As I briefly mentioned earlier, trying to do some filtering on model seems like a viable route too. If you are interested, I encourage you to go that way :). We both proposed two different solutions concerning model so I hope there's something there.
Cloning the whole model each time might not be optimal but I wouldn't worry unless you have very long documents. Trying to do it through conversion pipeline would be surely better as you would only convert what changed so document size wouldn't matter. |
Was this developed? I would like to download such plugin for ckeditor 5 |
Any progress regarding this plugin? |
@Reinmar Any updates on progress of this feature? |
thanks for this solution, Im able set/bind heading tags into my custom div containing ToC. |
Try https://ckeditor.com/docs/ckeditor5/latest/api/module_utils_dom_scroll.html#function-scrollViewportToShowTarget -- you will need to get HTMLElement or Range. Use |
@scofalik : I appreciate your quick help, I had model info on click, so this command editor.editing.view.domConverter.mapViewToDom() worked for me. Thanks Again. |
Any progress?? |
waiting for that feature |
That would be pretty awesome :-) I think I'd be pretty sad if I was forced to use CKEditor4 for this haha! |
Unfortunately, we are not working on this feature at the moment. BTW, on "issues" tab, we pin roadmap issues, so you can always take a look at what's cooking at the moment. Here's a link for the current iteration: #11205. |
Waiting for this feature of Table of contents (document outline) , are there any progress?? |
Both Table of contents and Document outline features has been shipped in version 37.1.0 and is available as a part of productivity pack feature set 🎉 They were added as a separate features because they have slightly different usage. Table of contents is meant to be included in the output content (and as such is visible in the content editable area). Document outline is intended to aid editing navigation - as such it's shown outside of the editor as a part of the runtime UI. |
Is this a bug report or feature request? (choose one)
🆕 Feature request
📋 Steps to reproduce
MS Word and Google Drive have an outline feature where the headers of a document are linked on the left in an overview of the document.
While trying to duplicate this feature, I'm finding that it needs to be invasive to CKEditor5. It would make sense to hook into the editing engine to keep the outline up to date for example, instead of parsing the entire document with
getData
. It would also need modification to the heading package. The ID of the heading could be configured with heading options, but only statically (as far as I know). Google Drive uses a token - something likeh.rvcgos38rf2
as the ID of the heading and linking to it with#heading=h.rvcgos38rf2
. I think it would be preferable to keep it human readable by URL-safing the contents of the heading like#an-example-heading
, if sensible.I believe it would make sense to have a new package which would tie into the editing engine and the heading package in order to maintain a second "document" which is the outline. Perhaps this would only make sense in the decoupled editor as the view/style assumptions with the other editors are limited. Showing and hiding the outline would be considerations beyond CKEditor5.
If you'd like to see this feature implemented, add 👍 to this post.
The text was updated successfully, but these errors were encountered: