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