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

Add support for check boxes #759

Open
sachinrekhi opened this issue Jun 20, 2016 · 55 comments
Open

Add support for check boxes #759

sachinrekhi opened this issue Jun 20, 2016 · 55 comments
Labels

Comments

@sachinrekhi
Copy link
Contributor

This is a feature request to add support for a check box list type.

In addition to the unordered and ordered list types, a checkbox list type would also be very helpful. It would enable users to create very light-weight task lists within a Quill editor instance.

The experience I'm imagining is that this list type would largely function exactly list the unordered list type in Quill with just a few additions:

  1. The bullet would be replaced with a checkable checkbox
  2. When the checkbox is checked, a styling class would be applied to the whole line that would by default grey out the line or put a strike-through across it (but customizable by customizing the class styles)

I understand this won't be in Quill 1.0, but wanted to get it on the list so others can express interest as well for future prioritization.

Also would be helpful to know if this could be added purely using the extensibility of blots? Could be a good third-party contribution.

@jhchen jhchen added the feature label Jun 22, 2016
@jhchen
Copy link
Member

jhchen commented Jun 22, 2016

Barring surprises, it may actually be fairly straightforward to add this. Other editors like Dropbox Paper uses the CSS content property and a background image to show the check and box and Quill also uses the content property for showing bullets and lists.

It would not be possible to change the checkbox's color in an edit session with this approach, though this is probably an acceptable limitation. Alternatively a unicode character could be used instead of a background image which could allow for color customization in an edit session.

To distinguish between the checklist from a bulleted list you could use either an attribute or class and check for this in the List blot implementation so create and formats does the correct thing.

Other than that I'm not sure anything else needs to be done but surprises do come up.

@sachinrekhi
Copy link
Contributor Author

sachinrekhi commented Jul 3, 2016

So I was able to use your suggested approach to create a new list attribute value "checkbox" in addition to "bullet" and "ordered" that will then render a checkbox image using a CSS content property instead of a bullet. I'm successfully creating new items in this list type with the rendered checkbox image.

But I'm struggling to figure out where and how I should I attach the event handler to handle checking the checkbox. It doesn't seem like I can target an event handler to a :before element. I could target the whole li item, but when they click on the item's text, I just want to focus the editor on the line so they can edit the value of the checklist item as opposed to toggling the checkbox.

After I figure out where to target, then I'm not sure how I go about modifying the list item's value to update the checked state. I could attach an event handler in the static create method, but then I'd need to know the specific index of the clicked element to be able to issue a quill.formatLine() command, but it doesn't look like I have access to that.

Any thoughts?

@jhchen
Copy link
Member

jhchen commented Jul 3, 2016

I would explore attaching to the <li> element and seeing if you can use any data from the event object. There does seem to be an offsetX you can use but you'd have to account for a potentially nested checklist. Ideally there would be some other data that does not involve pixel math.

You could format the blot directly with something like this:

var listBlot = Parchment.find(listNode);
listBlot.format('list', 'checked');

Quill's formatLine implementation actually does this to avoid index calculations as well.

@sachinrekhi
Copy link
Contributor Author

sachinrekhi commented Jul 5, 2016

Thanks for the suggestions, they both helped.

I ended up moving away from creating the checklist type as an extension of the list type. That's because it kept resulting in checking one list item causing all of the items within the parent list becoming checked as well, given each list item was part of the same list and I was keeping the value for checked or unchecked at the list level.

I instead implemented the checklist as an independent block-level format with tag DIV and className ql-checklist and still using a :before element to render the checkboxes. This worked a lot better since each checklist item could have it's independent checked or unchecked value that wasn't affected by neighboring items. I used the offsetX approach to decide when the click was on the checkbox, which is a bit brittle because it depends on coordinating the padding-left of the ql-checklist css class with the acceptable offsetX. But overall the checklist is now working great for me.

I assume you'd be interested in a PR only after 1.0?

@jhchen
Copy link
Member

jhchen commented Jul 7, 2016

