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