diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..1398317 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,43 @@ +module.exports = { + "root": true, + "extends": "eslint:recommended", + "env": { + "node": true, + "es6": true, + "amd": true, + "browser": true + }, + "parserOptions": { + "ecmaFeatures": { + "globalReturn": true, + "generators": false, + "objectLiteralDuplicateProperties": false, + "experimentalObjectRestSpread": true + }, + "ecmaVersion": 2017, + "sourceType": "module" + }, + "plugins": [ + "import" + ], + "settings": { + "import/core-modules": [], + "import/ignore": [ + "node_modules", + "\\.(coffee|scss|css|less|hbs|svg|json)$" + ] + }, + "rules": { + "no-console": 0, + "comma-dangle": [ + "error", + { + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "ignore" + } + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md index a6b272b..46251e6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ -# cocoon-vanilla-js +# cocoon-vanilla-js (private) + A vanilla JS replacement for (Rails) Cocoon's jQuery script + + +## Usage + +Replace `` and `` with the corresponding values: + +``` +yarn add git+https://:x-oauth-basic@github.com/kollegorna/cocoon-vanilla-js.git# +``` + +Import as ES6 module: + +```js +import 'cocoon-vanilla-js' +``` + +## Notes + +To broaden browser support, use the following polyfills: + +- [Element.closest](https://www.npmjs.com/package/element-closest) +- [Element.classList](https://www.npmjs.com/package/classlist-polyfill) diff --git a/index.js b/index.js new file mode 100644 index 0000000..bae19f7 --- /dev/null +++ b/index.js @@ -0,0 +1,162 @@ +let cocoon_element_counter = 0; + +const create_new_id = function() { + return (new Date().getTime() + cocoon_element_counter++); +}; + +const newcontent_braced = function(id) { + return '[' + id + ']$1'; +}; + +const newcontent_underscord = function(id) { + return '_' + id + '_$1'; +}; + +const getInsertionNodeElem = function(insertionNode, insertionTraversal, btn) { + if(!insertionNode){ + return btn.parentNode; + } + else { // string + if(insertionTraversal) { + // TODO: + // https://github.com/nathanvda/cocoon/blob/master/app/assets/javascripts/cocoon.js#L32 + // data-association-insertion-traversal: the jquery traversal method to + // allow node selection relative to the link. closest, next, children, etc. + // return $this[insertionTraversal](insertionNode); + return null; + } + else { + return document.querySelector(insertionNode); + } + } +}; + +const addFieldsHandler = (btn) => { + const assoc = btn.getAttribute('data-association'); + const assocs = btn.getAttribute('data-associations'); + const content = btn.getAttribute('data-association-insertion-template'); + const insertionNode = btn.getAttribute('data-association-insertion-node'); + const insertionTraversal = btn.getAttribute('data-association-insertion-traversal'); + let insertionMethod = btn.getAttribute('data-association-insertion-method') || btn.getAttribute('data-association-insertion-position') || 'before'; + let new_id = create_new_id(); + let count = parseInt(btn.getAttribute('data-count'), 10); + let regexp_braced = new RegExp('\\[new_' + assoc + '\\](.*?\\s)', 'g'); + let regexp_underscord = new RegExp('_new_' + assoc + '_(\\w*)', 'g'); + let new_content = content.replace(regexp_braced, newcontent_braced(new_id)); + let new_contents = []; + + if(new_content == content) { + regexp_braced = new RegExp('\\[new_' + assocs + '\\](.*?\\s)', 'g'); + regexp_underscord = new RegExp('_new_' + assocs + '_(\\w*)', 'g'); + new_content = content.replace(regexp_braced, newcontent_braced(new_id)); + } + + new_content = new_content.replace(regexp_underscord, newcontent_underscord(new_id)); + new_contents = [new_content]; + + count = (isNaN(count) ? 1 : Math.max(count, 1)); + count -= 1; + + while(count) { + new_id = create_new_id(); + new_content = content.replace(regexp_braced, newcontent_braced(new_id)); + new_content = new_content.replace(regexp_underscord, newcontent_underscord(new_id)); + new_contents.push(new_content); + count -= 1; + } + + const insertionNodeElem = getInsertionNodeElem(insertionNode, insertionTraversal, btn); + + if(!insertionNodeElem || (insertionNodeElem.length == 0)) { + console.warn("Couldn't find the element to insert the template. Make sure your `data-association-insertion-*` on `link_to_add_association` is correct."); + } + + new_contents.forEach((node) => { + const event = new CustomEvent('cocoon:before-insert', {detail: node}); + insertionNodeElem.dispatchEvent(event); + + if(!event.defaultPrevented) { + switch(insertionMethod) { + default: + case 'before': + insertionMethod = 'beforebegin'; + break; + case 'after': + insertionMethod = 'afterend'; + break; + case 'append': + insertionMethod = 'beforeend'; + break; + case 'prepend': + insertionMethod = 'afterbegin'; + break; + } + + insertionNodeElem.insertAdjacentHTML(insertionMethod, node); + insertionNodeElem.dispatchEvent( + CustomEvent('cocoon:before-insert', {detail: node}) + ); + } + }); +}; + +document.addEventListener('click', (e) => { + if(e.target.matches('.add_fields')) { + e.preventDefault(); + e.stopPropagation(); + addFieldsHandler(e.target); + } +}); + +const removeFieldsHandler = (btn) => { + const wrapperClass = btn.getAttribute('data-wrapper-class') || 'nested-fields'; + const nodeToDelete = btn.closest(`.${wrapperClass}`); + const triggerNode = nodeToDelete.parentNode; + + const event = new CustomEvent('cocoon:before-remove', {detail: nodeToDelete}); + triggerNode.dispatchEvent(event); + + if(!event.defaultPrevented) { + const timeout = triggerNode.getAttribute('data-remove-timeout') || 0; + + setTimeout(() => { + if(btn.classList.contains('dynamic')) { + // nodeToDelete.remove(); + nodeToDelete.parentNode.removeChild(nodeToDelete); + } + else { + const input = btn.previousElementSibling; + if(input && input.matches('input[type=hidden]')) { + input.value = 1; + } + nodeToDelete.style.display = 'none'; + } + + triggerNode.dispatchEvent( + CustomEvent('cocoon:after-remove', {detail: nodeToDelete}) + ); + }, timeout); + } +}; + +document.addEventListener('click', (e) => { + if( + e.target.matches('.remove_fields.dynamic') || + e.target.matches('.remove_fields.existing') + ) { + e.preventDefault(); + e.stopPropagation(); + removeFieldsHandler(e.target); + } +}); + +const hideFields = () => { + [...document.querySelectorAll('.remove_fields.existing.destroyed')].forEach((btn) => { + const wrapperClass = btn.getAttribute('data-wrapper-class') || 'nested-fields'; + btn.closest(`.${wrapperClass}`).style.display = 'none'; + }); +}; + +document.addEventListener('DOMContentLoaded', hideFields); +document.addEventListener('turbolinks:load', hideFields); +document.addEventListener('page:load', hideFields); diff --git a/package.json b/package.json new file mode 100644 index 0000000..f3725f2 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "cocoon-vanilla-js", + "version": "1.0.0", + "description": "A vanilla JS replacement for (Rails) Cocoon's jQuery script", + "main": "index.js", + "engines": { + "node": "<=9.11.2" + }, + "repository": "git+https://github.com/kollegorna/cocoon-vanilla-js.git", + "author": "Kollegorna", + "license": "MIT", + "bugs": { + "url": "https://github.com/kollegorna/cocoon-vanilla-js/issues" + }, + "homepage": "https://github.com/kollegorna/cocoon-vanilla-js#readme", + "devDependencies": {}, + "dependencies": {}, + "private": true +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..67ea6d5 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,7 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"js-utils@git+https://8265015e18616ae2d58e38f4fd9ca111b8beaff8:x-oauth-basic@github.com/kollegorna/js-utils.git#6507589": + version "1.0.0" + resolved "git+https://8265015e18616ae2d58e38f4fd9ca111b8beaff8:x-oauth-basic@github.com/kollegorna/js-utils.git#6507589ae81c91ff2db72d7c4905a3d2d52674f9"