I can take a look at a PR now but depending on the potential surface area for bugs it might be included after 1.0.

@alexkrolick
Copy link
Contributor

@sachinrekhi would it be possible to take a look at the format module you came up with?

@edisonator
Copy link

@sachinrekhi I was troubled by this problem for a long time, look forward to your PR!

@alainduruy
Copy link

@sachinrekhi I'm really interested as well in the solution you found for this! It would be a great feature

@koffeinfrei
Copy link

I implemented a task list plugin https://github.com/koffeinfrei/quill-task-list.
The implementation is fairly straight forward, but has one drawback:

For now the click triggers a dummy update, because the css class toggle doesn't trigger a change in quill. The proper way would be to change the state of the item by using a delta directly and maybe two kinds of list item classes (a normal TaskListItem and a CheckedTasklistItem or something. Fixing this would also fix the history. This may be easy for someone familiar with the internals of quill.

@jhchen
Copy link
Member

jhchen commented Dec 1, 2016

Thank you for sharing your implementation @koffeinfrei.

I made some additions that supports checklist on model/data layer along with some tests demonstrating how work.

The missing piece is how to check/uncheck on user click, and also which layer that belongs in. The following does work to try out but hardcodes some pixel values and is probably operating at the wrong abstraction layer:

this.quill.root.addEventListener('mousedown', (e) => {
  if (!this.quill.isEnabled()) return
  let blot = Parchment.find(e.target)
  if (blot == null || blot.statics.blotName !== 'list-item') return
  let value = blot.parent.formats().list
  if (value !== 'checked' && value !== 'unchecked') return
  let range = this.quill.getSelection()
  let index = blot.offset(this.quill.scroll)
  let bounds = this.quill.getBounds(index)
  let left = e.offsetX + blot.domNode.offsetLeft + blot.parent.domNode.offsetLeft
  if (left < bounds.left - 18 || bounds.left < left) return
  blot.format('list', value === 'checked' ? 'unchecked' : 'checked')
  this.quill.setSelection(null, Quill.sources.SILENT)
  this.quill.setSelection(range, Quill.sources.SILENT)
  e.preventDefault()
})

I'd appreciate if people can try this develop branch out with the checklists.

@koffeinfrei
Copy link

@jhchen there's also an open PR koffeinfrei/quill-task-list#1 where @CJEnright came up with a better solution for the click handling.

@jhchen
Copy link
Member

jhchen commented Dec 1, 2016

From reading the code it looks like the list will check/uncheck when the user clicks anywhere on the list, not just the box? That's the main challenge is locate/detect a click on just the box.

@CJEnright
Copy link

I think (and sorry if this is wrong) that because the matches function, e.target.matches('ul.task-list > li'), uses the immediate child selector (>) it only affects the the li element itself and none of its child elements, including the text.

@jhchen
Copy link
Member

jhchen commented Dec 2, 2016

That does exclude child elements but not child text nodes: https://jsfiddle.net/c335pu4e/

@seekforwings
Copy link

Well, I wonder if there is any final solution.

@CJEnright
Copy link

