mirror of
https://git.victorphan.net/basketballcantho/Teedy_custom.git
synced 2026-08-05 22:03:11 +07:00
đã phát triển tính năng pdf annotaion
This commit is contained in:
@@ -164,4 +164,10 @@ angular.module('docs').controller('DocumentView', function ($scope, $rootScope,
|
||||
}
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Return true if the comment is not an annotation.
|
||||
*/
|
||||
$scope.isNotAnnotation = function (comment) {
|
||||
return !comment.content.startsWith('[ANNOTATION]');
|
||||
};
|
||||
});
|
||||
@@ -14,18 +14,19 @@ angular.module('docs').controller('FileModalView', function ($uibModalInstance,
|
||||
});
|
||||
};
|
||||
|
||||
// Load files
|
||||
Restangular.one('file/list').get({ id: $stateParams.id }).then(function (data) {
|
||||
$scope.files = data.files;
|
||||
setFile(data.files);
|
||||
|
||||
// File not found, maybe it's a version
|
||||
if (!$scope.file) {
|
||||
Restangular.one('file/' + $stateParams.fileId + '/versions').get().then(function (data) {
|
||||
setFile(data.files);
|
||||
});
|
||||
}
|
||||
});
|
||||
// Load files - use $stateParams.id (document ID) to get all files of this document
|
||||
var docIdFromParams = $stateParams.documentId || $stateParams.id;
|
||||
if (docIdFromParams) {
|
||||
Restangular.one('file/list').get({ id: docIdFromParams }).then(function (data) {
|
||||
$scope.files = data.files;
|
||||
setFile(data.files);
|
||||
});
|
||||
} else {
|
||||
// No doc ID in URL - we only have the fileId, load via versions API
|
||||
Restangular.one('file/' + $stateParams.fileId + '/versions').get().then(function (data) {
|
||||
setFile(data.files);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the next file.
|
||||
@@ -40,6 +41,24 @@ angular.module('docs').controller('FileModalView', function ($uibModalInstance,
|
||||
return next;
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle maximize modal
|
||||
*/
|
||||
$scope.isMaximized = false;
|
||||
$scope.toggleMaximize = function () {
|
||||
$scope.isMaximized = !$scope.isMaximized;
|
||||
var modalEl = document.querySelector('.modal-fileview');
|
||||
if (modalEl) {
|
||||
if ($scope.isMaximized) {
|
||||
modalEl.classList.add('modal-fullscreen');
|
||||
} else {
|
||||
modalEl.classList.remove('modal-fullscreen');
|
||||
}
|
||||
// Fire resize event so that pdf viewers or other elements can recalculate layout if needed
|
||||
setTimeout(function() { window.dispatchEvent(new Event('resize')); }, 100);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the previous file.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,617 @@
|
||||
/**
|
||||
* PDF Annotation Viewer directive.
|
||||
*/
|
||||
var module;
|
||||
try {
|
||||
module = angular.module('docs');
|
||||
} catch (e) {
|
||||
module = angular.module('share');
|
||||
}
|
||||
|
||||
module.directive('pdfAnnotationViewer', function ($stateParams, Restangular, $timeout, $http) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
fileId: '=',
|
||||
documentId: '=',
|
||||
shareId: '='
|
||||
},
|
||||
template: `
|
||||
<div class="pdf-annotation-container">
|
||||
<div class="pdf-toolbar">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-default" ng-class="{active: tool === 'cursor'}" ng-click="setTool('cursor')" uib-tooltip="Cursor"><span class="fas fa-mouse-pointer"></span></button>
|
||||
<button class="btn btn-default" ng-class="{active: tool === 'pencil'}" ng-click="setTool('pencil')" uib-tooltip="Pencil"><span class="fas fa-pencil-alt"></span></button>
|
||||
<button class="btn btn-default" ng-class="{active: tool === 'highlight'}" ng-click="setTool('highlight')" uib-tooltip="Highlight"><span class="fas fa-highlighter"></span></button>
|
||||
<button class="btn btn-default" ng-class="{active: tool === 'text'}" ng-click="setTool('text')" uib-tooltip="Text"><span class="fas fa-font"></span></button>
|
||||
<button class="btn btn-default" ng-class="{active: tool === 'eraser'}" ng-click="setTool('eraser')" uib-tooltip="Eraser (click to delete)"><span class="fas fa-eraser"></span></button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-default" ng-click="zoomOut()"><span class="fas fa-search-minus"></span></button>
|
||||
<span class="btn btn-placeholder">{{ zoom * 100 | number:0 }}%</span>
|
||||
<button class="btn btn-default" ng-click="zoomIn()"><span class="fas fa-search-plus"></span></button>
|
||||
</div>
|
||||
<div class="btn-group pull-right">
|
||||
<button class="btn btn-default" ng-click="exportPdf()" ng-disabled="exporting" uib-tooltip="Export as PDF">
|
||||
<span class="fas fa-file-pdf"></span> {{ exporting === 'pdf' ? 'Exporting...' : 'PDF' }}
|
||||
</button>
|
||||
<button class="btn btn-danger" ng-click="clearAllAnnotations()" ng-disabled="saving || shareId || annotations.length === 0" uib-tooltip="Clear all annotations">
|
||||
<span class="fas fa-trash"></span>
|
||||
</button>
|
||||
<button class="btn btn-warning" ng-click="saveAsVersion()" ng-disabled="exporting || saving || shareId" uib-tooltip="Save as a new PDF version in Teedy">
|
||||
<span class="fas fa-file-upload"></span> {{ exporting === 'version' ? 'Uploading...' : 'Save Version' }}
|
||||
</button>
|
||||
<button class="btn btn-primary" ng-click="saveAnnotations()" ng-disabled="saving || shareId">
|
||||
<span class="fas fa-save"></span> {{ (saving ? 'Saving...' : 'Save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-viewer-scroll">
|
||||
<div class="pdf-pages-container" id="pdf-pages">
|
||||
<!-- Pages will be rendered here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.pdf-annotation-container { display: flex; flex-direction: column; height: 100%; position: relative; background: #525659; }
|
||||
.pdf-toolbar { background: #323639; padding: 10px; color: white; display: flex; gap: 10px; align-items: center; z-index: 100; box-shadow: 0 2px 5px rgba(0,0,0,0.3); }
|
||||
.pdf-viewer-scroll { flex: 1; overflow: auto; padding: 20px; }
|
||||
.pdf-pages-container { display: flex; flex-direction: column; align-items: center; gap: 20px; }
|
||||
.pdf-page-wrapper { position: relative; box-shadow: 0 0 10px rgba(0,0,0,0.5); background: white; }
|
||||
.pdf-canvas { display: block; }
|
||||
.annotation-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
|
||||
.annotation-layer.active { pointer-events: all; cursor: crosshair; }
|
||||
.annotation-layer.tool-cursor { cursor: default; }
|
||||
.pdf-text-input { position: absolute; border: 1px solid #2aabd2; background: rgba(255,255,255,0.8); z-index: 200; outline: none; padding: 2px; }
|
||||
</style>
|
||||
`,
|
||||
link: function (scope, element) {
|
||||
scope.tool = 'cursor';
|
||||
scope.zoom = 1.0;
|
||||
scope.annotations = [];
|
||||
scope.saving = false;
|
||||
|
||||
let pdfDoc = null;
|
||||
let pages = [];
|
||||
let currentRenderId = 0; // Global sequence ID to cancel stale renders
|
||||
let deregisterDocIdWatch = null;
|
||||
const getDocId = () => scope.documentId || $stateParams.documentId || $stateParams.id;
|
||||
const pdfUrl = `../api/file/${scope.fileId}/data` + (scope.shareId ? `?share=${scope.shareId}` : '');
|
||||
|
||||
// Initialize PDF.js
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'lib/pdf.worker.js';
|
||||
|
||||
// Fetch annotations from server as a Promise (does not render, just returns data)
|
||||
const fetchAnnotations = (docId) => {
|
||||
if (!docId) return Promise.resolve([]);
|
||||
return Restangular.one('comment', docId).get({ share: scope.shareId }).then(data => {
|
||||
const annoComment = data.comments.find(c => c.content.startsWith('[ANNOTATION]'));
|
||||
if (annoComment) {
|
||||
try {
|
||||
return JSON.parse(annoComment.content.replace('[ANNOTATION]', ''));
|
||||
} catch (e) {
|
||||
console.error('Error parsing annotations:', e);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, () => []);
|
||||
};
|
||||
|
||||
// Start loading: fetch PDF document + annotations in parallel.
|
||||
const startLoading = (url) => {
|
||||
const myRenderId = ++currentRenderId;
|
||||
|
||||
// IMMEDIATE CLEANUP
|
||||
scope.annotations = [];
|
||||
pages = [];
|
||||
|
||||
const container = element[0].querySelector('#pdf-pages');
|
||||
container.innerHTML = '<div style="color:white;padding:20px;text-align:center"><span class="fas fa-spinner fa-spin"></span> Loading...</div>';
|
||||
container.style.minHeight = '0px';
|
||||
|
||||
// Start fetching annotations immediately in parallel
|
||||
const docId = getDocId();
|
||||
let annotationsPromise;
|
||||
if (docId) {
|
||||
annotationsPromise = fetchAnnotations(docId);
|
||||
} else {
|
||||
annotationsPromise = new Promise(resolve => {
|
||||
if (deregisterDocIdWatch) deregisterDocIdWatch();
|
||||
deregisterDocIdWatch = scope.$watch(getDocId, (newDocId) => {
|
||||
if (!newDocId || myRenderId !== currentRenderId) return;
|
||||
deregisterDocIdWatch();
|
||||
deregisterDocIdWatch = null;
|
||||
fetchAnnotations(newDocId).then(resolve);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Load PDF document
|
||||
pdfjsLib.getDocument(url).promise.then(async (loadedPdf) => {
|
||||
if (myRenderId !== currentRenderId) return; // Stale request
|
||||
pdfDoc = loadedPdf;
|
||||
await renderPages(annotationsPromise, myRenderId);
|
||||
}).catch(e => {
|
||||
if (myRenderId !== currentRenderId) return;
|
||||
console.error('Error loading PDF:', e);
|
||||
element.html('<div class="alert alert-danger">Error loading PDF: ' + e.message + '</div>');
|
||||
});
|
||||
};
|
||||
|
||||
// Render a single page's annotations onto its SVG layer.
|
||||
// Each element is tagged with data-anno-idx for eraser identification.
|
||||
const renderPageAnnotations = (pageNum, svg) => {
|
||||
svg.innerHTML = '';
|
||||
const pageAnnos = scope.annotations
|
||||
.map((a, idx) => ({ a, idx }))
|
||||
.filter(({ a }) => a.page === pageNum);
|
||||
|
||||
pageAnnos.forEach(({ a, idx }) => {
|
||||
let el;
|
||||
if (a.type === 'pencil' || a.type === 'highlight') {
|
||||
el = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
el.setAttribute('fill', 'none');
|
||||
el.setAttribute('stroke', a.color);
|
||||
el.setAttribute('stroke-width', a.width * scope.zoom);
|
||||
el.setAttribute('stroke-linecap', 'round');
|
||||
// Increase hit area for eraser by adding a transparent wider path
|
||||
el.setAttribute('stroke-width', Math.max(a.width * scope.zoom, scope.tool === 'eraser' ? 12 : a.width * scope.zoom));
|
||||
const scaledPath = a.data.replace(/([0-9.]+)/g, (match) => parseFloat(match) * scope.zoom);
|
||||
el.setAttribute('d', scaledPath);
|
||||
} else if (a.type === 'text') {
|
||||
el = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
el.setAttribute('x', a.x * scope.zoom);
|
||||
el.setAttribute('y', a.y * scope.zoom);
|
||||
el.setAttribute('fill', 'red');
|
||||
el.setAttribute('font-size', (16 * scope.zoom) + 'px');
|
||||
el.setAttribute('dominant-baseline', 'hanging');
|
||||
}
|
||||
|
||||
if (el) {
|
||||
el.dataset.annoIdx = idx;
|
||||
if (scope.tool === 'eraser') {
|
||||
el.style.cursor = 'not-allowed';
|
||||
el.style.pointerEvents = 'all';
|
||||
el.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
scope.$apply(() => scope.deleteAnnotation(idx));
|
||||
});
|
||||
} else {
|
||||
el.style.pointerEvents = 'none';
|
||||
}
|
||||
if (a.type === 'text') el.textContent = a.text;
|
||||
svg.appendChild(el);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Render all pages progressively.
|
||||
const renderPages = async (annotationsPromise, myRenderId) => {
|
||||
if (!pdfDoc) return;
|
||||
if (!myRenderId) myRenderId = ++currentRenderId;
|
||||
|
||||
const scrollContainer = element[0].querySelector('.pdf-viewer-scroll');
|
||||
const container = element[0].querySelector('#pdf-pages');
|
||||
const scrollPercent = scrollContainer.scrollTop / (scrollContainer.scrollHeight || 1);
|
||||
|
||||
pages = [];
|
||||
|
||||
// If annotationsPromise provided, handle it safely
|
||||
if (annotationsPromise) {
|
||||
annotationsPromise.then(annos => {
|
||||
if (myRenderId !== currentRenderId) return;
|
||||
scope.annotations = annos;
|
||||
pages.forEach(p => renderPageAnnotations(p.i, p.svg));
|
||||
});
|
||||
}
|
||||
|
||||
if (container.children.length > 0) {
|
||||
container.style.minHeight = container.offsetHeight + 'px';
|
||||
}
|
||||
container.innerHTML = '';
|
||||
|
||||
try {
|
||||
for (let i = 1; i <= pdfDoc.numPages; i++) {
|
||||
if (myRenderId !== currentRenderId) return; // Stop rendering if ID changed
|
||||
|
||||
const page = await pdfDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: scope.zoom });
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'pdf-page-wrapper';
|
||||
wrapper.style.width = viewport.width + 'px';
|
||||
wrapper.style.height = viewport.height + 'px';
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.className = 'pdf-canvas';
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('class', `annotation-layer tool-${scope.tool}`);
|
||||
if (scope.tool !== 'cursor') svg.classList.add('active');
|
||||
svg.setAttribute('width', viewport.width);
|
||||
svg.setAttribute('height', viewport.height);
|
||||
|
||||
wrapper.appendChild(canvas);
|
||||
wrapper.appendChild(svg);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
|
||||
if (myRenderId !== currentRenderId) return;
|
||||
|
||||
pages.push({ i: i, page: page, wrapper: wrapper, svg: svg, viewport: viewport });
|
||||
setupDrawing(svg, i);
|
||||
renderPageAnnotations(i, svg);
|
||||
|
||||
if (i === 1 || i === Math.floor(pdfDoc.numPages * scrollPercent)) {
|
||||
scrollContainer.scrollTop = scrollPercent * scrollContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (myRenderId === currentRenderId) {
|
||||
container.style.minHeight = '';
|
||||
scrollContainer.scrollTop = scrollPercent * scrollContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Tool handling
|
||||
scope.setTool = (tool) => {
|
||||
scope.tool = tool;
|
||||
element[0].querySelectorAll('.annotation-layer').forEach(svg => {
|
||||
svg.className = `annotation-layer tool-${tool}`;
|
||||
if (tool === 'cursor') {
|
||||
svg.classList.remove('active');
|
||||
svg.style.cursor = 'default';
|
||||
} else if (tool === 'eraser') {
|
||||
svg.classList.add('active');
|
||||
svg.style.cursor = 'not-allowed';
|
||||
} else {
|
||||
svg.classList.add('active');
|
||||
svg.style.cursor = 'crosshair';
|
||||
}
|
||||
});
|
||||
// Re-render to update pointer-events on annotation elements
|
||||
renderAnnotations();
|
||||
};
|
||||
|
||||
// Delete a single annotation by index and re-render
|
||||
scope.deleteAnnotation = (idx) => {
|
||||
scope.annotations.splice(idx, 1);
|
||||
renderAnnotations();
|
||||
};
|
||||
|
||||
// Clear all annotations with confirmation
|
||||
scope.clearAllAnnotations = () => {
|
||||
if (!confirm('Clear all annotations on this document? This cannot be undone until you reload.')) return;
|
||||
scope.annotations = [];
|
||||
renderAnnotations();
|
||||
};
|
||||
|
||||
scope.zoomIn = () => { scope.zoom += 0.2; renderPages(); };
|
||||
scope.zoomOut = () => { if (scope.zoom > 0.4) { scope.zoom -= 0.2; renderPages(); } };
|
||||
|
||||
// ── Export helpers ──────────────────────────────────────────────────
|
||||
scope.exporting = null;
|
||||
|
||||
// Build a high-resolution merged canvas for export (independent of screen zoom)
|
||||
const buildHighResMergedCanvas = async (pageNum) => {
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
const EXPORT_SCALE = 2.0; // High res for crisp PDF
|
||||
const viewport = page.getViewport({ scale: EXPORT_SCALE });
|
||||
|
||||
// 1. Render PDF page to a hidden canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: ctx, viewport: viewport }).promise;
|
||||
|
||||
// 2. Prepare SVG overlay
|
||||
// We need to filter and scale annotations for the export scale
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||
svg.setAttribute('width', viewport.width);
|
||||
svg.setAttribute('height', viewport.height);
|
||||
|
||||
scope.annotations.filter(a => a.page === pageNum).forEach(a => {
|
||||
if (a.type === 'pencil' || a.type === 'highlight') {
|
||||
const el = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
el.setAttribute('fill', 'none');
|
||||
el.setAttribute('stroke', a.color);
|
||||
el.setAttribute('stroke-width', a.width * EXPORT_SCALE);
|
||||
el.setAttribute('stroke-linecap', 'round');
|
||||
const scaledPath = a.data.replace(/([0-9.]+)/g, (match) => parseFloat(match) * EXPORT_SCALE);
|
||||
el.setAttribute('d', scaledPath);
|
||||
svg.appendChild(el);
|
||||
} else if (a.type === 'text') {
|
||||
const el = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
el.setAttribute('x', a.x * EXPORT_SCALE);
|
||||
el.setAttribute('y', a.y * EXPORT_SCALE);
|
||||
el.setAttribute('fill', 'red');
|
||||
el.setAttribute('font-size', (16 * EXPORT_SCALE) + 'px');
|
||||
el.setAttribute('dominant-baseline', 'hanging');
|
||||
el.textContent = a.text;
|
||||
svg.appendChild(el);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Merge SVG into Canvas
|
||||
return new Promise((resolve) => {
|
||||
const svgStr = new XMLSerializer().serializeToString(svg);
|
||||
const svgBlob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
URL.revokeObjectURL(url);
|
||||
resolve(canvas);
|
||||
};
|
||||
img.onerror = () => { URL.revokeObjectURL(url); resolve(canvas); };
|
||||
img.src = url;
|
||||
});
|
||||
};
|
||||
|
||||
// Export all pages as a PDF file with annotations burned in
|
||||
scope.exportPdf = async () => {
|
||||
if (scope.exporting || !pdfDoc) return;
|
||||
scope.exporting = 'pdf';
|
||||
try {
|
||||
const pdf = await generateAnnotatedPdf();
|
||||
pdf.save('annotated.pdf');
|
||||
} catch (e) {
|
||||
console.error('PDF export error:', e);
|
||||
alert('Export failed: ' + e.message);
|
||||
} finally {
|
||||
scope.$apply(() => { scope.exporting = null; });
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to generate the jsPDF object
|
||||
const generateAnnotatedPdf = async () => {
|
||||
// Dynamically load jsPDF if not present
|
||||
if (typeof window.jspdf === 'undefined') {
|
||||
await new Promise((resolve, reject) => {
|
||||
const s = document.createElement('script');
|
||||
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
|
||||
s.onload = resolve;
|
||||
s.onerror = reject;
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
const { jsPDF } = window.jspdf;
|
||||
const pageIndices = Array.from({ length: pdfDoc.numPages }, (_, i) => i + 1);
|
||||
const canvases = await Promise.all(pageIndices.map(buildHighResMergedCanvas));
|
||||
|
||||
const firstW = canvases[0].width;
|
||||
const firstH = canvases[0].height;
|
||||
const pdf = new jsPDF({
|
||||
orientation: firstW > firstH ? 'landscape' : 'portrait',
|
||||
unit: 'px',
|
||||
format: [firstW, firstH],
|
||||
hotfixes: ['px_scaling']
|
||||
});
|
||||
canvases.forEach((c, i) => {
|
||||
if (i > 0) pdf.addPage([c.width, c.height], c.width > c.height ? 'landscape' : 'portrait');
|
||||
pdf.addImage(c.toDataURL('image/jpeg', 0.95), 'JPEG', 0, 0, c.width, c.height);
|
||||
});
|
||||
return pdf;
|
||||
};
|
||||
|
||||
// Save the annotated PDF back to Teedy as a new version
|
||||
scope.saveAsVersion = async () => {
|
||||
const docId = getDocId();
|
||||
if (scope.exporting || scope.shareId || !docId) return;
|
||||
if (!confirm('This will save a new PDF version with annotations permanently "burned in". Continue?')) return;
|
||||
|
||||
scope.exporting = 'version';
|
||||
try {
|
||||
const pdf = await generateAnnotatedPdf();
|
||||
const pdfBlob = pdf.output('blob');
|
||||
|
||||
// Prepare multipart form data for Teedy API
|
||||
const fd = new FormData();
|
||||
fd.append('id', docId);
|
||||
fd.append('previousFileId', scope.fileId);
|
||||
fd.append('file', pdfBlob, 'annotated.pdf');
|
||||
|
||||
console.log('Uploading new version to Teedy...');
|
||||
const resp = await $http.put('../api/file', fd, {
|
||||
transformRequest: angular.identity,
|
||||
headers: { 'Content-Type': undefined }
|
||||
});
|
||||
|
||||
console.log('Version saved successfully:', resp.data);
|
||||
alert('New version saved successfully! The page will now reload.');
|
||||
window.location.reload(); // Reload to show the new version
|
||||
} catch (e) {
|
||||
console.error('Save version error:', e);
|
||||
alert('Failed to save version: ' + (e.data ? e.data.message : e.message));
|
||||
} finally {
|
||||
scope.$apply(() => { scope.exporting = null; });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Drawing logic
|
||||
let isDrawing = false;
|
||||
let currentPath = null;
|
||||
let currentElement = null;
|
||||
|
||||
const setupDrawing = (svg, pageNum) => {
|
||||
svg.onmousedown = (e) => {
|
||||
if (scope.tool === 'cursor' || scope.tool === 'text' || scope.tool === 'eraser' || scope.shareId) return;
|
||||
|
||||
const rect = svg.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / scope.zoom;
|
||||
const y = (e.clientY - rect.top) / scope.zoom;
|
||||
|
||||
isDrawing = true;
|
||||
currentPath = `M ${x} ${y}`;
|
||||
currentElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
currentElement.setAttribute('fill', 'none');
|
||||
currentElement.setAttribute('stroke', scope.tool === 'highlight' ? 'rgba(255, 255, 0, 0.4)' : 'red');
|
||||
currentElement.setAttribute('stroke-width', scope.tool === 'highlight' ? '20' : '2');
|
||||
currentElement.setAttribute('stroke-linecap', 'round');
|
||||
currentElement.setAttribute('d', currentPath);
|
||||
svg.appendChild(currentElement);
|
||||
};
|
||||
|
||||
svg.onclick = (e) => {
|
||||
if (scope.tool !== 'text' || scope.shareId) return;
|
||||
const rect = svg.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / scope.zoom;
|
||||
const y = (e.clientY - rect.top) / scope.zoom;
|
||||
addTextInput(svg, x, y, pageNum);
|
||||
};
|
||||
|
||||
svg.onmousemove = (e) => {
|
||||
if (!isDrawing || !currentElement) return;
|
||||
const rect = svg.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / scope.zoom;
|
||||
const y = (e.clientY - rect.top) / scope.zoom;
|
||||
|
||||
currentPath += ` L ${x} ${y}`;
|
||||
const scaledPath = currentPath.replace(/([0-9.]+)/g, (match) => parseFloat(match) * scope.zoom);
|
||||
currentElement.setAttribute('d', scaledPath);
|
||||
};
|
||||
|
||||
svg.onmouseup = () => {
|
||||
if (!isDrawing) return;
|
||||
isDrawing = false;
|
||||
if (currentElement) {
|
||||
scope.annotations.push({
|
||||
type: scope.tool,
|
||||
page: pageNum,
|
||||
data: currentPath,
|
||||
color: currentElement.getAttribute('stroke'),
|
||||
width: parseFloat(currentElement.getAttribute('stroke-width')) / scope.zoom
|
||||
});
|
||||
currentElement = null;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const addTextInput = (svg, x, y, pageNum) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'pdf-text-input';
|
||||
|
||||
// Prevent browser extensions (e.g. password managers) from attaching autofill overlays
|
||||
// which cause DOM errors when the input is removed.
|
||||
input.setAttribute('autocomplete', 'off');
|
||||
input.setAttribute('spellcheck', 'false');
|
||||
input.setAttribute('data-lpignore', 'true');
|
||||
input.setAttribute('data-1p-ignore', 'true');
|
||||
|
||||
input.style.left = (x * scope.zoom) + 'px';
|
||||
input.style.top = (y * scope.zoom) + 'px';
|
||||
input.style.fontSize = (16 * scope.zoom) + 'px';
|
||||
input.style.color = 'red';
|
||||
input.style.minWidth = '120px';
|
||||
input.style.zIndex = '9999';
|
||||
input.placeholder = 'Type here, Enter to confirm';
|
||||
|
||||
// IMPORTANT: disable SVG pointer events while typing so they don't steal focus
|
||||
svg.style.pointerEvents = 'none';
|
||||
|
||||
svg.parentElement.appendChild(input);
|
||||
input.focus();
|
||||
|
||||
const finishInput = () => {
|
||||
// Re-enable SVG pointer events
|
||||
svg.style.pointerEvents = '';
|
||||
if (input.value.trim()) {
|
||||
scope.annotations.push({
|
||||
type: 'text',
|
||||
page: pageNum,
|
||||
x: x,
|
||||
y: y,
|
||||
text: input.value.trim()
|
||||
});
|
||||
renderAnnotations();
|
||||
}
|
||||
if (input.parentElement) input.remove();
|
||||
};
|
||||
|
||||
input.onblur = finishInput;
|
||||
|
||||
input.onkeydown = (e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter') { input.onblur = null; finishInput(); }
|
||||
else if (e.key === 'Escape') { input.onblur = null; svg.style.pointerEvents = ''; input.remove(); }
|
||||
};
|
||||
};
|
||||
|
||||
const renderAnnotations = () => {
|
||||
pages.forEach(p => renderPageAnnotations(p.i, p.svg));
|
||||
};
|
||||
|
||||
|
||||
|
||||
scope.saveAnnotations = () => {
|
||||
const docId = getDocId();
|
||||
if (scope.shareId) return;
|
||||
if (!docId) {
|
||||
alert('Cannot save: Document ID is missing. Are you in Quick Upload mode?');
|
||||
return;
|
||||
}
|
||||
scope.saving = true;
|
||||
Restangular.one('comment', docId).get().then(data => {
|
||||
const oldAnnos = data.comments.filter(c => c.content.startsWith('[ANNOTATION]'));
|
||||
const deleteNext = () => {
|
||||
if (oldAnnos.length > 0) {
|
||||
const c = oldAnnos.shift();
|
||||
Restangular.one('comment', c.id).remove().then(deleteNext).catch(err => {
|
||||
console.error('Error deleting old annotation:', err);
|
||||
alert('Failed to overwrite old annotations.');
|
||||
scope.$apply(() => { scope.saving = false; });
|
||||
});
|
||||
} else {
|
||||
// Server uses @PUT with @FormParam - must send as application/x-www-form-urlencoded
|
||||
const formData = 'id=' + encodeURIComponent(docId) +
|
||||
'&content=' + encodeURIComponent('[ANNOTATION]' + JSON.stringify(scope.annotations));
|
||||
$http({
|
||||
method: 'PUT',
|
||||
url: '../api/comment',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
}).then(() => {
|
||||
console.log('Annotations saved successfully!');
|
||||
scope.saving = false;
|
||||
}, err => {
|
||||
console.error('Error saving annotation:', err);
|
||||
alert('Failed to save annotations. Check console for details.');
|
||||
scope.saving = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
deleteNext();
|
||||
}).catch(err => {
|
||||
console.error('Error fetching comments:', err);
|
||||
alert('Failed to connect to the server to save annotations. Are you still logged in?');
|
||||
scope.$apply(() => { scope.saving = false; });
|
||||
});
|
||||
};
|
||||
|
||||
// Watch fileId - reset all state and reload when switching between files
|
||||
scope.$watch('fileId', (newFileId) => {
|
||||
if (!newFileId) return;
|
||||
// Reset state
|
||||
scope.annotations = [];
|
||||
pdfDoc = null;
|
||||
pages = [];
|
||||
const url = `../api/file/${newFileId}/data` + (scope.shareId ? `?share=${scope.shareId}` : '');
|
||||
|
||||
// Use $timeout to wait for the current digest cycle to finish.
|
||||
// This ensures scope.documentId is updated before we fetch annotations.
|
||||
$timeout(() => {
|
||||
startLoading(url);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -54,4 +54,10 @@ angular.module('share').controller('Share', function($scope, $state, $stateParam
|
||||
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* Return true if the comment is not an annotation.
|
||||
*/
|
||||
$scope.isNotAnnotation = function (comment) {
|
||||
return !comment.content.startsWith('[ANNOTATION]');
|
||||
};
|
||||
});
|
||||
@@ -51,7 +51,9 @@
|
||||
<script src="lib/angular.qrcode.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.timeago.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.ng-onboarding.js" type="text/javascript"></script>
|
||||
<script src="lib/pdf.js" type="text/javascript"></script>
|
||||
<script src="app/docs/app.js" type="text/javascript"></script>
|
||||
<script src="app/docs/directive/PdfAnnotationViewer.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/Login.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/Main.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/ModalPasswordLost.js" type="text/javascript"></script>
|
||||
|
||||
File diff suppressed because one or more lines are too long
+22
File diff suppressed because one or more lines are too long
@@ -143,7 +143,7 @@
|
||||
<p ng-show="!comments && commentsError">{{ 'document.view.error_loading_comments' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div ng-repeat="comment in comments" class="media" style="overflow: hidden">
|
||||
<div ng-repeat="comment in comments | filter:isNotAnnotation" class="media" style="overflow: hidden">
|
||||
<div class="pull-left">
|
||||
<img ng-src="https://www.gravatar.com/avatar/{{ comment.creator_gravatar }}?s=40&d=identicon" class="media-object" />
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<div class="text-center">
|
||||
<div class="btn-group pull-left">
|
||||
<button type="button" class="btn btn-default" ng-click="closeFile()">
|
||||
<button type="button" class="btn btn-default" ng-click="closeFile()" uib-tooltip="Close">
|
||||
<span class="fas fa-times"></span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" ng-click="toggleMaximize()" uib-tooltip="{{ isMaximized ? 'Restore Down' : 'Maximize' }}">
|
||||
<span class="fas" ng-class="{'fa-expand': !isMaximized, 'fa-compress': isMaximized}"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
@@ -50,7 +53,7 @@
|
||||
</a>
|
||||
|
||||
<!-- PDF viewer -->
|
||||
<iframe ng-src="{{ trustedFileUrl }}" class="pdf-viewer" scrolling="yes" ng-if="!error && file.mimetype == 'application/pdf'"></iframe>
|
||||
<pdf-annotation-viewer ng-if="!error && file.mimetype == 'application/pdf'" file-id="$stateParams.fileId" document-id="file.document_id" share-id="$stateParams.shareId" class="pdf-viewer"></pdf-annotation-viewer>
|
||||
|
||||
<!-- File not found -->
|
||||
<p class="well-lg" ng-show="error">
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
<p ng-show="!comments && commentsError">{{ 'document.view.error_loading_comments' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div ng-repeat="comment in comments" class="media" style="overflow: hidden">
|
||||
<div ng-repeat="comment in comments | filter:isNotAnnotation" class="media" style="overflow: hidden">
|
||||
<div class="pull-left">
|
||||
<img ng-src="https://www.gravatar.com/avatar/{{ comment.creator_gravatar }}?s=40&d=identicon" class="media-object" />
|
||||
</div>
|
||||
|
||||
@@ -35,8 +35,9 @@
|
||||
<script src="lib/angular.tmhDynamicLocale.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.ui-router.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.ui-bootstrap.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.restangular.js" type="text/javascript"></script>
|
||||
<script src="lib/pdf.js" type="text/javascript"></script>
|
||||
<script src="app/share/app.js" type="text/javascript"></script>
|
||||
<script src="app/docs/directive/PdfAnnotationViewer.js" type="text/javascript"></script>
|
||||
<script src="app/share/controller/Main.js" type="text/javascript"></script>
|
||||
<script src="app/share/controller/Share.js" type="text/javascript"></script>
|
||||
<script src="app/share/controller/ShareModalPdf.js" type="text/javascript"></script>
|
||||
|
||||
@@ -78,7 +78,7 @@ ul.tag-tree {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
|
||||
&> li + li:before {
|
||||
&>li+li:before {
|
||||
content: '/';
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ ul.tag-tree {
|
||||
border: none !important;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
|
||||
tbody tr {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -150,43 +150,66 @@ ul.tag-tree {
|
||||
|
||||
// $http loader
|
||||
.loader {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
|
||||
&.loader-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
&.loader-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// Users list
|
||||
.table-users {
|
||||
tbody tr {
|
||||
tbody tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Logs list
|
||||
.table-logs {
|
||||
tbody tr td {
|
||||
tbody tr td {
|
||||
&:first-child {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.cell-message {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// File view
|
||||
.modal-fileview {
|
||||
top: 2%;
|
||||
max-height: 96%;
|
||||
overflow-y: scroll;
|
||||
top: 2%;
|
||||
max-height: 96%;
|
||||
overflow-y: scroll;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.modal-fullscreen {
|
||||
top: 0;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.modal-dialog {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// File thumbnails
|
||||
@@ -262,7 +285,7 @@ ul.tag-tree {
|
||||
top: 50%;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
transform: translate(-50%,-50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,7 +299,7 @@ ul.tag-tree {
|
||||
|
||||
// Fields bound to datepicker
|
||||
input[readonly][datepicker-popup] {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// Share link
|
||||
@@ -286,31 +309,31 @@ input[readonly].share-link {
|
||||
|
||||
// Inline edition
|
||||
.inline-edit {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
display: none;
|
||||
}
|
||||
|
||||
input {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
.pagination > .active > a {
|
||||
.pagination>.active>a {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
@@ -334,7 +357,11 @@ input[readonly].share-link {
|
||||
}
|
||||
|
||||
// Dirty Bootstrap 3 fix, see https://github.com/twbs/bootstrap/issues/6686
|
||||
.row { margin: 0; padding: 0 }
|
||||
.row {
|
||||
margin: 0;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.navbar-nav.navbar-right:last-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
@@ -354,7 +381,7 @@ input[readonly].share-link {
|
||||
}
|
||||
|
||||
.thumbnail-checked {
|
||||
box-shadow: inset 0 1px 2px rgba(27,31,35,0.075), 0 0 0 0.2em rgba(3,102,214,0.3);
|
||||
box-shadow: inset 0 1px 2px rgba(27, 31, 35, 0.075), 0 0 0 0.2em rgba(3, 102, 214, 0.3);
|
||||
transition: box-shadow ease-in-out .15s;
|
||||
}
|
||||
|
||||
@@ -367,7 +394,7 @@ input[readonly].share-link {
|
||||
}
|
||||
|
||||
// Advanced search
|
||||
.btn-open-search > * {
|
||||
.btn-open-search>* {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -407,10 +434,10 @@ input[readonly].share-link {
|
||||
.settings-menu {
|
||||
.panel-default {
|
||||
border: 1px solid #eee;
|
||||
box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
|
||||
box-shadow: 0 7px 14px 0 rgba(50, 50, 93, .1), 0 3px 6px 0 rgba(0, 0, 0, .07);
|
||||
border-radius: 4px;
|
||||
|
||||
& > .panel-heading {
|
||||
&>.panel-heading {
|
||||
border-bottom: none;
|
||||
background: #f9f9f9;
|
||||
|
||||
@@ -435,7 +462,7 @@ input[readonly].share-link {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.well {
|
||||
box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
|
||||
box-shadow: 0 7px 14px 0 rgba(50, 50, 93, .1), 0 3px 6px 0 rgba(0, 0, 0, .07);
|
||||
background: none;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
@@ -477,7 +504,8 @@ input[readonly].share-link {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
&:active, &:focus {
|
||||
&:active,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
@@ -522,13 +550,14 @@ input[readonly].share-link {
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
// Vertical alignment
|
||||
.vertical-center {
|
||||
min-height: 100vh;
|
||||
height: 100vh; /* IE fix */
|
||||
height: 100vh;
|
||||
/* IE fix */
|
||||
|
||||
/* Make it a flex container */
|
||||
display: -webkit-box;
|
||||
@@ -538,11 +567,11 @@ input[readonly].share-link {
|
||||
display: flex;
|
||||
|
||||
/* Align the bootstrap's container vertically */
|
||||
-webkit-box-align : center;
|
||||
-webkit-align-items : center;
|
||||
-moz-box-align : center;
|
||||
-ms-flex-align : center;
|
||||
align-items : center;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-moz-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Login
|
||||
@@ -550,7 +579,7 @@ input[readonly].share-link {
|
||||
background: url('../../api/theme/image/background') no-repeat center;
|
||||
background-size: cover;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 0 1px rgba(136,152,170,.1), 0 15px 35px 0 rgba(49,49,93,.1), 0 5px 15px 0 rgba(0,0,0,.13);
|
||||
box-shadow: 0 0 0 1px rgba(136, 152, 170, .1), 0 15px 35px 0 rgba(49, 49, 93, .1), 0 5px 15px 0 rgba(0, 0, 0, .13);
|
||||
}
|
||||
|
||||
.login-box {
|
||||
@@ -579,7 +608,8 @@ input[readonly].share-link {
|
||||
z-index: 99998;
|
||||
background-color: green;
|
||||
color: green;
|
||||
box-shadow: 0 0 10px 0; /* Inherits the font color */
|
||||
box-shadow: 0 0 10px 0;
|
||||
/* Inherits the font color */
|
||||
height: 2px;
|
||||
opacity: 0;
|
||||
|
||||
@@ -614,15 +644,17 @@ input[readonly].share-link {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse{
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform:scale(1.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
50%{
|
||||
transform:scale(0.8);
|
||||
|
||||
50% {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
100%{
|
||||
transform:scale(1.1);
|
||||
|
||||
100% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,17 +692,17 @@ input[readonly].share-link {
|
||||
transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 1px rgba(50,50,93,.1), 0 2px 5px 0 rgba(50,50,93,.08), 0 1px 1.5px 0 rgba(0,0,0,.07), 0 1px 2px 0 rgba(0,0,0,.08), 0 0 0 0 transparent;
|
||||
box-shadow: 0 0 0 1px rgba(50, 50, 93, .1), 0 2px 5px 0 rgba(50, 50, 93, .08), 0 1px 1.5px 0 rgba(0, 0, 0, .07), 0 1px 2px 0 rgba(0, 0, 0, .08), 0 0 0 0 transparent;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 1px rgba(49,49,93,.03), 0 2px 5px 0 rgba(49,49,93,.1), 0 1px 2px 0 rgba(0,0,0,.08);
|
||||
box-shadow: 0 0 0 1px rgba(49, 49, 93, .03), 0 2px 5px 0 rgba(49, 49, 93, .1), 0 1px 2px 0 rgba(0, 0, 0, .08);
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
border: none;
|
||||
box-shadow: 0 0 0 1px rgba(136,152,170,.1), 0 15px 35px 0 rgba(49,49,93,.1), 0 5px 15px 0 rgba(0,0,0,.13);
|
||||
box-shadow: 0 0 0 1px rgba(136, 152, 170, .1), 0 15px 35px 0 rgba(49, 49, 93, .1), 0 5px 15px 0 rgba(0, 0, 0, .13);
|
||||
}
|
||||
|
||||
.navbar-default {
|
||||
@@ -686,20 +718,20 @@ input[readonly].share-link {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
}
|
||||
|
||||
.nav > li {
|
||||
.nav>li {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav > .active > a,
|
||||
.navbar-default .navbar-nav > .active > a:hover,
|
||||
.navbar-default .navbar-nav > .active > a:focus {
|
||||
.navbar-default .navbar-nav>.active>a,
|
||||
.navbar-default .navbar-nav>.active>a:hover,
|
||||
.navbar-default .navbar-nav>.active>a:focus {
|
||||
color: #2ab2dc;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.well-3d {
|
||||
border: 1px solid #eee;
|
||||
box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
|
||||
box-shadow: 0 7px 14px 0 rgba(50, 50, 93, .1), 0 3px 6px 0 rgba(0, 0, 0, .07);
|
||||
background: none;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -733,7 +765,7 @@ input[readonly].share-link {
|
||||
word-wrap: break-word;
|
||||
background-color: #fff;
|
||||
background-clip: border-box;
|
||||
border: 1px solid rgba(0,0,0,.125);
|
||||
border: 1px solid rgba(0, 0, 0, .125);
|
||||
border-radius: .25em;
|
||||
padding: 1.25em;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user