diff --git a/.gitignore b/.gitignore index 1eae0cf..c2658d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -dist/ node_modules/ diff --git a/dist/index.esm.js b/dist/index.esm.js new file mode 100644 index 0000000..532f743 --- /dev/null +++ b/dist/index.esm.js @@ -0,0 +1,529 @@ +var sortHandlers = new WeakMap(); +var state = null; + +function isDragging() { + return !!state; +} + +function sortable(el, fn) { + sortHandlers.set(el, fn); + el.addEventListener('dragstart', onDragStart); + el.addEventListener('dragenter', onDragEnter); + el.addEventListener('dragend', onDragEnd); + el.addEventListener('drop', onDrop); + el.addEventListener('dragover', onDragOver); +} + +// Determine if item2 is before item1 in the tree. +function isBefore(item1, item2) { + if (item1.parentNode === item2.parentNode) { + var node = item1; + while (node) { + if (node === item2) return true; + node = node.previousElementSibling; + } + } + return false; +} + +function isSameContainer(item1, item2) { + return item1.closest('task-lists') === item2.closest('task-lists'); +} + +function onDragStart(event) { + // Ignore selected text dragging within list items. + if (event.currentTarget !== event.target) return; + + var target = event.currentTarget; + if (!(target instanceof Element)) return; + + var sourceList = target.closest('.contains-task-list'); + if (!sourceList) return; + + target.classList.add('is-ghost'); + + // Add data to drag operation (required for Firefox). + if (event.dataTransfer) { + event.dataTransfer.setData('text/plain', target.textContent.trim()); + } + + if (!target.parentElement) return; + var siblings = Array.from(target.parentElement.children); + var sourceIndex = siblings.indexOf(target); + + state = { + didDrop: false, + dragging: target, + dropzone: target, + sourceList: sourceList, + sourceSibling: siblings[sourceIndex + 1] || null, + sourceIndex: sourceIndex + }; +} + +function onDragEnter(event) { + // Allow dragging task list item, not text, links, or files. + if (!state) return; + + var dropzone = event.currentTarget; + if (!(dropzone instanceof Element)) return; + + if (!isSameContainer(state.dragging, dropzone)) { + event.stopPropagation(); + return; + } + + // Necessary for dragging + event.preventDefault(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move'; + } + + // Make sure we weren't already dragging in this task list item. + if (state.dropzone === dropzone) return; + + // Open an empty space in the list as a drop target. + state.dragging.classList.add('is-dragging'); + + state.dropzone = dropzone; + + if (isBefore(state.dragging, dropzone)) { + dropzone.before(state.dragging); + } else { + dropzone.after(state.dragging); + } +} + +function onDrop(event) { + // Allow dragging task list item, not text, links, or files. + if (!state) return; + + var dropzone = event.currentTarget; + if (!(dropzone instanceof Element)) return; + + // Notify dragend listener drag ended in a valid drop target. + state.didDrop = true; + + if (!state.dragging.parentElement) return; + var newIndex = Array.from(state.dragging.parentElement.children).indexOf(state.dragging); + + var currentList = dropzone.closest('.contains-task-list'); + if (!currentList) return; + + // Don't notify listener if the list hasn't changed. + if (state.sourceIndex === newIndex && state.sourceList === currentList) return; + + // Increment insertion index when moving item down in its own list. + if (state.sourceList === currentList && state.sourceIndex < newIndex) { + newIndex++; + } + + var src = { list: state.sourceList, index: state.sourceIndex }; + var dst = { list: currentList, index: newIndex }; + var fn = sortHandlers.get(state.dragging); + if (fn) fn({ src: src, dst: dst }); +} + +function onDragEnd() { + if (!state) return; + + state.dragging.classList.remove('is-dragging'); + state.dragging.classList.remove('is-ghost'); + + // If the drag ended in a valid drop target, the drop event listener will + // have already set this flag to true. If not, it'll still be false, and + // we'll know the drag was canceled. + if (!state.didDrop) { + state.sourceList.insertBefore(state.dragging, state.sourceSibling); + } + + // Reset for next drag. + state = null; +} + +function onDragOver(event) { + // Allow dragging task list item, not text, links, or files. + if (!state) return; + + var dropzone = event.currentTarget; + if (!(dropzone instanceof Element)) return; + + if (!isSameContainer(state.dragging, dropzone)) { + event.stopPropagation(); + return; + } + + // Necessary for dragging. + event.preventDefault(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move'; + } +} + +var classCallCheck = function (instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +}; + +var createClass = function () { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + return function (Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; +}(); + +var inherits = function (subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + enumerable: false, + writable: true, + configurable: true + } + }); + if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; +}; + +var possibleConstructorReturn = function (self, call) { + if (!self) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return call && (typeof call === "object" || typeof call === "function") ? call : self; +}; + +function _CustomElement() { + return Reflect.construct(HTMLElement, [], this.__proto__.constructor); +} +Object.setPrototypeOf(_CustomElement.prototype, HTMLElement.prototype); +Object.setPrototypeOf(_CustomElement, HTMLElement); + +var observers = new WeakMap(); + +var TaskListsElement = function (_CustomElement2) { + inherits(TaskListsElement, _CustomElement2); + + function TaskListsElement() { + classCallCheck(this, TaskListsElement); + + var _this = possibleConstructorReturn(this, (TaskListsElement.__proto__ || Object.getPrototypeOf(TaskListsElement)).call(this)); + + _this.addEventListener('change', function (event) { + var checkbox = event.target; + if (!(checkbox instanceof HTMLInputElement)) return; + if (!checkbox.classList.contains('task-list-item-checkbox')) return; + + _this.dispatchEvent(new CustomEvent('task-lists:check', { + bubbles: true, + detail: { + position: position(checkbox), + checked: checkbox.checked + } + })); + }); + + observers.set(_this, new MutationObserver(syncState.bind(null, _this))); + return _this; + } + + createClass(TaskListsElement, [{ + key: 'connectedCallback', + value: function connectedCallback() { + var observer = observers.get(this); + if (observer) { + observer.observe(this, { childList: true, subtree: true }); + } + syncState(this); + } + }, { + key: 'disconnectedCallback', + value: function disconnectedCallback() { + var observer = observers.get(this); + if (observer) { + observer.disconnect(); + } + } + }, { + key: 'attributeChangedCallback', + value: function attributeChangedCallback(name, oldValue, newValue) { + if (oldValue === newValue) return; + + switch (name) { + case 'disabled': + syncDisabled(this); + break; + } + } + }, { + key: 'disabled', + get: function get$$1() { + return this.hasAttribute('disabled'); + }, + set: function set$$1(value) { + if (value) { + this.setAttribute('disabled', ''); + } else { + this.removeAttribute('disabled'); + } + } + }, { + key: 'sortable', + get: function get$$1() { + return this.hasAttribute('sortable'); + }, + set: function set$$1(value) { + if (value) { + this.setAttribute('sortable', ''); + } else { + this.removeAttribute('sortable'); + } + } + }], [{ + key: 'observedAttributes', + get: function get$$1() { + return ['disabled']; + } + }]); + return TaskListsElement; +}(_CustomElement); + + +var handleTemplate = document.createElement('template'); +handleTemplate.innerHTML = '\n \n \n '; + +var initialized = new WeakMap(); + +// Only top-level lists are draggable, and nested lists drag with their parent item. +function initItem(el) { + if (initialized.get(el)) return; + initialized.set(el, true); + + var taskList = el.closest('task-lists'); + if (!(taskList instanceof TaskListsElement)) return; + + // Single item task lists are not draggable. + if (taskList.querySelectorAll('.task-list-item').length <= 1) return; + + var fragment = handleTemplate.content.cloneNode(true); + var handle = fragment.querySelector('.handle'); + el.prepend(fragment); + + if (!handle) throw new Error('handle not found'); + handle.addEventListener('mouseenter', onHandleMouseOver); + handle.addEventListener('mouseleave', onHandleMouseOut); + + sortable(el, onSorted); + + // Drag operations don't remove :hover styles, so manage drag handle hover state. + el.addEventListener('mouseenter', onListItemMouseOver); + el.addEventListener('mouseleave', onListItemMouseOut); +} + +function onListItemMouseOver(event) { + var item = event.currentTarget; + if (!(item instanceof Element)) return; + + var list = item.closest('task-lists'); + if (!(list instanceof TaskListsElement)) return; + + if (list.sortable && !list.disabled) { + item.classList.add('hovered'); + } +} + +function onListItemMouseOut(event) { + var item = event.currentTarget; + if (!(item instanceof Element)) return; + item.classList.remove('hovered'); +} + +// Returns the list item position as a (list index, item index) tuple. +// Listen on top-level task lists because item indexing includes nested task lists. +function position(el) { + var list = rootTaskList(el); + if (!list) throw new Error('.contains-task-list not found'); + var flattened = Array.from(list.querySelectorAll('li')); + var index = flattened.indexOf(el.closest('.task-list-item')); + return [listIndex(list), index]; +} + +// Finds the list item's parent task list. +function taskList(el) { + var parent = el.parentElement; + return parent ? parent.closest('.contains-task-list') : null; +} + +// Is the task list this element belongs to a root list? +function isRootTaskList(el) { + return taskList(el) === rootTaskList(el); +} + +// Finds the highest task list in this element's ancestor chain. +function rootTaskList(node) { + var list = taskList(node); + return list ? rootTaskList(list) || list : null; +} + +function syncState(list) { + var items = list.querySelectorAll('.contains-task-list > .task-list-item'); + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = items[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var el = _step.value; + + if (isRootTaskList(el)) { + initItem(el); + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + syncDisabled(list); +} + +function syncDisabled(list) { + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = list.querySelectorAll('.task-list-item')[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var el = _step2.value; + + el.classList.toggle('enabled', !list.disabled); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; + + try { + for (var _iterator3 = list.querySelectorAll('.task-list-item-checkbox')[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var _el = _step3.value; + + if (_el instanceof HTMLInputElement) { + _el.disabled = list.disabled; + } + } + } catch (err) { + _didIteratorError3 = true; + _iteratorError3 = err; + } finally { + try { + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); + } + } finally { + if (_didIteratorError3) { + throw _iteratorError3; + } + } + } +} + +// Given a top-level task list, return its index location in the container. All +// lists in the container, not just task lists, are indexed to match the +// server-side Markdown parser's indexing. +function listIndex(list) { + var container = list.closest('task-lists'); + if (!container) throw new Error('parent not found'); + var top = Array.from(container.querySelectorAll('ol, ul')); + return top.indexOf(list); +} + +function onSorted(_ref) { + var src = _ref.src, + dst = _ref.dst; + + var container = src.list.closest('task-lists'); + if (!container) return; + + container.dispatchEvent(new CustomEvent('task-lists:move', { + bubbles: true, + detail: { + src: [listIndex(src.list), src.index], + dst: [listIndex(dst.list), dst.index] + } + })); +} + +// Enable list item drag when handle is hovered. +function onHandleMouseOver(event) { + var target = event.currentTarget; + if (!(target instanceof Element)) return; + + var item = target.closest('.task-list-item'); + if (!item) return; + + var list = item.closest('task-lists'); + if (!(list instanceof TaskListsElement)) return; + + if (list.sortable && !list.disabled) { + item.setAttribute('draggable', 'true'); + } +} + +// Disable list item drag so text selection works. +function onHandleMouseOut(event) { + if (isDragging()) return; + + var target = event.currentTarget; + if (!(target instanceof Element)) return; + + var item = target.closest('.task-list-item'); + if (!item) return; + + item.setAttribute('draggable', 'false'); +} + +if (!window.customElements.get('task-lists')) { + window.TaskListsElement = TaskListsElement; + window.customElements.define('task-lists', TaskListsElement); +} + +export default TaskListsElement; diff --git a/dist/index.esm.js.flow b/dist/index.esm.js.flow new file mode 100644 index 0000000..571856c --- /dev/null +++ b/dist/index.esm.js.flow @@ -0,0 +1,12 @@ +/* @flow */ + +declare class TaskListsElement extends HTMLElement { + get disabled(): boolean; + set disabled(value: boolean): void; + get sortable(): boolean; + set sortable(value: boolean): void; +} + +declare module '@github/task-lists-element' { + declare export default typeof TaskListsElement +} diff --git a/dist/index.umd.js b/dist/index.umd.js new file mode 100644 index 0000000..7abb5cb --- /dev/null +++ b/dist/index.umd.js @@ -0,0 +1,537 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.TaskListsElement = factory()); +}(this, (function () { 'use strict'; + + var sortHandlers = new WeakMap(); + var state = null; + + function isDragging() { + return !!state; + } + + function sortable(el, fn) { + sortHandlers.set(el, fn); + el.addEventListener('dragstart', onDragStart); + el.addEventListener('dragenter', onDragEnter); + el.addEventListener('dragend', onDragEnd); + el.addEventListener('drop', onDrop); + el.addEventListener('dragover', onDragOver); + } + + // Determine if item2 is before item1 in the tree. + function isBefore(item1, item2) { + if (item1.parentNode === item2.parentNode) { + var node = item1; + while (node) { + if (node === item2) return true; + node = node.previousElementSibling; + } + } + return false; + } + + function isSameContainer(item1, item2) { + return item1.closest('task-lists') === item2.closest('task-lists'); + } + + function onDragStart(event) { + // Ignore selected text dragging within list items. + if (event.currentTarget !== event.target) return; + + var target = event.currentTarget; + if (!(target instanceof Element)) return; + + var sourceList = target.closest('.contains-task-list'); + if (!sourceList) return; + + target.classList.add('is-ghost'); + + // Add data to drag operation (required for Firefox). + if (event.dataTransfer) { + event.dataTransfer.setData('text/plain', target.textContent.trim()); + } + + if (!target.parentElement) return; + var siblings = Array.from(target.parentElement.children); + var sourceIndex = siblings.indexOf(target); + + state = { + didDrop: false, + dragging: target, + dropzone: target, + sourceList: sourceList, + sourceSibling: siblings[sourceIndex + 1] || null, + sourceIndex: sourceIndex + }; + } + + function onDragEnter(event) { + // Allow dragging task list item, not text, links, or files. + if (!state) return; + + var dropzone = event.currentTarget; + if (!(dropzone instanceof Element)) return; + + if (!isSameContainer(state.dragging, dropzone)) { + event.stopPropagation(); + return; + } + + // Necessary for dragging + event.preventDefault(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move'; + } + + // Make sure we weren't already dragging in this task list item. + if (state.dropzone === dropzone) return; + + // Open an empty space in the list as a drop target. + state.dragging.classList.add('is-dragging'); + + state.dropzone = dropzone; + + if (isBefore(state.dragging, dropzone)) { + dropzone.before(state.dragging); + } else { + dropzone.after(state.dragging); + } + } + + function onDrop(event) { + // Allow dragging task list item, not text, links, or files. + if (!state) return; + + var dropzone = event.currentTarget; + if (!(dropzone instanceof Element)) return; + + // Notify dragend listener drag ended in a valid drop target. + state.didDrop = true; + + if (!state.dragging.parentElement) return; + var newIndex = Array.from(state.dragging.parentElement.children).indexOf(state.dragging); + + var currentList = dropzone.closest('.contains-task-list'); + if (!currentList) return; + + // Don't notify listener if the list hasn't changed. + if (state.sourceIndex === newIndex && state.sourceList === currentList) return; + + // Increment insertion index when moving item down in its own list. + if (state.sourceList === currentList && state.sourceIndex < newIndex) { + newIndex++; + } + + var src = { list: state.sourceList, index: state.sourceIndex }; + var dst = { list: currentList, index: newIndex }; + var fn = sortHandlers.get(state.dragging); + if (fn) fn({ src: src, dst: dst }); + } + + function onDragEnd() { + if (!state) return; + + state.dragging.classList.remove('is-dragging'); + state.dragging.classList.remove('is-ghost'); + + // If the drag ended in a valid drop target, the drop event listener will + // have already set this flag to true. If not, it'll still be false, and + // we'll know the drag was canceled. + if (!state.didDrop) { + state.sourceList.insertBefore(state.dragging, state.sourceSibling); + } + + // Reset for next drag. + state = null; + } + + function onDragOver(event) { + // Allow dragging task list item, not text, links, or files. + if (!state) return; + + var dropzone = event.currentTarget; + if (!(dropzone instanceof Element)) return; + + if (!isSameContainer(state.dragging, dropzone)) { + event.stopPropagation(); + return; + } + + // Necessary for dragging. + event.preventDefault(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move'; + } + } + + var classCallCheck = function (instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + }; + + var createClass = function () { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + return function (Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; + }(); + + var inherits = function (subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + enumerable: false, + writable: true, + configurable: true + } + }); + if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; + }; + + var possibleConstructorReturn = function (self, call) { + if (!self) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return call && (typeof call === "object" || typeof call === "function") ? call : self; + }; + + function _CustomElement() { + return Reflect.construct(HTMLElement, [], this.__proto__.constructor); + } + Object.setPrototypeOf(_CustomElement.prototype, HTMLElement.prototype); + Object.setPrototypeOf(_CustomElement, HTMLElement); + + var observers = new WeakMap(); + + var TaskListsElement = function (_CustomElement2) { + inherits(TaskListsElement, _CustomElement2); + + function TaskListsElement() { + classCallCheck(this, TaskListsElement); + + var _this = possibleConstructorReturn(this, (TaskListsElement.__proto__ || Object.getPrototypeOf(TaskListsElement)).call(this)); + + _this.addEventListener('change', function (event) { + var checkbox = event.target; + if (!(checkbox instanceof HTMLInputElement)) return; + if (!checkbox.classList.contains('task-list-item-checkbox')) return; + + _this.dispatchEvent(new CustomEvent('task-lists:check', { + bubbles: true, + detail: { + position: position(checkbox), + checked: checkbox.checked + } + })); + }); + + observers.set(_this, new MutationObserver(syncState.bind(null, _this))); + return _this; + } + + createClass(TaskListsElement, [{ + key: 'connectedCallback', + value: function connectedCallback() { + var observer = observers.get(this); + if (observer) { + observer.observe(this, { childList: true, subtree: true }); + } + syncState(this); + } + }, { + key: 'disconnectedCallback', + value: function disconnectedCallback() { + var observer = observers.get(this); + if (observer) { + observer.disconnect(); + } + } + }, { + key: 'attributeChangedCallback', + value: function attributeChangedCallback(name, oldValue, newValue) { + if (oldValue === newValue) return; + + switch (name) { + case 'disabled': + syncDisabled(this); + break; + } + } + }, { + key: 'disabled', + get: function get$$1() { + return this.hasAttribute('disabled'); + }, + set: function set$$1(value) { + if (value) { + this.setAttribute('disabled', ''); + } else { + this.removeAttribute('disabled'); + } + } + }, { + key: 'sortable', + get: function get$$1() { + return this.hasAttribute('sortable'); + }, + set: function set$$1(value) { + if (value) { + this.setAttribute('sortable', ''); + } else { + this.removeAttribute('sortable'); + } + } + }], [{ + key: 'observedAttributes', + get: function get$$1() { + return ['disabled']; + } + }]); + return TaskListsElement; + }(_CustomElement); + + + var handleTemplate = document.createElement('template'); + handleTemplate.innerHTML = '\n \n \n '; + + var initialized = new WeakMap(); + + // Only top-level lists are draggable, and nested lists drag with their parent item. + function initItem(el) { + if (initialized.get(el)) return; + initialized.set(el, true); + + var taskList = el.closest('task-lists'); + if (!(taskList instanceof TaskListsElement)) return; + + // Single item task lists are not draggable. + if (taskList.querySelectorAll('.task-list-item').length <= 1) return; + + var fragment = handleTemplate.content.cloneNode(true); + var handle = fragment.querySelector('.handle'); + el.prepend(fragment); + + if (!handle) throw new Error('handle not found'); + handle.addEventListener('mouseenter', onHandleMouseOver); + handle.addEventListener('mouseleave', onHandleMouseOut); + + sortable(el, onSorted); + + // Drag operations don't remove :hover styles, so manage drag handle hover state. + el.addEventListener('mouseenter', onListItemMouseOver); + el.addEventListener('mouseleave', onListItemMouseOut); + } + + function onListItemMouseOver(event) { + var item = event.currentTarget; + if (!(item instanceof Element)) return; + + var list = item.closest('task-lists'); + if (!(list instanceof TaskListsElement)) return; + + if (list.sortable && !list.disabled) { + item.classList.add('hovered'); + } + } + + function onListItemMouseOut(event) { + var item = event.currentTarget; + if (!(item instanceof Element)) return; + item.classList.remove('hovered'); + } + + // Returns the list item position as a (list index, item index) tuple. + // Listen on top-level task lists because item indexing includes nested task lists. + function position(el) { + var list = rootTaskList(el); + if (!list) throw new Error('.contains-task-list not found'); + var flattened = Array.from(list.querySelectorAll('li')); + var index = flattened.indexOf(el.closest('.task-list-item')); + return [listIndex(list), index]; + } + + // Finds the list item's parent task list. + function taskList(el) { + var parent = el.parentElement; + return parent ? parent.closest('.contains-task-list') : null; + } + + // Is the task list this element belongs to a root list? + function isRootTaskList(el) { + return taskList(el) === rootTaskList(el); + } + + // Finds the highest task list in this element's ancestor chain. + function rootTaskList(node) { + var list = taskList(node); + return list ? rootTaskList(list) || list : null; + } + + function syncState(list) { + var items = list.querySelectorAll('.contains-task-list > .task-list-item'); + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = items[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var el = _step.value; + + if (isRootTaskList(el)) { + initItem(el); + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + syncDisabled(list); + } + + function syncDisabled(list) { + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = list.querySelectorAll('.task-list-item')[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var el = _step2.value; + + el.classList.toggle('enabled', !list.disabled); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; + + try { + for (var _iterator3 = list.querySelectorAll('.task-list-item-checkbox')[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var _el = _step3.value; + + if (_el instanceof HTMLInputElement) { + _el.disabled = list.disabled; + } + } + } catch (err) { + _didIteratorError3 = true; + _iteratorError3 = err; + } finally { + try { + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); + } + } finally { + if (_didIteratorError3) { + throw _iteratorError3; + } + } + } + } + + // Given a top-level task list, return its index location in the container. All + // lists in the container, not just task lists, are indexed to match the + // server-side Markdown parser's indexing. + function listIndex(list) { + var container = list.closest('task-lists'); + if (!container) throw new Error('parent not found'); + var top = Array.from(container.querySelectorAll('ol, ul')); + return top.indexOf(list); + } + + function onSorted(_ref) { + var src = _ref.src, + dst = _ref.dst; + + var container = src.list.closest('task-lists'); + if (!container) return; + + container.dispatchEvent(new CustomEvent('task-lists:move', { + bubbles: true, + detail: { + src: [listIndex(src.list), src.index], + dst: [listIndex(dst.list), dst.index] + } + })); + } + + // Enable list item drag when handle is hovered. + function onHandleMouseOver(event) { + var target = event.currentTarget; + if (!(target instanceof Element)) return; + + var item = target.closest('.task-list-item'); + if (!item) return; + + var list = item.closest('task-lists'); + if (!(list instanceof TaskListsElement)) return; + + if (list.sortable && !list.disabled) { + item.setAttribute('draggable', 'true'); + } + } + + // Disable list item drag so text selection works. + function onHandleMouseOut(event) { + if (isDragging()) return; + + var target = event.currentTarget; + if (!(target instanceof Element)) return; + + var item = target.closest('.task-list-item'); + if (!item) return; + + item.setAttribute('draggable', 'false'); + } + + if (!window.customElements.get('task-lists')) { + window.TaskListsElement = TaskListsElement; + window.customElements.define('task-lists', TaskListsElement); + } + + return TaskListsElement; + +}))); diff --git a/dist/index.umd.js.flow b/dist/index.umd.js.flow new file mode 100644 index 0000000..571856c --- /dev/null +++ b/dist/index.umd.js.flow @@ -0,0 +1,12 @@ +/* @flow */ + +declare class TaskListsElement extends HTMLElement { + get disabled(): boolean; + set disabled(value: boolean): void; + get sortable(): boolean; + set sortable(value: boolean): void; +} + +declare module '@github/task-lists-element' { + declare export default typeof TaskListsElement +}