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