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

Table of contents (document outline) #966

Closed
archonic opened this issue Apr 16, 2018 · 28 comments
Closed

Table of contents (document outline) #966

archonic opened this issue Apr 16, 2018 · 28 comments
Labels
squad:features Issue to be handled by the Features team. support:2 An issue reported by a commercially licensed client. type:feature This issue reports a feature request (an idea for a new functionality or a missing option).

Comments

@archonic
Copy link

archonic commented Apr 16, 2018

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.
just_testing_-_google_docs

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 like h.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.

@archonic archonic changed the title Feature Request: Outline / Table of Contents Feature Request: Outline Apr 16, 2018
@Reinmar
Copy link
Member

Reinmar commented Apr 19, 2018

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

Table of contents feature screencast

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:

  • Such a feature does not require modifying the heading plugin or any other plugin. It can be implemented as a completely separate feature. That's because CKEditor 5's architecture is event-based and the conversion from the model to the view is pluggable (and event-based as well) so one can e.g. add the id attributes to all headings or get updates about the list of headings without involving the getData() method. BTW, the easiest solution will be scanning the model on every structure change and its performance can be tweaked if necessary.
  • Unless you're thinking about the "data" (the editor's output HTML), the ids should not be even necessary. If you need the outline only during editing (like in Google Docs), then you can programmatically scroll to concrete heading elements in the content. If you're thinking about including ToC in the editor's output, then it's a bit different story and a different feature in fact.
  • I think that such a plugin could be used with every type of editor, not only with the decoupled one. It would expose a container element which the application in which the editor is integrated could insert wherever it wishes.

This plugin is not on our radar yet. I hope, though, that it will get some 👍 :)

@Reinmar Reinmar added type:feature This issue reports a feature request (an idea for a new functionality or a missing option). status:confirmed labels Apr 19, 2018
@Reinmar Reinmar added this to the backlog milestone Apr 19, 2018
@pjasiun
Copy link

pjasiun commented Apr 19, 2018

I'm not 100% sure that it'd be easy to achieve today (the API has changed), but architecturally speaking this is supported.

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.

@archonic
Copy link
Author

archonic commented Apr 19, 2018

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.

@wwalc
Copy link
Member

wwalc commented Apr 19, 2018

one expects to have links and to scroll the content to the proper place in the document, not to be able to edit.

👍

@oleq
Copy link
Member

oleq commented Apr 20, 2018

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.

Yes, we must figure out what the feature is supposed to be:

  • In Google Docs, it's purely navigational. It helps users navigate across massive chunks of content.
  • But if we consider the Document Editor, we may also find out that people might want to actually include the ToC in their prints, e.g. at the beginning of the content, like in academic papers.

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:

  • In the limited–editing scenario, they can only change the text of the headings. No enter key, no way to change the heading level, etc.
  • In the extended scenario, we could bring some advanced tools, like what @archonic mentioned.

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?

  • If yes, would they look OK? Users may select a heading in the ToC and change the alignment using the main toolbar. Should that heading change visually in the ToC? If not, wouldn't it feel "broken"? If yes, wouldn't it look weird that nothing happened?
  • If no, how would it look, if the user typed in an already highlighted subset of a header in the ToC? Would it expand the highlighted content in the (main) editable? What about deleting, etc.?

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.

@scofalik
Copy link
Contributor

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?

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.

@Reinmar
Copy link
Member

Reinmar commented Apr 20, 2018

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.

@scofalik
Copy link
Contributor

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.

@archonic
Copy link
Author

archonic commented May 26, 2018

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?

@wwalc
Copy link
Member

wwalc commented May 28, 2018

@archonic we are always open for various initiatives, please get in touch with us through https://ckeditor.com/contact/ to discuss possible options.

@tmpaul06
Copy link

tmpaul06 commented Feb 28, 2019

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 editor-toc (can be configured if you need)

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:

  1. I tried to override dispatcher.on method in an attempt to debounce the updates. Basically I do not want the ToC to update immediately as the user types. I was able to do this DraftJS. When I tried this in CKEditor5, I got errors from the Views & Mapper. Is there a clean way of debouncing this secondary view update ?

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' });
	};
}
  1. How do I let the second view ignore other elements such as figure, img etc without explicitly declaring them ? In the example above, if I do not define the model in downcastElementToElement(...), the plugin fails. Is there a NO-OP view element, so to speak ? I only want certain elements to be written in the second view: heading*.

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' } );
		}
	};
}

@scofalik scofalik self-assigned this Mar 7, 2019
@scofalik
Copy link
Contributor

scofalik commented Mar 7, 2019

Hi, sorry for the late reply.

