diff --git a/.travis.yml b/.travis.yml index c06a92f0..f7bc70f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ cache: before_script: - | - if [[ "$WP_TRAVISCI" == "travis:phpunit" ]] ; then + if [[ "$WP_TRAVISCI" == "travis:phpunit" ]] || [[ "$WP_TRAVISCI" == "travis:js-tests" ]]; then bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION fi - | diff --git a/Gruntfile.js b/Gruntfile.js index 18d0b464..89faa00c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,7 +2,20 @@ module.exports = function( grunt ) { 'use strict'; var remapify = require('remapify'); - var banner = '/**\n * <%= pkg.homepage %>\n * Copyright (c) <%= grunt.template.today("yyyy") %>\n * This file is generated automatically. Do not edit.\n */\n'; + var banner = '/**\n * <%= pkg.homepage %>\n * Copyright (c) <%= grunt.template.today("yyyy") %>\n * This file is generated automatically. Do not edit.\n */\n'; + + // Path to WordPress install. Either absoloute or relative to this plugin. + // Change this by passing --abspath="new/path" as a grunt option. + var abspath; + + if ( grunt.option( "abspath" ) ) { + abspath = grunt.option( "abspath" ); + } else if ( 'WP_DEVELOP_DIR' in process.env ) { + abspath = process.env.WP_DEVELOP_DIR; + } else { + abspath = '/tmp/wordpress'; + } + // Project configuration grunt.initConfig( { @@ -135,12 +148,14 @@ module.exports = function( grunt ) { specs: 'js-tests/build/specs.js', helpers: 'js-tests/build/helpers.js', vendor: [ - 'js-tests/vendor/jquery.js', - 'js-tests/vendor/underscore.js', - 'js-tests/vendor/backbone.js', - 'js-tests/vendor/wp-shortcode.js', - 'js-tests/vendor/wp-util.js', - 'js-tests/vendor/wp-editors.js', + abspath + '/wp-includes/js/jquery/jquery.js', + abspath + '/wp-includes/js/underscore.min.js', + abspath + '/wp-includes/js/backbone.min.js', + abspath + '/wp-includes/js/wp-util.js', + abspath + '/wp-includes/js/shortcode.js', + abspath + '/wp-admin/js/editor.js', + abspath + '/wp-includes/js/media-models.js', + abspath + '/wp-includes/js/media-views.js', 'js-tests/vendor/mock-ajax.js', ], } @@ -196,7 +211,7 @@ module.exports = function( grunt ) { grunt.loadNpmTasks( 'grunt-contrib-jasmine' ); grunt.loadNpmTasks( 'grunt-contrib-jshint' ); - grunt.registerTask( 'scripts', [ 'browserify', 'jasmine', 'jshint' ] ); + grunt.registerTask( 'scripts', [ 'browserify', 'jshint' ] ); grunt.registerTask( 'styles', [ 'sass', 'postcss' ] ); grunt.registerTask( 'default', [ 'scripts', 'styles' ] ); grunt.registerTask( 'i18n', ['addtextdomain', 'makepot'] ); diff --git a/css/sass/shortcode-ui.scss b/css/sass/shortcode-ui.scss index 58f2dfdf..5d914394 100644 --- a/css/sass/shortcode-ui.scss +++ b/css/sass/shortcode-ui.scss @@ -16,60 +16,85 @@ } -.add-shortcode-list { - padding: 0 10px; - - .shortcode-list-item { - margin: 10px; - float: left; - -webkit-box-shadow: inset 0 0 15px rgba( 0, 0, 0, 0.1 ), inset 0 0 0 1px rgba( 0, 0, 0, 0.1 ); - box-shadow: inset 0 0 15px rgba( 0, 0, 0, 0.1 ), inset 0 0 0 1px rgba( 0, 0, 0, 0.1 ); - background: #eee; - cursor: pointer; - position: relative; - text-align: center; - width: 150px; - height: 150px; +.button-shortcode-ui-insert span { + display: inline-block; + width: 18px; + height: 18px; + vertical-align: text-top; + margin: -2px 7px 0 -3px; + left: -2px; + color: #82878c; +} + +.insert-shortcode-list { + + .add-shortcode-list { + padding: 0 10px; - .add-shortcode-list-item-icon { + .shortcode-list-item { + margin: 10px; + float: left; + -webkit-box-shadow: inset 0 0 15px rgba( 0, 0, 0, 0.1 ), inset 0 0 0 1px rgba( 0, 0, 0, 0.1 ); + box-shadow: inset 0 0 15px rgba( 0, 0, 0, 0.1 ), inset 0 0 0 1px rgba( 0, 0, 0, 0.1 ); + background: #eee; + cursor: pointer; position: relative; - height: 120px; - font-size: 64px; + text-align: center; + width: 150px; + height: 150px; + + .add-shortcode-list-item-icon { + position: relative; + height: 120px; + font-size: 64px; + + .dashicons, + img { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: inherit; + line-height: inherit; + width: auto; + height: auto; + max-width: 80%; + max-height: 80%; + } - .dashicons, - img { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: inherit; - line-height: inherit; - width: auto; - height: auto; - max-width: 80%; - max-height: 80%; } + .add-shortcode-list-item-title { + box-sizing: border-box; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + margin: 0; + line-height: 1.2; + padding: 8px; + overflow: hidden; + max-height: 100%; + word-wrap: break-word; + text-align: center; + font-weight: bold; + background: rgba( 255, 255, 255, 0.8 ); + -webkit-box-shadow: inset 0 0 0 1px rgba( 0, 0, 0, 0.15 ); + box-shadow: inset 0 0 0 1px rgba( 0, 0, 0, 0.15 ); + } } + } - .add-shortcode-list-item-title { - box-sizing: border-box; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - margin: 0; - line-height: 1.2; - padding: 8px; - overflow: hidden; - max-height: 100%; - word-wrap: break-word; - text-align: center; - font-weight: bold; - background: rgba( 255, 255, 255, 0.8 ); - -webkit-box-shadow: inset 0 0 0 1px rgba( 0, 0, 0, 0.15 ); - box-shadow: inset 0 0 0 1px rgba( 0, 0, 0, 0.15 ); - } + &.has-search { + padding-top: 25px; + } + + .search { + position: absolute; + top: 0px; + right: 10px; + width: 100%; + max-width: 300px; } } diff --git a/css/shortcode-ui-editor-styles.css b/css/shortcode-ui-editor-styles.css index d552a2db..598428bf 100644 --- a/css/shortcode-ui-editor-styles.css +++ b/css/shortcode-ui-editor-styles.css @@ -1,8 +1,10 @@ .wpview-wrap.wp-mce-view-show-toolbar .toolbar { display: block; } + .wpview-wrap .shortcake-error { color: red; font-weight: bold; } + .wpview-wrap .shortcake-empty { font-family: Consolas, Monaco, monospace; color: #666; diff --git a/css/shortcode-ui-editor-styles.css.map b/css/shortcode-ui-editor-styles.css.map index fab02612..ff67e1d8 100644 --- a/css/shortcode-ui-editor-styles.css.map +++ b/css/shortcode-ui-editor-styles.css.map @@ -1 +1 @@ -{"version":3,"sources":["../shortcode-ui-editor-styles.scss"],"names":[],"mappings":"AAGA;EACG,eAAA,EAAA;AAIH;EACE,WAAA;EACA,kBAAA,EAAA;AAGF;EACE,yCAAA;EACA,YAAA;EACA,gBAAA,EAAA;;AAIF;EACC,sBAAA;EACA,YAAA;EACA,iBAAA,EAAA","file":"shortcode-ui-editor-styles.css"} \ No newline at end of file +{"version":3,"sources":["sass/shortcode-ui-editor-styles.scss"],"names":[],"mappings":"AAAA;EAIG,eAAe,EACf;;AALH;EASE,WAAW;EACX,kBAAkB,EAClB;;AAXF;EAcE,yCAAyC;EACzC,YAAY;EACZ,gBAAgB,EAChB;;AAGF;EACC,sBAAsB;EACtB,YAAY;EACZ,iBAAiB,EACjB","file":"shortcode-ui-editor-styles.css"} \ No newline at end of file diff --git a/css/shortcode-ui.css b/css/shortcode-ui.css index c933bd5c..bbdf83cc 100644 --- a/css/shortcode-ui.css +++ b/css/shortcode-ui.css @@ -5,12 +5,22 @@ max-width: 800px; max-height: 800px; margin: auto; } + .shortcode-ui-insert-modal .media-frame-content { top: 54px; } -.add-shortcode-list { +.button-shortcode-ui-insert span { + display: inline-block; + width: 18px; + height: 18px; + vertical-align: text-top; + margin: -2px 7px 0 -3px; + left: -2px; + color: #82878c; } + +.insert-shortcode-list .add-shortcode-list { padding: 0 10px; } - .add-shortcode-list .shortcode-list-item { + .insert-shortcode-list .add-shortcode-list .shortcode-list-item { margin: 10px; float: left; box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.1), inset 0 0 0 1px rgba(0, 0, 0, 0.1); @@ -20,16 +30,16 @@ text-align: center; width: 150px; height: 150px; } - .add-shortcode-list .shortcode-list-item .add-shortcode-list-item-icon { + .insert-shortcode-list .add-shortcode-list .shortcode-list-item .add-shortcode-list-item-icon { position: relative; height: 120px; font-size: 64px; } - .add-shortcode-list .shortcode-list-item .add-shortcode-list-item-icon .dashicons, .add-shortcode-list .shortcode-list-item .add-shortcode-list-item-icon img { + .insert-shortcode-list .add-shortcode-list .shortcode-list-item .add-shortcode-list-item-icon .dashicons, + .insert-shortcode-list .add-shortcode-list .shortcode-list-item .add-shortcode-list-item-icon img { position: absolute; top: 50%; left: 50%; -webkit-transform: translate(-50%, -50%); - -ms-transform: translate(-50%, -50%); transform: translate(-50%, -50%); font-size: inherit; line-height: inherit; @@ -37,7 +47,7 @@ height: auto; max-width: 80%; max-height: 80%; } - .add-shortcode-list .shortcode-list-item .add-shortcode-list-item-title { + .insert-shortcode-list .add-shortcode-list .shortcode-list-item .add-shortcode-list-item-title { box-sizing: border-box; position: absolute; bottom: 0; @@ -54,14 +64,27 @@ background: rgba(255, 255, 255, 0.8); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15); } +.insert-shortcode-list.has-search { + padding-top: 25px; } + +.insert-shortcode-list .search { + position: absolute; + top: 0px; + right: 10px; + width: 100%; + max-width: 300px; } + .shortcode-ui-content .edit-shortcode-tabs { height: 34px; padding: 10px 10px 0; } + .shortcode-ui-content .edit-shortcode-tabs-content { padding: 10px; border-top: 1px solid #ddd; } + .shortcode-ui-content a.wp-color-result { border-bottom: 1px solid #ccc; } + .shortcode-ui-content .media-toolbar { position: relative; height: auto; } @@ -71,7 +94,8 @@ .edit-shortcode-form label { display: block; clear: both; } - .edit-shortcode-form input, .edit-shortcode-form textarea { + .edit-shortcode-form input, + .edit-shortcode-form textarea { border: 1px solid #ddd; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); background-color: #fff; @@ -116,7 +140,7 @@ .edit-shortcode-form .shortcake-attachment-preview .thumbnail:hover:after { background: rgba(0, 0, 0, 0.1); -webkit-transition: all .3s linear; - transition: all .3s linear; } + transition: all .3s linear; } .edit-shortcode-form .shortcake-attachment-preview:not(.has-attachment) { border: 2px dashed #DDD; border-radius: 2px; @@ -146,7 +170,8 @@ left: 5px; font-size: 22px; text-indent: 0; } - .edit-shortcode-form .shortcake-attachment-preview .loading-indicator, .edit-shortcode-form .shortcake-attachment-preview.loading .button.add { + .edit-shortcode-form .shortcake-attachment-preview .loading-indicator, + .edit-shortcode-form .shortcake-attachment-preview.loading .button.add { display: none; } .edit-shortcode-form .shortcake-attachment-preview.loading .loading-indicator { display: block; @@ -189,14 +214,12 @@ @-webkit-keyframes attachment-preview-loading { 0% { margin-left: -60px; } - 100% { margin-left: 60px; } } @keyframes attachment-preview-loading { 0% { margin-left: -60px; } - 100% { margin-left: 60px; } } /*# sourceMappingURL=shortcode-ui.css.map */ \ No newline at end of file diff --git a/css/shortcode-ui.css.map b/css/shortcode-ui.css.map index 63de7233..0a398fd2 100644 --- a/css/shortcode-ui.css.map +++ b/css/shortcode-ui.css.map @@ -1 +1 @@ -{"version":3,"sources":["../shortcode-ui.scss","../_field-image.scss","shortcode-ui.css"],"names":[],"mappings":"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","file":"shortcode-ui.css"} \ No newline at end of file +{"version":3,"sources":["sass/shortcode-ui.scss","sass/_field-image.scss"],"names":[],"mappings":"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","file":"shortcode-ui.css"} \ No newline at end of file diff --git a/inc/class-shortcode-ui.php b/inc/class-shortcode-ui.php index b761eb2e..18c2f811 100644 --- a/inc/class-shortcode-ui.php +++ b/inc/class-shortcode-ui.php @@ -67,6 +67,7 @@ private function __construct() { private function setup_actions() { add_action( 'admin_enqueue_scripts', array( $this, 'action_admin_enqueue_scripts' ) ); add_action( 'wp_enqueue_editor', array( $this, 'action_wp_enqueue_editor' ) ); + add_action( 'media_buttons', array( $this, 'action_media_buttons' ) ); add_action( 'wp_ajax_bulk_do_shortcode', array( $this, 'handle_ajax_bulk_do_shortcode' ) ); add_filter( 'wp_editor_settings', array( $this, 'filter_wp_editor_settings' ), 10, 2 ); } @@ -273,10 +274,23 @@ public function action_wp_enqueue_editor() { do_action( 'shortcode_ui_loaded_editor' ); } + /** + * Output an "Add Post Element" button with the media buttons. + */ + public function action_media_buttons( $editor_id ) { + printf( '', + esc_attr( $editor_id ), + esc_html__( 'Add Post Element', 'shortcode-ui' ) + ); + } + /** * Output required underscore.js templates in the footer */ public function action_admin_print_footer_scripts() { + echo $this->get_view( 'media-frame' ); // WPCS: xss ok echo $this->get_view( 'list-item' ); // WPCS: xss ok echo $this->get_view( 'edit-form' ); // WPCS: xss ok diff --git a/inc/templates/edit-form.tpl.php b/inc/templates/edit-form.tpl.php index c28c8a2f..09410d2e 100644 --- a/inc/templates/edit-form.tpl.php +++ b/inc/templates/edit-form.tpl.php @@ -1,7 +1,6 @@ diff --git a/js-tests/build/specs.js b/js-tests/build/specs.js index 00c87622..4b2e53af 100644 --- a/js-tests/build/specs.js +++ b/js-tests/build/specs.js @@ -27,7 +27,7 @@ describe( "Shortcode Inner Content Model", function() { } ); -},{"../../js/src/models/inner-content":9}],2:[function(require,module,exports){ +},{"../../js/src/models/inner-content":10}],2:[function(require,module,exports){ var ShortcodeAttribute = require('../../js/src/models/shortcode-attribute'); describe( "Shortcode Attribute Model", function() { @@ -53,7 +53,7 @@ describe( "Shortcode Attribute Model", function() { } ); -},{"../../js/src/models/shortcode-attribute":10}],3:[function(require,module,exports){ +},{"../../js/src/models/shortcode-attribute":11}],3:[function(require,module,exports){ (function (global){ var Shortcode = require('../../js/src/models/shortcode'); var InnerContent = require('../../js/src/models/inner-content'); @@ -153,7 +153,7 @@ describe( "Shortcode Model", function() { }); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"../../js/src/collections/shortcode-attributes":7,"../../js/src/models/inner-content":9,"../../js/src/models/shortcode":11,"../../js/src/models/shortcode-attribute":10}],4:[function(require,module,exports){ +},{"../../js/src/collections/shortcode-attributes":7,"../../js/src/models/inner-content":10,"../../js/src/models/shortcode":12,"../../js/src/models/shortcode-attribute":11}],4:[function(require,module,exports){ (function (global){ var Shortcode = require('../../js/src/models/shortcode'); var ShortcodeViewConstructor = require('../../js/src/utils/shortcode-view-constructor'); @@ -305,7 +305,7 @@ describe( 'Shortcode View Constructor', function(){ }); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"../../js/src/models/shortcode":11,"../../js/src/utils/shortcode-view-constructor":13,"../../js/src/utils/sui":14}],5:[function(require,module,exports){ +},{"../../js/src/models/shortcode":12,"../../js/src/utils/shortcode-view-constructor":14,"../../js/src/utils/sui":15}],5:[function(require,module,exports){ (function (global){ var Shortcode = require('./../../../js/src/models/shortcode.js'); var MceViewConstructor = require('./../../../js/src/utils/shortcode-view-constructor.js'); @@ -457,7 +457,7 @@ describe( "MCE View Constructor", function() { it( 'parses shortcode with dashes in name and attribute', function() { var shortcode = MceViewConstructor.parseShortcodeString( '[test-shortcode test-attr="test value 2"]'); expect( shortcode instanceof Shortcode ).toEqual( true ); - expect( shortcode.get( 'attrs' ).findWhere( { attr: 'test-attr' }).get('value') ).not.toEqual( 'test value 2' ); + expect( shortcode.get( 'attrs' ).findWhere( { attr: 'test-attr' }).get('value') ).toEqual( 'test value 2' ); }); // https://github.com/fusioneng/Shortcake/issues/171 @@ -495,7 +495,7 @@ describe( "MCE View Constructor", function() { } ); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./../../../js/src/models/shortcode.js":11,"./../../../js/src/utils/shortcode-view-constructor.js":13,"./../../../js/src/utils/sui.js":14}],6:[function(require,module,exports){ +},{"./../../../js/src/models/shortcode.js":12,"./../../../js/src/utils/shortcode-view-constructor.js":14,"./../../../js/src/utils/sui.js":15}],6:[function(require,module,exports){ var Shortcodes = require('./../../../js/src/collections/shortcodes.js'); var sui = require('./../../../js/src/utils/sui.js'); @@ -507,12 +507,12 @@ describe( "SUI Util", function() { it( 'expected properties', function() { expect( sui.shortcodes instanceof Shortcodes ).toEqual( true ); - expect( sui.views ).toEqual( {} ); + expect( typeof sui.views ).toEqual( 'object' ); }); } ); -},{"./../../../js/src/collections/shortcodes.js":8,"./../../../js/src/utils/sui.js":14}],7:[function(require,module,exports){ +},{"./../../../js/src/collections/shortcodes.js":8,"./../../../js/src/utils/sui.js":15}],7:[function(require,module,exports){ (function (global){ var Backbone = (typeof window !== "undefined" ? window['Backbone'] : typeof global !== "undefined" ? global['Backbone'] : null); var ShortcodeAttribute = require('./../models/shortcode-attribute.js'); @@ -534,7 +534,7 @@ var ShortcodeAttributes = Backbone.Collection.extend({ module.exports = ShortcodeAttributes; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./../models/shortcode-attribute.js":10}],8:[function(require,module,exports){ +},{"./../models/shortcode-attribute.js":11}],8:[function(require,module,exports){ (function (global){ var Backbone = (typeof window !== "undefined" ? window['Backbone'] : typeof global !== "undefined" ? global['Backbone'] : null); var Shortcode = require('./../models/shortcode.js'); @@ -547,7 +547,77 @@ var Shortcodes = Backbone.Collection.extend({ module.exports = Shortcodes; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./../models/shortcode.js":11}],9:[function(require,module,exports){ +},{"./../models/shortcode.js":12}],9:[function(require,module,exports){ +(function (global){ +var Backbone = (typeof window !== "undefined" ? window['Backbone'] : typeof global !== "undefined" ? global['Backbone'] : null), + wp = (typeof window !== "undefined" ? window['wp'] : typeof global !== "undefined" ? global['wp'] : null), + sui = require('./../utils/sui.js'); + +var FrameState = wp.media.controller.State.extend({ + + initialize: function( options ){ + + _.bindAll( this, 'refresh', 'insert', 'reset', 'setShortcode', 'getShortcode' ); + + this.props = new Backbone.Model({ + shortcode: null, + search: null + }); + + if ( 'shortcode' in options ) { + this.setShortcode( options.shortcode ); + } + + // Allow setting a custom insertAction method. + if ( 'insertAction' in options ) { + this.insertAction = options.insertAction; + } + + }, + + insertAction: function( shortcode ) { + send_to_editor( shortcode.formatShortcode() ); + }, + + refresh: function() { + if ( this.frame && this.frame.toolbar ) { + this.frame.toolbar.get().refresh(); + } + }, + + insert: function() { + + var shortcode = this.props.get('shortcode'); + + if ( shortcode ) { + this.insertAction( shortcode ); + this.reset(); + this.frame.close(); + } + }, + + reset: function() { + this.props.set( 'shortcode', null ); + this.props.set( 'search', null ); + }, + + setShortcode: function( shortcode ) { + this.props.set( 'shortcode', shortcode ); + }, + + getShortcode: function( shortcode ) { + return this.props.get( 'shortcode' ); + }, + +}); + +// Make this available globally. +sui.controllers.FrameState = FrameState; + +module.exports = FrameState; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../utils/sui.js":15}],10:[function(require,module,exports){ (function (global){ var Backbone = (typeof window !== "undefined" ? window['Backbone'] : typeof global !== "undefined" ? global['Backbone'] : null); @@ -566,7 +636,7 @@ var InnerContent = Backbone.Model.extend({ module.exports = InnerContent; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{}],10:[function(require,module,exports){ +},{}],11:[function(require,module,exports){ (function (global){ var Backbone = (typeof window !== "undefined" ? window['Backbone'] : typeof global !== "undefined" ? global['Backbone'] : null); @@ -589,7 +659,7 @@ var ShortcodeAttribute = Backbone.Model.extend({ module.exports = ShortcodeAttribute; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{}],11:[function(require,module,exports){ +},{}],12:[function(require,module,exports){ (function (global){ var Backbone = (typeof window !== "undefined" ? window['Backbone'] : typeof global !== "undefined" ? global['Backbone'] : null); var ShortcodeAttributes = require('./../collections/shortcode-attributes.js'); @@ -627,14 +697,19 @@ Shortcode = Backbone.Model.extend({ * Handles converting the attribute collection to JSON. */ toJSON: function( options ) { + options = Backbone.Model.prototype.toJSON.call(this, options); + if ( options.attrs && ( options.attrs instanceof ShortcodeAttributes ) ) { options.attrs = options.attrs.toJSON(); } + if ( options.inner_content && ( options.inner_content instanceof InnerContent ) ) { options.inner_content = options.inner_content.toJSON(); } + return options; + }, /** @@ -642,12 +717,17 @@ Shortcode = Backbone.Model.extend({ * Make sure we don't clone a reference to attributes. */ clone: function() { + var clone = Backbone.Model.prototype.clone.call( this ); + clone.set( 'attrs', clone.get( 'attrs' ).clone() ); + if ( clone.get( 'inner_content' ) ) { clone.set( 'inner_content', clone.get( 'inner_content' ).clone() ); } + return clone; + }, /** @@ -702,12 +782,14 @@ Shortcode = Backbone.Model.extend({ return template; }, + + }); module.exports = Shortcode; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./../collections/shortcode-attributes.js":7,"./inner-content.js":9}],12:[function(require,module,exports){ +},{"./../collections/shortcode-attributes.js":7,"./inner-content.js":10}],13:[function(require,module,exports){ (function (global){ var $ = (typeof window !== "undefined" ? window['jQuery'] : typeof global !== "undefined" ? global['jQuery'] : null); var _ = (typeof window !== "undefined" ? window['_'] : typeof global !== "undefined" ? global['_'] : null); @@ -822,12 +904,13 @@ var Fetcher = (function() { module.exports = Fetcher; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{}],13:[function(require,module,exports){ +},{}],14:[function(require,module,exports){ (function (global){ -var sui = require('./sui.js'), +var sui = require('./sui.js'), fetcher = require('./fetcher.js'), - wp = (typeof window !== "undefined" ? window['wp'] : typeof global !== "undefined" ? global['wp'] : null), - $ = (typeof window !== "undefined" ? window['jQuery'] : typeof global !== "undefined" ? global['jQuery'] : null); + Frame = require('./../views/media-frame.js'), + wp = (typeof window !== "undefined" ? window['wp'] : typeof global !== "undefined" ? global['wp'] : null), + $ = (typeof window !== "undefined" ? window['jQuery'] : typeof global !== "undefined" ? global['jQuery'] : null); /** * Generic shortcode MCE view constructor. @@ -986,24 +1069,26 @@ var shortcodeViewConstructor = { * @param {string} shortcodeString String representation of the shortcode */ edit: function( shortcodeString ) { - var currentShortcode; + + var shortcode; // Backwards compatability for WP pre-4.2 if ( 'object' === typeof( shortcodeString ) ) { shortcodeString = decodeURIComponent( $(shortcodeString).attr('data-wpview-text') ); } - currentShortcode = this.parseShortcodeString( shortcodeString ); + shortcode = this.parseShortcodeString( shortcodeString ); - if ( currentShortcode ) { + if ( shortcode ) { - var wp_media_frame = wp.media.frames.wp_media_frame = wp.media({ - frame : "post", - state : 'shortcode-ui', - currentShortcode : currentShortcode, + var frame = new Frame({ + shortcodes : sui.shortcodes, + shortcode : shortcode, + title : shortcodeUIData.strings.media_frame_menu_insert_label, + updateTitle : shortcodeUIData.strings.media_frame_menu_update_label, }); - wp_media_frame.open(); + frame.open(); } @@ -1072,139 +1157,1110 @@ var shortcodeViewConstructor = { * @return {string} */ pregQuote: function( str, delimiter ) { - return String(str) - .replace( - new RegExp( '[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + ( delimiter || '' ) + '-]', 'g' ), - '\\$&' ); + var regexp = new RegExp( '[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + ( delimiter || '' ) + '-]', 'g' ); + return String( str ).replace( regexp, '\\$&' ); }, - // Backwards compatability for Pre WP 4.2. - View: { +}; - overlay: true, +module.exports = sui.utils.shortcodeViewConstructor = shortcodeViewConstructor; - initialize: function( options ) { - this.shortcode = this.getShortcode( options ); - this.fetch(); - }, +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../views/media-frame.js":24,"./fetcher.js":13,"./sui.js":15}],15:[function(require,module,exports){ +var Shortcodes = require('./../collections/shortcodes.js'); - getShortcode: function( options ) { +window.Shortcode_UI = window.Shortcode_UI || { + shortcodes: new Shortcodes(), + views: {}, + controllers: {}, + utils: {}, +}; - var shortcodeModel, shortcode; +module.exports = window.Shortcode_UI; - shortcodeModel = sui.shortcodes.findWhere( { shortcode_tag: options.shortcode.tag } ); +},{"./../collections/shortcodes.js":8}],16:[function(require,module,exports){ +var sui = require('./../utils/sui.js'); - if (!shortcodeModel) { - return; +var editAttributeFieldAttachment = sui.views.editAttributeField.extend( { + + events: { + 'click .add' : '_openMediaFrame', + 'click .remove' : '_removeAttachment', + 'click .thumbnail' : '_openMediaFrame', + 'selectAttachment' : '_selectAttachment', + }, + + /** + * Update the field attachment. + * Re-renders UI. + * If ID is empty - does nothing. + * + * @param {int} id Attachment ID + */ + updateValue: function( id ) { + + if ( ! id ) { + return; + } + + this.setValue( id ); + + var self = this; + + if ( editAttributeFieldAttachment.getFromCache( id ) ) { + self._renderPreview( editAttributeFieldAttachment.getFromCache( id ) ); + + // Call the updateValue() function, to trigger any listeners + // hooked on it. + self.triggerCallbacks(); + return; + } + + this.$container.addClass( 'loading' ); + + wp.ajax.post( 'get-attachment', { + 'id': id + } ).done( function( attachment ) { + // Cache for later. + editAttributeFieldAttachment.setInCache( id, attachment ); + self._renderPreview( attachment ); + + // Call the updateValue() function, to trigger any listeners + // hooked on it. + self.triggerCallbacks(); + } ).always( function( attachment ) { + self.$container.removeClass( 'loading' ); + }); + }, + + render: function() { + + // Set model default values. + for ( var arg in ShortcakeImageFieldData.defaultArgs ) { + if ( ! this.model.get( arg ) ) { + this.model.set( arg, ShortcakeImageFieldData.defaultArgs[ arg ] ); } + } - shortcode = shortcodeModel.clone(); + this.$el.html( this.template( this.model.toJSON() ) ); - shortcode.get('attrs').each( - function(attr) { + this.$container = this.$el.find( '.shortcake-attachment-preview' ); + this.$thumbnailDetailsContainer = this.$el.find( '.thumbnail-details-container' ); + var $addButton = this.$container.find( 'button.add' ); - if (attr.get('attr') in options.shortcode.attrs.named) { - attr.set('value', - options.shortcode.attrs.named[attr - .get('attr')]); - } + this.frame = wp.media( { + multiple: false, + title: this.model.get( 'frameTitle' ), + library: { + type: this.model.get( 'libraryType' ), + }, + } ); + + // Add initial Attachment if available. + this.updateValue( this.model.get( 'value' ) ); + + }, + + /** + * Renders attachment preview in field. + * @param {object} attachment model + * @return null + */ + _renderPreview: function( attachment ) { + + var $thumbnail = jQuery('
'); + + if ( 'image' !== attachment.type ) { + + jQuery( '', { + src: attachment.icon, + alt: attachment.title, + } ).appendTo( $thumbnail ); + + jQuery( '
', { + class: 'filename', + html: '
' + attachment.title + '
', + } ).appendTo( $thumbnail ); + + } else { + + attachmentThumb = (typeof attachment.sizes.thumbnail !== 'undefined') ? + attachment.sizes.thumbnail : + _.first( _.sortBy( attachment.sizes, 'width' ) ); + + jQuery( '', { + src: attachmentThumb.url, + width: attachmentThumb.width, + height: attachmentThumb.height, + alt: attachment.alt, + } ) .appendTo( $thumbnail ); + + } + + $thumbnail.find( 'img' ).wrap( '
' ); + this.$container.append( $thumbnail ); + this.$container.toggleClass( 'has-attachment', true ); + + this.$thumbnailDetailsContainer.find( '.filename' ).text( attachment.filename ); + this.$thumbnailDetailsContainer.find( '.date-formatted' ).text( attachment.dateFormatted ); + this.$thumbnailDetailsContainer.find( '.size' ).text( attachment.filesizeHumanReadable ); + this.$thumbnailDetailsContainer.find( '.dimensions' ).text( attachment.height + ' × ' + attachment.width ); + this.$thumbnailDetailsContainer.find( '.edit-link a' ).attr( "href", attachment.editLink ); + this.$thumbnailDetailsContainer.toggleClass( 'has-attachment', true ); + + }, + + /** + * Open media frame when add button is clicked. + * + */ + _openMediaFrame: function(e) { + e.preventDefault(); + this.frame.open(); + if ( this.model.get( 'value' ) ) { + var selection = this.frame.state().get('selection'); + var attachment = wp.media.attachment( this.model.get( 'value' ) ); + attachment.fetch(); + selection.reset( attachment ? [ attachment ] : [] ); + this.frame.state().set('selection', selection); + } + + var self = this; + this.frame.on( 'select', function() { + self.$el.trigger( 'selectAttachment' ); + } ); + + }, + + /** + * When an attachment is selected from the media frame, update the model value. + * + */ + _selectAttachment: function(e) { + var selection = this.frame.state().get('selection'), + attachment = selection.first(); + if ( attachment.id != this.model.get( 'value' ) ){ + this.model.set( 'value', null ); + this.$container.toggleClass( 'has-attachment', false ); + this.$container.find( '.thumbnail' ).remove(); + this.updateValue( attachment.id ); + } + this.frame.close(); + }, + + /** + * Remove the attachment. + * Render preview & Update the model. + */ + _removeAttachment: function(e) { + e.preventDefault(); + + this.model.set( 'value', null ); + + this.$container.toggleClass( 'has-attachment', false ); + this.$container.find( '.thumbnail' ).remove(); + this.$thumbnailDetailsContainer.toggleClass( 'has-attachment', false ); + }, + +}, { + + _idCache: {}, + + /** + * Store attachments in a cache for quicker loading. + */ + setInCache: function( id, attachment ) { + this._idCache[ id ] = attachment; + }, - }); + /** + * Retrieve an attachment from the cache. + */ + getFromCache: function( id ){ + if ( 'undefined' === typeof this._idCache[ id ] ) { + return false; + } + return this._idCache[ id ]; + }, - if ('content' in options.shortcode) { - var inner_content = shortcode.get('inner_content'); - if ( inner_content ) { - inner_content.set('value', options.shortcode.content); +}); + +module.exports = sui.views.editAttributeFieldAttachment = editAttributeFieldAttachment; + + +},{"./../utils/sui.js":15}],17:[function(require,module,exports){ +(function (global){ +var sui = require('./../utils/sui.js'), + editAttributeField = require('./edit-attribute-field.js'), + $ = (typeof window !== "undefined" ? window['jQuery'] : typeof global !== "undefined" ? global['jQuery'] : null); + +sui.views.editAttributeFieldColor = editAttributeField.extend({ + + // All events are being listened by iris, and they don't bubble very well, + // so remove Backbone's listeners. + events: {}, + + render: function() { + var self = this; + + var data = jQuery.extend( { + id: 'shortcode-ui-' + this.model.get( 'attr' ) + '-' + this.model.cid, + }, this.model.toJSON() ); + + // Convert meta JSON to attribute string. + var _meta = []; + for ( var key in data.meta ) { + + // Boolean attributes can only require attribute key, not value. + if ( 'boolean' === typeof( data.meta[ key ] ) ) { + + // Only set truthy boolean attributes. + if ( data.meta[ key ] ) { + _meta.push( _.escape( key ) ); } + + } else { + + _meta.push( _.escape( key ) + '="' + _.escape( data.meta[ key ] ) + '"' ); + + } + + } + + data.meta = _meta.join( ' ' ); + + this.$el.html( this.template( data ) ); + this.triggerCallbacks(); + + this.$el.find('input[type="text"]:not(.wp-color-picker)').wpColorPicker({ + change: function(e, ui) { + self.setValue( $(this).wpColorPicker('color') ); + self.triggerCallbacks(); } + }); + + return this; + } + +}); + + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../utils/sui.js":15,"./edit-attribute-field.js":19}],18:[function(require,module,exports){ +( function( $ ) { + + var sui = window.Shortcode_UI; + + // Cached Data. + var postSelectCache = {}; - return shortcode; + sui.views.editAttributeFieldPostSelect = sui.views.editAttributeField.extend( { + events: { + 'change .shortcode-ui-post-select': 'inputChanged', }, - fetch : function() { + inputChanged: function(e) { + this.setValue( e.val ); + this.triggerCallbacks(); + }, + + render: function() { + + var self = this, + defaults = { multiple: false }; + + for ( var arg in defaults ) { + if ( ! this.model.get( arg ) ) { + this.model.set( arg, defaults[ arg ] ); + } + } + + var data = this.model.toJSON(); + data.id = 'shortcode-ui-' + this.model.get( 'attr' ) + '-' + this.model.cid; + + this.$el.html( this.template( data ) ); + + var ajaxData = { + action : 'shortcode_ui_post_field', + nonce : shortcodeUiPostFieldData.nonce, + shortcode : this.shortcode.get( 'shortcode_tag'), + attr : this.model.get( 'attr' ) + }; + + var $field = this.$el.find( '.shortcode-ui-post-select' ); + + $field.select2({ + + placeholder: "Search", + multiple: this.model.get( 'multiple' ), + ajax: { + url: ajaxurl, + dataType: 'json', + quietMillis: 250, + data: function (term, page) { + ajaxData.s = term; + ajaxData.page = page; + return ajaxData; + }, + results: function ( response, page ) { + + if ( ! response.success ) { + return { results: {}, more: false }; + } + + // Cache data for quicker rendering later. + postSelectCache = $.extend( postSelectCache, response.data.posts ); + + var more = ( page * response.data.posts_per_page ) < response.data.found_posts; // whether or not there are more results available + return { results: response.data.posts, more: more }; + + }, + }, + + /** + * Initialize Callback + * Used to set render the initial value. + * Has to make a request to get the title for the current ID. + */ + initSelection: function(element, callback) { + + var ids, parsedData = [], cached; + + // Convert stored value to array of IDs (int). + ids = $(element) + .val() + .split(',') + .map( function (str) { return str.trim(); } ) + .map( function (str) { return parseInt( str ); } ); - var self = this; + if ( ids.length < 1 ) { + return; + } + + // Check if there is already cached data. + for ( var i = 0; i < ids.length; i++ ) { + cached = _.find( postSelectCache, _.matches( { id: ids[i] } ) ); + if ( cached ) { + parsedData.push( cached ); + } + } + + // If not multiple - return single value if we have one. + if ( parsedData.length && ! self.model.get( 'multiple' ) ) { + callback( parsedData[0] ); + return; + } + + var uncachedIds = _.difference( ids, _.pluck( parsedData, 'id' ) ); + + if ( ! uncachedIds.length ) { - if ( ! this.parsed ) { + callback( parsedData ); - wp.ajax.post( 'do_shortcode', { - post_id: $( '#post_ID' ).val(), - shortcode: this.shortcode.formatShortcode(), - nonce: shortcodeUIData.nonces.preview, - }).done( function( response ) { - if ( response.indexOf( ' ids.indexOf( b.id ) ) return 1; + if ( ids.indexOf( a.id ) < ids.indexOf( b.id ) ) return -1; + return 0; + }); + + callback( parsedData ); + return; + + } ); + } - }).fail( function() { - self.parsed = '' + shortcodeUIData.strings.mce_view_error + ''; - self.render( true ); - } ); + }, + + } ); + + // Make multiple values sortable. + if ( this.model.get( 'multiple' ) ) { + $field.select2('container').find('ul.select2-choices').sortable({ + containment: 'parent', + start: function() { $('.shortcode-ui-post-select').select2('onSortStart'); }, + update: function() { $('.shortcode-ui-post-select').select2('onSortEnd'); } + }); } + return this; + + } + + } ); + + /** + * Extending SUI Media Controller to hide Select2 UI Drop-Down when menu + * changes in Meida modal + * 1. going back/forth between different shortcakes (refresh) + * 2. changing the menu in left column (deactivate) + * 3. @TODO closing the modal. + */ + var mediaController = sui.controllers.FrameState; + sui.controllers.FrameState = mediaController.extend({ + + refresh: function(){ + mediaController.prototype.refresh.apply( this, arguments ); + this.destroySelect2UI(); }, - /** - * Render the shortcode - * - * To ensure consistent rendering - this makes an ajax request to the - * admin and displays. - * - * @return string html - */ - getHtml : function() { - return this.parsed; + //doesn't need to call parent as it already an "abstract" method in parent to provide callback + deactivate: function() { + this.destroySelect2UI(); }, - /** - * Returns an array of tags for stylesheets applied to the TinyMCE editor. + destroySelect2UI: function() { + $('.shortcode-ui-post-select.select2-container').select2( "close" ); + } + + }); + +} )( jQuery ); + +},{}],19:[function(require,module,exports){ +(function (global){ +var Backbone = (typeof window !== "undefined" ? window['Backbone'] : typeof global !== "undefined" ? global['Backbone'] : null), + sui = require('./../utils/sui.js'), + $ = (typeof window !== "undefined" ? window['jQuery'] : typeof global !== "undefined" ? global['jQuery'] : null); + +var editAttributeField = Backbone.View.extend( { + + tagName: "div", + + events: { + 'input input': 'inputChanged', + 'input textarea': 'inputChanged', + 'change select': 'inputChanged', + 'change input[type="radio"]': 'inputChanged', + 'change input[type="checkbox"]': 'inputChanged' + }, + + render: function() { + + var data = jQuery.extend( { + id: 'shortcode-ui-' + this.model.get( 'attr' ) + '-' + this.model.cid, + }, this.model.toJSON() ); + + // Convert meta JSON to attribute string. + var _meta = []; + for ( var key in data.meta ) { + + // Boolean attributes can only require attribute key, not value. + if ( 'boolean' === typeof( data.meta[ key ] ) ) { + + // Only set truthy boolean attributes. + if ( data.meta[ key ] ) { + _meta.push( _.escape( key ) ); + } + + } else { + + _meta.push( _.escape( key ) + '="' + _.escape( data.meta[ key ] ) + '"' ); + + } + + } + + data.meta = _meta.join( ' ' ); + + this.$el.html( this.template( data ) ); + this.triggerCallbacks(); + + return this; + }, + + /** + * Input Changed Update Callback. + * + * If the input field that has changed is for content or a valid attribute, + * then it should update the model. If a callback function is registered + * for this attribute, it should be called as well. + */ + inputChanged: function( e ) { + + var $el; + + if ( this.model.get( 'attr' ) ) { + $el = this.$el.find( '[name="' + this.model.get( 'attr' ) + '"]' ); + } else { + $el = this.$el.find( '[name="inner_content"]' ); + } + + if ( 'radio' === this.model.attributes.type ) { + this.setValue( $el.filter(':checked').first().val() ); + } else if ( 'checkbox' === this.model.attributes.type ) { + this.setValue( $el.is( ':checked' ) ); + } else if ( 'range' === this.model.attributes.type ) { + var rangeId = '#' + e.target.id + '_indicator'; + var rangeValue = e.target.value; + document.querySelector( rangeId ).value = rangeValue; + this.setValue( $el.val() ); + } else { + this.setValue( $el.val() ); + } + + this.triggerCallbacks(); + }, + + getValue: function() { + return this.model.get( 'value' ); + }, + + setValue: function( val ) { + this.model.set( 'value', val ); + }, + + triggerCallbacks: function() { + + var shortcodeName = this.shortcode.attributes.shortcode_tag, + attributeName = this.model.get( 'attr' ), + hookName = [ shortcodeName, attributeName ].join( '.' ), + changed = this.model.changed, + collection = _.flatten( _.values( this.views.parent.views._views ) ), + shortcode = this.shortcode; + + /* + * Action run when an attribute value changes on a shortcode * - * @method getEditorStyles - * @returns {Array} + * Called as `{shortcodeName}.{attributeName}`. + * + * @param changed (object) + * The update, ie. { "changed": "newValue" } + * @param viewModels (array) + * The collections of views (editAttributeFields) + * which make up this shortcode UI form + * @param shortcode (object) + * Reference to the shortcode model which this attribute belongs to. */ - getEditorStyles: function() { + wp.shortcake.hooks.doAction( hookName, changed, collection, shortcode ); - var styles = ''; + } - this.getNodes( function ( editor, node, content ) { - var dom = editor.dom, - bodyClasses = editor.getBody().className || '', - iframe, iframeDoc, i, resize; +}, { - tinymce.each( dom.$( 'link[rel="stylesheet"]', editor.getDoc().head ), function( link ) { - if ( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1 && - link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 ) { + /** + * Get an attribute field from a shortcode by name. + * + * Usage: `sui.views.editAttributeField.getField( collection, 'title')` + * + * @param array collection of editAttributeFields + * @param string attribute name + * @return editAttributeField The view corresponding to the matching field + */ + getField: function( collection, attr ) { + return _.find( collection, + function( viewModel ) { + return attr === viewModel.model.get('attr'); + } + ); + } +}); - styles += dom.getOuterHTML( link ) + '\n'; - } +sui.views.editAttributeField = editAttributeField; +module.exports = editAttributeField; - }); +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../utils/sui.js":15}],20:[function(require,module,exports){ +(function (global){ +var wp = (typeof window !== "undefined" ? window['wp'] : typeof global !== "undefined" ? global['wp'] : null), +sui = require('./../utils/sui.js'), +backbone = (typeof window !== "undefined" ? window['Backbone'] : typeof global !== "undefined" ? global['Backbone'] : null), + editAttributeField = require('./edit-attribute-field.js'), - } ); + // Additional attribute field types: these fields are all standalone in functionality, + // but bundled here for simplicity to save an HTTP request. + editAttributeFieldAttachment = require('./edit-attribute-field-attachment.js'), + editAttributeFieldPostSelect = require('./edit-attribute-field-post-select.js'), + editAttributeFieldColor = require('./edit-attribute-field-color.js'); - return styles; - }, +/** + * Single edit shortcode content view. + */ +var EditShortcodeForm = wp.Backbone.View.extend({ + + template: wp.template('shortcode-default-edit-form'), + + events: { + 'click .edit-shortcode-form-cancel': 'cancel', }, -}; -module.exports = sui.utils.shortcodeViewConstructor = shortcodeViewConstructor; + initialize: function() { + + var t = this; + + var innerContent = this.model.get( 'inner_content' ); + if ( innerContent && typeof innerContent.attributes.type !== 'undefined' ) { + + // add UI for inner_content + var view = new editAttributeField( { model: innerContent } ); + + view.shortcode = t.model; + view.template = wp.media.template( 'shortcode-ui-content' ); + + t.views.add( '.edit-shortcode-form-fields', view ); + + } + + this.model.get( 'attrs' ).each( function( attr ) { + + // Get the field settings from localization data. + var type = attr.get('type'); + + if ( ! shortcodeUIFieldData[ type ] ) { + return; + } + + var templateData = { + value: attr.get('value'), + attr_raw: { + name: attr.get('value') + } + }; + + var viewObjName = shortcodeUIFieldData[ type ].view; + var tmplName = shortcodeUIFieldData[ type ].template; + + var view = new sui.views[viewObjName]( { model: attr } ); + view.template = wp.media.template( tmplName ); + view.shortcode = t.model; + + t.views.add( '.edit-shortcode-form-fields', view ); + + } ); + + if ( 0 === this.model.get( 'attrs' ).length && ( ! innerContent || typeof innerContent == 'undefined' ) ) { + var messageView = new Backbone.View({ + tagName: 'div', + className: 'notice updated', + }); + messageView.render = function() { + this.$el.append( '

' ); + this.$el.find('p').text( shortcodeUIData.strings.media_frame_no_attributes_message ); + return this; + }; + t.views.add( '.edit-shortcode-form-fields', messageView ); + } + + }, + + cancel: function() { + this.trigger( 'shortcode-ui:cancel' ); + } + +}); + +module.exports = EditShortcodeForm; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./fetcher.js":12,"./sui.js":14}],14:[function(require,module,exports){ +},{"./../utils/sui.js":15,"./edit-attribute-field-attachment.js":16,"./edit-attribute-field-color.js":17,"./edit-attribute-field-post-select.js":18,"./edit-attribute-field.js":19}],21:[function(require,module,exports){ +(function (global){ +var wp = (typeof window !== "undefined" ? window['wp'] : typeof global !== "undefined" ? global['wp'] : null), + $ = (typeof window !== "undefined" ? window['jQuery'] : typeof global !== "undefined" ? global['jQuery'] : null); + +/** + * Single shortcode list item view. + */ +var insertShortcodeListItem = wp.Backbone.View.extend({ + + tagName : 'li', + template : wp.template('add-shortcode-list-item'), + className : 'shortcode-list-item', + + render : function() { + + var data = this.model.toJSON(); + this.$el.attr('data-shortcode', data.shortcode_tag); + + if (('listItemImage' in data) && 0 === data.listItemImage.indexOf('dashicons-')) { + var fakeEl = $('

').addClass( 'dashicons' ).addClass( data.listItemImage ); + data.listItemImage = $('
').append( fakeEl ).html(); + } + + this.$el.html(this.template(data)); + + return this; + + } +}); + +module.exports = insertShortcodeListItem; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],22:[function(require,module,exports){ +(function (global){ +var $ = (typeof window !== "undefined" ? window['jQuery'] : typeof global !== "undefined" ? global['jQuery'] : null); +var wp = (typeof window !== "undefined" ? window['wp'] : typeof global !== "undefined" ? global['wp'] : null); +var Backbone = (typeof window !== "undefined" ? window['Backbone'] : typeof global !== "undefined" ? global['Backbone'] : null); var Shortcodes = require('./../collections/shortcodes.js'); +var insertShortcodeListItem = require('./insert-shortcode-list-item.js'); -window.Shortcode_UI = window.Shortcode_UI || { - shortcodes: new Shortcodes(), - views: {}, - controllers: {}, - utils: {}, -}; +var insertShortcodeList = wp.Backbone.View.extend({ -module.exports = window.Shortcode_UI; + tagName : 'div', + className : 'insert-shortcode-list', + template : wp.template('add-shortcode-list'), -},{"./../collections/shortcodes.js":8}]},{},[1,2,3,4,5,6]); + events: { + 'click .shortcode-list-item': 'selectShortcode', + }, + + initialize : function( options ) { + this.setShortcodes( ( 'shortcodes' in options ) ? options.shortcodes : [] ); + this.refresh(); + }, + + /** + * Set / Update shortcodes list. + */ + setShortcodes: function( shortcodes ) { + + if ( shortcodes instanceof Shortcodes ) { + this.shortcodes = shortcodes; + } else if ( Array.isArray( shortcodes ) ) { + this.shortcodes = new Shortcodes( shortcodes ); + } else { + this.shortcodes = new Shortcodes(); + } + + }, + + selectShortcode: function(e) { + + var target = $( e.currentTarget ); + var shortcode = this.shortcodes.findWhere( { shortcode_tag: target.attr( 'data-shortcode' ) } ); + + if ( shortcode ) { + this.trigger( 'shortcode-ui:select', shortcode ); + } + + }, + + /** + * Refresh & render shortcodes and sub-views. + */ + refresh: function( shortcodes ) { + + shortcodes = shortcodes || this.shortcodes; + + // Remove existing views. + _.each( this.views.get('ul'), function( view ) { + view.remove(); + } ); + + shortcodes.each( function( shortcode ) { + this.views.add( 'ul', new insertShortcodeListItem({ + model : shortcode + })); + }.bind(this) ); + + }, + + search: function( s ) { + + if ( s && s.length ) { + + var pattern = s.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&" ); + var regex = new RegExp( pattern, "i" ); + + var filteredShortcodes = this.shortcodes.filter( function( shortcode ) { + return regex.test( shortcode.get( "label" ) ); + }); + + this.refresh( new Shortcodes( filteredShortcodes ) ); + + } else { + + this.refresh(); + + } + + }, + +}); + +module.exports = insertShortcodeList; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../collections/shortcodes.js":8,"./insert-shortcode-list-item.js":21}],23:[function(require,module,exports){ +(function (global){ +var wp = (typeof window !== "undefined" ? window['wp'] : typeof global !== "undefined" ? global['wp'] : null); + +/** + * Toolbar view that extends wp.media.view.Toolbar + * to define cusotm refresh method + */ +var Toolbar = wp.media.view.Toolbar.extend({ + + refresh : function() { + + var action = this.controller.content.mode(); + + if ( action ) { + this.get('insert').model.set( 'disabled', action !== 'shortcode-ui-content-edit' ); + } + + /** + * call 'refresh' directly on the parent class + */ + wp.media.view.Toolbar.prototype.refresh.apply(this, arguments); + + } +}); + +module.exports = Toolbar; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],24:[function(require,module,exports){ +(function (global){ +var wp = (typeof window !== "undefined" ? window['wp'] : typeof global !== "undefined" ? global['wp'] : null), + $ = (typeof window !== "undefined" ? window['jQuery'] : typeof global !== "undefined" ? global['jQuery'] : null), + sui = require('./../utils/sui.js'), + State = require('./../controllers/media-controller.js'), + Toolbar = require('./media-frame-toolbar.js'), + ListView = require('./insert-shortcode-list.js'), + EditView = require('./edit-shortcode-form.js'), + Frame = wp.media.view.Frame; + +var ShortcodeUiFrame = Frame.extend( { + + className: 'media-frame', + regions: [ 'title', 'content', 'toolbar' ], + template: wp.template('media-frame'), + + initialize: function() { + + Frame.prototype.initialize.apply( this, arguments ); + + _.bindAll( this, 'select', 'reset' ); + + this.options = _.defaults( this.options, { + state: 'shortcode-ui', + modal: true, + title: '', + updateTitle: '', + shortcodes: [], + insertCallback: null, + } ); + + // Initialize modal container view. + if ( this.options.modal ) { + this.modal = new wp.media.view.Modal({ + controller: this, + title: this.options.title + }); + this.modal.content( this ); + } + + this.createStates(); + + this.on( 'attach', _.bind( this.views.ready, this.views ), this ); + + this.on( 'title:create:default', this.createTitle, this ); + this.on( 'toolbar:create:shortcode-ui-toolbar', this.createToolbar, this ); + this.on( 'content:render:shortcode-ui-content-browse', this.renderBrowseMode, this ); + this.on( 'content:render:shortcode-ui-content-edit', this.renderEditMode, this ); + this.on( 'content:render:shortcode-ui-content-update', this.renderEditMode, this ); + + }, + + /** + * @returns {wp.media.view.ShortcodeUiFrame} Returns itself to allow chaining + */ + render: function() { + + // Activate the default state if no active state exists. + if ( ! this.state() && this.options.state ) { + this.setState( this.options.state ); + } + + return Frame.prototype.render.apply( this, arguments ); + + }, + + createStates: function() { + + var mode, opts, state; + + mode = ( 'shortcode' in this.options ) ? 'update' : 'browse'; + + opts = { + id : 'shortcode-ui', + toolbar : 'shortcode-ui-toolbar', + content : 'shortcode-ui-content-' + mode, + menu : false, + search : true, + router : false, + title : this.options.title, + }; + + if ( 'shortcode' in this.options ) { + console.log( this.options ); + opts.title = this.options.updateTitle.replace( /%s/, this.options.shortcode.attributes.label ); + } + + state = new State( opts ); + this.states.add( state ); + + if ( 'shortcode' in this.options ) { + state.props.set( 'shortcode', this.options.shortcode ); + } + + + + }, + + /** + * @param {Object} title + * @this wp.media.controller.Region + */ + createTitle: function( title ) { + title.view = new wp.media.View({ + controller: this, + tagName: 'h1' + }); + }, + + select: function( shortcode ) { + this.state().setShortcode( shortcode.clone() ); + this.content.mode( 'shortcode-ui-content-edit' ); + }, + + reset: function() { + this.state('shortcode-ui').reset(); + this.content.mode( 'shortcode-ui-content-browse' ); + }, + + renderBrowseMode : function( contentRegion ) { + + var view = new ListView({ + shortcodes: this.options.shortcodes + }); + + this.content.set( view.render() ); + + $( '.media-menu-item', this.$el ).click( this.reset ); + view.on( 'shortcode-ui:select', this.select ); + + this.renderSearch( view ); + + this.state().refresh(); + + }, + + renderEditMode : function( id, tab ) { + + var view = new EditView({ + model: this.state('shortcode-ui').getShortcode() + }); + + this.content.set( view.render() ); + + view.on( 'shortcode-ui:cancel', this.reset ); + + if ( 'shortcode-ui-content-update' === this.content.mode() ) { + $( '.edit-shortcode-form-cancel',view.$el ).hide(); + } + + this.state().refresh(); + + }, + + createToolbar : function( toolbar ) { + + var text; + + if ( 'shortcode' in this.options ) { + text = shortcodeUIData.strings.media_frame_toolbar_update_label; + } else { + text = shortcodeUIData.strings.media_frame_toolbar_insert_label; + } + + toolbar.view = new Toolbar( { + controller : this, + items: { + insert: { + text: text, + style: 'primary', + priority: 80, + requires: false, + click: function() { + this.controller.state().insert(); + }, + disabled: true, + } + } + } ); + + }, + + /** + * Render Search Toolbar. + * + * Pass in the parent view. + */ + renderSearch: function( parentView ) { + + var state, listView; + + state = this.state( 'shortcode-ui' ); + + parentView.views.add( new wp.media.view.Search( { + controller: state, + model: state.props, + } ) ); + + listView = this.content.get(); + listView.$el.addClass( 'has-search' ); + + // Listen for change in search query, and call search method on listView. + state.props.on( 'change:search', function() { + listView.search( state.props.get('search' ) ); + }.bind(this) ); + + }, + +} ); + +// Map some of the modal's methods to the frame. +_.each(['open','close','attach','detach','escape'], function( method ) { + /** + * @returns {wp.media.view.ShortcodeUiFrame} Returns itself to allow chaining + */ + ShortcodeUiFrame.prototype[ method ] = function() { + if ( this.modal ) { + this.modal[ method ].apply( this.modal, arguments ); + } + return this; + }; +}); + +module.exports = ShortcodeUiFrame; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../controllers/media-controller.js":9,"./../utils/sui.js":15,"./edit-shortcode-form.js":20,"./insert-shortcode-list.js":22,"./media-frame-toolbar.js":23}]},{},[1,2,3,4,5,6]); diff --git a/js-tests/src/utils/mceViewConstructorSpec.js b/js-tests/src/utils/mceViewConstructorSpec.js index befb43d3..3f28e61c 100644 --- a/js-tests/src/utils/mceViewConstructorSpec.js +++ b/js-tests/src/utils/mceViewConstructorSpec.js @@ -148,7 +148,7 @@ describe( "MCE View Constructor", function() { it( 'parses shortcode with dashes in name and attribute', function() { var shortcode = MceViewConstructor.parseShortcodeString( '[test-shortcode test-attr="test value 2"]'); expect( shortcode instanceof Shortcode ).toEqual( true ); - expect( shortcode.get( 'attrs' ).findWhere( { attr: 'test-attr' }).get('value') ).not.toEqual( 'test value 2' ); + expect( shortcode.get( 'attrs' ).findWhere( { attr: 'test-attr' }).get('value') ).toEqual( 'test value 2' ); }); // https://github.com/fusioneng/Shortcake/issues/171 diff --git a/js-tests/src/utils/suiSpec.js b/js-tests/src/utils/suiSpec.js index eba37962..f7bc4f49 100644 --- a/js-tests/src/utils/suiSpec.js +++ b/js-tests/src/utils/suiSpec.js @@ -9,7 +9,7 @@ describe( "SUI Util", function() { it( 'expected properties', function() { expect( sui.shortcodes instanceof Shortcodes ).toEqual( true ); - expect( sui.views ).toEqual( {} ); + expect( typeof sui.views ).toEqual( 'object' ); }); } ); diff --git a/js-tests/vendor/backbone.js b/js-tests/vendor/backbone.js deleted file mode 100644 index fae2db21..00000000 --- a/js-tests/vendor/backbone.js +++ /dev/null @@ -1 +0,0 @@ -!function(a,b){if("function"==typeof define&&define.amd)define(["underscore","jquery","exports"],function(c,d,e){a.Backbone=b(a,e,c,d)});else if("undefined"!=typeof exports){var c=require("underscore");b(a,exports,c)}else a.Backbone=b(a,{},a._,a.jQuery||a.Zepto||a.ender||a.$)}(this,function(a,b,c,d){{var e=a.Backbone,f=[],g=(f.push,f.slice);f.splice}b.VERSION="1.1.2",b.$=d,b.noConflict=function(){return a.Backbone=e,this},b.emulateHTTP=!1,b.emulateJSON=!1;var h=b.Events={on:function(a,b,c){if(!j(this,"on",a,[b,c])||!b)return this;this._events||(this._events={});var d=this._events[a]||(this._events[a]=[]);return d.push({callback:b,context:c,ctx:c||this}),this},once:function(a,b,d){if(!j(this,"once",a,[b,d])||!b)return this;var e=this,f=c.once(function(){e.off(a,f),b.apply(this,arguments)});return f._callback=b,this.on(a,f,d)},off:function(a,b,d){var e,f,g,h,i,k,l,m;if(!this._events||!j(this,"off",a,[b,d]))return this;if(!a&&!b&&!d)return this._events=void 0,this;for(h=a?[a]:c.keys(this._events),i=0,k=h.length;k>i;i++)if(a=h[i],g=this._events[a]){if(this._events[a]=e=[],b||d)for(l=0,m=g.length;m>l;l++)f=g[l],(b&&b!==f.callback&&b!==f.callback._callback||d&&d!==f.context)&&e.push(f);e.length||delete this._events[a]}return this},trigger:function(a){if(!this._events)return this;var b=g.call(arguments,1);if(!j(this,"trigger",a,b))return this;var c=this._events[a],d=this._events.all;return c&&k(c,b),d&&k(d,arguments),this},stopListening:function(a,b,d){var e=this._listeningTo;if(!e)return this;var f=!b&&!d;d||"object"!=typeof b||(d=this),a&&((e={})[a._listenId]=a);for(var g in e)a=e[g],a.off(b,d,this),(f||c.isEmpty(a._events))&&delete this._listeningTo[g];return this}},i=/\s+/,j=function(a,b,c,d){if(!c)return!0;if("object"==typeof c){for(var e in c)a[b].apply(a,[e,c[e]].concat(d));return!1}if(i.test(c)){for(var f=c.split(i),g=0,h=f.length;h>g;g++)a[b].apply(a,[f[g]].concat(d));return!1}return!0},k=function(a,b){var c,d=-1,e=a.length,f=b[0],g=b[1],h=b[2];switch(b.length){case 0:for(;++dm;m++)this.trigger("change:"+h[m],this,l[h[m]],d)}if(j)return this;if(!i)for(;this._pending;)d=this._pending,this._pending=!1,this.trigger("change",this,d);return this._pending=!1,this._changing=!1,this},unset:function(a,b){return this.set(a,void 0,c.extend({},b,{unset:!0}))},clear:function(a){var b={};for(var d in this.attributes)b[d]=void 0;return this.set(b,c.extend({},a,{unset:!0}))},hasChanged:function(a){return null==a?!c.isEmpty(this.changed):c.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?c.clone(this.changed):!1;var b,d=!1,e=this._changing?this._previousAttributes:this.attributes;for(var f in a)c.isEqual(e[f],b=a[f])||((d||(d={}))[f]=b);return d},previous:function(a){return null!=a&&this._previousAttributes?this._previousAttributes[a]:null},previousAttributes:function(){return c.clone(this._previousAttributes)},fetch:function(a){a=a?c.clone(a):{},void 0===a.parse&&(a.parse=!0);var b=this,d=a.success;return a.success=function(c){return b.set(b.parse(c,a),a)?(d&&d(b,c,a),void b.trigger("sync",b,c,a)):!1},L(this,a),this.sync("read",this,a)},save:function(a,b,d){var e,f,g,h=this.attributes;if(null==a||"object"==typeof a?(e=a,d=b):(e={})[a]=b,d=c.extend({validate:!0},d),e&&!d.wait){if(!this.set(e,d))return!1}else if(!this._validate(e,d))return!1;e&&d.wait&&(this.attributes=c.extend({},h,e)),void 0===d.parse&&(d.parse=!0);var i=this,j=d.success;return d.success=function(a){i.attributes=h;var b=i.parse(a,d);return d.wait&&(b=c.extend(e||{},b)),c.isObject(b)&&!i.set(b,d)?!1:(j&&j(i,a,d),void i.trigger("sync",i,a,d))},L(this,d),f=this.isNew()?"create":d.patch?"patch":"update","patch"===f&&(d.attrs=e),g=this.sync(f,this,d),e&&d.wait&&(this.attributes=h),g},destroy:function(a){a=a?c.clone(a):{};var b=this,d=a.success,e=function(){b.trigger("destroy",b,b.collection,a)};if(a.success=function(c){(a.wait||b.isNew())&&e(),d&&d(b,c,a),b.isNew()||b.trigger("sync",b,c,a)},this.isNew())return a.success(),!1;L(this,a);var f=this.sync("delete",this,a);return a.wait||e(),f},url:function(){var a=c.result(this,"urlRoot")||c.result(this.collection,"url")||K();return this.isNew()?a:a.replace(/([^\/])$/,"$1/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return!this.has(this.idAttribute)},isValid:function(a){return this._validate({},c.extend(a||{},{validate:!0}))},_validate:function(a,b){if(!b.validate||!this.validate)return!0;a=c.extend({},this.attributes,a);var d=this.validationError=this.validate(a,b)||null;return d?(this.trigger("invalid",this,d,c.extend(b,{validationError:d})),!1):!0}});var n=["keys","values","pairs","invert","pick","omit"];c.each(n,function(a){m.prototype[a]=function(){var b=g.call(arguments);return b.unshift(this.attributes),c[a].apply(c,b)}});var o=b.Collection=function(a,b){b||(b={}),b.model&&(this.model=b.model),void 0!==b.comparator&&(this.comparator=b.comparator),this._reset(),this.initialize.apply(this,arguments),a&&this.reset(a,c.extend({silent:!0},b))},p={add:!0,remove:!0,merge:!0},q={add:!0,remove:!1};c.extend(o.prototype,h,{model:m,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},sync:function(){return b.sync.apply(this,arguments)},add:function(a,b){return this.set(a,c.extend({merge:!1},b,q))},remove:function(a,b){var d=!c.isArray(a);a=d?[a]:c.clone(a),b||(b={});var e,f,g,h;for(e=0,f=a.length;f>e;e++)h=a[e]=this.get(a[e]),h&&(delete this._byId[h.id],delete this._byId[h.cid],g=this.indexOf(h),this.models.splice(g,1),this.length--,b.silent||(b.index=g,h.trigger("remove",h,this,b)),this._removeReference(h,b));return d?a[0]:a},set:function(a,b){b=c.defaults({},b,p),b.parse&&(a=this.parse(a,b));var d=!c.isArray(a);a=d?a?[a]:[]:c.clone(a);var e,f,g,h,i,j,k,l=b.at,n=this.model,o=this.comparator&&null==l&&b.sort!==!1,q=c.isString(this.comparator)?this.comparator:null,r=[],s=[],t={},u=b.add,v=b.merge,w=b.remove,x=!o&&u&&w?[]:!1;for(e=0,f=a.length;f>e;e++){if(i=a[e]||{},g=i instanceof m?h=i:i[n.prototype.idAttribute||"id"],j=this.get(g))w&&(t[j.cid]=!0),v&&(i=i===h?h.attributes:i,b.parse&&(i=j.parse(i,b)),j.set(i,b),o&&!k&&j.hasChanged(q)&&(k=!0)),a[e]=j;else if(u){if(h=a[e]=this._prepareModel(i,b),!h)continue;r.push(h),this._addReference(h,b)}h=j||h,!x||!h.isNew()&&t[h.id]||x.push(h),t[h.id]=!0}if(w){for(e=0,f=this.length;f>e;++e)t[(h=this.models[e]).cid]||s.push(h);s.length&&this.remove(s,b)}if(r.length||x&&x.length)if(o&&(k=!0),this.length+=r.length,null!=l)for(e=0,f=r.length;f>e;e++)this.models.splice(l+e,0,r[e]);else{x&&(this.models.length=0);var y=x||r;for(e=0,f=y.length;f>e;e++)this.models.push(y[e])}if(k&&this.sort({silent:!0}),!b.silent){for(e=0,f=r.length;f>e;e++)(h=r[e]).trigger("add",h,this,b);(k||x&&x.length)&&this.trigger("sort",this,b)}return d?a[0]:a},reset:function(a,b){b||(b={});for(var d=0,e=this.models.length;e>d;d++)this._removeReference(this.models[d],b);return b.previousModels=this.models,this._reset(),a=this.add(a,c.extend({silent:!0},b)),b.silent||this.trigger("reset",this,b),a},push:function(a,b){return this.add(a,c.extend({at:this.length},b))},pop:function(a){var b=this.at(this.length-1);return this.remove(b,a),b},unshift:function(a,b){return this.add(a,c.extend({at:0},b))},shift:function(a){var b=this.at(0);return this.remove(b,a),b},slice:function(){return g.apply(this.models,arguments)},get:function(a){return null==a?void 0:this._byId[a]||this._byId[a.id]||this._byId[a.cid]},at:function(a){return this.models[a]},where:function(a,b){return c.isEmpty(a)?b?void 0:[]:this[b?"find":"filter"](function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},findWhere:function(a){return this.where(a,!0)},sort:function(a){if(!this.comparator)throw new Error("Cannot sort a set without a comparator");return a||(a={}),c.isString(this.comparator)||1===this.comparator.length?this.models=this.sortBy(this.comparator,this):this.models.sort(c.bind(this.comparator,this)),a.silent||this.trigger("sort",this,a),this},pluck:function(a){return c.invoke(this.models,"get",a)},fetch:function(a){a=a?c.clone(a):{},void 0===a.parse&&(a.parse=!0);var b=a.success,d=this;return a.success=function(c){var e=a.reset?"reset":"set";d[e](c,a),b&&b(d,c,a),d.trigger("sync",d,c,a)},L(this,a),this.sync("read",this,a)},create:function(a,b){if(b=b?c.clone(b):{},!(a=this._prepareModel(a,b)))return!1;b.wait||this.add(a,b);var d=this,e=b.success;return b.success=function(a,c){b.wait&&d.add(a,b),e&&e(a,c,b)},a.save(null,b),a},parse:function(a){return a},clone:function(){return new this.constructor(this.models)},_reset:function(){this.length=0,this.models=[],this._byId={}},_prepareModel:function(a,b){if(a instanceof m)return a;b=b?c.clone(b):{},b.collection=this;var d=new this.model(a,b);return d.validationError?(this.trigger("invalid",this,d.validationError,b),!1):d},_addReference:function(a){this._byId[a.cid]=a,null!=a.id&&(this._byId[a.id]=a),a.collection||(a.collection=this),a.on("all",this._onModelEvent,this)},_removeReference:function(a){this===a.collection&&delete a.collection,a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"!==a&&"remove"!==a||c===this)&&("destroy"===a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],null!=b.id&&(this._byId[b.id]=b)),this.trigger.apply(this,arguments))}});var r=["forEach","each","map","collect","reduce","foldl","inject","reduceRight","foldr","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","max","min","toArray","size","first","head","take","initial","rest","tail","drop","last","without","difference","indexOf","shuffle","lastIndexOf","isEmpty","chain","sample"];c.each(r,function(a){o.prototype[a]=function(){var b=g.call(arguments);return b.unshift(this.models),c[a].apply(c,b)}});var s=["groupBy","countBy","sortBy","indexBy"];c.each(s,function(a){o.prototype[a]=function(b,d){var e=c.isFunction(b)?b:function(a){return a.get(b)};return c[a](this.models,e,d)}});var t=b.View=function(a){this.cid=c.uniqueId("view"),a||(a={}),c.extend(this,c.pick(a,v)),this._ensureElement(),this.initialize.apply(this,arguments),this.delegateEvents()},u=/^(\S+)\s*(.*)$/,v=["model","collection","el","id","attributes","className","tagName","events"];c.extend(t.prototype,h,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){return this.$el.remove(),this.stopListening(),this},setElement:function(a,c){return this.$el&&this.undelegateEvents(),this.$el=a instanceof b.$?a:b.$(a),this.el=this.$el[0],c!==!1&&this.delegateEvents(),this},delegateEvents:function(a){if(!a&&!(a=c.result(this,"events")))return this;this.undelegateEvents();for(var b in a){var d=a[b];if(c.isFunction(d)||(d=this[a[b]]),d){var e=b.match(u),f=e[1],g=e[2];d=c.bind(d,this),f+=".delegateEvents"+this.cid,""===g?this.$el.on(f,d):this.$el.on(f,g,d)}}return this},undelegateEvents:function(){return this.$el.off(".delegateEvents"+this.cid),this},_ensureElement:function(){if(this.el)this.setElement(c.result(this,"el"),!1);else{var a=c.extend({},c.result(this,"attributes"));this.id&&(a.id=c.result(this,"id")),this.className&&(a["class"]=c.result(this,"className"));var d=b.$("<"+c.result(this,"tagName")+">").attr(a);this.setElement(d,!1)}}}),b.sync=function(a,d,e){var f=x[a];c.defaults(e||(e={}),{emulateHTTP:b.emulateHTTP,emulateJSON:b.emulateJSON});var g={type:f,dataType:"json"};if(e.url||(g.url=c.result(d,"url")||K()),null!=e.data||!d||"create"!==a&&"update"!==a&&"patch"!==a||(g.contentType="application/json",g.data=JSON.stringify(e.attrs||d.toJSON(e))),e.emulateJSON&&(g.contentType="application/x-www-form-urlencoded",g.data=g.data?{model:g.data}:{}),e.emulateHTTP&&("PUT"===f||"DELETE"===f||"PATCH"===f)){g.type="POST",e.emulateJSON&&(g.data._method=f);var h=e.beforeSend;e.beforeSend=function(a){return a.setRequestHeader("X-HTTP-Method-Override",f),h?h.apply(this,arguments):void 0}}"GET"===g.type||e.emulateJSON||(g.processData=!1),"PATCH"===g.type&&w&&(g.xhr=function(){return new ActiveXObject("Microsoft.XMLHTTP")});var i=e.xhr=b.ajax(c.extend(g,e));return d.trigger("request",d,i,e),i};var w=!("undefined"==typeof window||!window.ActiveXObject||window.XMLHttpRequest&&(new XMLHttpRequest).dispatchEvent),x={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};b.ajax=function(){return b.$.ajax.apply(b.$,arguments)};var y=b.Router=function(a){a||(a={}),a.routes&&(this.routes=a.routes),this._bindRoutes(),this.initialize.apply(this,arguments)},z=/\((.*?)\)/g,A=/(\(\?)?:\w+/g,B=/\*\w+/g,C=/[\-{}\[\]+?.,\\\^$|#\s]/g;c.extend(y.prototype,h,{initialize:function(){},route:function(a,d,e){c.isRegExp(a)||(a=this._routeToRegExp(a)),c.isFunction(d)&&(e=d,d=""),e||(e=this[d]);var f=this;return b.history.route(a,function(c){var g=f._extractParameters(a,c);f.execute(e,g),f.trigger.apply(f,["route:"+d].concat(g)),f.trigger("route",d,g),b.history.trigger("route",f,d,g)}),this},execute:function(a,b){a&&a.apply(this,b)},navigate:function(a,c){return b.history.navigate(a,c),this},_bindRoutes:function(){if(this.routes){this.routes=c.result(this,"routes");for(var a,b=c.keys(this.routes);null!=(a=b.pop());)this.route(a,this.routes[a])}},_routeToRegExp:function(a){return a=a.replace(C,"\\$&").replace(z,"(?:$1)?").replace(A,function(a,b){return b?a:"([^/?]+)"}).replace(B,"([^?]*?)"),new RegExp("^"+a+"(?:\\?([\\s\\S]*))?$")},_extractParameters:function(a,b){var d=a.exec(b).slice(1);return c.map(d,function(a,b){return b===d.length-1?a||null:a?decodeURIComponent(a):null})}});var D=b.History=function(){this.handlers=[],c.bindAll(this,"checkUrl"),"undefined"!=typeof window&&(this.location=window.location,this.history=window.history)},E=/^[#\/]|\s+$/g,F=/^\/+|\/+$/g,G=/msie [\w.]+/,H=/\/$/,I=/#.*$/;D.started=!1,c.extend(D.prototype,h,{interval:50,atRoot:function(){return this.location.pathname.replace(/[^\/]$/,"$&/")===this.root},getHash:function(a){var b=(a||this).location.href.match(/#(.*)$/);return b?b[1]:""},getFragment:function(a,b){if(null==a)if(this._hasPushState||!this._wantsHashChange||b){a=decodeURI(this.location.pathname+this.location.search);var c=this.root.replace(H,"");a.indexOf(c)||(a=a.slice(c.length))}else a=this.getHash();return a.replace(E,"")},start:function(a){if(D.started)throw new Error("Backbone.history has already been started");D.started=!0,this.options=c.extend({root:"/"},this.options,a),this.root=this.options.root,this._wantsHashChange=this.options.hashChange!==!1,this._wantsPushState=!!this.options.pushState,this._hasPushState=!!(this.options.pushState&&this.history&&this.history.pushState);var d=this.getFragment(),e=document.documentMode,f=G.exec(navigator.userAgent.toLowerCase())&&(!e||7>=e);if(this.root=("/"+this.root+"/").replace(F,"/"),f&&this._wantsHashChange){var g=b.$('