diff --git a/bower.json b/bower.json index 4214bac..ddbc409 100644 --- a/bower.json +++ b/bower.json @@ -18,7 +18,8 @@ "dependencies": { "angular": "~1.x", "jquery": "2.x", - "jquery-ui": "latest" + "jquery-ui": "latest", + "jison":"git://github.com/zaach/jison.git" }, "devDependencies": { "angular-mocks": "~1.x" diff --git a/lexersrc/README.md b/lexersrc/README.md new file mode 100644 index 0000000..2e840f4 --- /dev/null +++ b/lexersrc/README.md @@ -0,0 +1,10 @@ +These files generate the Parser.js found in the main source of this repo. The parser was generated using a tool called jison which can be found here. Currently the primary purpose of the parser is to add the ability to parse json arguments to the drag and drop callbacks. + +##Changing the parser. + +If you need to make changes to the parser you can update the parser.lex and the parser.y files then use jison to generate the Parser.js file.The command should look like this: + +node_modules/.bin/jison lexersrc/parser.y lexersrc/parser.lex --outfile src/Parser.js + +This will generate a javascript parsing engine in the src/Parser.js file, based on your two grammar files. + diff --git a/lexersrc/parser.lex b/lexersrc/parser.lex new file mode 100644 index 0000000..1362f42 --- /dev/null +++ b/lexersrc/parser.lex @@ -0,0 +1,69 @@ +%{ + /* Note for future hackers */ + /* Jison by default gets the first matched item, working top down + * This is NOT the same as lex, which matches longest + * Adding the %option flex to the file will make jison work like lex + * but, this is not heavily tested + * + * The safest thing to do is have more important rules before less + * important rules, which is why . is last + */ +%} +int "-"?([0-9]|[1-9][0-9]+) +exp [eE][-+]?[0-9]+ +frac "."[0-9]+ + +%x json singleQuoteString dblQuoteString +%flex +%% + +[^{,][^,]+ return 'LITERAL'; +"{" { + saver.currText = "{"; + this.begin('json'); +} +"," { + // do nothing we don't want commas +} +<> return 'EOF'; + +"}" { + saver.currText = saver.currText + yytext; + if (saver.braceCount == 0) { + yytext = saver.currText; + saver.currText = ""; + this.popState(); + return 'JSON'; + } + else { + saver.braceCount -= 1; + } +} +"{" { + saver.currText = saver.currText + yytext; + saver.braceCount += 1; +} +<> { + yytext = saver.currText; + saver.currText = ""; + this.popState(); + return 'EOF'; +} +\s+ { saver.currText = saver.currText + yytext; } +"'" { saver.currText = saver.currText + yytext; this.begin('singleQuoteString'); } +\" { saver.currText = saver.currText + yytext; this.begin('dblQuoteString'); } +"//" { this.begin('singleLineComment'); } +"/*" { this.begin('multiLineComment'); } +. { saver.currText = saver.currText + yytext; } + +"\'" { saver.currText = saver.currText + yytext; } +"'" { saver.currText = saver.currText + yytext; this.popState(); } +. { saver.currText = saver.currText + yytext; } + +"\\\"" { saver.currText = saver.currText + yytext; } +\" { saver.currText = saver.currText + yytext; this.popState(); } +. { saver.currText = saver.currText + yytext; } + +%% + +var saver = { currText: "", braceCount:0 } diff --git a/lexersrc/parser.y b/lexersrc/parser.y new file mode 100644 index 0000000..de06015 --- /dev/null +++ b/lexersrc/parser.y @@ -0,0 +1,53 @@ +%start parsedargs + +%{ + +%} + +%% + +parsedargs + : EOF + { + $$ = [undefined]; + return $$ + } + | arguments EOF + { + $$ = $1; + return $$; + } + ; +arguments + : stringarg + { + $$ = [$1]; + } + | JSON + { + $$ = [$1]; + } + | arguments stringarg + { + $1.push($2); + $$ = $1; + } + | arguments JSON + { + $1.push($2) + $$ = $1; + } + ; + +stringarg + : LITERAL + { + $$ = $1; + } + | LITERAL stringarg + { + $$ = $1 + $2; + } + ; + +%% diff --git a/package.json b/package.json index 67be4b5..0241dfd 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,12 @@ "license": "MIT", "homepage": "http://codef0rmer.github.io/angular-dragdrop", "main": "src/angular-dragdrop.js", - "dependencies": {}, + "dependencies": { + }, "devDependencies": { "grunt": "~0.4.1", - "grunt-karma": "~0.4.4" + "grunt-karma": "~0.4.4", + "jison":"*" }, "scripts": {}, "repository": { diff --git a/src/Parser.js b/src/Parser.js new file mode 100644 index 0000000..0e3452d --- /dev/null +++ b/src/Parser.js @@ -0,0 +1,697 @@ +/* parser generated by jison 0.4.13 */ +/* + Returns a Parser object of the following structure: + + Parser: { + yy: {} + } + + Parser.prototype: { + yy: {}, + trace: function(), + symbols_: {associative list: name ==> number}, + terminals_: {associative list: number ==> name}, + productions_: [...], + performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), + table: [...], + defaultActions: {...}, + parseError: function(str, hash), + parse: function(input), + + lexer: { + EOF: 1, + parseError: function(str, hash), + setInput: function(input), + input: function(), + unput: function(str), + more: function(), + less: function(n), + pastInput: function(), + upcomingInput: function(), + showPosition: function(), + test_match: function(regex_match_array, rule_index), + next: function(), + lex: function(), + begin: function(condition), + popState: function(), + _currentRules: function(), + topState: function(), + pushState: function(condition), + + options: { + ranges: boolean (optional: true ==> token location info will include a .range[] member) + flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) + backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) + }, + + performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), + rules: [...], + conditions: {associative list: name ==> set}, + } + } + + + token location info (@$, _$, etc.): { + first_line: n, + last_line: n, + first_column: n, + last_column: n, + range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) + } + + + the parseError function receives a 'hash' object with these members for lexer and parser errors: { + text: (matched text) + token: (the produced terminal token, if any) + line: (yylineno) + } + while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { + loc: (yylloc) + expected: (string describing the set of expected tokens) + recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) + } +*/ +var Parser = (function(){ +var parser = {trace: function trace() { }, +yy: {}, +symbols_: {"error":2,"parsedargs":3,"EOF":4,"arguments":5,"stringarg":6,"JSON":7,"LITERAL":8,"$accept":0,"$end":1}, +terminals_: {2:"error",4:"EOF",7:"JSON",8:"LITERAL"}, +productions_: [0,[3,1],[3,2],[5,1],[5,1],[5,2],[5,2],[6,1],[6,2]], +performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { +/* this == yyval */ + +var $0 = $$.length - 1; +switch (yystate) { +case 1: + this.$ = [undefined]; + return this.$ + +break; +case 2: + this.$ = $$[$0-1]; + return this.$; + +break; +case 3: + this.$ = [$$[$0]]; + +break; +case 4: + this.$ = [$$[$0]]; + +break; +case 5: + $$[$0-1].push($$[$0]); + this.$ = $$[$0-1]; + +break; +case 6: + $$[$0-1].push($$[$0]) + this.$ = $$[$0-1]; + +break; +case 7: + this.$ = $$[$0]; + +break; +case 8: + this.$ = $$[$0-1] + $$[$0]; + +break; +} +}, +table: [{3:1,4:[1,2],5:3,6:4,7:[1,5],8:[1,6]},{1:[3]},{1:[2,1]},{4:[1,7],6:8,7:[1,9],8:[1,6]},{4:[2,3],7:[2,3],8:[2,3]},{4:[2,4],7:[2,4],8:[2,4]},{4:[2,7],6:10,7:[2,7],8:[1,6]},{1:[2,2]},{4:[2,5],7:[2,5],8:[2,5]},{4:[2,6],7:[2,6],8:[2,6]},{4:[2,8],7:[2,8],8:[2,8]}], +defaultActions: {2:[2,1],7:[2,2]}, +parseError: function parseError(str, hash) { + if (hash.recoverable) { + this.trace(str); + } else { + throw new Error(str); + } +}, +parse: function parse(input) { + var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; + var args = lstack.slice.call(arguments, 1); + this.lexer.setInput(input); + this.lexer.yy = this.yy; + this.yy.lexer = this.lexer; + this.yy.parser = this; + if (typeof this.lexer.yylloc == 'undefined') { + this.lexer.yylloc = {}; + } + var yyloc = this.lexer.yylloc; + lstack.push(yyloc); + var ranges = this.lexer.options && this.lexer.options.ranges; + if (typeof this.yy.parseError === 'function') { + this.parseError = this.yy.parseError; + } else { + this.parseError = Object.getPrototypeOf(this).parseError; + } + function popStack(n) { + stack.length = stack.length - 2 * n; + vstack.length = vstack.length - n; + lstack.length = lstack.length - n; + } + function lex() { + var token; + token = self.lexer.lex() || EOF; + if (typeof token !== 'number') { + token = self.symbols_[token] || token; + } + return token; + } + var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; + while (true) { + state = stack[stack.length - 1]; + if (this.defaultActions[state]) { + action = this.defaultActions[state]; + } else { + if (symbol === null || typeof symbol == 'undefined') { + symbol = lex(); + } + action = table[state] && table[state][symbol]; + } + if (typeof action === 'undefined' || !action.length || !action[0]) { + var errStr = ''; + expected = []; + for (p in table[state]) { + if (this.terminals_[p] && p > TERROR) { + expected.push('\'' + this.terminals_[p] + '\''); + } + } + if (this.lexer.showPosition) { + errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + this.lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; + } else { + errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); + } + this.parseError(errStr, { + text: this.lexer.match, + token: this.terminals_[symbol] || symbol, + line: this.lexer.yylineno, + loc: yyloc, + expected: expected + }); + } + if (action[0] instanceof Array && action.length > 1) { + throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); + } + switch (action[0]) { + case 1: + stack.push(symbol); + vstack.push(this.lexer.yytext); + lstack.push(this.lexer.yylloc); + stack.push(action[1]); + symbol = null; + if (!preErrorSymbol) { + yyleng = this.lexer.yyleng; + yytext = this.lexer.yytext; + yylineno = this.lexer.yylineno; + yyloc = this.lexer.yylloc; + if (recovering > 0) { + recovering--; + } + } else { + symbol = preErrorSymbol; + preErrorSymbol = null; + } + break; + case 2: + len = this.productions_[action[1]][1]; + yyval.$ = vstack[vstack.length - len]; + yyval._$ = { + first_line: lstack[lstack.length - (len || 1)].first_line, + last_line: lstack[lstack.length - 1].last_line, + first_column: lstack[lstack.length - (len || 1)].first_column, + last_column: lstack[lstack.length - 1].last_column + }; + if (ranges) { + yyval._$.range = [ + lstack[lstack.length - (len || 1)].range[0], + lstack[lstack.length - 1].range[1] + ]; + } + r = this.performAction.apply(yyval, [ + yytext, + yyleng, + yylineno, + this.yy, + action[1], + vstack, + lstack + ].concat(args)); + if (typeof r !== 'undefined') { + return r; + } + if (len) { + stack = stack.slice(0, -1 * len * 2); + vstack = vstack.slice(0, -1 * len); + lstack = lstack.slice(0, -1 * len); + } + stack.push(this.productions_[action[1]][0]); + vstack.push(yyval.$); + lstack.push(yyval._$); + newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; + stack.push(newState); + break; + case 3: + return true; + } + } + return true; +}}; + + + +/* generated by jison-lex 0.2.1 */ +var lexer = (function(){ +var lexer = { + +EOF:1, + +parseError:function parseError(str, hash) { + if (this.yy.parser) { + this.yy.parser.parseError(str, hash); + } else { + throw new Error(str); + } + }, + +// resets the lexer, sets new input +setInput:function (input) { + this._input = input; + this._more = this._backtrack = this.done = false; + this.yylineno = this.yyleng = 0; + this.yytext = this.matched = this.match = ''; + this.conditionStack = ['INITIAL']; + this.yylloc = { + first_line: 1, + first_column: 0, + last_line: 1, + last_column: 0 + }; + if (this.options.ranges) { + this.yylloc.range = [0,0]; + } + this.offset = 0; + return this; + }, + +// consumes and returns one char from the input +input:function () { + var ch = this._input[0]; + this.yytext += ch; + this.yyleng++; + this.offset++; + this.match += ch; + this.matched += ch; + var lines = ch.match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno++; + this.yylloc.last_line++; + } else { + this.yylloc.last_column++; + } + if (this.options.ranges) { + this.yylloc.range[1]++; + } + + this._input = this._input.slice(1); + return ch; + }, + +// unshifts one char (or a string) into the input +unput:function (ch) { + var len = ch.length; + var lines = ch.split(/(?:\r\n?|\n)/g); + + this._input = ch + this._input; + this.yytext = this.yytext.substr(0, this.yytext.length - len - 1); + //this.yyleng -= len; + this.offset -= len; + var oldLines = this.match.split(/(?:\r\n?|\n)/g); + this.match = this.match.substr(0, this.match.length - 1); + this.matched = this.matched.substr(0, this.matched.length - 1); + + if (lines.length - 1) { + this.yylineno -= lines.length - 1; + } + var r = this.yylloc.range; + + this.yylloc = { + first_line: this.yylloc.first_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.first_column, + last_column: lines ? + (lines.length === oldLines.length ? this.yylloc.first_column : 0) + + oldLines[oldLines.length - lines.length].length - lines[0].length : + this.yylloc.first_column - len + }; + + if (this.options.ranges) { + this.yylloc.range = [r[0], r[0] + this.yyleng - len]; + } + this.yyleng = this.yytext.length; + return this; + }, + +// When called from action, caches matched text and appends it on next action +more:function () { + this._more = true; + return this; + }, + +// When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. +reject:function () { + if (this.options.backtrack_lexer) { + this._backtrack = true; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + + } + return this; + }, + +// retain first n characters of the match +less:function (n) { + this.unput(this.match.slice(n)); + }, + +// displays already matched input, i.e. for error messages +pastInput:function () { + var past = this.matched.substr(0, this.matched.length - this.match.length); + return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); + }, + +// displays upcoming input, i.e. for error messages +upcomingInput:function () { + var next = this.match; + if (next.length < 20) { + next += this._input.substr(0, 20-next.length); + } + return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); + }, + +// displays the character position where the lexing error occurred, i.e. for error messages +showPosition:function () { + var pre = this.pastInput(); + var c = new Array(pre.length + 1).join("-"); + return pre + this.upcomingInput() + "\n" + c + "^"; + }, + +// test the lexed token: return FALSE when not a match, otherwise return token +test_match:function (match, indexed_rule) { + var token, + lines, + backup; + + if (this.options.backtrack_lexer) { + // save context + backup = { + yylineno: this.yylineno, + yylloc: { + first_line: this.yylloc.first_line, + last_line: this.last_line, + first_column: this.yylloc.first_column, + last_column: this.yylloc.last_column + }, + yytext: this.yytext, + match: this.match, + matches: this.matches, + matched: this.matched, + yyleng: this.yyleng, + offset: this.offset, + _more: this._more, + _input: this._input, + yy: this.yy, + conditionStack: this.conditionStack.slice(0), + done: this.done + }; + if (this.options.ranges) { + backup.yylloc.range = this.yylloc.range.slice(0); + } + } + + lines = match[0].match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno += lines.length; + } + this.yylloc = { + first_line: this.yylloc.last_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.last_column, + last_column: lines ? + lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : + this.yylloc.last_column + match[0].length + }; + this.yytext += match[0]; + this.match += match[0]; + this.matches = match; + this.yyleng = this.yytext.length; + if (this.options.ranges) { + this.yylloc.range = [this.offset, this.offset += this.yyleng]; + } + this._more = false; + this._backtrack = false; + this._input = this._input.slice(match[0].length); + this.matched += match[0]; + token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); + if (this.done && this._input) { + this.done = false; + } + if (token) { + return token; + } else if (this._backtrack) { + // recover context + for (var k in backup) { + this[k] = backup[k]; + } + return false; // rule action called reject() implying the next rule should be tested instead. + } + return false; + }, + +// return next match in input +next:function () { + if (this.done) { + return this.EOF; + } + if (!this._input) { + this.done = true; + } + + var token, + match, + tempMatch, + index; + if (!this._more) { + this.yytext = ''; + this.match = ''; + } + var rules = this._currentRules(); + for (var i = 0; i < rules.length; i++) { + tempMatch = this._input.match(this.rules[rules[i]]); + if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { + match = tempMatch; + index = i; + if (this.options.backtrack_lexer) { + token = this.test_match(tempMatch, rules[i]); + if (token !== false) { + return token; + } else if (this._backtrack) { + match = false; + continue; // rule action called reject() implying a rule MISmatch. + } else { + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + } else if (!this.options.flex) { + break; + } + } + } + if (match) { + token = this.test_match(match, rules[index]); + if (token !== false) { + return token; + } + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + if (this._input === "") { + return this.EOF; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + } + }, + +// return next match that has a token +lex:function lex() { + var r = this.next(); + if (r) { + return r; + } else { + return this.lex(); + } + }, + +// activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) +begin:function begin(condition) { + this.conditionStack.push(condition); + }, + +// pop the previously active lexer condition state off the condition stack +popState:function popState() { + var n = this.conditionStack.length - 1; + if (n > 0) { + return this.conditionStack.pop(); + } else { + return this.conditionStack[0]; + } + }, + +// produce the lexer rule set which is active for the currently active lexer condition state +_currentRules:function _currentRules() { + if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { + return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; + } else { + return this.conditions["INITIAL"].rules; + } + }, + +// return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available +topState:function topState(n) { + n = this.conditionStack.length - 1 - Math.abs(n || 0); + if (n >= 0) { + return this.conditionStack[n]; + } else { + return "INITIAL"; + } + }, + +// alias for begin(condition) +pushState:function pushState(condition) { + this.begin(condition); + }, + +// return the number of states currently on the stack +stateStackSize:function stateStackSize() { + return this.conditionStack.length; + }, +options: {}, +performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { + + /* Note for future hackers */ + /* Jison by default gets the first matched item, working top down + * This is NOT the same as lex, which matches longest + * Adding the %option flex to the file will make jison work like lex + * but, this is not heavily tested + * + * The safest thing to do is have more important rules before less + * important rules, which is why . is last + */ + +var YYSTATE=YY_START; +switch($avoiding_name_collisions) { +case 0:return 8; +break; +case 1: + saver.currText = "{"; + this.begin('json'); + +break; +case 2: + // do nothing we don't want commas + +break; +case 3:return 4; +break; +case 4: + saver.currText = saver.currText + yy_.yytext; + if (saver.braceCount == 0) { + yy_.yytext = saver.currText; + saver.currText = ""; + this.popState(); + return 7; + } + else { + saver.braceCount -= 1; + } + +break; +case 5: + saver.currText = saver.currText + yy_.yytext; + saver.braceCount += 1; + +break; +case 6: + yy_.yytext = saver.currText; + saver.currText = ""; + this.popState(); + return 4; + +break; +case 7: saver.currText = saver.currText + yy_.yytext; +break; +case 8: saver.currText = saver.currText + yy_.yytext; this.begin('singleQuoteString'); +break; +case 9: saver.currText = saver.currText + yy_.yytext; this.begin('dblQuoteString'); +break; +case 10: this.begin('singleLineComment'); +break; +case 11: this.begin('multiLineComment'); +break; +case 12: saver.currText = saver.currText + yy_.yytext; +break; +case 13: saver.currText = saver.currText + yy_.yytext; +break; +case 14: saver.currText = saver.currText + yy_.yytext; this.popState(); +break; +case 15: saver.currText = saver.currText + yy_.yytext; +break; +case 16: saver.currText = saver.currText + yy_.yytext; +break; +case 17: saver.currText = saver.currText + yy_.yytext; this.popState(); +break; +case 18: saver.currText = saver.currText + yy_.yytext; +break; +} +}, +rules: [/^(?:[^{,][^,]+)/,/^(?:\{)/,/^(?:,)/,/^(?:$)/,/^(?:\})/,/^(?:\{)/,/^(?:$)/,/^(?:\s+)/,/^(?:')/,/^(?:")/,/^(?:\/\/)/,/^(?:\/\*)/,/^(?:.)/,/^(?:\\')/,/^(?:')/,/^(?:.)/,/^(?:\\")/,/^(?:")/,/^(?:.)/], +conditions: {"json":{"rules":[4,5,6,7,8,9,10,11,12],"inclusive":false},"singleQuoteString":{"rules":[13,14,15],"inclusive":false},"dblQuoteString":{"rules":[16,17,18],"inclusive":false},"INITIAL":{"rules":[0,1,2,3],"inclusive":true}} +}; +var saver = { currText: "", braceCount:0 }; +return lexer; +})(); +parser.lexer = lexer; +function Parser () { + this.yy = {}; +} +Parser.prototype = parser;parser.Parser = Parser; +return new Parser; +})(); + + +if (typeof require !== 'undefined' && typeof exports !== 'undefined') { +exports.parser = Parser; +exports.Parser = Parser.Parser; +exports.parse = function () { return Parser.parse.apply(Parser, arguments); }; +exports.main = function commonjsMain(args) { + if (!args[1]) { + console.log('Usage: '+args[0]+' FILE'); + process.exit(1); + } + var source = require('fs').readFileSync(require('path').normalize(args[1]), "utf8"); + return exports.parser.parse(source); +}; +if (typeof module !== 'undefined' && require.main === module) { + exports.main(process.argv.slice(1)); +} +} \ No newline at end of file diff --git a/src/angular-dragdrop.js b/src/angular-dragdrop.js index b4ade31..114a2cd 100644 --- a/src/angular-dragdrop.js +++ b/src/angular-dragdrop.js @@ -27,299 +27,342 @@ * (c) 2013 Amit Gharat a.k.a codef0rmer - amitgharat.wordpress.com */ -(function (window, angular, undefined) { -'use strict'; - -var jqyoui = angular.module('ngDragDrop', []).service('ngDragDropService', ['$timeout', '$parse', function($timeout, $parse) { - this.callEventCallback = function (scope, callbackName, event, ui) { - if (!callbackName) return; - - var objExtract = extract(callbackName), +(function(window, angular, undefined) { + 'use strict'; + + var jqyoui = angular.module('ngDragDrop', []).service('ngDragDropService', ['$timeout', '$parse', + function($timeout, $parse) { + this.callEventCallback = function(scope, callbackName, event, ui) { + if (!callbackName) return; + var self = this; + var objExtract = extract(callbackName), callback = objExtract.callback, constructor = objExtract.constructor, args = [event, ui].concat(objExtract.args); - - // call either $scoped method i.e. $scope.dropCallback or constructor's method i.e. this.dropCallback - scope.$apply((scope[callback] || scope[constructor][callback]).apply(scope, args)); - - function extract(callbackName) { - var atStartBracket = callbackName.indexOf('(') !== -1 ? callbackName.indexOf('(') : callbackName.length, + + // call either $scoped method i.e. $scope.dropCallback or constructor's method i.e. this.dropCallback + scope.$apply((scope[callback] || scope[constructor][callback]).apply(scope, args)); + + function extract(callbackName) { + var atStartBracket = callbackName.indexOf('(') !== -1 ? callbackName.indexOf('(') : callbackName.length, atEndBracket = callbackName.lastIndexOf(')') !== -1 ? callbackName.lastIndexOf(')') : callbackName.length, args = callbackName.substring(atStartBracket + 1, atEndBracket), // matching function arguments inside brackets constructor = callbackName.match(/^[^.]+.\s*/)[0].slice(0, -1); // matching a string upto a dot to check ctrl as syntax - constructor = scope[constructor] && typeof scope[constructor].constructor === 'function' ? constructor : null; - - return { - callback: callbackName.substring(constructor && constructor.length + 1 || 0, atStartBracket), - args: (args && args.split(',') || []).map(function(item) { return $parse(item)(scope); }), - constructor: constructor + constructor = scope[constructor] && typeof scope[constructor].constructor === 'function' ? constructor : null; + self.joinedArguments = []; + var parsedArgs = Parser.parse(args); + return { + callback: callbackName.substring(constructor && constructor.length + 1 || 0, atStartBracket), + args: parsedArgs.map(function(item) { + return $parse(item)(scope); + }), + constructor: constructor + }; } - } - }; - - this.invokeDrop = function ($draggable, $droppable, event, ui) { - var dragModel = '', - dropModel = '', - dragSettings = {}, - dropSettings = {}, - jqyoui_pos = null, - dragItem = {}, - dropItem = {}, - dragModelValue, - dropModelValue, - $droppableDraggable = null, - droppableScope = $droppable.scope(), - draggableScope = $draggable.scope(); - - dragModel = $draggable.ngattr('ng-model'); - dropModel = $droppable.ngattr('ng-model'); - dragModelValue = draggableScope.$eval(dragModel); - dropModelValue = droppableScope.$eval(dropModel); - - $droppableDraggable = $droppable.find('[jqyoui-draggable]:last,[data-jqyoui-draggable]:last'); - dropSettings = droppableScope.$eval($droppable.attr('jqyoui-droppable') || $droppable.attr('data-jqyoui-droppable')) || []; - dragSettings = draggableScope.$eval($draggable.attr('jqyoui-draggable') || $draggable.attr('data-jqyoui-draggable')) || []; - - // Helps pick up the right item - dragSettings.index = this.fixIndex(draggableScope, dragSettings, dragModelValue); - dropSettings.index = this.fixIndex(droppableScope, dropSettings, dropModelValue); - - jqyoui_pos = angular.isArray(dragModelValue) ? dragSettings.index : null; - dragItem = angular.copy(angular.isArray(dragModelValue) ? dragModelValue[jqyoui_pos] : dragModelValue); - - if (angular.isArray(dropModelValue) && dropSettings && dropSettings.index !== undefined) { - dropItem = dropModelValue[dropSettings.index]; - } else if (!angular.isArray(dropModelValue)) { - dropItem = dropModelValue; - } else { - dropItem = {}; - } - dropItem = angular.copy(dropItem); - - if (dragSettings.animate === true) { - this.move($draggable, $droppableDraggable.length > 0 ? $droppableDraggable : $droppable, null, 'fast', dropSettings, null); - this.move($droppableDraggable.length > 0 && !dropSettings.multiple ? $droppableDraggable : [], $draggable.parent('[jqyoui-droppable],[data-jqyoui-droppable]'), jqyoui.startXY, 'fast', dropSettings, angular.bind(this, function() { - $timeout(angular.bind(this, function() { - // Do not move this into move() to avoid flickering issue - $draggable.css({'position': 'relative', 'left': '', 'top': ''}); - // Angular v1.2 uses ng-hide to hide an element not display property - // so we've to manually remove display:none set in this.move() - $droppableDraggable.css({'position': 'relative', 'left': '', 'top': '', 'display': ''}); + }; + + this.invokeDrop = function($draggable, $droppable, event, ui) { + var dragModel = '', + dropModel = '', + dragSettings = {}, + dropSettings = {}, + jqyoui_pos = null, + dragItem = {}, + dropItem = {}, + dragModelValue, + dropModelValue, + $droppableDraggable = null, + droppableScope = $droppable.scope(), + draggableScope = $draggable.scope(); + + dragModel = $draggable.ngattr('ng-model'); + dropModel = $droppable.ngattr('ng-model'); + dragModelValue = draggableScope.$eval(dragModel); + dropModelValue = droppableScope.$eval(dropModel); + + $droppableDraggable = $droppable.find('[jqyoui-draggable]:last,[data-jqyoui-draggable]:last'); + dropSettings = droppableScope.$eval($droppable.attr('jqyoui-droppable') || $droppable.attr('data-jqyoui-droppable')) || []; + dragSettings = draggableScope.$eval($draggable.attr('jqyoui-draggable') || $draggable.attr('data-jqyoui-draggable')) || []; + + // Helps pick up the right item + dragSettings.index = this.fixIndex(draggableScope, dragSettings, dragModelValue); + dropSettings.index = this.fixIndex(droppableScope, dropSettings, dropModelValue); + + jqyoui_pos = angular.isArray(dragModelValue) ? dragSettings.index : null; + dragItem = angular.copy(angular.isArray(dragModelValue) ? dragModelValue[jqyoui_pos] : dragModelValue); + + if (angular.isArray(dropModelValue) && dropSettings && dropSettings.index !== undefined) { + dropItem = dropModelValue[dropSettings.index]; + } else if (!angular.isArray(dropModelValue)) { + dropItem = dropModelValue; + } else { + dropItem = {}; + } + dropItem = angular.copy(dropItem); + + if (dragSettings.animate === true) { + this.move($draggable, $droppableDraggable.length > 0 ? $droppableDraggable : $droppable, null, 'fast', dropSettings, null); + this.move($droppableDraggable.length > 0 && !dropSettings.multiple ? $droppableDraggable : [], $draggable.parent('[jqyoui-droppable],[data-jqyoui-droppable]'), jqyoui.startXY, 'fast', dropSettings, angular.bind(this, function() { + $timeout(angular.bind(this, function() { + // Do not move this into move() to avoid flickering issue + $draggable.css({ + 'position': 'relative', + 'left': '', + 'top': '' + }); + // Angular v1.2 uses ng-hide to hide an element not display property + // so we've to manually remove display:none set in this.move() + $droppableDraggable.css({ + 'position': 'relative', + 'left': '', + 'top': '', + 'display': '' + }); + this.mutateDraggable(draggableScope, dropSettings, dragSettings, dragModel, dropModel, dropItem, $draggable); + this.mutateDroppable(droppableScope, dropSettings, dragSettings, dropModel, dragItem, jqyoui_pos); + this.callEventCallback(droppableScope, dropSettings.onDrop, event, ui); + })); + })); + } else { + $timeout(angular.bind(this, function() { this.mutateDraggable(draggableScope, dropSettings, dragSettings, dragModel, dropModel, dropItem, $draggable); this.mutateDroppable(droppableScope, dropSettings, dragSettings, dropModel, dragItem, jqyoui_pos); this.callEventCallback(droppableScope, dropSettings.onDrop, event, ui); })); - })); - } else { - $timeout(angular.bind(this, function() { - this.mutateDraggable(draggableScope, dropSettings, dragSettings, dragModel, dropModel, dropItem, $draggable); - this.mutateDroppable(droppableScope, dropSettings, dragSettings, dropModel, dragItem, jqyoui_pos); - this.callEventCallback(droppableScope, dropSettings.onDrop, event, ui); - })); - } - }; - - this.move = function($fromEl, $toEl, toPos, duration, dropSettings, callback) { - if ($fromEl.length === 0) { - if (callback) { - window.setTimeout(function() { - callback(); - }, 300); } - return false; - } - - var zIndex = 9999, - fromPos = $fromEl[dropSettings.containment || 'offset'](), - wasVisible = $toEl && $toEl.is(':visible'), - hadNgHideCls = $toEl.hasClass('ng-hide'); - - if (toPos === null && $toEl.length > 0) { - if (($toEl.attr('jqyoui-draggable') || $toEl.attr('data-jqyoui-draggable')) !== undefined && $toEl.ngattr('ng-model') !== undefined && $toEl.is(':visible') && dropSettings && dropSettings.multiple) { - toPos = $toEl[dropSettings.containment || 'offset'](); - if (dropSettings.stack === false) { - toPos.left+= $toEl.outerWidth(true); - } else { - toPos.top+= $toEl.outerHeight(true); + }; + + this.move = function($fromEl, $toEl, toPos, duration, dropSettings, callback) { + if ($fromEl.length === 0) { + if (callback) { + window.setTimeout(function() { + callback(); + }, 300); } - } else { - // Angular v1.2 uses ng-hide to hide an element - // so we've to remove it in order to grab its position - if (hadNgHideCls) $toEl.removeClass('ng-hide'); - toPos = $toEl.css({'visibility': 'hidden', 'display': 'block'})[dropSettings.containment || 'offset'](); - $toEl.css({'visibility': '','display': wasVisible ? 'block' : 'none'}); + return false; } - } - - $fromEl.css({'position': 'absolute', 'z-index': zIndex}) - .css(fromPos) - .animate(toPos, duration, function() { - // Angular v1.2 uses ng-hide to hide an element - // and as we remove it above, we've to put it back to - // hide the element (while swapping) if it was hidden already - // because we remove the display:none in this.invokeDrop() - if (hadNgHideCls) $toEl.addClass('ng-hide'); - if (callback) callback(); - }); - }; - this.mutateDroppable = function(scope, dropSettings, dragSettings, dropModel, dragItem, jqyoui_pos) { - var dropModelValue = scope.$eval(dropModel); - - scope.dndDragItem = dragItem; + var zIndex = 9999, + fromPos = $fromEl[dropSettings.containment || 'offset'](), + wasVisible = $toEl && $toEl.is(':visible'), + hadNgHideCls = $toEl.hasClass('ng-hide'); + + if (toPos === null && $toEl.length > 0) { + if (($toEl.attr('jqyoui-draggable') || $toEl.attr('data-jqyoui-draggable')) !== undefined && $toEl.ngattr('ng-model') !== undefined && $toEl.is(':visible') && dropSettings && dropSettings.multiple) { + toPos = $toEl[dropSettings.containment || 'offset'](); + if (dropSettings.stack === false) { + toPos.left += $toEl.outerWidth(true); + } else { + toPos.top += $toEl.outerHeight(true); + } + } else { + // Angular v1.2 uses ng-hide to hide an element + // so we've to remove it in order to grab its position + if (hadNgHideCls) $toEl.removeClass('ng-hide'); + toPos = $toEl.css({ + 'visibility': 'hidden', + 'display': 'block' + })[dropSettings.containment || 'offset'](); + $toEl.css({ + 'visibility': '', + 'display': wasVisible ? 'block' : 'none' + }); + } + } - if (angular.isArray(dropModelValue)) { - if (dropSettings && dropSettings.index >= 0) { - dropModelValue[dropSettings.index] = dragItem; + $fromEl.css({ + 'position': 'absolute', + 'z-index': zIndex + }) + .css(fromPos) + .animate(toPos, duration, function() { + // Angular v1.2 uses ng-hide to hide an element + // and as we remove it above, we've to put it back to + // hide the element (while swapping) if it was hidden already + // because we remove the display:none in this.invokeDrop() + if (hadNgHideCls) $toEl.addClass('ng-hide'); + if (callback) callback(); + }); + }; + + this.mutateDroppable = function(scope, dropSettings, dragSettings, dropModel, dragItem, jqyoui_pos) { + var dropModelValue = scope.$eval(dropModel); + + scope.dndDragItem = dragItem; + + if (angular.isArray(dropModelValue)) { + if (dropSettings && dropSettings.index >= 0) { + dropModelValue[dropSettings.index] = dragItem; + } else { + dropModelValue.push(dragItem); + } + if (dragSettings && dragSettings.placeholder === true) { + dropModelValue[dropModelValue.length - 1]['jqyoui_pos'] = jqyoui_pos; + } } else { - dropModelValue.push(dragItem); - } - if (dragSettings && dragSettings.placeholder === true) { - dropModelValue[dropModelValue.length - 1]['jqyoui_pos'] = jqyoui_pos; - } - } else { - $parse(dropModel + ' = dndDragItem')(scope); - if (dragSettings && dragSettings.placeholder === true) { - dropModelValue['jqyoui_pos'] = jqyoui_pos; + $parse(dropModel + ' = dndDragItem')(scope); + if (dragSettings && dragSettings.placeholder === true) { + dropModelValue['jqyoui_pos'] = jqyoui_pos; + } } - } - }; + }; - this.mutateDraggable = function(scope, dropSettings, dragSettings, dragModel, dropModel, dropItem, $draggable) { - var isEmpty = angular.equals(dropItem, {}), - dragModelValue = scope.$eval(dragModel); + this.mutateDraggable = function(scope, dropSettings, dragSettings, dragModel, dropModel, dropItem, $draggable) { + var isEmpty = angular.equals(dropItem, {}), + dragModelValue = scope.$eval(dragModel); - scope.dndDropItem = dropItem; + scope.dndDropItem = dropItem; - if (dragSettings && dragSettings.placeholder) { - if (dragSettings.placeholder != 'keep'){ - if (angular.isArray(dragModelValue) && dragSettings.index !== undefined) { - dragModelValue[dragSettings.index] = dropItem; - } else { - $parse(dragModel + ' = dndDropItem')(scope); - } - } - } else { - if (angular.isArray(dragModelValue)) { - if (isEmpty) { - if (dragSettings && ( dragSettings.placeholder !== true && dragSettings.placeholder !== 'keep' )) { - dragModelValue.splice(dragSettings.index, 1); + if (dragSettings && dragSettings.placeholder) { + if (dragSettings.placeholder != 'keep') { + if (angular.isArray(dragModelValue) && dragSettings.index !== undefined) { + dragModelValue[dragSettings.index] = dropItem; + } else { + $parse(dragModel + ' = dndDropItem')(scope); } - } else { - dragModelValue[dragSettings.index] = dropItem; } } else { - // Fix: LIST(object) to LIST(array) - model does not get updated using just scope[dragModel] = {...} - // P.S.: Could not figure out why it happened - $parse(dragModel + ' = dndDropItem')(scope); - if (scope.$parent) { - $parse(dragModel + ' = dndDropItem')(scope.$parent); + if (angular.isArray(dragModelValue)) { + if (isEmpty) { + if (dragSettings && (dragSettings.placeholder !== true && dragSettings.placeholder !== 'keep')) { + dragModelValue.splice(dragSettings.index, 1); + } + } else { + dragModelValue[dragSettings.index] = dropItem; + } + } else { + // Fix: LIST(object) to LIST(array) - model does not get updated using just scope[dragModel] = {...} + // P.S.: Could not figure out why it happened + $parse(dragModel + ' = dndDropItem')(scope); + if (scope.$parent) { + $parse(dragModel + ' = dndDropItem')(scope.$parent); + } } } - } - $draggable.css({'z-index': '', 'left': '', 'top': ''}); - }; + $draggable.css({ + 'z-index': '', + 'left': '', + 'top': '' + }); + }; - this.fixIndex = function(scope, settings, modelValue) { - if (settings.applyFilter && angular.isArray(modelValue) && modelValue.length > 0) { - var dragModelValueFiltered = scope[settings.applyFilter](), + this.fixIndex = function(scope, settings, modelValue) { + if (settings.applyFilter && angular.isArray(modelValue) && modelValue.length > 0) { + var dragModelValueFiltered = scope[settings.applyFilter](), lookup = dragModelValueFiltered[settings.index], - actualIndex = undefined; + actualIndex; - modelValue.forEach(function(item, i) { - if (angular.equals(item, lookup)) { - actualIndex = i; - } - }); + modelValue.forEach(function(item, i) { + if (angular.equals(item, lookup)) { + actualIndex = i; + } + }); - return actualIndex; - } - - return settings.index; - }; - }]).directive('jqyouiDraggable', ['ngDragDropService', function(ngDragDropService) { - return { - require: '?jqyouiDroppable', - restrict: 'A', - link: function(scope, element, attrs) { - var dragSettings, jqyouiOptions, zIndex; - var updateDraggable = function(newValue, oldValue) { - if (newValue) { - dragSettings = scope.$eval(element.attr('jqyoui-draggable') || element.attr('data-jqyoui-draggable')) || {}; - jqyouiOptions = scope.$eval(attrs.jqyouiOptions) || {}; - element - .draggable({disabled: false}) - .draggable(jqyouiOptions) - .draggable({ - start: function(event, ui) { - zIndex = angular.element(jqyouiOptions.helper ? ui.helper : this).css('z-index'); - angular.element(jqyouiOptions.helper ? ui.helper : this).css('z-index', 9999); - jqyoui.startXY = angular.element(this)[dragSettings.containment || 'offset'](); - ngDragDropService.callEventCallback(scope, dragSettings.onStart, event, ui); - }, - stop: function(event, ui) { - angular.element(jqyouiOptions.helper ? ui.helper : this).css('z-index', zIndex); - ngDragDropService.callEventCallback(scope, dragSettings.onStop, event, ui); - }, - drag: function(event, ui) { - ngDragDropService.callEventCallback(scope, dragSettings.onDrag, event, ui); - } - }); - } else { - element.draggable({disabled: true}); - } - }; - scope.$watch(function() { return scope.$eval(attrs.drag); }, updateDraggable); - updateDraggable(); + return actualIndex; + } - element.on('$destroy', function() { - element.draggable('destroy'); - }); - } - }; - }]).directive('jqyouiDroppable', ['ngDragDropService', function(ngDragDropService) { - return { - restrict: 'A', - priority: 1, - link: function(scope, element, attrs) { - var dropSettings; - var updateDroppable = function(newValue, oldValue) { - if (newValue) { - dropSettings = scope.$eval(angular.element(element).attr('jqyoui-droppable') || angular.element(element).attr('data-jqyoui-droppable')) || {}; - element - .droppable({disabled: false}) - .droppable(scope.$eval(attrs.jqyouiOptions) || {}) - .droppable({ - over: function(event, ui) { - ngDragDropService.callEventCallback(scope, dropSettings.onOver, event, ui); - }, - out: function(event, ui) { - ngDragDropService.callEventCallback(scope, dropSettings.onOut, event, ui); - }, - drop: function(event, ui) { - if (angular.element(ui.draggable).ngattr('ng-model') && attrs.ngModel) { - ngDragDropService.invokeDrop(angular.element(ui.draggable), angular.element(this), event, ui); - } else { - ngDragDropService.callEventCallback(scope, dropSettings.onDrop, event, ui); + return settings.index; + }; + } + ]).directive('jqyouiDraggable', ['ngDragDropService', + function(ngDragDropService) { + return { + require: '?jqyouiDroppable', + restrict: 'A', + link: function(scope, element, attrs) { + var dragSettings, jqyouiOptions, zIndex; + var updateDraggable = function(newValue, oldValue) { + if (newValue) { + dragSettings = scope.$eval(element.attr('jqyoui-draggable') || element.attr('data-jqyoui-draggable')) || {}; + jqyouiOptions = scope.$eval(attrs.jqyouiOptions) || {}; + element + .draggable({ + disabled: false + }) + .draggable(jqyouiOptions) + .draggable({ + start: function(event, ui) { + zIndex = angular.element(jqyouiOptions.helper ? ui.helper : this).css('z-index'); + angular.element(jqyouiOptions.helper ? ui.helper : this).css('z-index', 9999); + jqyoui.startXY = angular.element(this)[dragSettings.containment || 'offset'](); + ngDragDropService.callEventCallback(scope, dragSettings.onStart, event, ui); + }, + stop: function(event, ui) { + angular.element(jqyouiOptions.helper ? ui.helper : this).css('z-index', zIndex); + ngDragDropService.callEventCallback(scope, dragSettings.onStop, event, ui); + }, + drag: function(event, ui) { + ngDragDropService.callEventCallback(scope, dragSettings.onDrag, event, ui); } - } + }); + } else { + element.draggable({ + disabled: true }); - } else { - element.droppable({disabled: true}); - } - }; + } + }; + scope.$watch(function() { + return scope.$eval(attrs.drag); + }, updateDraggable); + updateDraggable(); + + element.on('$destroy', function() { + element.draggable('destroy'); + }); + } + }; + } + ]).directive('jqyouiDroppable', ['ngDragDropService', + function(ngDragDropService) { + return { + restrict: 'A', + priority: 1, + link: function(scope, element, attrs) { + var dropSettings; + var updateDroppable = function(newValue, oldValue) { + if (newValue) { + dropSettings = scope.$eval(angular.element(element).attr('jqyoui-droppable') || angular.element(element).attr('data-jqyoui-droppable')) || {}; + element + .droppable({ + disabled: false + }) + .droppable(scope.$eval(attrs.jqyouiOptions) || {}) + .droppable({ + over: function(event, ui) { + ngDragDropService.callEventCallback(scope, dropSettings.onOver, event, ui); + }, + out: function(event, ui) { + ngDragDropService.callEventCallback(scope, dropSettings.onOut, event, ui); + }, + drop: function(event, ui) { + if (angular.element(ui.draggable).ngattr('ng-model') && attrs.ngModel) { + ngDragDropService.invokeDrop(angular.element(ui.draggable), angular.element(this), event, ui); + } else { + ngDragDropService.callEventCallback(scope, dropSettings.onDrop, event, ui); + } + } + }); + } else { + element.droppable({ + disabled: true + }); + } + }; - scope.$watch(function() { return scope.$eval(attrs.drop); }, updateDroppable); - updateDroppable(); + scope.$watch(function() { + return scope.$eval(attrs.drop); + }, updateDroppable); + updateDroppable(); - element.on('$destroy', function() { - element.droppable('destroy'); - }); - } - }; - }]); + element.on('$destroy', function() { + element.droppable('destroy'); + }); + } + }; + } + ]); $.fn.ngattr = function(name, value) { var element = angular.element(this).get(0); diff --git a/src/angular-dragdrop.min.js b/src/angular-dragdrop.min.js index ec57732..4ff6611 100644 --- a/src/angular-dragdrop.min.js +++ b/src/angular-dragdrop.min.js @@ -16,14 +16,11 @@ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. - */ - -/** + *//** * Implementing Drag and Drop functionality in AngularJS is easier than ever. * Demo: http://codef0rmer.github.com/angular-dragdrop/ - * + * * @version 1.0.7 * * (c) 2013 Amit Gharat a.k.a codef0rmer - amitgharat.wordpress.com - */ -(function(e,t,n){"use strict";var r=t.module("ngDragDrop",[]).service("ngDragDropService",["$timeout","$parse",function(i,s){this.callEventCallback=function(e,t,n,r){function f(t){var n=t.indexOf("(")!==-1?t.indexOf("("):t.length,r=t.lastIndexOf(")")!==-1?t.lastIndexOf(")"):t.length,i=t.substring(n+1,r),o=t.match(/^[^.]+.\s*/)[0].slice(0,-1);o=e[o]&&typeof e[o].constructor==="function"?o:null;return{callback:t.substring(o&&o.length+1||0,n),args:(i&&i.split(",")||[]).map(function(t){return s(t)(e)}),constructor:o}}if(!t)return;var i=f(t),o=i.callback,u=i.constructor,a=[n,r].concat(i.args);e.$apply((e[o]||e[u][o]).apply(e,a))};this.invokeDrop=function(e,s,o,u){var a="",f="",l={},c={},h=null,p={},d={},v,m,g=null,y=s.scope(),b=e.scope();a=e.ngattr("ng-model");f=s.ngattr("ng-model");v=b.$eval(a);m=y.$eval(f);g=s.find("[jqyoui-draggable]:last,[data-jqyoui-draggable]:last");c=y.$eval(s.attr("jqyoui-droppable")||s.attr("data-jqyoui-droppable"))||[];l=b.$eval(e.attr("jqyoui-draggable")||e.attr("data-jqyoui-draggable"))||[];l.index=this.fixIndex(b,l,v);c.index=this.fixIndex(y,c,m);h=t.isArray(v)?l.index:null;p=t.copy(t.isArray(v)?v[h]:v);if(t.isArray(m)&&c&&c.index!==n){d=m[c.index]}else if(!t.isArray(m)){d=m}else{d={}}d=t.copy(d);if(l.animate===true){this.move(e,g.length>0?g:s,null,"fast",c,null);this.move(g.length>0&&!c.multiple?g:[],e.parent("[jqyoui-droppable],[data-jqyoui-droppable]"),r.startXY,"fast",c,t.bind(this,function(){i(t.bind(this,function(){e.css({position:"relative",left:"",top:""});g.css({position:"relative",left:"",top:"",display:""});this.mutateDraggable(b,c,l,a,f,d,e);this.mutateDroppable(y,c,l,f,p,h);this.callEventCallback(y,c.onDrop,o,u)}))}))}else{i(t.bind(this,function(){this.mutateDraggable(b,c,l,a,f,d,e);this.mutateDroppable(y,c,l,f,p,h);this.callEventCallback(y,c.onDrop,o,u)}))}};this.move=function(t,r,i,s,o,u){if(t.length===0){if(u){e.setTimeout(function(){u()},300)}return false}var a=9999,f=t[o.containment||"offset"](),l=r&&r.is(":visible"),c=r.hasClass("ng-hide");if(i===null&&r.length>0){if((r.attr("jqyoui-draggable")||r.attr("data-jqyoui-draggable"))!==n&&r.ngattr("ng-model")!==n&&r.is(":visible")&&o&&o.multiple){i=r[o.containment||"offset"]();if(o.stack===false){i.left+=r.outerWidth(true)}else{i.top+=r.outerHeight(true)}}else{if(c)r.removeClass("ng-hide");i=r.css({visibility:"hidden",display:"block"})[o.containment||"offset"]();r.css({visibility:"",display:l?"block":"none"})}}t.css({position:"absolute","z-index":a}).css(f).animate(i,s,function(){if(c)r.addClass("ng-hide");if(u)u()})};this.mutateDroppable=function(e,n,r,i,o,u){var a=e.$eval(i);e.dndDragItem=o;if(t.isArray(a)){if(n&&n.index>=0){a[n.index]=o}else{a.push(o)}if(r&&r.placeholder===true){a[a.length-1]["jqyoui_pos"]=u}}else{s(i+" = dndDragItem")(e);if(r&&r.placeholder===true){a["jqyoui_pos"]=u}}};this.mutateDraggable=function(e,r,i,o,u,a,f){var l=t.equals(a,{}),c=e.$eval(o);e.dndDropItem=a;if(i&&i.placeholder){if(i.placeholder!="keep"){if(t.isArray(c)&&i.index!==n){c[i.index]=a}else{s(o+" = dndDropItem")(e)}}}else{if(t.isArray(c)){if(l){if(i&&i.placeholder!==true&&i.placeholder!=="keep"){c.splice(i.index,1)}}else{c[i.index]=a}}else{s(o+" = dndDropItem")(e);if(e.$parent){s(o+" = dndDropItem")(e.$parent)}}}f.css({"z-index":"",left:"",top:""})};this.fixIndex=function(e,r,i){if(r.applyFilter&&t.isArray(i)&&i.length>0){var s=e[r.applyFilter](),o=s[r.index],u=n;i.forEach(function(e,n){if(t.equals(e,o)){u=n}});return u}return r.index}}]).directive("jqyouiDraggable",["ngDragDropService",function(e){return{require:"?jqyouiDroppable",restrict:"A",link:function(n,i,s){var o,u,a;var f=function(f,l){if(f){o=n.$eval(i.attr("jqyoui-draggable")||i.attr("data-jqyoui-draggable"))||{};u=n.$eval(s.jqyouiOptions)||{};i.draggable({disabled:false}).draggable(u).draggable({start:function(i,s){a=t.element(u.helper?s.helper:this).css("z-index");t.element(u.helper?s.helper:this).css("z-index",9999);r.startXY=t.element(this)[o.containment||"offset"]();e.callEventCallback(n,o.onStart,i,s)},stop:function(r,i){t.element(u.helper?i.helper:this).css("z-index",a);e.callEventCallback(n,o.onStop,r,i)},drag:function(t,r){e.callEventCallback(n,o.onDrag,t,r)}})}else{i.draggable({disabled:true})}};n.$watch(function(){return n.$eval(s.drag)},f);f();i.on("$destroy",function(){i.draggable("destroy")})}}}]).directive("jqyouiDroppable",["ngDragDropService",function(e){return{restrict:"A",priority:1,link:function(n,r,i){var s;var o=function(o,u){if(o){s=n.$eval(t.element(r).attr("jqyoui-droppable")||t.element(r).attr("data-jqyoui-droppable"))||{};r.droppable({disabled:false}).droppable(n.$eval(i.jqyouiOptions)||{}).droppable({over:function(t,r){e.callEventCallback(n,s.onOver,t,r)},out:function(t,r){e.callEventCallback(n,s.onOut,t,r)},drop:function(r,o){if(t.element(o.draggable).ngattr("ng-model")&&i.ngModel){e.invokeDrop(t.element(o.draggable),t.element(this),r,o)}else{e.callEventCallback(n,s.onDrop,r,o)}}})}else{r.droppable({disabled:true})}};n.$watch(function(){return n.$eval(i.drop)},o);o();r.on("$destroy",function(){r.droppable("destroy")})}}}]);$.fn.ngattr=function(e,n){var r=t.element(this).get(0);return r.getAttribute(e)||r.getAttribute("data-"+e)}})(window,window.angular) + */(function(e,t,n){"use strict";var r=t.module("ngDragDrop",[]).service("ngDragDropService",["$timeout","$parse",function(i,s){this.callEventCallback=function(e,t,n,r){function l(t){var n=t.indexOf("(")!==-1?t.indexOf("("):t.length,r=t.lastIndexOf(")")!==-1?t.lastIndexOf(")"):t.length,o=t.substring(n+1,r),u=t.match(/^[^.]+.\s*/)[0].slice(0,-1);u=e[u]&&typeof e[u].constructor=="function"?u:null,i.joinedArguments=[];var a=Parser.parse(o);return{callback:t.substring(u&&u.length+1||0,n),args:a.map(function(t){return s(t)(e)}),constructor:u}}if(!t)return;var i=this,o=l(t),u=o.callback,a=o.constructor,f=[n,r].concat(o.args);e.$apply((e[u]||e[a][u]).apply(e,f))},this.invokeDrop=function(e,s,o,u){var a="",f="",l={},c={},h=null,p={},d={},v,m,g=null,y=s.scope(),b=e.scope();a=e.ngattr("ng-model"),f=s.ngattr("ng-model"),v=b.$eval(a),m=y.$eval(f),g=s.find("[jqyoui-draggable]:last,[data-jqyoui-draggable]:last"),c=y.$eval(s.attr("jqyoui-droppable")||s.attr("data-jqyoui-droppable"))||[],l=b.$eval(e.attr("jqyoui-draggable")||e.attr("data-jqyoui-draggable"))||[],l.index=this.fixIndex(b,l,v),c.index=this.fixIndex(y,c,m),h=t.isArray(v)?l.index:null,p=t.copy(t.isArray(v)?v[h]:v),t.isArray(m)&&c&&c.index!==n?d=m[c.index]:t.isArray(m)?d={}:d=m,d=t.copy(d),l.animate===!0?(this.move(e,g.length>0?g:s,null,"fast",c,null),this.move(g.length>0&&!c.multiple?g:[],e.parent("[jqyoui-droppable],[data-jqyoui-droppable]"),r.startXY,"fast",c,t.bind(this,function(){i(t.bind(this,function(){e.css({position:"relative",left:"",top:""}),g.css({position:"relative",left:"",top:"",display:""}),this.mutateDraggable(b,c,l,a,f,d,e),this.mutateDroppable(y,c,l,f,p,h),this.callEventCallback(y,c.onDrop,o,u)}))}))):i(t.bind(this,function(){this.mutateDraggable(b,c,l,a,f,d,e),this.mutateDroppable(y,c,l,f,p,h),this.callEventCallback(y,c.onDrop,o,u)}))},this.move=function(t,r,i,s,o,u){if(t.length===0)return u&&e.setTimeout(function(){u()},300),!1;var a=9999,f=t[o.containment||"offset"](),l=r&&r.is(":visible"),c=r.hasClass("ng-hide");i===null&&r.length>0&&((r.attr("jqyoui-draggable")||r.attr("data-jqyoui-draggable"))!==n&&r.ngattr("ng-model")!==n&&r.is(":visible")&&o&&o.multiple?(i=r[o.containment||"offset"](),o.stack===!1?i.left+=r.outerWidth(!0):i.top+=r.outerHeight(!0)):(c&&r.removeClass("ng-hide"),i=r.css({visibility:"hidden",display:"block"})[o.containment||"offset"](),r.css({visibility:"",display:l?"block":"none"}))),t.css({position:"absolute","z-index":a}).css(f).animate(i,s,function(){c&&r.addClass("ng-hide"),u&&u()})},this.mutateDroppable=function(e,n,r,i,o,u){var a=e.$eval(i);e.dndDragItem=o,t.isArray(a)?(n&&n.index>=0?a[n.index]=o:a.push(o),r&&r.placeholder===!0&&(a[a.length-1].jqyoui_pos=u)):(s(i+" = dndDragItem")(e),r&&r.placeholder===!0&&(a.jqyoui_pos=u))},this.mutateDraggable=function(e,r,i,o,u,a,f){var l=t.equals(a,{}),c=e.$eval(o);e.dndDropItem=a,i&&i.placeholder?i.placeholder!="keep"&&(t.isArray(c)&&i.index!==n?c[i.index]=a:s(o+" = dndDropItem")(e)):t.isArray(c)?l?i&&i.placeholder!==!0&&i.placeholder!=="keep"&&c.splice(i.index,1):c[i.index]=a:(s(o+" = dndDropItem")(e),e.$parent&&s(o+" = dndDropItem")(e.$parent)),f.css({"z-index":"",left:"",top:""})},this.fixIndex=function(e,n,r){if(n.applyFilter&&t.isArray(r)&&r.length>0){var i=e[n.applyFilter](),s=i[n.index],o;return r.forEach(function(e,n){t.equals(e,s)&&(o=n)}),o}return n.index}}]).directive("jqyouiDraggable",["ngDragDropService",function(e){return{require:"?jqyouiDroppable",restrict:"A",link:function(n,i,s){var o,u,a,f=function(f,l){f?(o=n.$eval(i.attr("jqyoui-draggable")||i.attr("data-jqyoui-draggable"))||{},u=n.$eval(s.jqyouiOptions)||{},i.draggable({disabled:!1}).draggable(u).draggable({start:function(i,s){a=t.element(u.helper?s.helper:this).css("z-index"),t.element(u.helper?s.helper:this).css("z-index",9999),r.startXY=t.element(this)[o.containment||"offset"](),e.callEventCallback(n,o.onStart,i,s)},stop:function(r,i){t.element(u.helper?i.helper:this).css("z-index",a),e.callEventCallback(n,o.onStop,r,i)},drag:function(t,r){e.callEventCallback(n,o.onDrag,t,r)}})):i.draggable({disabled:!0})};n.$watch(function(){return n.$eval(s.drag)},f),f(),i.on("$destroy",function(){i.draggable("destroy")})}}}]).directive("jqyouiDroppable",["ngDragDropService",function(e){return{restrict:"A",priority:1,link:function(n,r,i){var s,o=function(o,u){o?(s=n.$eval(t.element(r).attr("jqyoui-droppable")||t.element(r).attr("data-jqyoui-droppable"))||{},r.droppable({disabled:!1}).droppable(n.$eval(i.jqyouiOptions)||{}).droppable({over:function(t,r){e.callEventCallback(n,s.onOver,t,r)},out:function(t,r){e.callEventCallback(n,s.onOut,t,r)},drop:function(r,o){t.element(o.draggable).ngattr("ng-model")&&i.ngModel?e.invokeDrop(t.element(o.draggable),t.element(this),r,o):e.callEventCallback(n,s.onDrop,r,o)}})):r.droppable({disabled:!0})};n.$watch(function(){return n.$eval(i.drop)},o),o(),r.on("$destroy",function(){r.droppable("destroy")})}}}]);$.fn.ngattr=function(e,n){var r=t.element(this).get(0);return r.getAttribute(e)||r.getAttribute("data-"+e)}})(window,window.angular); \ No newline at end of file diff --git a/test/spec/tests.js b/test/spec/tests.js index 82ea83f..2747e8b 100644 --- a/test/spec/tests.js +++ b/test/spec/tests.js @@ -169,4 +169,51 @@ describe('Service: ngDragDropService', function() { scope.list = {title: 'Item 1'}; expect(ngDragDropService.fixIndex(scope, {applyFilter: 'filterIt'}, scope.list)).toBe(undefined); }); + + it('should parse json arguments as json', function(){ + var localScope = rootScope.$new(); + var expectedData = {r1: 1, r2:2}; + localScope.startDrag = function(event, ui, data){ + expect(data).toEqual(expectedData); + }; + var callbackName = "startDrag("+ JSON.stringify(expectedData) +")"; + ngDragDropService.callEventCallback(localScope, callbackName, {}, {}); + }); + + it('should parse json objects along with non json objects', function(){ + var localScope = rootScope.$new(); + var expectedData = {r1: 1, r2: 2}; + var expectedFirstArg = "arg1"; + localScope.arg1 = expectedFirstArg; + localScope.startDrag = function(event, ui,firstArg, data, thirdArg){ + expect(firstArg).toEqual(expectedFirstArg); + expect(data).toEqual(expectedData); + }; + var callbackName = "startDrag("+ expectedFirstArg +","+ JSON.stringify(expectedData) +")"; + ngDragDropService.callEventCallback(localScope, callbackName, {}, {}); + }); + + it('should parse nested json objects as a single argument', function(){ + var localScope = rootScope.$new(); + var expectedData = {r1: 1, r2: {rs1: "someId", rs2: "someother"}}; + localScope.startDrag = function(event, ui, data){ + expect(data).toEqual(expectedData); + }; + var callbackName = "startDrag("+ JSON.stringify(expectedData) +")"; + ngDragDropService.callEventCallback(localScope, callbackName, {}, {}); + }); + + it('should parse empty arguments', function(){ + var localScope = rootScope.$new(); + var expectedEvent = {"foo": "bar"}; + var expectedUI = {"bar":"baz"}; + localScope.startDrag = function(event, ui, data){ + expect(event).toEqual(expectedEvent); + expect(expectedUI).toEqual(expectedUI); + expect(data).toBe(undefined); + }; + var callbackName = "startDrag()"; + ngDragDropService.callEventCallback(localScope, callbackName, expectedEvent, expectedUI); + }); + }); \ No newline at end of file diff --git a/test/test.conf.js b/test/test.conf.js index 015ed66..dd8df59 100644 --- a/test/test.conf.js +++ b/test/test.conf.js @@ -7,6 +7,7 @@ files = [ 'components/angular/angular.js', 'components/angular-mocks/angular-mocks.js', 'src/angular-dragdrop.js', + 'src/Parser.js', 'test/spec/*.js' ]; singleRun = true;