diff --git a/Gemfile b/Gemfile index 6148ff8..6e48664 100644 --- a/Gemfile +++ b/Gemfile @@ -44,6 +44,9 @@ gem 'font-awesome-sass' gem 'haml-rails', '~> 2.0', '>= 2.0.1' gem 'hirb' gem 'jquery-rails' +gem 'rubocop-rails', '~> 2.2', '>= 2.2.1' +gem 'toastr-rails', '~> 1.0', '>= 1.0.3' +gem "mustache", "~> 1.0" group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console diff --git a/Gemfile.lock b/Gemfile.lock index 09fec99..53f325d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,6 +61,7 @@ GEM archive-zip (0.12.0) io-like (~> 0.3.0) arel (9.0.0) + ast (2.4.0) autoprefixer-rails (9.6.1) execjs babel-source (5.8.35) @@ -142,6 +143,7 @@ GEM railties (>= 4.2, < 5.3) responders io-like (0.3.0) + jaro_winkler (1.5.3) jbuilder (2.9.1) activesupport (>= 4.2.0) jquery-rails (4.3.5) @@ -177,10 +179,14 @@ GEM mini_portile2 (2.4.0) minitest (5.11.3) msgpack (1.3.0) + mustache (1.1.0) nio4r (2.4.0) nokogiri (1.10.3) mini_portile2 (~> 2.4.0) orm_adapter (0.5.0) + parallel (1.17.0) + parser (2.6.3.0) + ast (~> 2.4.0) popper_js (1.14.5) public_suffix (3.1.1) puma (3.12.1) @@ -211,6 +217,7 @@ GEM method_source rake (>= 0.8.7) thor (>= 0.19.0, < 2.0) + rainbow (3.0.0) rake (12.3.2) ransack (2.1.1) actionpack (>= 5.0) @@ -224,6 +231,17 @@ GEM responders (3.0.0) actionpack (>= 5.0) railties (>= 5.0) + rubocop (0.73.0) + jaro_winkler (~> 1.5.1) + parallel (~> 1.10) + parser (>= 2.6) + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 1.7) + rubocop-rails (2.2.1) + rack (>= 1.1) + rubocop (>= 0.72.0) + ruby-progressbar (1.10.1) ruby_dep (1.5.0) ruby_parser (3.13.1) sexp_processor (~> 4.9) @@ -272,6 +290,8 @@ GEM thor (0.20.3) thread_safe (0.3.6) tilt (2.0.9) + toastr-rails (1.0.3) + railties (>= 3.1.0) turbolinks (5.2.0) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) @@ -279,6 +299,7 @@ GEM thread_safe (~> 0.1) uglifier (4.1.20) execjs (>= 0.3.0, < 3) + unicode-display_width (1.6.0) warden (1.2.8) rack (>= 2.0.6) web-console (3.7.0) @@ -310,13 +331,16 @@ DEPENDENCIES jbuilder (~> 2.5) jquery-rails listen (>= 3.0.5, < 3.2) + mustache (~> 1.0) puma (~> 3.11) rails (~> 5.2.3) + rubocop-rails (~> 2.2, >= 2.2.1) sass-rails (~> 5.0) selenium-webdriver spring spring-watcher-listen (~> 2.0.0) sqlite3 + toastr-rails (~> 1.0, >= 1.0.3) turbolinks (~> 5) tzinfo-data uglifier (>= 1.3.0) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index cd1a3e7..5d979eb 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -13,8 +13,9 @@ //= require rails-ujs //= require activestorage //= require turbolinks -//= require_tree . //= require jquery -function ajaxcall(tasklist_id,id){ - $.ajax({url: location.origin+"/updatecheck/"+tasklist_id+"/"+id,success: function(result){console.log(result);}}) -} +//= require popper +//= require bootstrap +//= require custom.js +//= require toastr +//= require mustache/mustache.js diff --git a/app/assets/javascripts/custom.js b/app/assets/javascripts/custom.js new file mode 100644 index 0000000..d3d373a --- /dev/null +++ b/app/assets/javascripts/custom.js @@ -0,0 +1,294 @@ +$(document).ready(function() { + const selectedTasklist = localStorage.getItem("selectedTasklistCount"); + $("#newTasklistButton").on("click", addTasklist); + $(".tasklist_list").on("click", renderTask); + $("#tasklists").on("click", ".tasklist_list" ,renderTask); + $("#tasklists").on("click", ".tasklistUpdateButton", renderUpdateTasklist); + $(".tasklistname").on("click", ".tasklistUpdateButton", renderUpdateTasklist); + $("#tasks").on("click", ".taskname", renderUpdate); + $("#tasks").on("click", ".taskUpdate", renderUpdate); + $(":checkbox").on("click", updateTaskStatus); + $("#newtasklist").on("click", "#createTasklistButton", createTasklist); + $("#newtasklist").on("click", "#cancelCreateTasklist", restoreCreateTasklist); + $("#tasks").on("click", ".createNewTaskButton", createTask); + $("#tasks").on("click", ".cancelNewTaskButton", restoreCreateTask); + $("#tasks").on("click", "#addTaskButton", addTask); + $("#tasks").on("click", ".taskDelete", deleteTask); + $("#tasks").on("click", ".updateTaskSaveButton", updateTask); + $("#tasks").on("click", ".updateTaskCancelButton", restoreTask); + $("#tasklists").on("click", ".saveUpdateTasklistButton" ,updateTasklist); + $("#tasklists").on("click", ".cancelUpdateTasklistButton" ,restoreUpdateTasklist); + if (selectedTasklist === null) { + } else { + $(document) + .find("#tasklist" + selectedTasklist) + .click(); + setSelectedTasklist(selectedTasklist); + } +}); + +function inputForTask() { + return ( + "
  • " + + "
    " + + "
    " + + "
    " + + "
    " + + "
    Cancel
  • " + ); +} + +function restoreTask() { + const tasklistId = $(this).data("tasklistid"); + const taskId = $(this).data("taskid"); + const title = $("#card" + taskId).data("title"); + $("#card" + taskId).empty(); + $("#card" + taskId).append(Mustache.render(taskView(),{tasklistId: tasklistId, taskId: taskId,title: title})); + if ($("#card" + taskId).data("status") == true) { + $("#checkbox" + taskId).attr("checked", "checked"); + } +} + +function taskView() { + return ( + "
    " + + ""+ + "
    " + + "
    " + + "
    {{title}}
    " + + "
    " + + "
    " + + "" + + "
    " + + "
    " + + "" + + "
    " + ); +} + +function inputForTasklist() { + return ( + "
    " + + "
    " + + "
    " + + "
    " + + "
    " + + "
    " + + "
    " + + "" + + "Create" + + "" + + "
    " + + "
    " + + "Cancel
    " + ); +} + +function addTasklist() { + $("#newtasklist").html(Mustache.render(inputForTasklist())); +} + +function addTask() { + if ($("#newTask").length == 0) { + const input = Mustache.render(inputForTask(),{tasklistId: $(this).data("tasklistid")}); + $("#newtasks").show(); + $("#newtasks").append(input); + } +} + +function renderTask() { + const tasklistId = $(this).data("tasklistid"); + const index = $(this).data("index"); + $.ajax({ + url: "/tasklists/" + tasklistId + "/get_tasks", + success: function(result) { + $("#tasks").html(result); + } + }); + setSelectedTasklist(tasklistId); +} + +function updateTaskView() { + return ( + "
    " + + "
    " + + "
    " + + "
    " + + "
    " + + "Cancel
    " + ); +} +function renderUpdate() { + const tasklistId = $(this).data("tasklistid"); + const taskId = $(this).data("taskid"); + if ( + $("#task" + taskId).prop("tagName") == "DIV" && + $("#task" + taskId).find($("#inputTask" + taskId)).length == 0 + ) { + const text = $("#task" + taskId).text(); + $("#card" + taskId).attr("data-title", text); + $("#card" + taskId).attr( + "data-status", + $("#checkbox" + taskId).prop("checked") + ); + $("#card" + taskId).html(Mustache.render(updateTaskView(),{tasklistId: tasklistId, taskId: taskId,text: text})); + } +} + +function createTasklist() { + Rails.ajax({ + type: "post", + url: "/tasklists", + data: "name=" + $("#tasklistnew").val(), + success: function(data) { + }, + error: function(data) { + } + }); +} + +function updateTasklist() { + const tasklistId = $(this).data("tasklistid"); + Rails.ajax({ + type: "put", + url: "/tasklists/" + tasklistId + "/", + data: "name=" + $("#updateTasklistInput" + tasklistId).val(), + success: function(data) { + }, + error: function(data) { + } + }); +} +function createTask() { + const tasklistId = $(this).data("tasklistid"); + Rails.ajax({ + type: "post", + url: "tasklists/" + tasklistId + "/tasks/", + data: "name=" + $("#newTask").val(), + success: function(data) { + }, + error: function(data) { + + } + }); +} + +function updateTask() { + const tasklistId = $(this).data("tasklistid"); + const taskId = $(this).data("taskid"); + Rails.ajax({ + type: "put", + url: "tasklists/" + tasklistId + "/tasks/" + taskId, + data: "name=" + $("#inputTask" + taskId).val(), + success: function(data) { + }, + error: function(data) { + } + }); +} + +function deleteTask() { + const tasklistId = $(this).data("tasklistid"); + const taskId = $(this).data("taskid"); + Rails.ajax({ + type: "delete", + url: "tasklists/" + tasklistId + "/tasks/" + taskId, + success: function(data) { + $("#tasklist" + tasklistId).click(); + } + }); +} + +function setSelectedTasklist(selectedTasklist) { + var currentTasklist = localStorage.getItem("selectedTasklistCount"); + $("#tasklist_list" + currentTasklist) + .parent() + .removeClass("active"); + localStorage.setItem("selectedTasklistCount", selectedTasklist); + currentTasklist = localStorage.getItem("selectedTasklistCount"); + $("#tasklist_list" + currentTasklist) + .parent() + .addClass("active"); +} + +function inputForUpdateTasklist() { + return ( + "
    " + + "
    " + + "
    " + + "
    " + + " Save" + + "
    " + + "
    " + + "Cancel" + + "
    " + ); +} + +function renderTasklistEditView() { + return ( + "
    " + + "
    {{title}}"+ + "
    " + + "
    " + + "
    " + + "
    " + + "
    " + + "
    " + + "
    " + + "
    " + ); +} + +function restoreUpdateTasklist() { + const tasklistId = $(this).data("tasklistid"); + const index = $(this).data("index"); + const title = $("#tasklist_list" + tasklistId).data("name"); + $("#tasklist_list" + tasklistId).html( + Mustache.render(renderTasklistEditView(tasklistId, index), + {tasklistId: tasklistId, index: index, title:title})); + $("#tasklist_list" + tasklistId).removeAttr("data-index"); + $("#tasklist_list" + tasklistId).removeAttr("data-name"); +} + +function renderUpdateTasklist() { + const tasklistId = $(this).data("tasklistid"); + const index = $(this).data("index"); + const title = $("#tasklist" + tasklistId).text(); + tasklistIndex = index; + $("#tasklist_list" + tasklistId).attr("data-name",$("#tasklist" + tasklistId).text()); + $("#tasklist_list" + tasklistId).attr("data-index", index); + $("#tasklist_list" + tasklistId).html( + Mustache.render(inputForUpdateTasklist(), + {tasklistId: tasklistId, index: index, title: title})); +} + +function updateTaskStatus() { + const tasklistId = $(this).data("tasklistid"); + const taskId = $(this).data("taskid"); + Rails.ajax({ + url: "/tasklists/" + tasklistId + "/tasks/" + taskId + "/toggle_status", + type: "patch", + success: function(data) {} + }); +} + +function restoreCreateTasklist() { + $("#newtasklist").empty(); +} + +function restoreCreateTask() { + $("#newtasks").empty(); +} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index e786c9c..cc5fa5d 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -10,9 +10,9 @@ * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * - *= require_tree . - *= require_self */ @import "bootstrap"; @import "font-awesome-sprockets"; @import "font-awesome"; + @import "tasklist"; + @import 'toastr'; diff --git a/app/assets/stylesheets/tasklist.scss b/app/assets/stylesheets/tasklist.scss index 4182dca..3f8b133 100644 --- a/app/assets/stylesheets/tasklist.scss +++ b/app/assets/stylesheets/tasklist.scss @@ -1,3 +1,43 @@ // Place all the styles related to the tasklist controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ +@import "bootstrap"; + +.tasklistname:hover { + cursor: pointer; + #delete { + visibility: visible; + } + + #update { + visibility: visible; + } +} + +#update, +#taskUpdate { + color: #93922a; + visibility: hidden; +} + +#delete, +#taskDelete { + color: #ad423f; + visibility: hidden; +} + +.tab-content li:hover { + #taskDelete { + visibility: visible; + } + + #taskUpdate { + visibility: visible; + } +} + +checkbox:checked { + .card-body { + background-color: grey; + } +} diff --git a/app/controllers/tasklists_controller.rb b/app/controllers/tasklists_controller.rb index 9d2615b..c5a215c 100644 --- a/app/controllers/tasklists_controller.rb +++ b/app/controllers/tasklists_controller.rb @@ -2,33 +2,46 @@ class TasklistsController < ApplicationController before_action :authenticate_user! + before_action :set_tasklist, except: %i[index create] def index - @tasklist = current_user.tasklists - redirect_to new_tasklist_path if @tasklist.count.zero? - end - - def new - @tasklist = current_user.tasklists.build + @tasklists = current_user.tasklists end def create @tasklist = current_user.tasklists.build(tasklist_params) if @tasklist.save - redirect_to tasklists_path + render :create_tasklist else - render 'new' + render :errors end end - def show - @tasklist = current_user.tasklists.find(params[:id]) + def update + if @tasklist.update(tasklist_params) + render :update_tasklist + else + render :errors + end + end + + def get_tasks + @tasks = @tasklist.tasks + render partial: 'tasks' + end + + def destroy + @tasklist.destroy + redirect_to root_path end private def tasklist_params - print params.inspect - params[:tasklist].permit(:name) + params.permit(:name) + end + + def set_tasklist + @tasklist = current_user.tasklists.find(params[:id]) end end diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb index 855894b..13035b3 100644 --- a/app/controllers/tasks_controller.rb +++ b/app/controllers/tasks_controller.rb @@ -3,50 +3,33 @@ class TasksController < ApplicationController before_action :authenticate_user! before_action :set_tasklist + before_action :set_task, except: :create - def index - @tasks = @tasklist.tasks - end - - def new - @tasks = @tasklist.tasks.build - end + def index; end def create - @tasks = @tasklist.tasks.new(permit_params) - if @tasks.save - redirect_to tasklist_tasks_path + @task = @tasklist.tasks.build(tasklist_params) + if @task.save + render :create_task else - render 'new' + render :errors end end - def show - @tasks = @tasklist.tasks.find(params[:id]) - end - - def edit - @tasks = @tasklist.tasks.find(params[:id]) - end - def update - @tasks = @tasklist.tasks.find(params[:id]) - if @tasks.update(permit_params) - redirect_to tasklist_tasks_path(@tasks.tasklist_id) + if @task.update(tasklist_params) + render :update_task else - render 'edit' + render :errors end end def destroy - @tasks = @tasklist.tasks.find(params[:id]) - @tasks.destroy - redirect_to tasklist_tasks_path(@tasks.tasklist_id) + @task.destroy end - def checkbox_update - task = @tasklist.tasks.find(params[:id]) - task.update!(status: !task.status) + def toggle_status + @task.update!(status: !@task.status) end private @@ -55,7 +38,15 @@ def permit_params params[:task].permit(:name, :body, :status, :tasklist_id) end + def tasklist_params + params.permit(:tasklist_id, :name) + end + def set_tasklist @tasklist = current_user.tasklists.find(params[:tasklist_id]) end + + def set_task + @task = @tasklist.tasks.find(params[:id]) + end end diff --git a/app/models/task.rb b/app/models/task.rb index 030dda2..10a6174 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -2,5 +2,5 @@ class Task < ApplicationRecord belongs_to :tasklist - validates :name, :body, presence: true + validates :name, presence: true end diff --git a/app/models/tasklist.rb b/app/models/tasklist.rb index 9b7811e..0ad2203 100644 --- a/app/models/tasklist.rb +++ b/app/models/tasklist.rb @@ -1,5 +1,5 @@ class Tasklist < ApplicationRecord validates :name, presence: true - has_many :tasks + has_many :tasks, dependent: :destroy belongs_to :user end diff --git a/app/views/tasklists/_tasklist.html.haml b/app/views/tasklists/_tasklist.html.haml new file mode 100644 index 0000000..52969b7 --- /dev/null +++ b/app/views/tasklists/_tasklist.html.haml @@ -0,0 +1,10 @@ +%li.list-group-item + .row.align-middle.tasklistname{id: "tasklist_list#{@tasklist.id}"} + .col-sm-6 + .tasklist_list{"data-index": current_user.tasklists.count - 1, "data-tasklistid": @tasklist.id, id: "tasklist#{@tasklist.id}"} + = @tasklist.name + .col-sm-1 + %a.fa.fa-edit.pull-right.tasklistUpdateButton{"data-index": current_user.tasklists.count - 1, "data-tasklistid": @tasklist.id, id: "update"} + .col-sm-1 + =link_to '', tasklist_path(@tasklist.id),method: 'delete', class: "pull-right fa fa-trash-alt" ,id: "delete" + diff --git a/app/views/tasklists/_tasks.html.haml b/app/views/tasklists/_tasks.html.haml new file mode 100644 index 0000000..611926a --- /dev/null +++ b/app/views/tasklists/_tasks.html.haml @@ -0,0 +1,25 @@ +.tab-content + %ul.list-group + -@tasks.each do |task| + %li.list-group-item + .row + .col-sm-12 + .row{id: "card#{task.id}"} + .col-sm-1.align-self-center + %input{type: 'checkbox', id: "checkbox#{task.id}", "data-tasklistid": task.tasklist.id, "data-taskid": task.id, "checked": task.status} + .col-sm-8.align-self-center + %div.taskname{id:"task#{task.id}", "data-tasklistid": task.tasklist.id, "data-taskid": task.id} + =task.name + .col-sm-1.align-self-center + %span.fa.fa-edit#taskUpdate.taskUpdate{"data-tasklistid": task.tasklist.id, "data-taskid": task.id} + .col-sm-1.align-self-center + %span.far.fa-trash-alt#taskDelete.taskDelete{"data-tasklistid": task.tasklist.id, "data-taskid": task.id} +%br +%ul.list-group + .row + #newtasks{class: 'col-sm-12'} + %li.list-group-item + .row + %br + %button.btn.btn-primary#addTaskButton{"data-tasklistid": params[:id]} Add Task + diff --git a/app/views/tasklists/_update.html.haml b/app/views/tasklists/_update.html.haml new file mode 100644 index 0000000..a3a35fb --- /dev/null +++ b/app/views/tasklists/_update.html.haml @@ -0,0 +1,8 @@ +.col-sm-6 + .tasklist_list{"data-index": current_user.tasklists.count - 1, "data-tasklistid": @tasklist.id, id: "tasklist#{@tasklist.id}"} + = @tasklist.name +.col-sm-1 + %a.fa.fa-edit.pull-right.tasklistUpdateButton{"data-index": current_user.tasklists.count - 1, "data-tasklistid": @tasklist.id, id: "update"} +.col-sm-1 + =link_to '', tasklist_path(@tasklist.id),method: 'delete', class: "pull-right fa fa-trash-alt" ,id: "delete" + diff --git a/app/views/tasklists/create_tasklist.js.haml b/app/views/tasklists/create_tasklist.js.haml new file mode 100644 index 0000000..e5615e4 --- /dev/null +++ b/app/views/tasklists/create_tasklist.js.haml @@ -0,0 +1,4 @@ +$("#tasklists").append("#{j render partial: 'tasklist'}"); +$("#newtasklist").empty(); +$("#tasklistnew").val(""); +$("#tasklists").find(".tasklist_list").click(); diff --git a/app/views/tasklists/errors.js.haml b/app/views/tasklists/errors.js.haml new file mode 100644 index 0000000..1a32c08 --- /dev/null +++ b/app/views/tasklists/errors.js.haml @@ -0,0 +1,2 @@ +-@tasklist.errors.full_messages.each do |error_message| + toastr.error("#{error_message}"); diff --git a/app/views/tasklists/index.html.haml b/app/views/tasklists/index.html.haml index cb0e80d..146012f 100644 --- a/app/views/tasklists/index.html.haml +++ b/app/views/tasklists/index.html.haml @@ -1,14 +1,32 @@ -%h2 Hello #{current_user.email} +%nav.navbar.navbar-light.bg-warning + .container-fluid + .navbar-header + %a.navbar-brand{href:"#"} Todo List + %ul.nav.nav-pills + %li.nav-item + -if user_signed_in? + =link_to 'Sign out', destroy_user_session_path, method: 'delete', class:"nav-link" %br -- if @tasklist.length - %h3 You have #{@tasklist.length} tasklist - -@tasklist.each do |tasklist| - =link_to tasklist.name, tasklist_tasks_path(tasklist.id) - %br - - %br - -=link_to 'Create new tasklist', new_tasklist_path - -%br -=link_to 'Sign Out',destroy_user_session_path,method: :delete if user_signed_in? \ No newline at end of file +.container + .row + .col-sm-2 + %ul.list-group.list-group-flush + %li.list-group-item + Task lists + %a-#newTasklistButton.float-right.fa.fa-plus + %li.list-group-item + #newtasklist + #tasklists + -@tasklists.each_with_index do |tasklist,index| + %li.list-group-item + .row.align-middle.tasklistname{id: "tasklist_list#{tasklist.id}"} + .col-sm-7 + .tasklist_list{"data-tasklistid": tasklist.id, "data-index": index, id:"tasklist#{tasklist.id}"} #{tasklist.name} + .col-sm-1 + %a.fa.fa-edit.pull-right.tasklistUpdateButton{"data-tasklistid": tasklist.id, "data-index": index, id: 'update'} + .col-sm-1 + =link_to '', tasklist_path(tasklist.id),method: 'delete', class: "pull-right fa fa-trash-alt" ,id: "delete" + .col-sm-1 + .col-sm-8 + .row + #tasks{style:"width:90%"} diff --git a/app/views/tasklists/new.html.haml b/app/views/tasklists/new.html.haml deleted file mode 100644 index e630a5c..0000000 --- a/app/views/tasklists/new.html.haml +++ /dev/null @@ -1,8 +0,0 @@ - -=form_with model: @tasklist,url: {action: "create"},local: true do |form| - -if @tasklist.errors.any? - -@tasklist.errors.full_messages.each do |error| - = error - =form.label :name - =form.text_field :name - =form.submit \ No newline at end of file diff --git a/app/views/tasklists/show.html.haml b/app/views/tasklists/show.html.haml deleted file mode 100644 index e69de29..0000000 diff --git a/app/views/tasklists/update_tasklist.js.haml b/app/views/tasklists/update_tasklist.js.haml new file mode 100644 index 0000000..ecb1377 --- /dev/null +++ b/app/views/tasklists/update_tasklist.js.haml @@ -0,0 +1,2 @@ +$("#tasklist_list#{@tasklist.id}").empty(); +$("#tasklist_list#{@tasklist.id}").append("#{j render partial: 'update'}") diff --git a/app/views/tasks/_task.html.haml b/app/views/tasks/_task.html.haml new file mode 100644 index 0000000..d71d921 --- /dev/null +++ b/app/views/tasks/_task.html.haml @@ -0,0 +1,14 @@ +%li.list-group-item + .row + .col-sm-12 + .row{id: "card#{@task.id}"} + .col-sm-1.align-self-center + %input{type: 'checkbox', id: "checkbox#{@task.id}", "data-tasklistid": @task.tasklist.id, "data-taskid": @task.id, "checked": @task.status} + .col-sm-8.align-self-center + %div.taskname{id:"task#{@task.id}", "data-tasklistid": @task.tasklist.id, "data-taskid": @task.id} + =@task.name + .col-sm-1.align-self-center + %span.fa.fa-edit#taskUpdate.updateTask{"data-tasklistid": @task.tasklist.id, "data-taskid": @task.id} + .col-sm-1.align-self-center + %span.far.fa-trash-alt.taskDelete#taskDelete{"data-tasklistid": @task.tasklist.id, "data-taskid": @task.id} + %br diff --git a/app/views/tasks/_update.html.haml b/app/views/tasks/_update.html.haml new file mode 100644 index 0000000..d92e9ef --- /dev/null +++ b/app/views/tasks/_update.html.haml @@ -0,0 +1,10 @@ +.col-sm-1.align-self-center + %input{type: 'checkbox', id: "checkbox#{@task.id}", "data-tasklistid": @task.tasklist.id, "data-taskid": @task.id, "checked": @task.status} +.col-sm-8.align-self-center + %div.taskname{id:"task#{@task.id}", "data-tasklistid": @task.tasklist.id, "data-taskid": @task.id} + =@task.name +.col-sm-1.align-self-center + %span.fa.fa-edit#taskUpdate.updateTask{"data-tasklistid": @task.tasklist.id, "data-taskid": @task.id} +.col-sm-1.align-self-center + %span.far.fa-trash-alt.taskDelete#taskDelete{"data-tasklistid": @task.tasklist.id, "data-taskid": @task.id} + %br diff --git a/app/views/tasks/create_task.js.haml b/app/views/tasks/create_task.js.haml new file mode 100644 index 0000000..14528e9 --- /dev/null +++ b/app/views/tasks/create_task.js.haml @@ -0,0 +1,2 @@ +$(".tab-content .list-group").append("#{j render partial: 'task'}"); +$("#newtasks").empty(); diff --git a/app/views/tasks/edit.html.haml b/app/views/tasks/edit.html.haml deleted file mode 100644 index 513c0f4..0000000 --- a/app/views/tasks/edit.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -=form_with model: @tasks,url: tasklist_task_path,local: true do |form| - -if @tasks.errors.any? - -@tasks.errors.full_messages.each do |messages| - %br - = messages - =form.label :name - =form.text_field :name - %br - =form.label :body - =form.text_field :body - %br - =form.label :status - =form.check_box :status - %br - =form.submit \ No newline at end of file diff --git a/app/views/tasks/errors.js.haml b/app/views/tasks/errors.js.haml new file mode 100644 index 0000000..c4e1ef5 --- /dev/null +++ b/app/views/tasks/errors.js.haml @@ -0,0 +1,2 @@ +- @task.errors.full_messages.each do |error_message| + toastr.error("#{error_message}"); diff --git a/app/views/tasks/index.html.haml b/app/views/tasks/index.html.haml deleted file mode 100644 index e2b79a0..0000000 --- a/app/views/tasks/index.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -.container - %h1{:class =>"text-center"}Hello, You have #{@tasks.length} tasks -.container{:class => "col-sm-6"} - - @tasks.each do |task| - .card{:style=>'margin-bottom:10px'} - .card-body - .card-title - %b #{task.name} - %h5{:class =>"card-text"} - =task.body - .card-footer - .container - .row - .custom-control{:class=>"custom-switch col-sm-2"} - %input{:class=>"custom-control-input",:type=>"checkbox",:id=>"status#{task.id}",checked: task.status} - %label{:class=>"custom-control-label",:for=>"status#{task.id}" }Done - =link_to '',tasklist_task_path(task.tasklist_id,task.id),method: :delete,data:{confirm: "Are you sure? "},class: "fa fa-trash col-sm-1" - =link_to '', edit_tasklist_task_path(task.tasklist_id,task.id),class: "fa fa-edit col-sm-1" - - - %script - $('#status#{task.id}').click(function(){ajaxcall(#{task.tasklist_id},#{task.id})}); - = link_to 'Create new task', new_tasklist_task_path - %br - =link_to 'Go Back',tasklists_path - %br - = link_to 'Sign Out',destroy_user_session_path,method: :delete if user_signed_in? \ No newline at end of file diff --git a/app/views/tasks/new.html.haml b/app/views/tasks/new.html.haml deleted file mode 100644 index ed3a9e4..0000000 --- a/app/views/tasks/new.html.haml +++ /dev/null @@ -1,18 +0,0 @@ - -= form_with model: @tasks, url: {action: "create"},local: true do |form| - -if @tasks.errors.any? - -@tasks.errors.full_messages.each do |messages| - %br - = messages - = form.label :name - = form.text_field :name - %br - = form.label :body - = form.text_area :body - %br - =form.label :status - =form.check_box :status - %br - =form.hidden_field :tasklist_id, value:1 - %br - = form.submit \ No newline at end of file diff --git a/app/views/tasks/show.html.haml b/app/views/tasks/show.html.haml deleted file mode 100644 index f59ea7f..0000000 --- a/app/views/tasks/show.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -%h1 #{@tasks.name} -%h4 #{@tasks.body} -Status: #{@tasks.status} -%br -%br -Tasklist: #{@tasks.tasklist.name} -%br -%br -=link_to 'Back', tasklist_tasks_path \ No newline at end of file diff --git a/app/views/tasks/update_task.js.haml b/app/views/tasks/update_task.js.haml new file mode 100644 index 0000000..07db514 --- /dev/null +++ b/app/views/tasks/update_task.js.haml @@ -0,0 +1,3 @@ +$("#card#{@task.id}").empty(); +$("#card#{@task.id}").html("#{j render partial: 'update'}"); + diff --git a/config/routes.rb b/config/routes.rb index ea3221b..2caaa3e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,12 +3,19 @@ Rails.application.routes.draw do devise_for :admin_users, ActiveAdmin::Devise.config ActiveAdmin.routes(self) + devise_for :users, controllers: { session: 'users/session' } # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html root 'tasklists#index' + resources :tasklists do - resources :tasks + member do + get 'get_tasks' + end + resources :tasks do + member do + patch 'toggle_status' + end + end end - - get 'updatecheck/:tasklist_id/:id', to: 'tasks#checkbox_update' end diff --git a/db/migrate/20190806091556_remove_body_from_tasks.rb b/db/migrate/20190806091556_remove_body_from_tasks.rb new file mode 100644 index 0000000..acb572b --- /dev/null +++ b/db/migrate/20190806091556_remove_body_from_tasks.rb @@ -0,0 +1,5 @@ +class RemoveBodyFromTasks < ActiveRecord::Migration[5.2] + def change + remove_column :tasks, :body + end +end diff --git a/db/schema.rb b/db/schema.rb index 2fb65ae..e1d6764 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_07_18_063627) do +ActiveRecord::Schema.define(version: 2019_08_06_091556) do create_table "active_admin_comments", force: :cascade do |t| t.string "namespace" @@ -48,7 +48,6 @@ create_table "tasks", force: :cascade do |t| t.string "name" - t.text "body" t.boolean "status" t.integer "tasklist_id" t.datetime "created_at", null: false diff --git a/package.json b/package.json index 4a29ede..ead6a81 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,7 @@ { "name": "todo", "private": true, - "dependencies": {} + "dependencies": { + "mustache": "^3.0.1" + } } diff --git a/vendor/mustache/mustache.js b/vendor/mustache/mustache.js new file mode 100644 index 0000000..8ec1b44 --- /dev/null +++ b/vendor/mustache/mustache.js @@ -0,0 +1,682 @@ +/*! + * mustache.js - Logic-less {{mustache}} templates with JavaScript + * http://github.com/janl/mustache.js + */ + +/*global define: false Mustache: true*/ + +(function defineMustache (global, factory) { + if (typeof exports === 'object' && exports && typeof exports.nodeName !== 'string') { + factory(exports); // CommonJS + } else if (typeof define === 'function' && define.amd) { + define(['exports'], factory); // AMD + } else { + global.Mustache = {}; + factory(global.Mustache); // script, wsh, asp + } +}(this, function mustacheFactory (mustache) { + + var objectToString = Object.prototype.toString; + var isArray = Array.isArray || function isArrayPolyfill (object) { + return objectToString.call(object) === '[object Array]'; + }; + + function isFunction (object) { + return typeof object === 'function'; + } + + /** + * More correct typeof string handling array + * which normally returns typeof 'object' + */ + function typeStr (obj) { + return isArray(obj) ? 'array' : typeof obj; + } + + function escapeRegExp (string) { + return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); + } + + /** + * Null safe way of checking whether or not an object, + * including its prototype, has a given property + */ + function hasProperty (obj, propName) { + return obj != null && typeof obj === 'object' && (propName in obj); + } + + /** + * Safe way of detecting whether or not the given thing is a primitive and + * whether it has the given property + */ + function primitiveHasOwnProperty (primitive, propName) { + return ( + primitive != null + && typeof primitive !== 'object' + && primitive.hasOwnProperty + && primitive.hasOwnProperty(propName) + ); + } + + // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 + // See https://github.com/janl/mustache.js/issues/189 + var regExpTest = RegExp.prototype.test; + function testRegExp (re, string) { + return regExpTest.call(re, string); + } + + var nonSpaceRe = /\S/; + function isWhitespace (string) { + return !testRegExp(nonSpaceRe, string); + } + + var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=' + }; + + function escapeHtml (string) { + return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) { + return entityMap[s]; + }); + } + + var whiteRe = /\s*/; + var spaceRe = /\s+/; + var equalsRe = /\s*=/; + var curlyRe = /\s*\}/; + var tagRe = /#|\^|\/|>|\{|&|=|!/; + + /** + * Breaks up the given `template` string into a tree of tokens. If the `tags` + * argument is given here it must be an array with two string values: the + * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of + * course, the default is to use mustaches (i.e. mustache.tags). + * + * A token is an array with at least 4 elements. The first element is the + * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag + * did not contain a symbol (i.e. {{myValue}}) this element is "name". For + * all text that appears outside a symbol this element is "text". + * + * The second element of a token is its "value". For mustache tags this is + * whatever else was inside the tag besides the opening symbol. For text tokens + * this is the text itself. + * + * The third and fourth elements of the token are the start and end indices, + * respectively, of the token in the original template. + * + * Tokens that are the root node of a subtree contain two more elements: 1) an + * array of tokens in the subtree and 2) the index in the original template at + * which the closing tag for that section begins. + */ + function parseTemplate (template, tags) { + if (!template) + return []; + + var sections = []; // Stack to hold section tokens + var tokens = []; // Buffer to hold the tokens + var spaces = []; // Indices of whitespace tokens on the current line + var hasTag = false; // Is there a {{tag}} on the current line? + var nonSpace = false; // Is there a non-space char on the current line? + + // Strips all whitespace tokens array for the current line + // if there was a {{#tag}} on it and otherwise only space. + function stripSpace () { + if (hasTag && !nonSpace) { + while (spaces.length) + delete tokens[spaces.pop()]; + } else { + spaces = []; + } + + hasTag = false; + nonSpace = false; + } + + var openingTagRe, closingTagRe, closingCurlyRe; + function compileTags (tagsToCompile) { + if (typeof tagsToCompile === 'string') + tagsToCompile = tagsToCompile.split(spaceRe, 2); + + if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) + throw new Error('Invalid tags: ' + tagsToCompile); + + openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*'); + closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1])); + closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1])); + } + + compileTags(tags || mustache.tags); + + var scanner = new Scanner(template); + + var start, type, value, chr, token, openSection; + while (!scanner.eos()) { + start = scanner.pos; + + // Match any text between tags. + value = scanner.scanUntil(openingTagRe); + + if (value) { + for (var i = 0, valueLength = value.length; i < valueLength; ++i) { + chr = value.charAt(i); + + if (isWhitespace(chr)) { + spaces.push(tokens.length); + } else { + nonSpace = true; + } + + tokens.push([ 'text', chr, start, start + 1 ]); + start += 1; + + // Check for whitespace on the current line. + if (chr === '\n') + stripSpace(); + } + } + + // Match the opening tag. + if (!scanner.scan(openingTagRe)) + break; + + hasTag = true; + + // Get the tag type. + type = scanner.scan(tagRe) || 'name'; + scanner.scan(whiteRe); + + // Get the tag value. + if (type === '=') { + value = scanner.scanUntil(equalsRe); + scanner.scan(equalsRe); + scanner.scanUntil(closingTagRe); + } else if (type === '{') { + value = scanner.scanUntil(closingCurlyRe); + scanner.scan(curlyRe); + scanner.scanUntil(closingTagRe); + type = '&'; + } else { + value = scanner.scanUntil(closingTagRe); + } + + // Match the closing tag. + if (!scanner.scan(closingTagRe)) + throw new Error('Unclosed tag at ' + scanner.pos); + + token = [ type, value, start, scanner.pos ]; + tokens.push(token); + + if (type === '#' || type === '^') { + sections.push(token); + } else if (type === '/') { + // Check section nesting. + openSection = sections.pop(); + + if (!openSection) + throw new Error('Unopened section "' + value + '" at ' + start); + + if (openSection[1] !== value) + throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); + } else if (type === 'name' || type === '{' || type === '&') { + nonSpace = true; + } else if (type === '=') { + // Set the tags for the next time around. + compileTags(value); + } + } + + // Make sure there are no open sections when we're done. + openSection = sections.pop(); + + if (openSection) + throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); + + return nestTokens(squashTokens(tokens)); + } + + /** + * Combines the values of consecutive text tokens in the given `tokens` array + * to a single token. + */ + function squashTokens (tokens) { + var squashedTokens = []; + + var token, lastToken; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + token = tokens[i]; + + if (token) { + if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { + lastToken[1] += token[1]; + lastToken[3] = token[3]; + } else { + squashedTokens.push(token); + lastToken = token; + } + } + } + + return squashedTokens; + } + + /** + * Forms the given array of `tokens` into a nested tree structure where + * tokens that represent a section have two additional items: 1) an array of + * all tokens that appear in that section and 2) the index in the original + * template that represents the end of that section. + */ + function nestTokens (tokens) { + var nestedTokens = []; + var collector = nestedTokens; + var sections = []; + + var token, section; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + token = tokens[i]; + + switch (token[0]) { + case '#': + case '^': + collector.push(token); + sections.push(token); + collector = token[4] = []; + break; + case '/': + section = sections.pop(); + section[5] = token[2]; + collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; + break; + default: + collector.push(token); + } + } + + return nestedTokens; + } + + /** + * A simple string scanner that is used by the template parser to find + * tokens in template strings. + */ + function Scanner (string) { + this.string = string; + this.tail = string; + this.pos = 0; + } + + /** + * Returns `true` if the tail is empty (end of string). + */ + Scanner.prototype.eos = function eos () { + return this.tail === ''; + }; + + /** + * Tries to match the given regular expression at the current position. + * Returns the matched text if it can match, the empty string otherwise. + */ + Scanner.prototype.scan = function scan (re) { + var match = this.tail.match(re); + + if (!match || match.index !== 0) + return ''; + + var string = match[0]; + + this.tail = this.tail.substring(string.length); + this.pos += string.length; + + return string; + }; + + /** + * Skips all text until the given regular expression can be matched. Returns + * the skipped string, which is the entire tail if no match can be made. + */ + Scanner.prototype.scanUntil = function scanUntil (re) { + var index = this.tail.search(re), match; + + switch (index) { + case -1: + match = this.tail; + this.tail = ''; + break; + case 0: + match = ''; + break; + default: + match = this.tail.substring(0, index); + this.tail = this.tail.substring(index); + } + + this.pos += match.length; + + return match; + }; + + /** + * Represents a rendering context by wrapping a view object and + * maintaining a reference to the parent context. + */ + function Context (view, parentContext) { + this.view = view; + this.cache = { '.': this.view }; + this.parent = parentContext; + } + + /** + * Creates a new context using the given view with this context + * as the parent. + */ + Context.prototype.push = function push (view) { + return new Context(view, this); + }; + + /** + * Returns the value of the given name in this context, traversing + * up the context hierarchy if the value is absent in this context's view. + */ + Context.prototype.lookup = function lookup (name) { + var cache = this.cache; + + var value; + if (cache.hasOwnProperty(name)) { + value = cache[name]; + } else { + var context = this, intermediateValue, names, index, lookupHit = false; + + while (context) { + if (name.indexOf('.') > 0) { + intermediateValue = context.view; + names = name.split('.'); + index = 0; + + /** + * Using the dot notion path in `name`, we descend through the + * nested objects. + * + * To be certain that the lookup has been successful, we have to + * check if the last object in the path actually has the property + * we are looking for. We store the result in `lookupHit`. + * + * This is specially necessary for when the value has been set to + * `undefined` and we want to avoid looking up parent contexts. + * + * In the case where dot notation is used, we consider the lookup + * to be successful even if the last "object" in the path is + * not actually an object but a primitive (e.g., a string, or an + * integer), because it is sometimes useful to access a property + * of an autoboxed primitive, such as the length of a string. + **/ + while (intermediateValue != null && index < names.length) { + if (index === names.length - 1) + lookupHit = ( + hasProperty(intermediateValue, names[index]) + || primitiveHasOwnProperty(intermediateValue, names[index]) + ); + + intermediateValue = intermediateValue[names[index++]]; + } + } else { + intermediateValue = context.view[name]; + + /** + * Only checking against `hasProperty`, which always returns `false` if + * `context.view` is not an object. Deliberately omitting the check + * against `primitiveHasOwnProperty` if dot notation is not used. + * + * Consider this example: + * ``` + * Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"}) + * ``` + * + * If we were to check also against `primitiveHasOwnProperty`, as we do + * in the dot notation case, then render call would return: + * + * "The length of a football field is 9." + * + * rather than the expected: + * + * "The length of a football field is 100 yards." + **/ + lookupHit = hasProperty(context.view, name); + } + + if (lookupHit) { + value = intermediateValue; + break; + } + + context = context.parent; + } + + cache[name] = value; + } + + if (isFunction(value)) + value = value.call(this.view); + + return value; + }; + + /** + * A Writer knows how to take a stream of tokens and render them to a + * string, given a context. It also maintains a cache of templates to + * avoid the need to parse the same template twice. + */ + function Writer () { + this.cache = {}; + } + + /** + * Clears all cached templates in this writer. + */ + Writer.prototype.clearCache = function clearCache () { + this.cache = {}; + }; + + /** + * Parses and caches the given `template` according to the given `tags` or + * `mustache.tags` if `tags` is omitted, and returns the array of tokens + * that is generated from the parse. + */ + Writer.prototype.parse = function parse (template, tags) { + var cache = this.cache; + var cacheKey = template + ':' + (tags || mustache.tags).join(':'); + var tokens = cache[cacheKey]; + + if (tokens == null) + tokens = cache[cacheKey] = parseTemplate(template, tags); + + return tokens; + }; + + /** + * High-level method that is used to render the given `template` with + * the given `view`. + * + * The optional `partials` argument may be an object that contains the + * names and templates of partials that are used in the template. It may + * also be a function that is used to load partial templates on the fly + * that takes a single argument: the name of the partial. + * + * If the optional `tags` argument is given here it must be an array with two + * string values: the opening and closing tags used in the template (e.g. + * [ "<%", "%>" ]). The default is to mustache.tags. + */ + Writer.prototype.render = function render (template, view, partials, tags) { + var tokens = this.parse(template, tags); + var context = (view instanceof Context) ? view : new Context(view); + return this.renderTokens(tokens, context, partials, template, tags); + }; + + /** + * Low-level method that renders the given array of `tokens` using + * the given `context` and `partials`. + * + * Note: The `originalTemplate` is only ever used to extract the portion + * of the original template that was contained in a higher-order section. + * If the template doesn't use higher-order sections, this argument may + * be omitted. + */ + Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, tags) { + var buffer = ''; + + var token, symbol, value; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + value = undefined; + token = tokens[i]; + symbol = token[0]; + + if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate); + else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate); + else if (symbol === '>') value = this.renderPartial(token, context, partials, tags); + else if (symbol === '&') value = this.unescapedValue(token, context); + else if (symbol === 'name') value = this.escapedValue(token, context); + else if (symbol === 'text') value = this.rawValue(token); + + if (value !== undefined) + buffer += value; + } + + return buffer; + }; + + Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) { + var self = this; + var buffer = ''; + var value = context.lookup(token[1]); + + // This function is used to render an arbitrary template + // in the current context by higher-order sections. + function subRender (template) { + return self.render(template, context, partials); + } + + if (!value) return; + + if (isArray(value)) { + for (var j = 0, valueLength = value.length; j < valueLength; ++j) { + buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); + } + } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { + buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); + } else if (isFunction(value)) { + if (typeof originalTemplate !== 'string') + throw new Error('Cannot use higher-order sections without the original template'); + + // Extract the portion of the original template that the section contains. + value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); + + if (value != null) + buffer += value; + } else { + buffer += this.renderTokens(token[4], context, partials, originalTemplate); + } + return buffer; + }; + + Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) { + var value = context.lookup(token[1]); + + // Use JavaScript's definition of falsy. Include empty arrays. + // See https://github.com/janl/mustache.js/issues/186 + if (!value || (isArray(value) && value.length === 0)) + return this.renderTokens(token[4], context, partials, originalTemplate); + }; + + Writer.prototype.renderPartial = function renderPartial (token, context, partials, tags) { + if (!partials) return; + + var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; + if (value != null) + return this.renderTokens(this.parse(value, tags), context, partials, value); + }; + + Writer.prototype.unescapedValue = function unescapedValue (token, context) { + var value = context.lookup(token[1]); + if (value != null) + return value; + }; + + Writer.prototype.escapedValue = function escapedValue (token, context) { + var value = context.lookup(token[1]); + if (value != null) + return mustache.escape(value); + }; + + Writer.prototype.rawValue = function rawValue (token) { + return token[1]; + }; + + mustache.name = 'mustache.js'; + mustache.version = '3.0.1'; + mustache.tags = [ '{{', '}}' ]; + + // All high-level mustache.* functions use this writer. + var defaultWriter = new Writer(); + + /** + * Clears all cached templates in the default writer. + */ + mustache.clearCache = function clearCache () { + return defaultWriter.clearCache(); + }; + + /** + * Parses and caches the given template in the default writer and returns the + * array of tokens it contains. Doing this ahead of time avoids the need to + * parse templates on the fly as they are rendered. + */ + mustache.parse = function parse (template, tags) { + return defaultWriter.parse(template, tags); + }; + + /** + * Renders the `template` with the given `view` and `partials` using the + * default writer. If the optional `tags` argument is given here it must be an + * array with two string values: the opening and closing tags used in the + * template (e.g. [ "<%", "%>" ]). The default is to mustache.tags. + */ + mustache.render = function render (template, view, partials, tags) { + if (typeof template !== 'string') { + throw new TypeError('Invalid template! Template should be a "string" ' + + 'but "' + typeStr(template) + '" was given as the first ' + + 'argument for mustache#render(template, view, partials)'); + } + + return defaultWriter.render(template, view, partials, tags); + }; + + // This is here for backwards compatibility with 0.4.x., + /*eslint-disable */ // eslint wants camel cased function name + mustache.to_html = function to_html (template, view, partials, send) { + /*eslint-enable*/ + + var result = mustache.render(template, view, partials); + + if (isFunction(send)) { + send(result); + } else { + return result; + } + }; + + // Export the escaping function so that the user may override it. + // See https://github.com/janl/mustache.js/issues/244 + mustache.escape = escapeHtml; + + // Export these mainly for testing, but also for advanced usage. + mustache.Scanner = Scanner; + mustache.Context = Context; + mustache.Writer = Writer; + + return mustache; +}));