You discuss some interesting problems in your post.

  1. I'd be very cautious with changing timings of conversion and rendering pipelines as some mechanisms heavily rely on things getting executed in some order. Is your ToC editable? If you are debouncing stuff, I'd guess no, as that might lead to some weird UX. If you really want to try that approach, try debouncing DowncastDispatcher#convertChanges. However, maybe it is better to go deeper and try debouncing tableOfContents.view.change(), or maybe even tableOfContents.view._render(). There are a lot of places that you could try hacking but, unfortunately, it is all pretty tightly coupled so I'd expect problems :(. I can't give you a good answer without doing serious research and we don't have time for that on our roadmap now.

  2. As for that, unfortunately, this is not doable at the moment. We have this ticket here: https://github.com/ckeditor/ckeditor5-engine/issues/611 and as you see we didn't get back in it in more than two years. Simply - there are not enough cases that would warrant bringing this enhancement. For now, the rule is that every model element needs to be mapped with something in the view. This guarantees that model<->view mapping is correct and the algorithms there are sane. To solve an issue like your, we'd need to either enable not-rendered view element (like you and we proposed in the issue) or to make it possible to not render model element and still have mapping working (which gets a little tricky if the model element has children which change).

However, if your ToC is not editable, I have a different idea for you. You could add a callback to render event on the view element with lowest priority and in that callback, with a timeout/debounce, you could get current editor view, clone view root and filter out unwanted elements and then use HtmlDataProcessor to parse the view to data. Then, set it in the <div> containing ToC.

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;
		}
	}
}

@scofalik
Copy link
Contributor

scofalik commented Mar 7, 2019

If you want to have different conversion to the ToC section, you could try mixing those approaches, also you could try using (extending) DataController (check out stringify method, where you could filter out stuff directly from the model) and listening to model.Document#change event. Although I haven't tested that.

@tmpaul06
Copy link

tmpaul06 commented Mar 8, 2019

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. document.querySelectorAll("h1, h2, h3").

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

// Pseudocode

const model = firstEditor.model;

// Get a clone with filtered out elements. Must handle model elements recursively
const secondModel = modelUtils.filter(model, ['heading1', 'heading2', ...])

const secondView = downcastConverter(secondModel);

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.

@scofalik
Copy link
Contributor

scofalik commented Mar 8, 2019

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.

If this is fast, debouncing would not be required at all.

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.

@lslowikowska lslowikowska added the support:2 An issue reported by a commercially licensed client. label Apr 30, 2019
@mlewand mlewand changed the title Feature Request: Outline Table of contents (document outline) Aug 6, 2019
@Reinmar Reinmar modified the milestones: backlog, nice-to-have Feb 12, 2020
@Reinmar Reinmar added the squad:core Issue to be handled by the Core team. label Jul 30, 2020
@offirpeer
Copy link

Was this developed? I would like to download such plugin for ckeditor 5

@kuku711
Copy link

kuku711 commented Apr 11, 2021

Any progress regarding this plugin?

@Gentleman03
Copy link

@Reinmar Any updates on progress of this feature?

@Riyas-M
Copy link

Riyas-M commented May 28, 2021

@scofalik : #966 (comment)

thanks for this solution, Im able set/bind heading tags into my custom div containing ToC.
Bt I Had a req. wherein If I click on any heading tag, It should scroll to exact node inside ck-editor.
Any feasible solution for this ?

@scofalik
Copy link
Contributor

Try https://ckeditor.com/docs/ckeditor5/latest/api/module_utils_dom_scroll.html#function-scrollViewportToShowTarget -- you will need to get HTMLElement or Range. Use Mapper (editor.editing.mapper.toViewElement()) and then DomConverter (editor.editing.view.domConverter.mapViewToDom()) to get from model to DOM.

@Riyas-M
Copy link

Riyas-M commented May 31, 2021

@scofalik : I appreciate your quick help,

I had model info on click, so this command editor.editing.view.domConverter.mapViewToDom() worked for me.
Just curious to understand, editor.editing.mapper.toViewElement() this takes HTMLElement/Range, so HTMLElement meant Header tags inside CK-Editor content?

Thanks Again.

@finzzz
Copy link

finzzz commented Jul 13, 2021

Any progress??

@m8
Copy link

m8 commented Oct 17, 2021

waiting for that feature

@Cheaterman
Copy link

That would be pretty awesome :-) I think I'd be pretty sad if I was forced to use CKEditor4 for this haha!

@trialley
Copy link

trialley commented Feb 17, 2022

Hi, @tmpaul06 @scofalik any progress?
ckeditor5 already have minimap, when will this content feature (#966) online?😂

I will be glad to push this feature forward if you guys don't have enough time.

@scofalik
Copy link
Contributor

scofalik commented Feb 17, 2022

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.

@pomek pomek removed this from the nice-to-have milestone Feb 21, 2022
@zgpnuaa
Copy link

zgpnuaa commented Sep 5, 2022

Waiting for this feature of Table of contents (document outline) , are there any progress??

@mlewand
Copy link
Contributor

mlewand commented May 4, 2023

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.

@mlewand mlewand closed this as completed May 4, 2023
@mlewand mlewand added this to the iteration 62 (v37.1.0) milestone May 4, 2023
@mlewand mlewand added squad:features Issue to be handled by the Features team. and removed squad:core Issue to be handled by the Core team. labels May 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
squad:features Issue to be handled by the Features team. support:2 An issue reported by a commercially licensed client. type:feature This issue reports a feature request (an idea for a new functionality or a missing option).
Projects
None yet
Development

No branches or pull requests