diff --git a/.gitignore b/.gitignore index 4d50219..9c34e25 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ node_modules bower_components -mock.png .*.sw* .build* jquery.fn.* -/test +.vscode \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3b9c6cb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contributing + +This element contains a script to automatically rebuild it for choosen Sortable release. + +**TL;DR** + +- Change `polymer-sortablejs-template.html` file instead of ~~`polymer-sortablejs.html`~~ +- Run `$ node bower_components/polymer-sortablejs/build.js` to update and rebuild element + +## Overview + +When _there's an update to Sortable framework_ for which this element is a wraper or you want to _rebuild to include changes you made in template_, you should run `build.js` script inside of element folder with **Node 8** or newer. It will overwrite `polymer-sortable.html` with content of template file `polymer-sortable-template.html` and all options in currently installed Sortable version, excluding those that are defined at the top of `build.js` file. + +### Choosing SOrtable version + +By default script looks for Sortable files in bower_components and node_modules and will use currently installed verison. +If it can't find them, script will download from GitHub (RubaXa/Sortable#master). You can use `-b` to specify branch or `-u` - username from which Sortable will be downloaded. + +### Exluding options + +On top of `build.js` file there is a constant Array + +````javascript +const DISABLED_PROPERTIES = [ + 'draggable', + 'setData', + 'supportPointer' +]; +```` + +Those properties will not be included in built file so you either need to manually set them in the element or make sure they aren't nescessary. + +### Editing template + +You can do whatever you want in the template file as long as you don't remove two comments: + +````javascript +/*properties*/ + +/*propertyobservers*/ +```` + +Those two must stay in their places and you mustn't forget they will be overwriten with normal property/function definiton - do not remove commas! + +## Pull Requests + +Before submitting a Pull Request be sure to run `build.js`. + +If you change the code, commit the same message both to the template and generated file if possible. + +If you change is only update to latest Sortable, update version in `bower.json` and commit your change as `Rebuilt for Sortable [verison-number]`. \ No newline at end of file diff --git a/bower.json b/bower.json index 94be943..471d5ac 100644 --- a/bower.json +++ b/bower.json @@ -25,7 +25,15 @@ "tests" ], "dependencies": { - "polymer": "polymer/polymer#^1.6.0", - "Sortable": "sortablejs#^1.4.2" + "polymer": "polymer/polymer#1.9 - 2", + "Sortable": "sortablejs#^1.6.0" + }, + "variants": { + "1.x": { + "dependencies": { + "polymer": "polymer/polymer#1.9", + "Sortable": "sortablejs#^1.6.0" + } + } } } diff --git a/build.js b/build.js new file mode 100644 index 0000000..ec76af6 --- /dev/null +++ b/build.js @@ -0,0 +1,182 @@ +const DISABLED_PROPERTIES = [ + 'draggable', + 'setData', + 'supportPointer' +]; + + +const fsCallbacks = require('fs'), + path = require('path'), + https = require('https'), + promisify = require('util').promisify; + +const fs = { + stat: promisify(fsCallbacks.stat), + readFile: promisify(fsCallbacks.readFile), + writeFile: promisify(fsCallbacks.writeFile), + exists: fsCallbacks.existsSync +}; + +// From https://stackoverflow.com/a/17676794/6166832 +function donwload(url, dest) { + return new Promise((resolve, reject) => { + const file = fsCallbacks.createWriteStream(dest), + request = http.get(url, response => { + response.pipe(file); + file.on('finish', () => file.close(reslove)); + }); + }); +} + + +// Helper for folder lookup +function generatePath(depth, ending) { + const generatedDepth = new Array(depth).fill('..'); + return path.join(__dirname, ...generatedDepth, ending); +} + +let found = false; + +// Look in bower_components +for (let depth = 0; depth <= 2; depth++) { + if (fs.exists(generatePath(depth, 'bower_components/Sortable/Sortable.js'))) { + found = true; + console.log('Building from version in bower_components'); + loadFromFile(generatePath(depth, 'bower_components/Sortable/Sortable.js')); + break; + } +} + +if (!found) { + // Look in node_modules + for (let depth = 0; depth <= 2; depth++) { + if (fs.exists(generatePath(depth, 'node_modules/Sortable/Sortable.js'))) { + found = true; + console.log('Building from version in node_modules'); + loadFromFile(generatePath(depth, 'node_modules/Sortable/Sortable.js')); + break; + } + } +} + +if (!found) { + downloadFromGit(); +} + + +async function loadFromFile(filePath) { + const file = await fs.readFile(filePath, 'utf8'); + proceed(file); +} + +async function downloadFromGit() { + const gitUsername = process.argv.includes('-u') + ? process.argv[process.argv.indexOf('-u') + 1] : 'RubaXa'; + const gitBranch = process.argv.includes('-b') + ? process.argv[process.argv.indexOf('-b') + 1] : 'master'; + https.get(`https://raw.githubusercontent.com/${gitUsername}/Sortable/${gitBranch}/Sortable.js`, resp => { + let data = ''; + resp.on('data', (chunk) => { + data += chunk; + }); + resp.on('end', () => { + proceed(data); + }); + }).on("error", err => { + console.trace(err); + throw new Error('Couldn\'t get Sortable.js'); + }); + console.log(`Building from Gtihub ${gitUsername}/Sortable#${gitBranch}`); +} + +async function proceed(string) { + // Get options from source code of Sortblejs + const optionsString = /var\s+defaults\s*=\s*{\s*(([^:]+:[^,}]+)+\s*)}/m.exec(string)[1], + optionsStringSplit = optionsString.split(''), + optionsArray = []; + // Read them into an array [ key, value, key, value... ] + let current = '', + depthLevel = 0, + depthExpectColon = false, + depthStringOpen = false; + for (let i in optionsStringSplit) { + const character = optionsStringSplit[i]; + if (character === '{' + || (character === '\'' && !depthStringOpen) || (character === '"' && !depthStringOpen) + || character === '?' + || character === '(') { + depthLevel++; + if (character === '?') depthExpectColon = true; + if (character === '\'' || character === '"') depthStringOpen = true; + if (depthLevel > 0) current += character; + } else if (character === '}' + || (character === '\'' && depthStringOpen) || (character === '"' && depthStringOpen) + || (depthExpectColon && character === ':') + || character === ')') { + depthLevel--; + if (character === ':') depthExpectColon = false; + if (character === '\'' || character === '"') depthStringOpen = false; + if (depthLevel >= 0) current += character; + } else if ((character === ',' + || character === ':') + && depthLevel === 0) { + optionsArray.push(current); + current = ''; + } else if (depthLevel > 0 || /[^\s:,]/.test(character)) { + current += character; + } + if (depthLevel < 0) { + optionsArray.push(current); + break; + } + } + // Throw if read options aren't even + if (optionsArray.length % 2) { + console.log('Options that were read:', optionsArray); + throw new Error('Something went wrong when reading options'); + } + // Process the array to attach types + const computedOptions = {}; + let key, value, type; + optionsArray.forEach((item, index) => { + index % 2 ? value = item : key = item; + if (value && key) { + if (!DISABLED_PROPERTIES.includes(key)) { + if (value === 'false' || value === 'true') type = 'Boolean' + else if (Number(value) !== NaN) type = 'Number' + else if (/$\s*\{/.test(value)) type = 'Object' + else if (/$\s*\[/.test(value)) type = 'Array' + else type = 'String' + computedOptions[key] = { value, type }; + } + key = value = type = null; + } + }); + + // Get template file + const template = await fs.readFile(path.join(__dirname, 'polymer-sortablejs-template.html'), 'utf8'); + let generatedTemplate = template; + + // Generate properties + generatedTemplate = generatedTemplate.replace(/^(\s*)\/\*properties\*\//m, (_, spacing) => { + let output = ''; + for (key in computedOptions) { + const type = computedOptions[key].type, + value = computedOptions[key].value; + output += `${spacing}${key}: { type: ${type}, value: function() { return ${value} }, observer: '${key}Changed' },\n` + } + return output.slice(0, -2); + }); + + // Generate observers + generatedTemplate = generatedTemplate.replace(/^(\s*)\/\*propertyobservers\*\//m, (_, spacing) => { + let output = ''; + for (key in computedOptions) { + if (!new RegExp(key + 'Changed').test(template)) + output += `${spacing}${key}Changed: function(value) { this.sortable && this.sortable.option("${key}", value); },\n` + } + return output.slice(0, -2); + }); + + await fs.writeFile(path.join(__dirname, 'polymer-sortablejs.html'), generatedTemplate, 'utf8'); +} \ No newline at end of file diff --git a/polymer-sortablejs-template.html b/polymer-sortablejs-template.html new file mode 100644 index 0000000..964c486 --- /dev/null +++ b/polymer-sortablejs-template.html @@ -0,0 +1,163 @@ +<link rel="import" href="../polymer/polymer.html"> +<script src="../Sortable/Sortable.js"></script> + +<dom-module id="sortable-js"> + <template> + <slot></slot> + </template> +</dom-module> +<script> +'use strict'; + Polymer({ + is: "sortable-js", + + properties: { + /*properties*/ + }, + + created: function() { + // Override default DOM property behavior + Object.defineProperties(this, { + draggable: { get: function() { return this._draggable || this.getAttribute("draggable") || ">*";}, set: function(value) { this._draggable = value; this.draggableChanged(value);} }, + }); + }, + + attached: function() { + // Given + // <sortable-js> + // <template is="dom-repeat" items={{data}}> + // <div> + // <template is="dom-if" if="true"> + // <span>hello</span></template></div> + // After render, it becomes + // <sortable-js> + // <div> + // <span>hello</span> + // <template is="dom-if"> + // <tempalte is="dom-repeat"> + this.initialize(); + + }, + + detached: function() { + this.destroy(); + }, + + + initialize: function() { + var templates = this.querySelectorAll(Polymer.Element ? "dom-repeat" : "template[is='dom-repeat']"); + var template = templates[templates.length-1]; + + var options = {}; + Object.keys(this.properties).forEach(function(key) { + options[key] = this[key]; + }.bind(this)); + + var _this = this; + var eventCallbacks = { + onUpdate: function (e) { + if (template) { + var elements = _this.children; + Array.prototype.filter.call(elements, function(el) { + return !el.classList.contains(_this.ghostClass) || + !el.nodeName.toLowerCase() === 'tempalte' || + !el.nodeName.toLowerCase() === 'dom-repeat'; + }); + if (e.oldIndex < e.newIndex) { + elements[e.oldIndex].parentNode.insertBefore(elements[e.newIndex], elements[e.oldIndex]); + } else { + elements[e.oldIndex].parentNode.insertBefore(elements[e.newIndex], elements[e.oldIndex].nextSibling); + } + template.splice("items", e.newIndex, 0, template.splice("items", e.oldIndex, 1)[0]); + /* + if (manuallyHandleUpdateEvents) { + template.items.splice(e.newIndex, 0, template.items.splice(e.oldIndex, 1)[0]); + } else { + template.splice("items", e.newIndex, 0, template.splice("items", e.oldIndex, 1)[0]); + } + */ + } + _this.fire("update", e); + }, + + onAdd: function(e) { + if (template) { + var froms = e.from.querySelectorAll(Polymer.Element ? "dom-repeat" : "template[is='dom-repeat']"); + var from = froms[froms.length-1]; + var model = from.modelForElement(e.item); + template.splice("items", e.newIndex, 0, model.item); + e.model = model; + } + _this.fire("add", e); + }, + + onRemove: function(e) { + // Donot remove if group.pull is clone + if (e.target.group.pull === 'clone') { + return false; + } + if (template) { + var item = template.splice("items", e.oldIndex, 1)[0]; + e.model = {item: item}; + } + _this.fire("remove", e); + }, + + onChoose: function(e) { + _this.fire("choose", e); + }, + + onStart: function(e) { + /* _this._elements = []; + var elements = _this.querySelectorAll(':not(dom-repeat):not(template):not(.'+ _this.ghostClass + ')'); + Array.prototype.forEach.call(elements, function(element) { + _this._elements.push(element); + }); */ + + _this.fire("start", e); + }, + + onEnd: function(e) { + _this.fire("end", e); + }, + + onSort: function(e) { + _this.fire("sort", e); + }, + + onFilter: function(e) { + _this.fire("filter", e); + }, + + onMove: function(e) { + _this.fire("move", e); + }, + + onClone: function(e) { + _this.fire("clone", e); + } + }; + + Object.keys(eventCallbacks).forEach(function(name){ + options[name] = eventCallbacks[name]; + }); + + this.sortable = Sortable.create(this, options); + }, + + destroy: function() { + if(this.sortable) { + this.sortable.destroy(); + } + }, + + groupChanged: function(value) { + if (typeof value === 'string') { + return this.set('group', { name: value }); + } + this.sortable && this.sortable.option("group", value); + }, + + /*propertyobservers*/ + }); +</script> diff --git a/polymer-sortablejs.html b/polymer-sortablejs.html index 215174b..de1e1e4 100644 --- a/polymer-sortablejs.html +++ b/polymer-sortablejs.html @@ -3,7 +3,7 @@ <dom-module id="sortable-js"> <template> - <content></content> + <slot></slot> </template> </dom-module> <script> @@ -12,34 +12,36 @@ is: "sortable-js", properties: { - group : { type: Object, value: () => {return {name: Math.random()};}, observer: "groupChanged" }, - sort : { type: Boolean, value: true, observer: "sortChanged" }, - disabled : { type: Boolean, value: false, observer: "disabledChanged" }, - store : { type: Object, value: null, observer: "storeChanged" }, - handle : { type: String, value: null, observer: "handleChanged" }, - scrollSensitivity : { type: Number, value: 30, observer: "scrollSensitivityChanged" }, - scrollSpeed : { type: Number, value: 10, observer: "scrollSpeedChanged" }, - ghostClass : { type: String, value: "sortable-ghost", observer: "ghostClassChanged" }, - chosenClass : { type: String, value: "sortable-chosen", observer: "chosenClassChanged" }, - ignore : { type: String, value: "a, img", observer: "ignoreChanged" }, - filter : { type: Object, value: null, observer: "filterChanged" }, - animation : { type: Number, value: 0, observer: "animationChanged" }, - dropBubble : { type: Boolean, value: false, observer: "dropBubbleChanged" }, - dragoverBubble : { type: Boolean, value: false, observer: "dragoverBubbleChanged" }, - dataIdAttr : { type: String, value: "data-id", observer: "dataIdAttrChanged" }, - delay : { type: Number, value: 0, observer: "delayChanged" }, - forceFallback : { type: Boolean, value: false, observer: "forceFallbackChanged" }, - fallbackClass : { type: String, value: "sortable-fallback", observer: "fallbackClassChanged" }, - fallbackOnBody : { type: Boolean, value: false, observer: "fallbackOnBodyChanged" }, - draggable : {}, - scroll : {} + group: { type: Number, value: function() { return Math.random() }, observer: 'groupChanged' }, + sort: { type: Boolean, value: function() { return true }, observer: 'sortChanged' }, + disabled: { type: Boolean, value: function() { return false }, observer: 'disabledChanged' }, + store: { type: Number, value: function() { return null }, observer: 'storeChanged' }, + handle: { type: Number, value: function() { return null }, observer: 'handleChanged' }, + scroll: { type: Boolean, value: function() { return true }, observer: 'scrollChanged' }, + scrollSensitivity: { type: Number, value: function() { return 30 }, observer: 'scrollSensitivityChanged' }, + scrollSpeed: { type: Number, value: function() { return 10 }, observer: 'scrollSpeedChanged' }, + ghostClass: { type: Number, value: function() { return 'sortable-ghost' }, observer: 'ghostClassChanged' }, + chosenClass: { type: Number, value: function() { return 'sortable-chosen' }, observer: 'chosenClassChanged' }, + dragClass: { type: Number, value: function() { return 'sortable-drag' }, observer: 'dragClassChanged' }, + ignore: { type: Number, value: function() { return 'a, img' }, observer: 'ignoreChanged' }, + filter: { type: Number, value: function() { return null }, observer: 'filterChanged' }, + preventOnFilter: { type: Boolean, value: function() { return true }, observer: 'preventOnFilterChanged' }, + animation: { type: Number, value: function() { return 0 }, observer: 'animationChanged' }, + dropBubble: { type: Boolean, value: function() { return false }, observer: 'dropBubbleChanged' }, + dragoverBubble: { type: Boolean, value: function() { return false }, observer: 'dragoverBubbleChanged' }, + dataIdAttr: { type: Number, value: function() { return 'data-id' }, observer: 'dataIdAttrChanged' }, + delay: { type: Number, value: function() { return 0 }, observer: 'delayChanged' }, + forceFallback: { type: Boolean, value: function() { return false }, observer: 'forceFallbackChanged' }, + fallbackClass: { type: Number, value: function() { return 'sortable-fallback' }, observer: 'fallbackClassChanged' }, + fallbackOnBody: { type: Boolean, value: function() { return false }, observer: 'fallbackOnBodyChanged' }, + fallbackTolerance: { type: Number, value: function() { return 0 }, observer: 'fallbackToleranceChanged' }, + fallbackOffset: { type: Number, value: function() { return {x: 0, y: 0} }, observer: 'fallbackOffsetChanged' } }, created: function() { - // override default DOM property behavior + // Override default DOM property behavior Object.defineProperties(this, { draggable: { get: function() { return this._draggable || this.getAttribute("draggable") || ">*";}, set: function(value) { this._draggable = value; this.draggableChanged(value);} }, - scroll: { get: function() { return this._scroll || JSON.parse(this.getAttribute("scroll") || "true"); }, set: function(value) { this._scroll = value; this.scrollChanged(value);} } }); }, @@ -66,7 +68,7 @@ initialize: function() { - var templates = this.querySelectorAll("template[is='dom-repeat']"); + var templates = this.querySelectorAll(Polymer.Element ? "dom-repeat" : "template[is='dom-repeat']"); var template = templates[templates.length-1]; var options = {}; @@ -78,6 +80,17 @@ var eventCallbacks = { onUpdate: function (e) { if (template) { + var elements = _this.children; + Array.prototype.filter.call(elements, function(el) { + return !el.classList.contains(_this.ghostClass) || + !el.nodeName.toLowerCase() === 'tempalte' || + !el.nodeName.toLowerCase() === 'dom-repeat'; + }); + if (e.oldIndex < e.newIndex) { + elements[e.oldIndex].parentNode.insertBefore(elements[e.newIndex], elements[e.oldIndex]); + } else { + elements[e.oldIndex].parentNode.insertBefore(elements[e.newIndex], elements[e.oldIndex].nextSibling); + } template.splice("items", e.newIndex, 0, template.splice("items", e.oldIndex, 1)[0]); /* if (manuallyHandleUpdateEvents) { @@ -92,10 +105,11 @@ onAdd: function(e) { if (template) { - var froms = e.from.querySelectorAll("template[is='dom-repeat']"); + var froms = e.from.querySelectorAll(Polymer.Element ? "dom-repeat" : "template[is='dom-repeat']"); var from = froms[froms.length-1]; - var item = from.items[e.oldIndex]; - template.splice("items", e.newIndex, 0, item); + var model = from.modelForElement(e.item); + template.splice("items", e.newIndex, 0, model.item); + e.model = model; } _this.fire("add", e); }, @@ -106,7 +120,8 @@ return false; } if (template) { - template.splice("items", e.oldIndex, 1)[0]; + var item = template.splice("items", e.oldIndex, 1)[0]; + e.model = {item: item}; } _this.fire("remove", e); }, @@ -116,6 +131,12 @@ }, onStart: function(e) { + /* _this._elements = []; + var elements = _this.querySelectorAll(':not(dom-repeat):not(template):not(.'+ _this.ghostClass + ')'); + Array.prototype.forEach.call(elements, function(element) { + _this._elements.push(element); + }); */ + _this.fire("start", e); }, @@ -153,31 +174,57 @@ } }, - groupChanged : function(value) { - if(typeof(value) === 'string') { - return this.set('group', {name: value}); + groupChanged: function(value) { + if (typeof value === 'string') { + return this.set('group', { name: value }); } - this.sortable && this.sortable.option("group", value ); + this.sortable && this.sortable.option("group", value); }, - sortChanged : function(value) { this.sortable && this.sortable.option("sort", value); }, - disabledChanged : function(value) { this.sortable && this.sortable.option("disabled", value); }, - storeChanged : function(value) { this.sortable && this.sortable.option("store", value); }, - handleChanged : function(value) { this.sortable && this.sortable.option("handle", value); }, - scrollChanged : function(value) { this.sortable && this.sortable.option("scroll", value); }, - scrollSensitivityChanged : function(value) { this.sortable && this.sortable.option("scrollSensitivity", value); }, - scrollSpeedChanged : function(value) { this.sortable && this.sortable.option("scrollSpeed", value); }, - draggableChanged : function(value) { this.sortable && this.sortable.option("draggable", value); }, - ghostClassChanged : function(value) { this.sortable && this.sortable.option("ghostClass", value); }, - chosenClassChanged : function(value) { this.sortable && this.sortable.option("chosenClass", value); }, - ignoreChanged : function(value) { this.sortable && this.sortable.option("ignore", value); }, - filterChanged : function(value) { this.sortable && this.sortable.option("filter", value); }, - animationChanged : function(value) { this.sortable && this.sortable.option("animation", value); }, - dropBubbleChanged : function(value) { this.sortable && this.sortable.option("dropBubble", value); }, - dragoverBubbleChanged : function(value) { this.sortable && this.sortable.option("dragoverBubble", value); }, - dataIdAttrChanged : function(value) { this.sortable && this.sortable.option("dataIdAttr", value); }, - delayChanged : function(value) { this.sortable && this.sortable.option("delay", value); }, - forceFallbackChanged : function(value) { this.sortable && this.sortable.option("forceFallback", value); }, - fallbackClassChanged : function(value) { this.sortable && this.sortable.option("fallbackClass", value); }, - fallbackOnBodyChanged : function(value) { this.sortable && this.sortable.option("fallbackOnBody", value); } + + sortChanged: function(value) { this.sortable && this.sortable.option("sort", value); }, + + disabledChanged: function(value) { this.sortable && this.sortable.option("disabled", value); }, + + storeChanged: function(value) { this.sortable && this.sortable.option("store", value); }, + + handleChanged: function(value) { this.sortable && this.sortable.option("handle", value); }, + + scrollChanged: function(value) { this.sortable && this.sortable.option("scroll", value); }, + + scrollSensitivityChanged: function(value) { this.sortable && this.sortable.option("scrollSensitivity", value); }, + + scrollSpeedChanged: function(value) { this.sortable && this.sortable.option("scrollSpeed", value); }, + + ghostClassChanged: function(value) { this.sortable && this.sortable.option("ghostClass", value); }, + + chosenClassChanged: function(value) { this.sortable && this.sortable.option("chosenClass", value); }, + + dragClassChanged: function(value) { this.sortable && this.sortable.option("dragClass", value); }, + + ignoreChanged: function(value) { this.sortable && this.sortable.option("ignore", value); }, + + filterChanged: function(value) { this.sortable && this.sortable.option("filter", value); }, + + preventOnFilterChanged: function(value) { this.sortable && this.sortable.option("preventOnFilter", value); }, + + animationChanged: function(value) { this.sortable && this.sortable.option("animation", value); }, + + dropBubbleChanged: function(value) { this.sortable && this.sortable.option("dropBubble", value); }, + + dragoverBubbleChanged: function(value) { this.sortable && this.sortable.option("dragoverBubble", value); }, + + dataIdAttrChanged: function(value) { this.sortable && this.sortable.option("dataIdAttr", value); }, + + delayChanged: function(value) { this.sortable && this.sortable.option("delay", value); }, + + forceFallbackChanged: function(value) { this.sortable && this.sortable.option("forceFallback", value); }, + + fallbackClassChanged: function(value) { this.sortable && this.sortable.option("fallbackClass", value); }, + + fallbackOnBodyChanged: function(value) { this.sortable && this.sortable.option("fallbackOnBody", value); }, + + fallbackToleranceChanged: function(value) { this.sortable && this.sortable.option("fallbackTolerance", value); }, + + fallbackOffsetChanged: function(value) { this.sortable && this.sortable.option("fallbackOffset", value); } }); </script>