Merge pull request #463 from nextcloud/markdown-checkboxes
Implement github flavored markdown checkboxes
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
- 🚀 Get your project organized
|
- 🚀 Get your project organized
|
||||||
|
|
||||||
</description>
|
</description>
|
||||||
<version>0.4.0-alpha1</version>
|
<version>0.4.0-alpha3</version>
|
||||||
<licence>agpl</licence>
|
<licence>agpl</licence>
|
||||||
<author>Julius Härtl</author>
|
<author>Julius Härtl</author>
|
||||||
<namespace>Deck</namespace>
|
<namespace>Deck</namespace>
|
||||||
|
|||||||
@@ -461,6 +461,20 @@ input.input-inline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-tasks {
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 4px 4px 4px 0px;
|
||||||
|
padding: 0 2px;
|
||||||
|
font-size: 90%;
|
||||||
|
opacity: 0.5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 22px;
|
padding: 22px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -1219,6 +1233,17 @@ input.input-inline {
|
|||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type=checkbox] {
|
||||||
|
margin: 0px 10px 0px 0px;
|
||||||
|
line-height: 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
min-height: 12px;
|
||||||
|
}
|
||||||
|
li input[type=checkbox] {
|
||||||
|
margin: 0px 10px 0px -20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -25,18 +25,20 @@
|
|||||||
import app from './App.js';
|
import app from './App.js';
|
||||||
import md from 'angular-markdown-it';
|
import md from 'angular-markdown-it';
|
||||||
import markdownitLinkTarget from 'markdown-it-link-target';
|
import markdownitLinkTarget from 'markdown-it-link-target';
|
||||||
|
import markdownitCheckbox from 'legacy/markdown-it-checkbox.js';
|
||||||
|
|
||||||
app.config(function ($provide, $interpolateProvider, $httpProvider, $urlRouterProvider, $stateProvider, $compileProvider, markdownItConverterProvider) {
|
app.config(function ($provide, $interpolateProvider, $httpProvider, $urlRouterProvider, $stateProvider, $compileProvider, markdownItConverterProvider) {
|
||||||
'use strict';
|
'use strict';
|
||||||
$httpProvider.defaults.headers.common.requesttoken = oc_requesttoken;
|
$httpProvider.defaults.headers.common.requesttoken = oc_requesttoken;
|
||||||
|
|
||||||
|
|
||||||
$compileProvider.debugInfoEnabled(true);
|
$compileProvider.debugInfoEnabled(true);
|
||||||
|
|
||||||
markdownItConverterProvider.use(markdownitLinkTarget, {
|
markdownItConverterProvider.use(markdownitLinkTarget, {
|
||||||
breaks: true,
|
breaks: true,
|
||||||
linkify: true,
|
linkify: true,
|
||||||
xhtmlOut: true
|
xhtmlOut: true
|
||||||
});
|
}).use(markdownitCheckbox);
|
||||||
|
|
||||||
$urlRouterProvider.otherwise('/');
|
$urlRouterProvider.otherwise('/');
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,22 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St
|
|||||||
}, true);
|
}, true);
|
||||||
$scope.params = $state;
|
$scope.params = $state;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for markdown checkboxes in description to render the counter
|
||||||
|
*
|
||||||
|
* This should probably be moved to the backend at some point
|
||||||
|
*
|
||||||
|
* @param text
|
||||||
|
* @returns array of [finished, total] checkboxes
|
||||||
|
*/
|
||||||
|
$scope.getCheckboxes = function(text) {
|
||||||
|
const regTotal = /\[(X|\s|\_|\-)\]\s(.*)/ig;
|
||||||
|
const regFinished = /\[(X|\_|\-)\]\s(.*)/ig;
|
||||||
|
return [
|
||||||
|
((text || '').match(regFinished) || []).length,
|
||||||
|
((text || '').match(regTotal) || []).length
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
$scope.search = function (searchText) {
|
$scope.search = function (searchText) {
|
||||||
$scope.searchText = searchText;
|
$scope.searchText = searchText;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
/* global app moment */
|
/* global app moment */
|
||||||
import app from '../app/App.js';
|
import app from '../app/App.js';
|
||||||
|
|
||||||
app.controller('CardController', function ($scope, $rootScope, $location, $stateParams, $interval, $timeout, $filter, BoardService, CardService, StackService, StatusService) {
|
app.controller('CardController', function ($scope, $rootScope, $sce, $location, $stateParams, $interval, $timeout, $filter, BoardService, CardService, StackService, StatusService, markdownItConverter) {
|
||||||
$scope.sidebar = $rootScope.sidebar;
|
$scope.sidebar = $rootScope.sidebar;
|
||||||
$scope.status = {
|
$scope.status = {
|
||||||
lastEdit: 0,
|
lastEdit: 0,
|
||||||
@@ -38,9 +38,19 @@ app.controller('CardController', function ($scope, $rootScope, $location, $state
|
|||||||
|
|
||||||
$scope.statusservice.retainWaiting();
|
$scope.statusservice.retainWaiting();
|
||||||
|
|
||||||
|
$scope.description = function() {
|
||||||
|
return $scope.rendered;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.updateMarkdown = function(content) {
|
||||||
|
// only trust the html from markdown-it-checkbox
|
||||||
|
$scope.rendered = $sce.trustAsHtml(markdownItConverter.render(content || ''));
|
||||||
|
};
|
||||||
|
|
||||||
CardService.fetchOne($scope.cardId).then(function (data) {
|
CardService.fetchOne($scope.cardId).then(function (data) {
|
||||||
$scope.statusservice.releaseWaiting();
|
$scope.statusservice.releaseWaiting();
|
||||||
$scope.archived = CardService.getCurrent().archived;
|
$scope.archived = CardService.getCurrent().archived;
|
||||||
|
$scope.updateMarkdown(CardService.getCurrent().description);
|
||||||
}, function (error) {
|
}, function (error) {
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,7 +61,44 @@ app.controller('CardController', function ($scope, $rootScope, $location, $state
|
|||||||
$scope.status.cardRename = true;
|
$scope.status.cardRename = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
$scope.cardEditDescriptionShow = function ($event) {
|
|
||||||
|
$scope.toggleCheckbox = function (id) {
|
||||||
|
$('#markdown input[type=checkbox]').attr('disabled', true);
|
||||||
|
$scope.status.edit = angular.copy(CardService.getCurrent());
|
||||||
|
var reg = /\[(X|\s|\_|\-)\]\s(.*)/ig;
|
||||||
|
var nth = 0;
|
||||||
|
$scope.status.edit.description = $scope.status.edit.description.replace(reg, function (match, i, original) {
|
||||||
|
if (nth++ === id) {
|
||||||
|
var result;
|
||||||
|
if (match.match(/^\[\s\]/i)) {
|
||||||
|
result = match.replace(/\[\s\]/i, '[x]');
|
||||||
|
}
|
||||||
|
if (match.match(/^\[x\]/i)) {
|
||||||
|
result = match.replace(/\[x\]/i, '[ ]');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
CardService.update($scope.status.edit).then(function (data) {
|
||||||
|
var header = $('.section-header.card-description');
|
||||||
|
header.find('.save-indicator.unsaved').hide();
|
||||||
|
header.find('.save-indicator.saved').fadeIn(250).fadeOut(1000);
|
||||||
|
StackService.updateCard($scope.status.edit);
|
||||||
|
});
|
||||||
|
$('#markdown input[type=checkbox]').removeAttr('disabled');
|
||||||
|
|
||||||
|
};
|
||||||
|
$scope.clickCardDescription = function ($event) {
|
||||||
|
var checkboxId = $($event.target).data('id');
|
||||||
|
if ($event.target.tagName === 'LABEL') {
|
||||||
|
$scope.toggleCheckbox(checkboxId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($event.target.tagName === 'INPUT') {
|
||||||
|
$scope.toggleCheckbox(checkboxId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (BoardService.isArchived() || CardService.getCurrent().archived) {
|
if (BoardService.isArchived() || CardService.getCurrent().archived) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -71,8 +118,9 @@ app.controller('CardController', function ($scope, $rootScope, $location, $state
|
|||||||
$interval(function() {
|
$interval(function() {
|
||||||
var currentTime = Date.now();
|
var currentTime = Date.now();
|
||||||
var timeSinceEdit = currentTime-$scope.status.lastEdit;
|
var timeSinceEdit = currentTime-$scope.status.lastEdit;
|
||||||
if (timeSinceEdit > 1000 && $scope.status.lastEdit > $scope.status.lastSave) {
|
if (timeSinceEdit > 1000 && $scope.status.lastEdit > $scope.status.lastSave && !$scope.status.saving) {
|
||||||
$scope.status.lastSave = currentTime;
|
$scope.status.lastSave = currentTime;
|
||||||
|
$scope.status.saving = true;
|
||||||
var header = $('.section-header.card-description');
|
var header = $('.section-header.card-description');
|
||||||
header.find('.save-indicator.unsaved').fadeIn(500);
|
header.find('.save-indicator.unsaved').fadeIn(500);
|
||||||
CardService.update($scope.status.edit).then(function (data) {
|
CardService.update($scope.status.edit).then(function (data) {
|
||||||
@@ -80,6 +128,7 @@ app.controller('CardController', function ($scope, $rootScope, $location, $state
|
|||||||
header.find('.save-indicator.unsaved').hide();
|
header.find('.save-indicator.unsaved').hide();
|
||||||
header.find('.save-indicator.saved').fadeIn(250).fadeOut(1000);
|
header.find('.save-indicator.saved').fadeIn(250).fadeOut(1000);
|
||||||
StackService.updateCard($scope.status.edit);
|
StackService.updateCard($scope.status.edit);
|
||||||
|
$scope.status.saving = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 500, 0, false);
|
}, 500, 0, false);
|
||||||
|
|||||||
114
js/legacy/markdown-it-checkbox.js
Normal file
114
js/legacy/markdown-it-checkbox.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Original source code from https://github.com/mcecot/markdown-it-checkbox
|
||||||
|
* © 2015 Markus Cecot
|
||||||
|
* licenced under MIT
|
||||||
|
* https://github.com/mcecot/markdown-it-checkbox/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
var checkboxReplace;
|
||||||
|
|
||||||
|
checkboxReplace = function(md, options, Token) {
|
||||||
|
"use strict";
|
||||||
|
var arrayReplaceAt, createTokens, defaults, lastId, pattern, splitTextToken;
|
||||||
|
arrayReplaceAt = md.utils.arrayReplaceAt;
|
||||||
|
lastId = 0;
|
||||||
|
defaults = {
|
||||||
|
divWrap: false,
|
||||||
|
divClass: 'checkbox',
|
||||||
|
idPrefix: 'checkbox'
|
||||||
|
};
|
||||||
|
options = Object.assign(defaults, options);
|
||||||
|
pattern = /\[(X|\s|\_|\-)\]\s(.*)/i;
|
||||||
|
createTokens = function(checked, label, Token) {
|
||||||
|
var id, idNumeric, nodes, token;
|
||||||
|
nodes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <div class="checkbox">
|
||||||
|
*/
|
||||||
|
if (options.divWrap) {
|
||||||
|
token = new Token("checkbox_open", "div", 1);
|
||||||
|
token.attrs = [["class", options.divClass]];
|
||||||
|
nodes.push(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <input type="checkbox" id="checkbox{n}" checked="true">
|
||||||
|
*/
|
||||||
|
id = options.idPrefix + lastId;
|
||||||
|
idNumeric = lastId;
|
||||||
|
lastId += 1;
|
||||||
|
token = new Token("checkbox_input", "input", 0);
|
||||||
|
token.attrs = [["type", "checkbox"], ["id", id], ["data-id", idNumeric]];
|
||||||
|
if (checked === true) {
|
||||||
|
token.attrs.push(["checked", "true"]);
|
||||||
|
}
|
||||||
|
nodes.push(token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <label for="checkbox{n}">
|
||||||
|
*/
|
||||||
|
token = new Token("label_open", "label", 1);
|
||||||
|
token.attrs = [["for", id], ["data-id", idNumeric]];
|
||||||
|
nodes.push(token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* content of label tag
|
||||||
|
*/
|
||||||
|
token = new Token("text", "", 0);
|
||||||
|
token.content = label;
|
||||||
|
nodes.push(token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* closing tags
|
||||||
|
*/
|
||||||
|
nodes.push(new Token("label_close", "label", -1));
|
||||||
|
if (options.divWrap) {
|
||||||
|
nodes.push(new Token("checkbox_close", "div", -1));
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
};
|
||||||
|
splitTextToken = function(original, Token) {
|
||||||
|
var checked, label, matches, text, value;
|
||||||
|
text = original.content;
|
||||||
|
matches = text.match(pattern);
|
||||||
|
if (matches === null) {
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
checked = false;
|
||||||
|
value = matches[1];
|
||||||
|
label = matches[2];
|
||||||
|
if (value === "X" || value === "x") {
|
||||||
|
checked = true;
|
||||||
|
}
|
||||||
|
return createTokens(checked, label, Token);
|
||||||
|
};
|
||||||
|
return function(state) {
|
||||||
|
lastId = 0;
|
||||||
|
var blockTokens, i, j, l, token, tokens;
|
||||||
|
blockTokens = state.tokens;
|
||||||
|
j = 0;
|
||||||
|
l = blockTokens.length;
|
||||||
|
while (j < l) {
|
||||||
|
if (blockTokens[j].type !== "inline") {
|
||||||
|
j++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tokens = blockTokens[j].children;
|
||||||
|
i = tokens.length - 1;
|
||||||
|
while (i >= 0) {
|
||||||
|
token = tokens[i];
|
||||||
|
blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, splitTextToken(token, state.Token));
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*global module */
|
||||||
|
|
||||||
|
module.exports = function(md, options) {
|
||||||
|
"use strict";
|
||||||
|
md.core.ruler.push("checkbox", checkboxReplace(md, options));
|
||||||
|
};
|
||||||
@@ -80,6 +80,10 @@
|
|||||||
<i class="icon icon-badge"></i>
|
<i class="icon icon-badge"></i>
|
||||||
<span data-timestamp="{{ c.duedate | dateToTimestamp }}" class="live-relative-timestamp">{{ c.duedate | relativeDateFilterString }}</span>
|
<span data-timestamp="{{ c.duedate | dateToTimestamp }}" class="live-relative-timestamp">{{ c.duedate | relativeDateFilterString }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
<div class="card-tasks" ng-if="getCheckboxes(c.description)[1] > 0">
|
||||||
|
<i class="icon icon-checkmark"></i>
|
||||||
|
<span>{{ getCheckboxes(c.description)[0] }}/{{ getCheckboxes(c.description)[1] }}</span>
|
||||||
|
</div>
|
||||||
<div class="card-assigned-users">
|
<div class="card-assigned-users">
|
||||||
<div class="assigned-user" ng-repeat="user in c.assignedUsers | limitTo: 3">
|
<div class="assigned-user" ng-repeat="user in c.assignedUsers | limitTo: 3">
|
||||||
<avatar data-user="{{ user.participant.uid }}" data-displayname="{{ user.participant.displayname }}" data-tooltip></avatar>
|
<avatar data-user="{{ user.participant.uid }}" data-displayname="{{ user.participant.displayname }}" data-tooltip></avatar>
|
||||||
|
|||||||
@@ -98,14 +98,13 @@
|
|||||||
placeholder="<?php p($l->t('Add a card description…')); ?>"
|
placeholder="<?php p($l->t('Add a card description…')); ?>"
|
||||||
ng-blur="cardUpdate(status.edit)"
|
ng-blur="cardUpdate(status.edit)"
|
||||||
ng-model="status.edit.description"
|
ng-model="status.edit.description"
|
||||||
ng-change="cardEditDescriptionChanged()"
|
ng-change="cardEditDescriptionChanged(); updateMarkdown(status.edit.description)"
|
||||||
autofocus-on-insert> </textarea>
|
autofocus-on-insert> </textarea>
|
||||||
<div class="container" ng-click="cardEditDescriptionShow($event)"
|
<div class="container" ng-click="clickCardDescription($event)"
|
||||||
ng-if="!status.cardEditDescription" ng-animate>
|
ng-if="!status.cardEditDescription" ng-animate>
|
||||||
<div markdown-it="cardservice.getCurrent().description"
|
<div id="markdown" ng-bind-html="description()">{{ description() }}</div>
|
||||||
id="markdown"></div>
|
|
||||||
<div class="placeholder"
|
<div class="placeholder"
|
||||||
ng-if="!cardservice.getCurrent().description"><?php p($l->t('Add a card description…')); ?></div>
|
ng-if="!description()"><?php p($l->t('Add a card description…')); ?></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user