PluginProbe ʕ •ᴥ•ʔ
SiteOrigin CSS / 1.1.5
SiteOrigin CSS v1.1.5
1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.10 1.5.11 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 trunk 1.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.2.0
so-css / js / editor.js
so-css / js Last commit date
URI.js 9 years ago URI.min.js 8 years ago css.js 9 years ago css.min.js 8 years ago csslint.js 9 years ago csslint.min.js 8 years ago editor.js 8 years ago editor.min.js 8 years ago inspector.js 9 years ago inspector.min.js 8 years ago jquery.sizes.js 11 years ago jquery.sizes.min.js 8 years ago specificity.js 11 years ago specificity.min.js 9 years ago
editor.js
1697 lines
1 /* globals jQuery, _, socssOptions, Backbone, CodeMirror, console, cssjs, wp */
2
3 (function ($, _, socssOptions) {
4
5 var socss = {
6 model: {},
7 collection: {},
8 view: {},
9 fn: {}
10 };
11
12 window.socss = socss;
13
14 /**
15 * The toolbar view
16 */
17 socss.view.toolbar = Backbone.View.extend({
18
19 button: _.template('<li><a href="#" class="toolbar-button socss-button"><%= text %></a></li>'),
20
21 editor: null,
22
23 initialize: function ( attr ) {
24 this.editor = attr.editor;
25
26 var thisView = this;
27 this.$('.editor-expand').click(function (e) {
28 e.preventDefault();
29 $(this).blur();
30 thisView.trigger('click_expand');
31 });
32
33 this.$('.editor-visual').click(function (e) {
34 e.preventDefault();
35 $(this).blur();
36 thisView.trigger('click_visual');
37 });
38 },
39
40 addButton: function (text, action) {
41 var thisView = this;
42 var button = $(this.button({text: text}))
43 .appendTo(this.$('.toolbar-function-buttons .toolbar-buttons'))
44 .click(function (e) {
45 e.preventDefault();
46 $(this).blur();
47 thisView.trigger('click_' + action);
48 });
49
50 return button;
51 }
52 });
53
54 /**
55 * The editor view, which handles codemirror stuff
56 */
57 socss.view.editor = Backbone.View.extend({
58
59 codeMirror: null,
60 snippets: null,
61 toolbar: null,
62 visualProperties: null,
63
64 inspector: null,
65
66 cssSelectors: [],
67
68 initialize: function (args) {
69 this.setupEditor();
70 },
71
72 render: function () {
73 var thisView = this;
74
75 // Setup the toolbar
76 this.toolbar = new socss.view.toolbar({
77 editor: this,
78 el: this.$('.custom-css-toolbar')
79 });
80 this.toolbar.editor = this;
81 this.toolbar.render();
82
83 // Create the visual properties view
84 this.visualProperties = new socss.view.properties({
85 editor: this,
86 el: $('#so-custom-css-properties')
87 });
88 this.visualProperties.render();
89
90 this.toolbar.on('click_expand', function () {
91 thisView.toggleExpand();
92 });
93
94 this.toolbar.on('click_visual', function () {
95 thisView.visualProperties.loadCSS( thisView.codeMirror.getValue().trim() );
96 thisView.visualProperties.show();
97 });
98
99 this.preview = new socss.view.preview({
100 editor: this,
101 el: this.$('.custom-css-preview')
102 });
103 this.preview.render();
104 },
105
106 /**
107 * Do the initial setup of the CodeMirror editor
108 */
109 setupEditor: function () {
110 var thisView = this;
111 this.registerCodeMirrorAutocomplete();
112
113 // Setup the Codemirror instance
114 var $textArea = this.$('textarea.css-editor');
115 var initValue = $textArea.val();
116 // Pad with empty lines so the editor takes up all the white space. To try make sure user gets copy/paste
117 // options in context menu.
118 var newlineMatches = initValue.match(/\n/gm);
119 var lineCount = newlineMatches ? newlineMatches.length+1 : 1;
120 var paddedValue = initValue;
121 $textArea.val(paddedValue);
122 this.codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
123 tabSize: 2,
124 lineNumbers: true,
125 mode: 'css',
126 theme: 'neat',
127 inputStyle: 'contenteditable', //necessary to allow context menu (right click) copy/paste etc.
128 gutters: [
129 "CodeMirror-lint-markers"
130 ],
131 lint: true,
132 });
133
134 // Make sure the user doesn't leave without saving
135 this.$el.on('submit', function(){
136 initValue = thisView.codeMirror.getValue().trim();
137 });
138 $(window).bind('beforeunload', function(){
139 var editorValue = thisView.codeMirror.getValue().trim();
140 if( editorValue !== initValue ) {
141 return socssOptions.loc.leave;
142 }
143 });
144
145
146 // Set the container to visible overflow once the editor is setup
147 this.$el.find('.custom-css-container').css('overflow', 'visible');
148 this.scaleEditor();
149
150 // Scale the editor whenever the window is resized
151 $(window).resize(function () {
152 thisView.scaleEditor();
153 });
154
155 // Setup the extensions
156 this.setupCodeMirrorExtensions();
157 },
158
159 /**
160 * Register the autocomplete helper. Based on css-hint.js in the codemirror addon folder.
161 */
162 registerCodeMirrorAutocomplete: function () {
163 var thisView = this;
164
165 var pseudoClasses = {
166 link: 1, visited: 1, active: 1, hover: 1, focus: 1,
167 "first-letter": 1, "first-line": 1, "first-child": 1,
168 before: 1, after: 1, lang: 1
169 };
170
171 CodeMirror.registerHelper("hint", "css", function (cm) {
172 var cur = cm.getCursor(), token = cm.getTokenAt(cur);
173 var inner = CodeMirror.innerMode(cm.getMode(), token.state);
174 if (inner.mode.name !== "css") {
175 return;
176 }
177
178 if (token.type === "keyword" && "!important".indexOf(token.string) === 0) {
179 return {
180 list: ["!important"], from: CodeMirror.Pos(cur.line, token.start),
181 to: CodeMirror.Pos(cur.line, token.end)
182 };
183 }
184
185 var start = token.start, end = cur.ch, word = token.string.slice(0, end - start);
186 if (/[^\w$_-]/.test(word)) {
187 word = "";
188 start = end = cur.ch;
189 }
190
191 var spec = CodeMirror.resolveMode("text/css");
192
193 var result = [];
194
195 function add(keywords) {
196 for (var name in keywords) {
197 if (!word || name.lastIndexOf(word, 0) === 0) {
198 result.push(name);
199 }
200 }
201 }
202
203 var st = inner.state.state;
204
205 if (st === 'top') {
206 // We're going to autocomplete the selector using our own set of rules
207 var line = cm.getLine(cur.line).trim();
208
209 var selectors = thisView.cssSelectors;
210 for (var i = 0; i < selectors.length; i++) {
211 if (selectors[i].selector.indexOf(line) !== -1) {
212 result.push(selectors[i].selector);
213 }
214 }
215
216 if (result.length) {
217 return {
218 list: result,
219 from: CodeMirror.Pos(cur.line, 0),
220 to: CodeMirror.Pos(cur.line, end)
221 };
222 }
223 }
224 else {
225
226 if (st === "pseudo" || token.type === "variable-3") {
227 add(pseudoClasses);
228 }
229 else if (st === "block" || st === "maybeprop") {
230 add(spec.propertyKeywords);
231 }
232 else if (st === "prop" || st === "parens" || st === "at" || st === "params") {
233 add(spec.valueKeywords);
234 add(spec.colorKeywords);
235 }
236 else if (st === "media" || st === "media_parens") {
237 add(spec.mediaTypes);
238 add(spec.mediaFeatures);
239 }
240
241 if (result.length) {
242 return {
243 list: result,
244 from: CodeMirror.Pos(cur.line, start),
245 to: CodeMirror.Pos(cur.line, end)
246 };
247 }
248
249 }
250
251 });
252 },
253
254 setupCodeMirrorExtensions: function () {
255 var thisView = this;
256
257 this.codeMirror.on('cursorActivity', function (cm) {
258 var cur = cm.getCursor(), token = cm.getTokenAt(cur);
259 var inner = CodeMirror.innerMode(cm.getMode(), token.state);
260
261 // If we have a qualifier selected, then highlight that in the preview
262 if (token.type === 'qualifier' || token.type === 'tag' || token.type === 'builtin') {
263 var line = cm.getLine(cur.line);
264 var selector = line.substring(0, token.end);
265
266 thisView.preview.highlight(selector);
267 }
268 else {
269 thisView.preview.clearHighlight();
270 }
271 });
272
273 // This sets up automatic autocompletion at all times
274 this.codeMirror.on('keyup', function (cm, e) {
275 if (
276 ( e.keyCode >= 65 && e.keyCode <= 90 ) ||
277 ( e.keyCode === 189 && !e.shiftKey ) ||
278 ( e.keyCode === 190 && !e.shiftKey ) ||
279 ( e.keyCode === 51 && e.shiftKey ) ||
280 ( e.keyCode === 189 && e.shiftKey )
281 ) {
282 cm.showHint( {
283 completeSingle: false
284 } );
285 }
286 });
287 },
288
289 /**
290 * Scale the size of the editor depending on whether it's expanded or not
291 */
292 scaleEditor: function () {
293 var windowHeight = $( window ).outerHeight();
294 if (this.$el.hasClass('expanded')) {
295 // If we're in the expanded view, then resize the editor
296 this.$el.find( '.CodeMirror-scroll' ).css( 'max-height', '' );
297 this.codeMirror.setSize('100%', windowHeight - this.$('.custom-css-toolbar').outerHeight());
298 }
299 else {
300 // Attempt to calculate approximate space available for editor when not expanded.
301 var $form = $( '#so-custom-css-form' );
302 var otherEltsHeight = $('#wpadminbar').outerHeight( true ) +
303 $( '#siteorigin-custom-css' ).find( '> h2' ).outerHeight( true ) +
304 $form.find( '> .custom-css-toolbar' ).outerHeight( true ) +
305 $form.find( '> p.description' ).outerHeight( true ) +
306 $form.find( '> p.submit' ).outerHeight( true ) +
307 parseFloat( $( '#wpbody-content' ).css( 'padding-bottom' ) );
308 this.$el.find( '.CodeMirror-scroll' ).css( 'max-height', windowHeight - otherEltsHeight );
309 this.codeMirror.setSize( '100%', 'auto' );
310 }
311 },
312
313 /**
314 * Check if the editor is in expanded mode
315 * @returns bool
316 */
317 isExpanded: function () {
318 return this.$el.hasClass('expanded');
319 },
320
321 /**
322 * Toggle if this is expanded or not
323 */
324 toggleExpand: function () {
325 this.$el.toggleClass('expanded');
326 this.scaleEditor();
327 },
328
329 /**
330 * Set the expanded state of the editor
331 * @param expanded
332 */
333 setExpand: function (expanded) {
334 if (expanded) {
335 this.$el.addClass('expanded');
336 }
337 else {
338 this.$el.removeClass('expanded');
339 }
340 this.scaleEditor();
341 },
342
343 /**
344 * Set the snippets available to this editor
345 */
346 setSnippets: function (snippets) {
347 if (!_.isEmpty(snippets)) {
348 var thisView = this;
349
350 this.snippets = new socss.view.snippets({
351 snippets: snippets
352 });
353 this.snippets.editor = this;
354
355 this.snippets.render();
356 this.toolbar.addButton('Snippets', 'snippets');
357 this.toolbar.on('click_snippets', function () {
358 thisView.snippets.show();
359 });
360 }
361 },
362
363 /**
364 * Add some CSS to the editor.
365 * @param css
366 */
367 addCode: function (css) {
368 var editor = this.codeMirror;
369
370 var before_css = '';
371 if (editor.doc.lineCount() === 1 && editor.doc.getLine(editor.doc.lastLine()).length === 0) {
372 before_css = "";
373 }
374 else if (editor.doc.getLine(editor.doc.lastLine()).length === 0) {
375 before_css = "\n";
376 }
377 else {
378 before_css = "\n\n";
379 }
380
381 // Now insert the code in the editor
382 editor.doc.setCursor(
383 editor.doc.lastLine(),
384 editor.doc.getLine(editor.doc.lastLine()).length
385 );
386 editor.doc.replaceSelection(before_css + css);
387 },
388
389 addEmptySelector: function (selector) {
390 this.addCode(selector + " {\n \n}");
391 },
392
393 /**
394 * Sets the inspector view that's being used by the editor
395 */
396 setInspector: function (inspector) {
397 var thisView = this;
398 this.inspector = inspector;
399 this.cssSelectors = inspector.pageSelectors;
400
401 // A selector is clicked in the inspector
402 inspector.on('click_selector', function (selector) {
403 if ( thisView.visualProperties.isVisible() ) {
404 thisView.visualProperties.addSelector(selector);
405 }
406 else {
407 thisView.addEmptySelector(selector);
408 }
409 });
410
411 // A property is clicked in the inspector
412 inspector.on('click_property', function (property) {
413 if ( ! thisView.visualProperties.isVisible() ) {
414 thisView.codeMirror.replaceSelection(property + ";\n ");
415 }
416 });
417
418 inspector.on('set_active_element', function(el, selectors){
419 if ( thisView.visualProperties.isVisible() && selectors.length ) {
420 thisView.visualProperties.addSelector( selectors[0].selector );
421 }
422 });
423 }
424
425 });
426
427 /**
428 * The preview.
429 */
430 socss.view.preview = Backbone.View.extend({
431
432 template: _.template( $('#template-preview-window').html() ),
433 editor: null,
434 originalUri: null,
435 currentUri: null,
436
437 initialize: function (attr) {
438 this.editor = attr.editor;
439
440 var thisView = this;
441 this.editor.codeMirror.on('change', function (cm, c) {
442 thisView.updatePreviewCss();
443 });
444 },
445
446 render: function () {
447 var thisView = this;
448
449 this.$el.html( this.template() );
450
451 this.$( '#preview-iframe' )
452 .attr( 'src', socssOptions.homeURL )
453 .on( 'load', function () {
454 var $$ = $(this);
455
456 // Update the current URI with the iframe URI
457 thisView.currentUri = new URI( $$.contents().get(0).location.href );
458 thisView.currentUri.removeQuery( 'so_css_preview' );
459 thisView.$( '#preview-navigator input' ).val( thisView.currentUri.toString() );
460 thisView.currentUri.addQuery( 'so_css_preview', 1 );
461
462 $$.contents().find('a').each(function () {
463 var href = $(this).attr('href');
464 if (href === undefined) {
465 return true;
466 }
467
468 var firstSeperator = (href.indexOf('?') === -1 ? '?' : '&');
469 $(this).attr('href', href + firstSeperator + 'so_css_preview=1');
470 });
471
472 thisView.updatePreviewCss();
473 })
474 .mouseleave(function () {
475 thisView.clearHighlight();
476 });
477
478 this.$( '#preview-navigator input' ).keydown( function( e ){
479 var $$ = $(this);
480
481 if( e.keyCode == 13 ) {
482 e.preventDefault();
483
484 var newUri = new URI( $$.val() );
485
486 // Validate the URI
487 if(
488 thisView.originalUri.host() !== newUri.host() ||
489 thisView.originalUri.protocol() !== newUri.protocol()
490 ) {
491 $$.blur();
492 alert( $$.data( 'invalid-uri' ) );
493 $$.focus();
494 }
495 else {
496 newUri.addQuery( 'so_css_preview', 1 );
497 thisView.$( '#preview-iframe' ).attr( 'src', newUri.toString() );
498 }
499 }
500 } );
501
502 this.originalUri = new URI( socssOptions.homeURL );
503 this.currentUri = new URI( socssOptions.homeURL );
504
505 this.currentUri.removeQuery( 'so_css_preview' );
506 this.$('#preview-navigator input').val( this.currentUri.toString() );
507 this.currentUri.addQuery( 'so_css_preview', 1 );
508 },
509
510 /**
511 * Update the preview CSS from the CodeMirror value in the editor
512 */
513 updatePreviewCss: function () {
514 var preview = this.$('#preview-iframe');
515 if (preview.length === 0) {
516 return;
517 }
518
519 var head = preview.contents().find('head');
520 if (head.find('style.siteorigin-custom-css').length === 0) {
521 head.append('<style class="siteorigin-custom-css" type="text/css"></style>');
522 }
523 var style = head.find('style.siteorigin-custom-css');
524
525 // Update the CSS after a short delay
526 var css = this.editor.codeMirror.getValue().trim();
527 style.html(css);
528 },
529
530 /**
531 * Highlight all elements with a given selector
532 */
533 highlight: function (selector) {
534 try {
535 this.editor.inspector.hl.highlight(selector);
536 }
537 catch (err) {
538 console.log('No inspector to highlight with');
539 }
540 },
541
542 /**
543 * Clear the currently highlighted elements in preview
544 */
545 clearHighlight: function () {
546 try {
547 this.editor.inspector.hl.clear();
548 }
549 catch (err) {
550 console.log('No inspector to highlight with');
551 }
552 }
553
554 });
555
556 /**
557 * The dialog for the snippets browser
558 */
559 socss.view.snippets = Backbone.View.extend({
560 template: _.template($('#template-snippet-browser').html()),
561 snippet: _.template('<li class="snippet"><%- name %></li>'),
562 className: 'css-editor-snippet-browser',
563 snippets: null,
564 editor: null,
565
566 events: {
567 'click .close': 'hide',
568 'click .buttons .insert-snippet': 'insertSnippet'
569 },
570
571 currentSnippet: null,
572
573 initialize: function (args) {
574 this.snippets = args.snippets;
575 },
576
577 render: function () {
578 var thisView = this;
579
580
581 var clickSnippet = function (e) {
582 e.preventDefault();
583 var $$ = $(this);
584
585 thisView.$('.snippets li.snippet').removeClass('active');
586 $(this).addClass('active');
587 thisView.viewSnippet({
588 name: $$.html(),
589 description: $$.data('description'),
590 css: $$.data('css')
591 });
592 };
593
594 this.$el.html(this.template());
595 for (var i = 0; i < this.snippets.length; i++) {
596 $(this.snippet({name: this.snippets[i].Name}))
597 .data({
598 'description': this.snippets[i].Description,
599 'css': this.snippets[i].css
600 })
601 .appendTo(this.$('ul.snippets'))
602 .click(clickSnippet);
603 }
604
605 // Click on the first one
606 thisView.$('.snippets li.snippet').eq(0).click();
607
608 this.attach();
609 return this;
610 },
611
612 viewSnippet: function (args) {
613 var w = this.$('.main .snippet-view');
614
615 w.find('.snippet-title').html(args.name);
616 w.find('.snippet-description').html(args.description);
617 w.find('.snippet-code').html(args.css);
618
619 this.currentSnippet = args;
620 },
621
622 insertSnippet: function () {
623 var editor = this.editor.codeMirror;
624 var css = this.currentSnippet.css;
625
626 var before_css = '';
627 if (editor.doc.lineCount() === 1 && editor.doc.getLine(editor.doc.lastLine()).length === 0) {
628 before_css = "";
629 }
630 else if (editor.doc.getLine(editor.doc.lastLine()).length === 0) {
631 before_css = "\n";
632 }
633 else {
634 before_css = "\n\n";
635 }
636
637 // Now insert the code in the editor
638 editor.doc.setCursor(
639 editor.doc.lastLine(),
640 editor.doc.getLine(editor.doc.lastLine()).length
641 );
642 editor.doc.replaceSelection(before_css + css);
643
644 this.hide();
645 },
646
647 attach: function () {
648 this.$el.appendTo('body');
649 },
650
651 show: function () {
652 this.$el.show();
653 },
654
655 hide: function () {
656 this.$el.hide();
657 }
658 });
659
660
661 /**
662 * The visual properties editor
663 */
664 socss.view.properties = Backbone.View.extend({
665
666 model: socss.model.cssRules,
667
668 tabTemplate: _.template('<li data-section="<%- id %>"><span class="fa fa-<%- icon %>"></span> <%- title %></li>'),
669 sectionTemplate: _.template('<div class="section" data-section="<%- id %>"><table class="fields-table"><tbody></tbody></table></div>'),
670 controllerTemplate: _.template('<tr><th scope="row"><%- title %></th><td></td></tr>'),
671
672 /**
673 * The controllers for each of the properties
674 */
675 propertyControllers: [],
676
677 /**
678 * The editor view
679 */
680 editor: null,
681
682 /**
683 * The current, raw CSS
684 */
685 css: '',
686
687 /**
688 * Parsed CSS
689 */
690 parsed: {},
691
692 /**
693 * The current active selector
694 */
695 activeSelector: '',
696
697 /**
698 * Was the editor expanded before we went into the property editor
699 */
700 editorExpandedBefore: false,
701
702 events: {
703 'click .close': 'hide'
704 },
705
706 /**
707 * Initialize the properties editor with a new model
708 */
709 initialize: function ( attr ) {
710 this.parser = window.css;
711 this.editor = attr.editor;
712 },
713
714 /**
715 * Render the property editor
716 */
717 render: function () {
718 var thisView = this;
719
720 // Clean up for potential re-renders
721 this.$('.section-tabs').empty();
722 this.$('.sections').empty();
723 this.$('.toolbar select').off();
724 thisView.propertyControllers = [];
725
726 var controllers = socssOptions.propertyControllers;
727
728 for (var id in controllers) {
729 // Create the tabs
730 var $t = $(this.tabTemplate({
731 id: id,
732 icon: controllers[id].icon,
733 title: controllers[id].title
734 })).appendTo(this.$('.section-tabs'));
735
736 // Create the section wrapper
737 var $s = $(this.sectionTemplate({
738 id: id
739 })).appendTo(this.$('.sections'));
740
741 // Now lets add the controllers
742 if (!_.isEmpty(controllers[id].controllers)) {
743
744 for (var i = 0; i < controllers[id].controllers.length; i++) {
745
746 var $c = $(thisView.controllerTemplate({
747 title: controllers[id].controllers[i].title
748 })).appendTo($s.find('tbody'));
749
750 var controllerAtts = controllers[id].controllers[i];
751 var controller;
752
753 if (typeof socss.view.properties.controllers[controllerAtts.type] === 'undefined') {
754 // Setup a default controller
755 controller = new socss.view.propertyController({
756 el: $c.find('td'),
757 propertiesView: thisView,
758 args: ( typeof controllerAtts.args === 'undefined' ? {} : controllerAtts.args )
759 });
760 }
761 else {
762 // Setup a specific controller
763 controller = new socss.view.properties.controllers[controllerAtts.type]({
764 el: $c.find('td'),
765 propertiesView: thisView,
766 args: ( typeof controllerAtts.args === 'undefined' ? {} : controllerAtts.args )
767 });
768 }
769
770 thisView.propertyControllers.push(controller);
771
772 // Setup and render the controller
773 controller.render();
774 controller.initChangeEvents();
775 }
776 }
777 }
778
779 // Setup the tab switching for the property sections
780 this.$('.section-tabs li').click(function () {
781 var $$ = $(this);
782 var show = thisView.$('.sections .section[data-section="' + $$.data('section') + '"]');
783
784 thisView.$('.sections .section').not(show).hide().removeClass('active');
785 show.show().addClass('active');
786
787 thisView.$('.section-tabs li').not($$).removeClass('active');
788 $$.addClass('active');
789 }).eq(0).click();
790
791 this.$('.toolbar select').change(function () {
792 thisView.setActivateSelector($(this).find(':selected').data('selector'));
793 });
794 },
795
796 /**
797 * Sets the rule value for the active selector
798 * @param rule
799 * @param value
800 */
801 setRuleValue: function (rule, value) {
802 if (
803 typeof this.activeSelector === 'undefined' ||
804 typeof this.activeSelector.declarations === 'undefined'
805 ) {
806 return;
807 }
808
809 var declarations = this.activeSelector.declarations;
810 var newRule = true;
811 var valueChanged = false;
812 for (var i = 0; i < declarations.length; i++) {
813 if (declarations[i].property === rule) {
814 newRule = false;
815 var declaration = declarations[i];
816 if ( declaration.value !== value ) {
817 declaration.value = value;
818 valueChanged = true;
819 }
820
821 // Remove empty declarations
822 if ( _.isEmpty( declaration.value ) ) {
823 declarations.splice( declarations.indexOf( declaration ) );
824 }
825 break;
826 }
827 }
828
829 if ( newRule && !_.isEmpty( value ) ) {
830 declarations.push({
831 property: rule,
832 value: value,
833 type: 'declaration',
834 });
835 valueChanged = true;
836 }
837
838 if ( valueChanged ) {
839 this.updateMainEditor(false);
840 }
841 },
842
843 /**
844 * Adds the @import rule value if it doesn't already exist.
845 *
846 * @param newRule
847 *
848 */
849 addImport: function (newRule) {
850
851 // get @import rules
852 // check if any have the same value
853 // if not, then add the new @ rule
854
855 var importRules = _.filter( this.parsed.stylesheet.rules, function ( rule) {
856 return rule.type === 'import';
857 } );
858 var exists = _.any( importRules, function ( rule ) {
859 return rule.import === newRule.import;
860 } );
861
862 if ( !exists ) {
863 // Add it to the top!
864 // @import statements must precede other rule types.
865 this.parsed.stylesheet.rules.unshift( newRule );
866 this.updateMainEditor( false );
867 }
868
869 },
870
871 /**
872 * Find @import which completely or partially contains the specified value.
873 *
874 * @param value
875 */
876 findImport: function(value) {
877 return _.find( this.parsed.stylesheet.rules, function ( rule ) {
878 return rule.type === 'import' && rule.import.indexOf(value) > -1;
879 } );
880 },
881
882 /**
883 * Find @import which completely or partially contains the identifier value and update it's import property.
884 *
885 * @param identifier
886 * @param value
887 */
888 updateImport: function(identifier, value) {
889 var importRule = this.findImport(identifier);
890 if ( importRule.import !== value.import ) {
891 importRule.import = value.import;
892 this.updateMainEditor(false);
893 }
894 },
895
896 /**
897 * Find @import which completely or partially contains the identifier value and remove it.
898 *
899 * @param identifier
900 */
901 removeImport: function(identifier) {
902 var importIndex = _.findIndex( this.parsed.stylesheet.rules, function ( rule ) {
903 return rule.type === 'import' && rule.import.indexOf(identifier) > -1;
904 } );
905 if ( importIndex > -1 ) {
906 this.parsed.stylesheet.rules.splice(importIndex, 1);
907 }
908 },
909
910 /**
911 * Get the rule value for the active selector
912 * @param rule
913 */
914 getRuleValue: function (rule) {
915 if (typeof this.activeSelector === 'undefined' || typeof this.activeSelector.declarations === 'undefined') {
916 return '';
917 }
918
919 var declarations = this.activeSelector.declarations;
920 for (var i = 0; i < declarations.length; i++) {
921 if (declarations[i].property === rule) {
922 return declarations[i].value;
923 }
924 }
925 return '';
926 },
927
928 /**
929 * Update the main editor with the value of the parsed CSS
930 */
931 updateMainEditor: function ( compress ) {
932 //TODO: add back compress option to remove/merge duplicated CSS selectors.
933 this.editor.codeMirror.setValue( this.parser.stringify( this.parsed ) );
934 },
935
936 /**
937 * Show the properties editor
938 */
939 show: function () {
940 this.editorExpandedBefore = this.editor.isExpanded();
941 this.editor.setExpand(true);
942
943 this.$el.show().animate({'left': 0}, 'fast');
944 },
945
946 /**
947 * Hide the properties editor
948 */
949 hide: function () {
950 this.editor.setExpand(this.editorExpandedBefore);
951 this.$el.animate( {'left': -338}, 'fast', function(){
952 $(this).hide();
953 } );
954
955 // Update the main editor with compressed CSS when we close the properties editor
956 this.updateMainEditor( true );
957 },
958
959 /**
960 * @returns boolean
961 */
962 isVisible: function () {
963 return this.$el.is(':visible');
964 },
965
966 /**
967 * Loads a single CSS selector and associated properties into the model
968 * @param css
969 */
970 loadCSS: function (css, activeSelector) {
971 this.css = css;
972
973 // Load the CSS
974 this.parsed = this.parser.parse(css, {
975 silent:true
976 } );
977 var rules = this.parsed.stylesheet.rules;
978
979 // Add the dropdown menu items
980 var dropdown = this.$('.toolbar select').empty();
981 for (var i = 0; i < rules.length; i++) {
982 var rule = rules[i];
983
984 // Exclude @import statements
985 if ( ! _.contains( [ 'rule', 'media' ], rule.type) ) {
986 continue;
987 }
988
989 if( rule.type === 'media' ) {
990
991 for (var j = 0; j < rule.rules.length; j++) {
992 var mediaRule = '@media ' + rule.media;
993 var subRule = rule.rules[j];
994 if(subRule.type != 'rule') {
995 continue;
996 }
997 dropdown.append(
998 $('<option>')
999 .html( mediaRule + ': ' + subRule.selectors.join(',') )
1000 .attr( 'val', mediaRule + ': ' + subRule.selectors.join(',') )
1001 .data( 'selector', subRule )
1002 );
1003 }
1004
1005 }
1006 else {
1007 dropdown.append(
1008 $('<option>')
1009 .html( rule.selectors.join(',') )
1010 .attr( 'val', rule.selectors.join(',') )
1011 .data( 'selector', rule )
1012 );
1013 }
1014 }
1015
1016 if (typeof activeSelector === 'undefined') {
1017 activeSelector = dropdown.find('option').eq(0).attr('val');
1018 }
1019 if(!_.isEmpty(activeSelector)) {
1020 dropdown.val(activeSelector).change();
1021 }
1022 },
1023
1024 /**
1025 * Set the selector that we're currently dealing with
1026 * @param selector
1027 */
1028 setActivateSelector: function (selector) {
1029 this.activeSelector = selector;
1030 for (var i = 0; i < this.propertyControllers.length; i++) {
1031 this.propertyControllers[i].refreshFromRule();
1032 }
1033 },
1034
1035 /**
1036 * Add or select a selector.
1037 *
1038 * @param selector
1039 */
1040 addSelector: function(selector) {
1041 // Check if this selector already exists
1042 var dropdown = this.$('.toolbar select');
1043 dropdown.val( selector );
1044
1045 if( dropdown.val() === selector ) {
1046 // Trigger a change event to load the existing selector
1047 dropdown.change();
1048 }
1049 else {
1050 // The selector doesn't exist, so add it to the CSS, then reload
1051 this.editor.addEmptySelector(selector);
1052 this.loadCSS( this.editor.codeMirror.getValue().trim(), selector );
1053 }
1054
1055 dropdown.addClass('highlighted');
1056 setTimeout(function(){
1057 dropdown.removeClass('highlighted');
1058 }, 2000);
1059 }
1060
1061 });
1062
1063 // The basic property controller
1064 socss.view.propertyController = Backbone.View.extend({
1065
1066 template: _.template('<input type="text" value="" />'),
1067 activeRule: null,
1068 args: null,
1069 propertiesView: null,
1070
1071 initialize: function (args) {
1072
1073 this.args = args.args;
1074 this.propertiesView = args.propertiesView;
1075
1076 // By default, update the active rule whenever things change
1077 this.on('set_value', this.updateRule, this);
1078 this.on('change', this.updateRule, this);
1079 },
1080
1081 /**
1082 * Render the property field controller
1083 */
1084 render: function () {
1085 this.$el.append( $(this.template( {} )) );
1086 this.field = this.$('input');
1087 },
1088
1089 /**
1090 * Initialize the events that constitute a change
1091 */
1092 initChangeEvents: function () {
1093 var thisView = this;
1094 this.field.on( 'change keyup', function () {
1095 thisView.trigger('change', $(this).val());
1096 } );
1097 },
1098
1099
1100 /**
1101 * Update the value of an active rule
1102 */
1103 updateRule: function () {
1104 this.propertiesView.setRuleValue(
1105 this.args.property,
1106 this.getValue()
1107 );
1108 },
1109
1110 /**
1111 * This is called when the selector changes
1112 */
1113 refreshFromRule: function () {
1114 var value = this.propertiesView.getRuleValue(this.args.property);
1115 this.setValue(value, {silent: true});
1116 },
1117
1118 /**
1119 * Get the current value
1120 * @return string
1121 */
1122 getValue: function () {
1123 return this.field.val();
1124 },
1125
1126 /**
1127 * Set the current value
1128 * @param socss.view.properties val
1129 */
1130 setValue: function (val, options) {
1131 options = _.extend({silent: false}, options);
1132
1133 this.field.val(val);
1134
1135 if (!options.silent) {
1136 this.trigger('set_value', val);
1137 }
1138 },
1139
1140 /**
1141 * Reset the current value
1142 */
1143 reset: function (options) {
1144 options = _.extend({silent: false}, options);
1145
1146 this.setValue('', options);
1147 }
1148
1149 });
1150
1151 // All the value controllers
1152 socss.view.properties.controllers = {};
1153
1154 // The color controller
1155 socss.view.properties.controllers.color = socss.view.propertyController.extend({
1156
1157 template: _.template('<input type="text" value="" />'),
1158
1159 render: function () {
1160 var thisView = this;
1161
1162 this.$el.append($(this.template({})));
1163
1164 // Set this up as a color picker
1165 this.field = this.$el.find('input');
1166 this.field.minicolors({});
1167
1168 },
1169
1170 initChangeEvents: function () {
1171 var thisView = this;
1172 this.field.on('change keyup', function () {
1173 thisView.trigger('change', thisView.field.minicolors('value'));
1174 });
1175 },
1176
1177 getValue: function () {
1178 return this.field.minicolors('value').trim();
1179 },
1180
1181 setValue: function (val, options) {
1182 options = _.extend({silent: false}, options);
1183
1184 this.field.minicolors('value', val);
1185
1186 if (!options.silent) {
1187 this.trigger('set_value', val);
1188 }
1189 }
1190
1191 });
1192
1193 // The dropdown select box controller
1194 socss.view.properties.controllers.select = socss.view.propertyController.extend( {
1195 template: _.template('<select></select>'),
1196
1197 render: function(){
1198 var thisView = this;
1199
1200 this.$el.append($(this.template({})));
1201 this.field = this.$el.find('select');
1202
1203 // Add the unchanged option
1204 this.field.append( $('<option value=""></option>').html('') );
1205
1206 // Add all the options to the dropdown
1207 for( var k in this.args.options ) {
1208 this.field.append( $('<option></option>').attr('value', k).html( this.args.options[k] ) );
1209 }
1210
1211 if( typeof this.args.option_icons !== 'undefined' ) {
1212 this.setupVisualSelect();
1213 }
1214 },
1215
1216 setupVisualSelect: function(){
1217 var thisView = this;
1218 this.field.hide();
1219
1220 var $tc = $('<div class="select-tabs"></div>').appendTo( this.$el );
1221
1222 // Add the none value
1223 $('<div class="select-tab" data-value=""><span class="fa fa-circle-o"></span></div>').appendTo($tc);
1224
1225 // Now add one for each of the option icons
1226 for ( var k in this.args.option_icons ) {
1227 $('<div class="select-tab"></div>')
1228 .appendTo($tc)
1229 .append(
1230 $('<span class="fa"></span>')
1231 .addClass('fa-' + this.args.option_icons[k])
1232 )
1233 .attr('data-value', k)
1234 ;
1235 }
1236
1237 $tc.find('.select-tab')
1238 .css('width', 100/( $tc.find('>div').length ) + "%" )
1239 .click( function(){
1240 var $t = $(this);
1241 $tc.find('.select-tab').removeClass('active');
1242 $t.addClass('active');
1243 thisView.field.val( $t.data('value')).change();
1244 } );
1245 },
1246
1247 /**
1248 * Set the current value
1249 * @param socss.view.properties val
1250 */
1251 setValue: function (val, options) {
1252 options = _.extend({silent: false}, options);
1253
1254 this.field.val(val);
1255
1256 this.$('.select-tabs .select-tab').removeClass('active').filter('[data-value="' + val + '"]').addClass('active');
1257
1258 if (!options.silent) {
1259 this.trigger('set_value', val);
1260 }
1261 }
1262
1263 } );
1264
1265 // A field that lets a user upload an image
1266 socss.view.properties.controllers.image = socss.view.propertyController.extend( {
1267 template: _.template('<input type="text" value="" /> <span class="select socss-button"><span class="fa fa-upload"></span></span>'),
1268
1269 render: function(){
1270 var thisView = this;
1271
1272 this.media = wp.media({
1273 // Set the title of the modal.
1274 title: socssOptions.loc.select_image,
1275
1276 // Tell the modal to show only images.
1277 library: {
1278 type: 'image'
1279 },
1280
1281 // Customize the submit button.
1282 button: {
1283 // Set the text of the button.
1284 text: socssOptions.loc.select,
1285 // Tell the button not to close the modal, since we're
1286 // going to refresh the page when the image is selected.
1287 close: false
1288 }
1289 });
1290
1291 this.$el.append( $(this.template({
1292 select: socssOptions.loc.select
1293 })) );
1294
1295 this.field = this.$el.find('input');
1296
1297 this.$('.select').click(function(){
1298 thisView.media.open();
1299 });
1300
1301 this.media.on('select', function(){
1302 // Grab the selected attachment.
1303 var attachment = this.state().get('selection').first().attributes;
1304 var val = thisView.args.value.replace('{{url}}', attachment.url);
1305
1306 // Change the field value and trigger a change event
1307 thisView.field.val( val ).change();
1308
1309 // Close the image selector
1310 thisView.media.close();
1311
1312 }, this.media);
1313 }
1314
1315 } );
1316
1317 // A simple measurement field
1318 socss.view.properties.controllers.measurement = socss.view.propertyController.extend( {
1319
1320 wrapperClass: 'socss-field-measurement',
1321
1322 render: function(){
1323 this.$el.append($(this.template({})));
1324 this.field = this.$('input');
1325 this.setupMeasurementField( this.field, {} );
1326 },
1327
1328 setValue: function (val, options) {
1329 options = _.extend({silent: false}, options);
1330 this.field.val(val).trigger('measurement_refresh');
1331 if (!options.silent) {
1332 this.trigger('set_value', val);
1333 }
1334 },
1335
1336 units : [
1337 'px',
1338 '%',
1339 'em',
1340 'cm',
1341 'mm',
1342 'in',
1343 'pt',
1344 'pc',
1345 'ex',
1346 'ch',
1347 'rem',
1348 'vw',
1349 'vh',
1350 'vmin',
1351 'vmax'
1352 ],
1353
1354 parseUnits: function( value ){
1355 var escapeRegExp = function(str) {
1356 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
1357 };
1358
1359 var regexUnits = this.units.map(escapeRegExp);
1360 var regex = new RegExp('([0-9\\.\\-]+)(' + regexUnits.join('|') + ')?', 'i');
1361 var result = regex.exec( value );
1362
1363 if( result === null ) {
1364 return {
1365 value: '',
1366 unit: ''
1367 };
1368 }
1369 else {
1370 return {
1371 value: result[1],
1372 unit: result[2] === undefined ? '' : result[2]
1373 };
1374 }
1375 },
1376
1377 setupMeasurementField: function( $el, options ){
1378 var thisView = this;
1379 var $p = $el.parent();
1380
1381 options = _.extend( {
1382 defaultUnit: 'px'
1383 }, options );
1384
1385 $el.hide();
1386 $p.addClass( this.wrapperClass ).data('unit', options.defaultUnit);
1387
1388 // Create the fake input field
1389 var $fi = $('<input type="text" class="socss-field-input"/>').appendTo($p);
1390 var $da = $('<span class="dashicons dashicons-arrow-down"></span>').appendTo($p);
1391 var $dd = $('<ul class="dropdown"></ul>').appendTo($p);
1392 var $u = $('<span class="units"></span>').html( options.defaultUnit ).appendTo( $p );
1393
1394 for( var i = 0; i < thisView.units.length; i++ ) {
1395 var $o = $('<li></li>').html( thisView.units[i] ).data('unit', thisView.units[i]);
1396 if( thisView.units[i] === options.defaultUnit ) {
1397 $o.addClass('active');
1398 }
1399 $dd.append( $o );
1400 }
1401
1402 var updateValue = function(){
1403 var value = thisView.parseUnits( $fi.val() );
1404
1405 if( value.unit !== '' && value.unit !== $p.data( 'unit' ) ) {
1406 $fi.val( value.value );
1407 setUnit( value.unit );
1408 }
1409
1410 if( value.value === '' ) {
1411 $el.val( '' );
1412 }
1413 else {
1414 $el.val( value.value + $p.data( 'unit' ) );
1415 }
1416 };
1417
1418 var setUnit = function( unit ){
1419 $u.html( unit );
1420 $p.data( 'unit', unit );
1421 $fi.trigger('keydown');
1422 };
1423
1424 $da.click( function(){
1425 $dd.toggle();
1426 } );
1427
1428 $dd.find('li').click( function(){
1429 $dd.toggle();
1430 setUnit( $(this).data('unit') );
1431 updateValue();
1432 $el.trigger('change');
1433 } );
1434
1435 $fi.on( 'keyup keydown', function(e){
1436 var $$ = $(this);
1437
1438 var char = '';
1439 if( e.type === 'keydown' ) {
1440 if(e.keyCode >= 48 && e.keyCode <= 57 ) {
1441 char = String.fromCharCode(e.keyCode);
1442 }
1443 else if( e.keyCode === 189 ) {
1444 char = '-';
1445 }
1446 else if( e.keyCode === 190 ) {
1447 char = '.';
1448 }
1449 }
1450
1451 var $pl = $('<span class="socss-hidden-placeholder"></span>')
1452 .css( {
1453 'font-size' : '14px'
1454 } )
1455 .html( $fi.val() + char )
1456 .appendTo( 'body' );
1457 var width = $pl.width();
1458 width = Math.min(width, 63);
1459 $pl.remove();
1460
1461 $u.css('left', width + 12);
1462 } );
1463
1464 $fi.on('keyup', function(e){
1465 updateValue();
1466 $el.trigger('change');
1467 } );
1468
1469 $el.on('measurement_refresh', function(){
1470 var value = thisView.parseUnits( $el.val() );
1471 $fi.val( value.value );
1472
1473 var unit = value.unit === '' ? options.defaultUnit : value.unit;
1474 $p.data( 'unit', unit );
1475 $u.html( unit );
1476
1477 var $pl = $('<span class="socss-hidden-placeholder"></span>')
1478 .css({
1479 'font-size' : '14px'
1480 })
1481 .html( value.value )
1482 .appendTo( 'body' );
1483 var width = $pl.width();
1484 width = Math.min(width, 63);
1485 $pl.remove();
1486
1487 $u.css('left', width + 12);
1488 } );
1489
1490 // Now add the increment/decrement buttons
1491 var $diw = $('<div class="socss-diw"></div>').appendTo($p);
1492 var $dec = $('<div class="dec-button socss-button"><span class="fa fa-minus"></span></div>').appendTo($diw);
1493 var $inc = $('<div class="inc-button socss-button"><span class="fa fa-plus"></span></div>').appendTo($diw);
1494
1495 var stepValue = function( direction ) {
1496 var value = Number.parseInt( thisView.parseUnits( $el.val() ).value );
1497
1498 if( Number.isNaN( value ) ) {
1499 value = 0;
1500 }
1501
1502 var newVal = value + direction;
1503
1504 $fi.val( newVal );
1505 updateValue();
1506 $el.trigger('change').trigger('measurement_refresh');
1507 };
1508
1509 var setupStepButton = function ( $button ) {
1510 var direction = $button.is( '.dec-button' ) ? -1 : 1;
1511 var intervalId;
1512 var timeoutId;
1513 $button.mousedown( function(){
1514 stepValue( direction );
1515 timeoutId = setTimeout( function () {
1516 intervalId = setInterval( function () {
1517 stepValue( direction );
1518 }, 50 );
1519 }, 500 );
1520 } ).on( 'mouseup mouseout', function(){
1521 if ( timeoutId ) {
1522 clearTimeout( timeoutId );
1523 timeoutId = null;
1524 }
1525 if ( intervalId ) {
1526 clearInterval( intervalId );
1527 intervalId = null;
1528 }
1529 } );
1530 };
1531
1532 setupStepButton( $dec );
1533 setupStepButton( $inc );
1534
1535 }
1536
1537 } );
1538
1539 // A simple measurement field
1540 socss.view.properties.controllers.number = socss.view.propertyController.extend( {
1541
1542 render: function(){
1543 this.$el.append($(this.template({})));
1544 this.field = this.$('input');
1545
1546 // Setup the measurement field
1547 this.setupNumberField(this.field, this.args);
1548 },
1549
1550 /**
1551 * Setup the number field
1552 * @param el
1553 * @param options
1554 */
1555 setupNumberField: function($el, options){
1556 options = _.extend({
1557 change: null,
1558 default: 0,
1559 increment: 1,
1560 decrement: -1,
1561 max: null,
1562 min: null
1563 }, options);
1564
1565 var $p = $el.parent();
1566 $p.addClass('socss-field-number');
1567
1568 // Now add the increment/decrement buttons
1569 var $diw = $('<div class="socss-diw"></div>').appendTo($p);
1570 var $dec = $('<div class="dec-button socss-button">-</div>').appendTo($diw);
1571 var $inc = $('<div class="inc-button socss-button">+</div>').appendTo($diw);
1572
1573 // Increment is clicked
1574 $diw.find('> div').click( function(e){
1575 e.preventDefault();
1576
1577 var val = options.default;
1578 if( $el.val() !== '' ) {
1579 val = Number($el.val());
1580 }
1581 val = val + ( $(this).is( $dec ) ? options.decrement : options.increment );
1582
1583 val = Math.round(val*100)/100;
1584
1585 if( options.max !== null ) {
1586 val = Math.min( options.max, val);
1587 }
1588
1589 if( options.min !== null ) {
1590 val = Math.max( options.min, val);
1591 }
1592
1593 $el.val( val );
1594 $el.trigger('change');
1595 } );
1596
1597 return this;
1598 }
1599
1600 } );
1601
1602
1603 socss.view.properties.controllers.sides = socss.view.propertyController.extend( {
1604
1605 template: _.template( $('#template-sides-field').html().trim() ),
1606
1607 controllers: [],
1608
1609 render: function(){
1610 var thisView = this;
1611
1612 this.$el.append( $(this.template({})) );
1613 this.field = this.$el.find('input');
1614
1615 if( !thisView.args.hasAll ) {
1616 this.$('.select-tab').eq(0).remove();
1617 this.$('.select-tab').css('width', '25%');
1618 }
1619
1620 this.$('.select-tab').each( function(){
1621 var dir = $(this).data('direction');
1622
1623 var container = $('<li class="side">')
1624 .appendTo( thisView.$('.sides') )
1625 .hide();
1626
1627 for( var i = 0; i < thisView.args.controllers.length; i++ ) {
1628
1629 var controllerArgs = thisView.args.controllers[i];
1630
1631 if( typeof socss.view.properties.controllers[ controllerArgs.type ] ) {
1632
1633 // Create the measurement view
1634 var property = '';
1635 if( dir === 'all' ) {
1636 property = controllerArgs.args.propertyAll;
1637 }
1638 else {
1639 property = controllerArgs.args.property.replace('{dir}', dir);
1640 }
1641
1642 var theseControllerArgs = _.extend({}, controllerArgs.args, {property: property});
1643
1644 var controller = new socss.view.properties.controllers[ controllerArgs.type ]( {
1645 el: $('<div>').appendTo( container ),
1646 propertiesView: thisView.propertiesView,
1647 args: theseControllerArgs
1648 } );
1649
1650 // Setup and render the measurement controller and register it with the properties view
1651 controller.render();
1652 controller.initChangeEvents();
1653 thisView.propertiesView.propertyControllers.push(controller);
1654
1655 }
1656
1657 }
1658
1659 $(this).on( 'click', function(){
1660 thisView.$('.select-tab').removeClass('active');
1661 $(this).addClass('active');
1662
1663 thisView.$('.sides .side').hide();
1664 container.show();
1665 } );
1666
1667 } );
1668
1669 // Select the first tab by default
1670 this.$('.select-tab').eq(0).click();
1671 }
1672
1673 } );
1674
1675 })(jQuery, _, socssOptions);
1676
1677 // Setup the main editor
1678 jQuery(function ($) {
1679 var socss = window.socss;
1680
1681 // Setup the editor
1682 var editor = new socss.view.editor({
1683 el: $('#so-custom-css-form').get(0)
1684 });
1685 editor.render();
1686 editor.setSnippets(socssOptions.snippets);
1687
1688 window.socss.mainEditor = editor;
1689
1690 // This is for hiding the getting started video
1691 $('#so-custom-css-getting-started a.hide').click( function(e){
1692 e.preventDefault();
1693 $('#so-custom-css-getting-started').slideUp();
1694 $.get( $(this).attr('href') );
1695 } );
1696 });
1697