Thanks for the reminder @seekforwings, @jhchen's jsfiddle proves the concept doesn't work, but in practice it seems to function fine (besides the bugs issued on @koffeinfrei's repository). I've tried to look for the differences but wasn't able to find anything concrete as of yet. I've made a demo repository if anyone wants to test out the module in a full Quill environment.

@koffeinfrei
Copy link

koffeinfrei commented Jan 10, 2017

@jhchen
Copy link
Member

jhchen commented Jan 10, 2017

Thanks for the pointer @koffeinfrei ! I added a change adb980d that hooks up the click handlers and it seems to be working well. The only thing left I think is I cannot get the toolbar to light up both in the checked and unchecked case.

@ftwang
Copy link

ftwang commented May 24, 2017

Regarding getting the toolbar to light up, would the following be an acceptable hack: to check if the formats[format] contains the input.getAttribute('value') substring?

class Toolbar extends Module {
  update(range) {
...
else if (input.hasAttribute('value')) {
          // both being null should match (default values)
          // '1' should match with 1 (headers)
          let isActive = formats[format] === input.getAttribute('value') ||
                         (formats[format] != null && formats[format].toString() === input.getAttribute('value')) ||
                         (formats[format] != null && (formats[format].toString().indexOf(input.getAttribute('value')) !== -1) ||
                         (formats[format] == null && !input.getAttribute('value'));
          input.classList.toggle('ql-active', isActive);
...

  }
}

@jhchen
Copy link
Member

jhchen commented May 24, 2017

@ftwang I think the behavior would be too magical for my tastes. Thank you for the suggestion though. I'm leaning towards allowing a way to specify an array of values to match on ex:

<button class="ql-header" value="1">Header 1</button>
<button class="ql-header" value="1">Header 1</button>
<button class="ql-list" value="checked,unchecked">Checklist</button>

@patleeman
Copy link
Contributor

I couldn't find much documentation on this feature, but it seems like formatLine(idx, len, 'list', 'checked') and formatLine(idx, len, 'list', 'unchecked') insert checkboxes that work as intended.

@jhchen Is this feature still in development? Would it be feasible to use it in production?

Thank you!

@jhchen
Copy link
Member

jhchen commented Jul 2, 2017

@patleeman The API works but as stated earlier I still need to get the toolbar to work before official documentation + support.

@koffeinfrei
Copy link

@jhchen side question: Have you considered adding such functionality as plugins, similar to the one I built? This would lead to a cleaner separation. The current design will eventually lead to a very bloated core.

@jhchen
Copy link
Member

jhchen commented Jul 3, 2017

There already is the concept of modules which may be internal or external. How is this different from plugins you are describing?

@koffeinfrei
Copy link

@jhchen

The reason I asked was that you started implementing the checkbox functionallity in the core, while I had already written a plugin which could be used instead.

The module concept may be appropriate (although I didn't have a close look at it) but my point was that you didn't use that concept, but added more code to the existing list.

@jhchen
Copy link
Member

jhchen commented Jul 3, 2017

This was always envisioned to be added to core as checklists very useful and becoming commonplace in modern editing products (ex. Dropbox Paper, Quip) and the code for it is pretty minimal.

I'm not sure I agree that your plugin could be used instead. It was and still is in a demonstration/proof of concept stage in terms of robustness and completeness in terms of interacting with other parts of Quill in a bug free way.

I also had asked for a PR and no one submitted one nor expressed interest so I started working on it myself in the meantime. Quill has high code quality standards and this is not the easiest feature to implement (though there is not much code if one is familiar) so my expectation for a PR was not high.

Your implementation, which I did thank you already for sharing, came in the middle my working on a core implementation and was not implemented in the optimal way or layer given my attempts up to that point. I apologize if I misread your intentions. Either way our Contribution Guide is a great resource for those that want to get more involved in the future.

@koffeinfrei
Copy link

I didn't necessarily imply that my plugin should be used instead. I just merely stated that this could be implemented not in core directly, due to the above stated reasons and also because my attempt seemed easier. I was actually surprised how easily I was able to achieve this. Some things even seem to be easier the way I implemented it:

The only thing left I think is I cannot get the toolbar to light up both in the checked and unchecked case.

Curiously this just worked for me.

It was and still is in a demonstration/proof of concept stage in terms of robustness and completeness in terms of interacting with other parts of Quill in a bug free way.

What parts do you think aren't robust or complete enough? I'm aware of

I apologize if I misread your intentions.

No need to apologize. I didn't have any intentions actually other than just providing my implementation as a possible inspiration for the final implementation. That you're going in a different direction with this is totally up to you and fine with me. It's your project anyway, and you'll know better what's right.

@sferoze
Copy link
Contributor

sferoze commented Sep 7, 2017

whoah, checkboxes are already in core. I was browsing through the list Attributor code and noticed checkbox code all over.

So I tried adding the list check item to my toolbar like so

[{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }]

And guess what the checkbox icon appears! And it even works with the history stack.

@jhchen aside from the icon in the toolbar lighting up, is this feature production ready?

Also, the checkbox CSS could look better. It is quite small and hard to see.

With the new checklist option there are now 3 list options.

Would it be better to design the list element as a drop-down in the toolbar similar to the alignment drop-down?

@patleeman
Copy link
Contributor

patleeman commented Sep 12, 2017

Also for anyone looking to increase the size of the checkbox, you can replace the unicode symbols of the checkboxes for both checked and unchecked states with this:

.ql-editor ul[data-checked="false"] > li::before {
    content: '\2E3A';
    font-size: 1.5rem;
}

.ql-editor ul[data-checked="true"] li::before {
    content: '\2713';
    font-size: 1.5rem;
}

screen shot 2017-09-11 at 10 33 07 pm

@sferoze
Copy link
Contributor

sferoze commented Sep 14, 2017

@patleeman The css you provided for larger checkboxes appears to not work on mobile (tested with iOS 10.3.3).

The checked icon shows, but the box disappeared.

@patleeman
Copy link
Contributor

@sferoze

Oh, the unicode character might not render, i didn't check that.

You can use any unicode symbol by using the last 4 chars of the hex representation where it says

content: '\2713'

Here's a list of some useful unicode symbols.

@shengxiuzhou
Copy link

I used this task-list in my project, I have a problem, if I press the enter key after an empty task list, this list should be deleted, instead of generating another list, is this a bug?

@jhchen
Copy link
Member

jhchen commented Dec 15, 2017

The empty list should be deleted and it's working for me on Mac + Chrome.

check

If it's not working for you please create another Issue and fill out the entire issue template.

@dmarcelino
Copy link

I've just tried following @sferoze suggestion:

So I tried adding the list check item to my toolbar like so

[{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }]

And put it on a codepen: https://codepen.io/anon/pen/JLdyQq and it looks really good!

Great work @jhchen! Any ETA for official support?

@dmarcelino
Copy link

You can use any unicode symbol by using the last 4 chars of the hex representation where it says:
content: '\2713'
Here's a list of some useful unicode symbols.

@patleeman, @sferoze, content: '\25A1' (square) seems to work well, so far.

Regarding mobile have you had any issues ticking or un-ticking the boxes? While using Chrome on android, more often than not it seems to register 2 presses, instead of one. This results in the box returning to previous state instead of changing. I'm inclined to think some sort of debounce may be needed on list.js#L77. What is your experience?

@patleeman
Copy link
Contributor

It might be interpreting both touchstart and mousedown events and triggering twice. Instead of debounce, I wonder if there is a way to trigger only one of these events?

@dmarcelino
Copy link

dmarcelino commented Mar 13, 2018

Funny, this morning I was thinking about this and the same thought came to mind. I've tested Android Chrome with this jsfiddle (origin) and confirmed indeed both touchstart and mousedown events are being fired.

I see 2 options:

  1. Detect if the user is using touch and ignore the click event. This will likely fail for touchscreen laptops.
  2. Apply debounce with a short period to listEventHandler() (list.js#L66). This way it becomes irrelevant if both events are triggered simultaneously (or even if the user is shaking while pressing a checkbox). I've created a jsfiddle with the below code to test this and it seems to work fine.
var waiting = false;
var touchmouseFunc = function(e) {
  if (waiting) return;
  waiting = true;
  setTimeout(function() {
    waiting = false;
  }, 300);
  document.getElementById('testArea').innerHTML += 'touchmouse ';
};

document.addEventListener("touchstart", touchmouseFunc, false);
document.addEventListener("mousedown", touchmouseFunc, false);

@dmarcelino
Copy link

@patleeman, the solution can be made simpler, we can prevent the mousedown event from triggering on touch devices by calling e.preventDefault() on the touchstart event. Citing MDN:

if an application does not want mouse events fired on a specific touch target element, the element's touch event handlers should call preventDefault() and no additional mouse events will be dispatched.

I've tested the below code in this jsfiddle. On my desktop browser I only get mouse events and on my phone I only get touch events.

var touchFunc = function(e) {
  e.preventDefault()
  document.getElementById('testArea').innerHTML += 'touch ';
};
var mouseFunc = function() {
  document.getElementById('testArea').innerHTML += 'mouse ';
};
document.addEventListener("touchstart", touchFunc, { passive: false });
document.addEventListener("mousedown", mouseFunc, false);

@rgazelot
Copy link

rgazelot commented Jan 8, 2019

Hello guys, thanks for your work. I need to catch the []+space or [ ]+ space in the Keyboard module in order to create my own stuff. Actually I can't because of this feature which basically do the same stuff but I have no control over. If I press []+space, you guys transform the text into a unchecked list and set the content as empty. The Keyboard module is never trigger. I can have the control into the Keyboard module if I press []+space+space but the experience sucks and that's not what I want.

Is there a way for me to disable this formating?

@akhilaudable
Copy link

akhilaudable commented Mar 18, 2020

Hi, is there a way to check/uncheck the checkbox, when the Quill is not on editing mode?

@Kisama
Copy link

Kisama commented Apr 19, 2020

Hello, is there any progress about check toolbar light up?

@gdjohnson
Copy link

To those running into issues implementing checklists (toolbar icon's active status incorrectly rendering, inconsistent value parsing of 'check'/'checked'/'unchecked'), a custom toolbar module might be of use.

@damikun
Copy link

damikun commented Nov 3, 2020

What is current most recommended package for checkbox /task-list ?

I currently use buildin

           modules={{
              toolbar: [
                { list: "ordered" },
                { list: "bullet" },
                { list: "check" },
              ],
            }}

@afadil
Copy link

afadil commented Mar 15, 2022

If you wanna use Font Awsome icons

user buidIn:
modules={{ toolbar: [ { list: "ordered" }, { list: "bullet" }, { list: "check" }, ], }}

Customize CSS:

.ql-editor ul[data-checked=true] > li::before {
  font: normal normal normal 16px/1 "Font Awesome 5 Free" !important;
  content: '\f058' !important;
  color: #5e72e4 !important;
}

.ql-editor ul[data-checked=false] > li::before {
  font: normal normal normal 16px/1 "Font Awesome 5 Free" !important;
  content: '\f111' !important;
  color: #5e72e4 !important;
}

.ql-editor ul[data-checked=true] > li {
  text-decoration: line-through !important;
}

Screen Shot 2022-03-15 at 11 09 46 AM

@craighymo
Copy link

anyone ever figure out how to change the background color of the checkboxes? So it's not default blue.

@cccindyyyl
Copy link

Is there any way to make users able to check/uncheck the checkboxes, but not edit any other things? (this is for when we want to display the content of the editor in a non-editor instance, but currently the only way it seems is to have a disabled quill editor, but that doesn't allow checking/unchecking the checklists.)

@Djones4822
Copy link

@cccindyyyl I, too, need this feature. The way I've solved it is to use this package https://github.com/nozer/quill-delta-to-html to convert the delta to HTML. Then during representation, I iterate over the exported html to grab all the li elements with a data-checked element, and replace the inner html with a checkbox input. Then I add a listener for the checkbox interaction and update the delta.

I'm forced to be in jquery right now for this project, which is not ideal, but overall it still works fine.

Here's a link to my proof of concept in codesandbox https://codepen.io/djones4822/pen/NWeEBab

The implementation I use stores the delta in the preview in a data attribute, and fetches and updates it on each interaction. On preview toggle the delta is stored in the database. Some users only see the rendered html, and in that case we still serve the delta on their client and on update we store it in the db.

Hope that helps

@benbro
Copy link
Contributor

benbro commented Feb 3, 2024

@luin can we close this now that check box supported?

@AustineA
Copy link

@luin can we close this now that check box supported?

Can you point me to the docs for this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests